Why svg text disappears when setting x and y to 0? - javascript

I just started reading about svg and I came up with the following question
I am creating a simple svg with a text inside as shown below.
From my reading I understood that x and y of the text tag declares the position of the text inside the svg space.
Why when I set both x and y to 0 the text does not appear and when I change x and y to 10 for example it is displayed? Isn't x=0 and y=0 meaning the top left corner of the svg tag?
Thanks
<svg width="200" height="100">
<text x="0" y="0">hello</text>
</svg>

You're correct, (0,0) is indeed the top left corner of the SVG area (at least before you start transforming the coordinates).
However, your text element <text x="0" y="0">hello</text> is positioned with the leftmost end of its baseline at (0,0), which means the text will appear entirely off the top of the SVG image.
Try this: change your text tag to <text x="0" y="0">goodbye</text>. You should now be able to see the descending parts of the 'g' and 'y' at the top of your SVG.
You can shift your text down by one line if you provide a y coordinate equal to the line height, for example:
<svg width="200" height="100">
<text x="0" y="1em">hello</text>
</svg>
Here's a JSFiddle link for you to play with.

To make <text> behave in a more standard way, you can use dominant-baseline: hanging like so:
<text x="0" style="dominant-baseline: hanging;">Hello</text>
You can see examples of different values of this property here.

Related

Vertically centering a text element inside a rect element

I have some code in a project where I try to center a text element inside of a rect element based on the rect height + widht as well as the width and height of the text. It works fine horizontally but applying the same logic vertically fails.
Aligning the text horizontally is easy. I simply get the bounding box of the text to calculate how wide it is, then I subtract that width from the total width of the box to find how much space will be left inside the rectangle. Then I divide that width in half to know how much space I need to padd my x-coordinate of my text in order for it to be equal on both sides:
My code for getting information of the rectangle:
let rect = document.getElementById("rectangle");
let rectXCoordinate = Number(rect.getAttribute("x"));
let rectYCoordinate = Number(rect.getAttribute("y"));
let rectWidth = Number(rect.getAttribute("width"));
let rectHeight = Number(rect.getAttribute("height"));
My code for centering the text horizontally:
let text = document.getElementById("text");
let textBBox = text.getBBox();
let textWidth = textBBox.width;
let horizontalPadding = (rectWidth - textWidth) / 2;
let textX = rectXCoordinate + horizontalPadding;
text.setAttribute("x", textX);
However, the issue is when I try to apply similar logic vertically. The way I understand it, a rectangle is drawn with its 0,0 point at the top left corner. Which means positive y equals down, and positive x equals right.
Text seems to be drawn with 0,0 at the bottom of the text, meaning that positive y is up and positive x is right.
So in order to compensate for this I first calculate the bottom coordinate of the rectangle, by adding the rectangle y with the rectangle height.
Then I subtract the vertical padding using the same logic as in the horizontal example. I subtract the text height from the total height of the rectangle and divide it by 2.
See code below:
let textHeight = textBBox.height;
let verticalPadding = (rectHeight - textHeight) / 2;
let textY = rectYCoordinate + rectHeight - verticalPadding;
text.setAttribute("y", textY);
The problem is, the text is slightly off center and I cannot for the life of me understand why.
Is there some fundamental that I am misunderstanding in regards to how text is drawn?
I understand that there are other ways to do this, and I have solved it differently in my project, so please refrain from giving examples on alternative ways to solve this. I am just curious to know why this logic fails, I would like to understand what is going on.
Here is a codepen live example with the same code as I've inserted above:
https://codepen.io/Sorry-not-sorry/pen/QWBzwjW
Adding a more detailed explanation here for future readers:
The baseline affects the position of the text within the text bounding box. Note that the default for the alignment-baseline is baseline. Run the snippet below and you'll notice that the extender for letter 'y' goes below the line and the ascender on the letter 'b' doesn't reach to the top of the line above it when the baseline is set to the default.
What's happening in your JavaScript is that the getBBox().height is returning the height of the text bounding box, but not the placement of the text within it. Therefore, if you change the value of the baseline to ideographic or text-after-edge, the text will be placed fully within the bounding box.
<div style="display: flex;">
<div>
<h2>Dominant-baseline</h2>
<svg viewBox="0 0 180 120" xmlns="http://www.w3.org/2000/svg" height="200">
<path d="M0,15 L150,15 M0,30 L150,30 M0,45 L150,45 M0,60 L150,60 M0,75 L150,75 M0,90 L150,90" stroke="#ccc" />
<text dominant-baseline="ideographic" x="10" y="30">ideographic</text>
<text dominant-baseline="baseline" x="10" y="45">baseline y</text>
<text dominant-baseline="middle" x="10" y="60">middle</text>
<text dominant-baseline="hanging" x="10" y="75">hanging</text>
<text dominant-baseline="text-before-edge" x="10" y="90">text-before-edge</text>
<text dominant-baseline="text-after-edge" x="10" y="15">text-after-edge</text>
</svg>
</div>
<div>
<h2>Alignment-baseline</h2>
<svg viewBox="0 0 180 120" xmlns="http://www.w3.org/2000/svg" height="200">
<path d="M0,15 L150,15 M0,30 L150,30 M0,45 L150,45 M0,60 L150,60 M0,75 L150,75 M0,90 L150,90" stroke="#ccc" />
<text alignment-baseline="ideographic" x="10" y="30">ideographic</text>
<text alignment-baseline="baseline" x="10" y="45">baseline</text>
<text alignment-baseline="middle" x="10" y="60">middle</text>
<text alignment-baseline="hanging" x="10" y="75">hanging</text>
<text alignment-baseline="text-before-edge" x="10" y="90">text-before-edge</text>
<text alignment-baseline="text-after-edge" x="10" y="15">text-after-edge</text>
</svg>
</div>
</div>
Warning: The problem gets more complicated when you're dealing with SVGs in HTML because they can be affected by CSS. The default font-size of the text element can be overridden by setting the font-size on the HTML body if the text element font-size isn't being explicitly set on the text element using the font-size attribute. Even then, you can have CSS that overrides this such as text { font-size: 18px } in a stylesheet. The getBBox() method doesn't know anything about the font-size applied via CSS in order to calculate the height correctly.
How to center the text reliably
This approach will reliably set the text in the center of the rect regardless of any CSS affecting the font-size:
Group the text and rect together if you want to be able to move them from the 0,0 point. This way you don't have to account for the position of the rect relative from the SVG origin.
Set the rect height and width.
Set the text x position to 1/2 of the width of the rect.
Set the text y position to 1/2 of the height of the rect.
Set the text-anchor property to middle on the text element.
Set the baseline-alignment to central on the text element to center the text vertically. The central value matches the box's central baseline to the central baseline of its parent.
Set the text-anchor property on the text element to middle to center the text horizontally.
Use a transform to move the group into the final desired position within the viewBox.
Demo
<svg id="viewbox" viewBox="0 0 100 100" height="200">
<g transform="translate(25,25)">
<rect width="30" height="30" fill="white" stroke="currentColor" />
<text x="15" y="15" alignment-baseline="central" text-anchor="middle"> Hi </text>
</g>
</svg>
And, of course, this would be even easier to reproduce with JavaScript than your proposed approach, because you only need the rect x and y attributes to calculate the x and y position of the text element.

How do I make tspan inherits the parent text x? (Positioning multiline tspan in text)

I have an app that allow user to draw SVG multiline texts. I checked multiple solutions on StackOverflow and tspan seems to be the best, however I have the issue when the parent text is not positioned at the left (i.e x is not 0):
<svg viewBox="0 0 500 500">
<text x="100" y="100">
<tspan>A</tspan>
<tspan dx="0" dy="1.2em">B</tspan>
<tspan x="0" dy="1.2em">C</tspan>
</text>
</svg>
As you can see, the B line doesn't move back at all and the C line is moved to the left of the whole canvas instead of at text's 100px. Is there anyway other than setting each tspan manually?

D3.js: append path to a rect

I'm drawing a chart in D3.js, and I'm using a <rect> as the actual charting area (lines, plots, etc.). The plan is to only append the path to the <rect> such that it doesn't overflow the charting zone when user drags around. Right now it looks like this:
But when I tried to do:
var path = rect.append('path').attr(...)
The path itself doesn't even show up. The original version is just to append path to svg, whic works:
var path = svg.append('path').attr(...)
I also cannot do overflow:hidden like regular HTML elements because path is not contained inside the rect.
If appending path to rect is not possible or recommended, what is the best way to limit the "activity area" of path so that it doesn't cause overflow?
The reason that you can't see the path inside the rect is because SVG rect elements don't support having child elements, they aren't containers.
What you need to do is include an g (group) element which is a container and instead place both your rect and your path inside it. This is how a large number of D3 examples work because it means you can interact with the group, instead of all the individual sub-elements, which is great if you're zooming/moving things around.
In your case if you don't want you're line to be visible outside of the rect then you can simply omit drawing those points in the first place, or clip them once you have drawn them using a clip path.
I've put together a small example to illustrate using a clip-path with a rectangle which matches your use case. The blue stroke illustrates the area that would be filled without a clip-path. Once the clip-path has been applied however, you can see the actual filled area is much smaller than the full definition of the rectangle.
.fill {
fill: steelblue;
}
.outline {
stroke: steelblue;
fill: none;
}
<svg width="300" height="300">
<defs>
<clipPath id="demoClip">
<rect x="30" y="0" height="100" width="100"/>
</clipPath>
</defs>
<rect class="fill" x="0" y="0" width="200" height="200" clip-path="url(#demoClip)"/>
<rect class="outline" x="0" y="0" width="200" height="200"/>
</svg>

SVG Scaling and Coordinates

This is my first SVG project, and I’m not a programmer, but I dabble in interactive infographics. My previous experience in this area comes from working with ActionScript.
I’m using plain SVG (no Raphael, D3, etc.) and trying to create an interactive barchart. After some initial difficulty with the SVG coordinate system and scaling, I found some code online that handles the postscaling translation:
<text x="x_coord0" y="y_coord0" transform="scale(x_scale, y_scale) translate(-x_coord0*(x_scale-1)/x_scale, -y_coord0*(y_scale-1)/y_scale)" …>text</text>
And I converted it into this JavaScript:
var translationfactor = ((0 - y_position)*(y_scalefactor - 1) / y_scalefactor);
var matrix = "scale(1," + y_scalefactor + ") translate(0," + Number(translationfactor) + ")";
targetbar.setAttribute("transform", matrix);
The problem is that I need the bars “translated” back to the chart’s baseline, not the original locations of their topmost points. Currently the correctly scaled bars are hugging the top of the chart:
http://billgregg.net/miscellany/upsidedown-barchart.png
I’ve tried several fixes, including plugging the bars’ ”missing height” into translationfactor (the bars start out the full height of the chart and get scaled down dynamically). Nothing has worked. Part of my problem is that, besides being new to SVGs, I can stare at that code all day and my brain still can’t parse it. Multiplying negative numbers is too abstract and at a fundamental level I just don’t “get” the math, which of course makes modifying the code difficult.
My questions:
(1) What’s the fix for the code above to position the bars back on the baseline of the chart?
(2) Is there a more transparent, more pedestrian way of accomplishing the translation? My first thought along these lines was that if a bar’s height is reduced to 40% of its original value, then multiplying the original Y coordinate value by 250% should reset the bar to its original location (at least its topmost point), but that doesn’t seem to work.
(3) Is there a way to set a bar’s point of origin to its bottom? In Flash it’s possible, though as far as I know it’s a manual, not a programmatic task.
(4) Is there a method similar to .localToGlobal() in ActionScript that would allow me to avoid having to mess with the local coordinate system at all?
Behind the scenes there is matrix math going on and it can be hard to get your head around the pre and post multiplication of arrays.
It's not entirely clear what you are trying to achieve, but reading between the lines, it sounds like you are wanting to provide graph coordinates in their raw(ish) form and have the SVG scale and position them for you(?)
If that's the case, then I think the solution is simpler than what you think.
Assuming I'm right, we'll start with something that looks like this:
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<g transform="">
<rect x="0" width="1" height="5" fill="red"/>
<rect x="1" width="1" height="11" fill="green"/>
<rect x="2" width="1" height="12" fill="orange"/>
<rect x="3" width="1" height="8" fill="blue"/>
</g>
</svg>
Where x is obvious and the bar length is in height. y defaults to 0, so we don't need it here.
You basically want to know what goes in the transform to scale and position the bars on your page. The fact that your graph is "upside-down" helps a little. Because the origin in an SVG is at the top left.
First apply a scale. Let's make the bars 20 pixels wide, and scale the lengths up by 10.
<g transform="scale(20,10)">
Next you want to position the graph on the page. Let's put the top-left corner at (40,40).
In SVG the transformations are concatenated in order (post-multiplied) so in order for the translation to be what you specify and not be multiplied by the scale, you should put it first.
<g transform="translate(40,40) scale(20,10)">
So the final SVG looks like this:
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<g transform="translate(40,40) scale(20,10)">
<rect x="0" width="1" height="5" fill="red"/>
<rect x="1" width="1" height="11" fill="green"/>
<rect x="2" width="1" height="12" fill="orange"/>
<rect x="3" width="1" height="8" fill="blue"/>
</g>
</svg>
The above has been simplified by assuming you have already subtracted the values from your base 20%. If you wanted to keep the pure raw values, it's possible, but things get a bit trickier. You would need to either tinker with both the y and height value of each bar, or use clipping to hide the part of the bar above 20%.
For "right way up"/normal graphs. All you need to do is make the y scale negative and translate the graph so that the bottom-left is where you want it.
<g transform="translate(40,140) scale(20,-10)">
Hope this helps.

Trying to create a re-usable text-box (text with a square background-colour) in SVG 1.1?

I'm trying to create (what I thought would be!) a simple re-usable bit of SVG to show three lines of text, with a background colour - to simulate a 'post-it' note.
I have found some useful code here to get the Bounds of the Text http://my.opera.com/MacDev_ed/blog/2009/01/21/getting-boundingbox-of-svg-elements which I am using.
So: I'm creating an group of text elements like this in the 'defs' section of my SVG:
<svg id="canvas" width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="post_it">
<text x="0" y="30" id="heading" class="heading">My Heading</text>
<text x="0" y="45" id="description" class="description">This will contain the description</text>
<text x="0" y="60" id="company" class="company">Very Big Company Ltd.</text>
</g>
And I'm displaying the text with a 'use' element like this:
<use id="12345" class="postit" xlink:href="#post_it" onclick="showId(this);"/>
I'm using the onclick to trigger a call to the following javascript function (defined in 'defs' section):
function showId(elem) {
post_it_rect=getBBoxAsRectElement(elem);
document.getElementById('canvas').appendChild(post_it_rect);
}
(The 'getBBoxAsRectElement(elem)' is from the link I posted).
As this stands; this works just fine - however if I change my 'use' element to position the text in a different place like this:
<use x="100" y="100" id="12345" class="postit" xlink:href="#post_it" onclick="showId(this);"/>
Now, the text displays in the correct place, but the resultant 'background-color' (actually a 'rect' element with opacity of 0.5) still shows on the top-left of the svg canvass - and the function used to calculate the rect is returning '-2' rather than '100' ('-98'?) as I need (I think).
What do I need to do to line up the 'rect' elements and the text elements ?
The author of the (very helpful article btw) script provides a more advanced script to draw a box round any 'bb' in an SVG, but I couldn't get this to work (missing 'transform' functions?).
I'm using Firefox 7.x to render the SVG ; and I'm loading a .svg file (ie, not embedded in html etc) straight from disk to test this).
Yes, you may need to compensate yourself for the x and y attributes on the <use> element for the time being, I'll try to find some time to update the blogpost and script.
Here's a draft SVG 1.1 test that among other things checks that the effect of the x and y attributes are included in the bbox. The line starting [myUse] is the one that tests this case, if it's red then that subtest failed. Chromium and Opera Next both pass that subtest, while Firefox nightly and IE9 doesn't. Note that the test itself has not gone through full review yet, and that it may still change.

Categories