Best way to dynamically style svg marker elements - svg

My question: Can svg <marker> elements inherit color from the <line> they are referenced on?
The background: I have a D3 graph that has different styled lines, and I want my lines to have arrows at the end.
So at the top of my <svg> I have const defs = svg.append('defs'); and then further along I generate my defs using a generator function:
function makeDefs(defs: Selection<SVGDefsElement, unknown, null, undefined>, color: string, status: string) {
const markerSize = 3
const start = defs.append
.append('marker')
.attr('id', `arrow-start-${color}-${status}`)
.attr('viewBox', '-5 -10 20 20')
.attr('markerWidth', markerSize)
.attr('markerHeight', markerSize)
.attr('orient', 'auto-start-reverse');
start
.append('path')
.attr(
'd',
status === 'PUBLISHED' ? customPaths.arrowLarge : customPaths.arrowSmall
)
.attr('stroke', color)
.attr('fill', color);
}
And use it like so:
makeDefs(defs, 'red', 'DRAFT');
And then I add the markers to my lines with:
// d3 code to draw the lines etc
line.attr(
'marker-start',
(d) =>
`url(
#arrow-start-${d.color}-${d.status}
)`
);
This all works great, my arrows have lines. But my current setup feels burdensome and clunky. I have about 20 colors and 3 statuses. With my current setup that would be 60 different:
makeDefs(defs, 'one-of-20-colors', 'one-of-3-statues');
My understanding of markers is that they can inherit color using the currentcolor attribute. Currently my <defs> sit up top underneath my main <svg> so any color inherited is inherited directly from that top level svg which is not what I want. The issue in my case is my <line> elements are the elements who's color I want to inherit, but according to the MDN docs <line>s cannot have <defs> as children, thus leaving me with the only option, of defining all my <defs> up front all at once.
Is there a trick or some attribute I'm missing here?
Any way to pass color to my marker when doing:
line.attr(
'marker-start',
(d) =>
`url(
#arrow-start-${d.color}-${d.status}
)`
);
?
For what is is worth, I'm currently wrapping all my <line>s in <g>. I suppose I could wrap them in <svg>s instead, and apply the fill and stroke there, and then define my <defs> per svg container? I tried this briefly and swapping the <g> for an <svg> broke a lot, but I'm not even sure if it would work, or be better for that matter.

Related

How to change default color of SVG in MathJax-Node

I am using MathJax-node to generate SVGs of equations. I would like to change the default color, but so far nothing has worked.
I looked at this answer and tried
mjAPI.config({MathJax: {
styles: {
".MathJax_SVG, .MathJax_SVG_Display": {
fill: "#FFF",
stroke: "#FFF"
}
}}});
I also tried
mjAPI.config({MathJax: {
SVG: {color: "#0FF", fill: "#0F0", stroke: "#F00"}}});
and several variations, like including the styles under SVG, but so far nothing has worked.
I can work around this by setting the style from within the TeX expression, but I would prefer to be able to set a default configuration so I can process expressions without altering them.
MathJax-node wraps its output in a group with fill="currentColor stroke="currentColor" so that it inherits the color from the surrounding text. So one way to change the color would be to set the color of the container that will hold the SVG output. The configuration you give above would do that for MathJax in a browser, since MathJax surrounds its SVG output in a container with class="MathJax_SVG" or class="MathJax_SVG_Display"; but MathJax-node does not produce the container HTML elements that MathJax does, so there are no elements with class MathJax_SVG or MathJax_SVG_Display being generated. In any case, the styles are put into a stylesheet that would be added to the page, not used to add explicit styles to the SVG generated, so you would have to include that stylesheet into the page where the SVG output is being put in order for your configuration above to have an effect, even if the containers with the proper classes were being generated.
What you probably want, however, is to have the currentColor be some specific color instead, so that there is no inheriting of colors. Because SVG is a text-based image format, you can do that using a string replacement in your mathjax-node driver file. For example:
mjAPI.typeset({
math: "x+1",
format: "TeX",
svg: true,
useFontCache: false,
ex: 6, width: 100
}, function (data) {
if (!data.errors) {
console.log(data.svg.replace(/"currentColor"/g, '"red"'));
}
});
would set the color to red in the output.

SVG with size in px and percentage?

I'm trying to create an SVG element with a width defined by a percentage of the parent and a fixed value, say 50% + 20px. For normal html elements, in the CSS you can use calc(50% + 20px). Is there an equivalent way to do this for embedded SVGs? Specifically, I'm using snap.svg, though I'm not sure if this capability exists with SVGs in general.
EDIT:
Tried setting <svg> width with percentages and px, which I couldn't get to work. I tried both:
<svg width='calc(50% + 20px)'>...</svg>
<svg width='50% + 20px'>...</svg>
I also tried setting it in CSS:
svg {
width: calc(50% + 20px);
}
It should be possible with the upcoming SVG2 as width etc. become geometry properties and then you can style them with calc

Fading out two over-layed objects uniformly in RaphaelJS/SVG

I have two overlayed rectangles:
I'm trying to fade them out uniformly as if they were one object.
The problem:
When I animate their opacity from 1 to 0, the top rectangle becomes transparent and reveals the edges of the rectangle beneath it.
Here is my code:
var paper = Raphael(50,50,250,350)
var rect = paper.rect (20,40,200,200).attr({"fill":"red","stroke":"black"})
var rect2 = paper.rect (100,140,200,200).attr({"fill":"red","stroke":"black"})
var set=paper.set()
set.push(rect)
set.push(rect2)
set.click(function () {fadeOut()})
function fadeOut() {
rect.animate({"opacity":0},3000)
rect2.animate({"opacity":0},3000)
setTimeout(function () {
rect.attr({"opacity":1})
rect2.attr({"opacity":1})
},3100)
}
When the set is clicked, the rectangles fade out in 3 seconds. (look at the red rectangles in my fiddle, it will clarify my problem)
https://jsfiddle.net/apoL5rfp/1/
In my fiddle I also create a similar looking green path that performs the fade out CORRECTLY.
I can I achieve the same type of fadeout with multiple objects?
I think this is quite difficult in Raphael alone.
Few options spring to mind. Don't use Raphael, use something like Snap, put them in a group and change opacity in the group.
var g = paper.g(rect, rect2);
g.click(function () { fadeOut( this )} )
function fadeOut( el ) {
el.animate({"opacity":0},3000)
setTimeout(function () {
el.attr({"opacity":1})
},3100)
}
jsfiddle
However, you may be tied to Raphael, which makes things a bit tricky, as it doesn't support groups. You could place an 'blank' object over it (which matches same as background) and animate its opacity in the opposite way, like this..(note the disabling of clicks on top object in css)
var rectBlank = paper.rect(18,20,250,330).attr({ fill: 'white', stroke: "white", opacity: 0 });
var set=paper.set()
....
rectBlank.animate({"opacity":1},3000)
jsfiddle
Otherwise I think you may need to use a filter, which may help a bit. SO question

getting text width in SVG prior to rendering

I want to put a rectangle around a text in SVG.
The height of the text is known to me (the font-size attribute of the text element). But the width is dependent on the actual content. Using getBBox() or getComputedTextLength() should work. But this only works after rendering.
Is there a way to specify that in an other way? For example defining the x and width attributes relative to other values? I didn't find anything like that in the SVG Spec.
Figuring where text ends presumably requires roughly the same underlying code path as the rendering itself implements - going through the width of each character based on the font and style, etc... As I am not aware the SVG standards define a method for directly getting this information without doing the actual full rendering, till such methods emerge or are reported here by others, the approach should be to render invisibly before doing the actual rendering.
You can do that in a hidden layer (z-index, opacity and stuff) or outside the visible viewport, whichever works best in experimentation. You just need to get the browser do the rendering to find out, so you render invisibly for that sake, then use getComputedTextLength()
I know this is old, but a few ideas:
If you can choose a mono-spaced font, you know your width by a simple constant multiplication with glyph count
If you are bound to proportional fonts, you can find an average glyph size, do the math as with mono-space, and leave enough padding. Alternatively you can fill the padding with text element textLength attribute. If the constant is chosen carefully, the results are not very displeasing.
EDIT: As matanster found it to be hacky
Predetermine glyph widths with getComputedTextLength() and build a lookup table. Downside is that it does not account for kerning, but if your cache size is not a problem, you can append glyph-pair widths to this lookup.
Going beyond that is to find some way to do server side rendering: Is there a way to perform server side rendering of SVG graphics using React?
It is possible using canvas with measureText():
// Get text width before rendering
const getTextWidth = (text, font) => {
const element = document.createElement('canvas');
const context = element.getContext('2d');
context.font = font;
return context.measureText(text).width;
}
// Demo
const font = '16px serif';
const text = 'My svg text';
const textWidth = getTextWidth(text, font);
document.body.innerHTML = `
<svg>
<text x="0" y="20" font="${font}">${text}</text>
<rect x="0" y="30" width="${textWidth}" height="4" fill="red" />
</svg>
`;
Adapted from https://stackoverflow.com/a/31305410/1657101

Calculating viewBox parameters based on path elements in SVG

I get an XML or JSON with paths only, and I need to recreate the SVG image.
I create an empty
<svg xmlns='http://www.w3.org/2000/svg' version='1.1'></svg>,
I add a <g transform="scale(1 -1)" fill='#aaa' stroke='black' stroke-width='5' ></g> in it, and then in this element I add all of the paths in it (e.g. <path d= ... />).
In the end I get a SVG image, but because I haven't set the viewBox attribute in the SVG element the image isn't properly displayed - when I open it in browser, a part of it is displayed full size.
Can the viewBox be calculated from the values from the paths?
Thank you!
Similar to Martin Spa's answer, but a better way to do get the max viewport area is using the getBBox function:
var clientrect = path.getBBox();
var viewBox = clientrect.x+' '+clientrect.y+' '+clientrect.width+' '+clientrect.height;
You can then set the viewbox to these co-ordinates.
n.b. i think you can change the viewbox of an svg after it's rendered so you may have to re-render the svg.
OK so I solved it the following way:
removed all letters from the paths string and made an array out of it with
var values = pathValue.split('L').join(' ').split('M').join(' ').split('z').join('').split(' ');
found max and min from those values:
var max = Math.max.apply( Math, values );
var min = Math.min.apply( Math, values );
set the viewBox:
viewBox = max min max max
This worked in my case excellent. Hope that it will be helpful to someone else too.

Resources