Vertically centering a text element inside a rect element - javascript

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.

Related

Why does a SVG line disappear when I apply a SVG linearGradient? [duplicate]

This question already has answers here:
SVG Line with Gradient Stroke Won't Display if vertical or horizontal
(3 answers)
How to add a linearGradient to a vertical SVG line [duplicate]
(2 answers)
SVG Mask makes line disappear
(1 answer)
Closed 7 months ago.
Update: Gets weirder. If the line is horizontal it disappears, but if it has any slope at all, it shows up just fine. Look at id="horizontalNoShow" if you change it so y1 and y2 are not equal, it will render.
I think this is a bug but not sure. Happens in Chrome and Safari. Trying to add an SVG linearGradient to a line.
I can add it to all other shapes, but when I add it to the line, the line disappears. Still shows up in the DOM, but just not getting rendered for some reason?
I have a purple line that shows up great. I have a rectangle with a gradient stroke that shows up great. But when I combine the gradient stroke with the line, it doesn't show up.
<svg width="" height="">
<defs>
<linearGradient id="FirstGradient" >
<stop offset="0%" style="stop-color:#FF00FF"/>
<stop offset="100%" style="stop-color:#FFFF00"/>
</linearGradient>
</defs>
<line id="someSlopeShow" x1="50" y1="70" x2="250" y2="71"
stroke="url('#FirstGradient')"
stroke-width="6"
/>
<line id="horizontalNoShow" x1="55" y1="90" x2="255" y2="90"
stroke="url('#FirstGradient')"
stroke-width="6"
/>
<rect id="exampleTwoRectSVG"
x="10" y="10"
width="200" height="100"
stroke="url(#FirstGradient)"
stroke-width="15"
fill='transparent'
stroke-dasharray="110 20"
/>
</svg>
Found the answer. The spec for linearGradient show that is uses an object bounding box. https://www.w3.org/TR/SVG/coords.html#ObjectBoundingBoxUnits
However in the last paragraph:
"Keyword objectBoundingBox should not be used when the geometry of the
applicable element has no width or no height, such as the case of a
horizontal or vertical line, even when the line has actual thickness
when viewed due to having a non-zero stroke width since stroke width
is ignored for bounding box calculations. When the geometry of the
applicable element has no width or height and objectBoundingBox is
specified, then the given effect (e.g., a gradient or a filter) will
be ignored."
This makes sense since the box needs an area to work. And a horizontal line doesn’t have any area. 

A hacky fix is you can fix it by adding .001 to your coordinates to give it a tiny bit of area.
<svg width="" height="">
<defs>
<linearGradient id="FirstGradient" >
<stop offset="0%" style="stop-color:#FF00FF"/>
<stop offset="100%" style="stop-color:#FFFF00"/>
</linearGradient>
</defs>
<line id="horizontalNoShow" x1="55" y1="90" x2="255" y2="90.001"
stroke="url('#FirstGradient')"
stroke-width="6"
/>
</svg>
If I understand this correctly it means that none of the following features:
linearGradient
radialGradient
pattern
clipPath
mask
filter
will work if your line is horizontal or vertical.
I had a hard time finding the answer on StackOverFlow because I didn't realize it was the horizontal or vertical line that caused the issue.
These questions cover those well: How to add a linearGradient to a vertical SVG line
SVG Line with Gradient Stroke Won't Display if vertical or horizontal (this question name isn't great. Straight couldn't mean a lot of things.)
SVG Masks also don't work on vertical or horizontal lines: SVG Mask makes line disappear
feDropshadows also don't work on vertical or horizontal lines: Adding feDropShadow to a vertical line in SVG makes it disappear

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?

How to convert the value of the area of a SVG object into m2

I am using the math formulas to get the area of a circle or a rectangle of a SVG object. For the area of a polygon I use this formula.
But the values are in the viewbox scale and I want to convert it to square meters. For example, a rectangle object has the width and height properties. So it is just:
element.width x element.height = area
Considering the viewbox of a svg = "0 0 100 100" for example, and having a line object in svg that is used as a reference of a scale like in this image sample:
How can I convert the area value to a square meters? It is necessary to use the viewbox or just the definition that 1m is the object line width? I do not want to use the library d3.js.
Thank you.
UPDATE:
For the rectangule we must use the width and height and for the circle the radius. In both cases it is easy to convert the radius, height and width into meters. But in a polygon, what is used to calculate the areas are the vertices (points) with the values x and y axis. How to make the "translation" into meters?
NEW UPDATE
I still am unable to calculate well the polygons. I posted an example here: running example
No problems for circles and squares/rectangules. But for some reason it does not work for irregular polygon. Funny thing is that it does work if i create a polygon that is a square.
Any suggestions to solve this mistery? Thank you.
First you need to know how long is a meter in the svg element:
let meter = m.getTotalLength();
Next you get the bounding box of the rect. This is returning an svg rect with a position (x,y) a width and a height. You will need only the width and a height:
let rect = the_rect.getBBox();
Now you can calculate the equivalent in meters of the width and height of the rect:
let w = rect.width / meter;
let h = rect.height / meter;
Finally you can calculate the area of the rect
let area = w * h;
This is an working example:
// get the length of the line representing one meter
let meter = m.getTotalLength();
// get the bounding box of the rect. This is returning an svg rect with a position (x,y) a width and a height.
let rect = the_rect.getBBox();
//calculate the equivalent in meters of the width and height
let w = rect.width / meter;
let h = rect.height / meter;
//calculate thge area in merers
let area = w * h;
console.log("area:",area)
svg{border:solid; width:300px}
<svg viewBox="0 0 100 100">
<rect id="the_rect" x="10" y="10" width="30" height="50" stroke="black" fill="#cab" />
<path id="m" d="M50,70H90" stroke="black" />
<text text-anchor="middle" dy="-5">
<textPath xlink:href="#m" startOffset="50%">1 m</textPath>
</text>
</svg>
UPDATE
someone is commenting
Just note that getBBox "does not account for any transformation applied to the element or its parents" (just in case somebody needs it for transformed shape)
In the case the transformation applied is only a translation the transformation is irrelevant since you are using only the width and the height. If the transformation implies scaling you will need to wrap the shape in a group and get the bounding box of the group. If the transformation implies skewing you already have a lozenge not a rect so for this you will need a different approach.
The main idea about this answer is not showing you how to get the area of any king of polygon (which you know), but how to get the size from svg units to meters.

Rotating contents of an SVG text element

I 'm working on rotating the text in an SVG text element. Whene a rotation is performed using transform:rotate(angle,x,y) entire text element is rotated. This creates an issue that positioning of the text element will be modified.
I understand this behavior, because rotaion causes tanslation of co-ordinates. But what I want to do is, the 'x' and 'y' position should always be the bottom of text element.
For example, the following code will create a text element at 50, 50
<text x='50' y='50'>My Text </text>
Now, (left, bottom) of text is (50, 50). If I rotate it by 90, then (left, top) will be 50, 50.
So, for all kind of rotations, I don't want to change the position of text element. How can I do this?
You can anchor the text in different positions using text-anchor. For example, if you rotate the text like this:
<text id="mytext" x='50' y='50'>My Text
<animateTransform attributeName="transform" begin="0s" dur="5s" type="rotate"
from="0 50 50" to="360 50 50" repeatCount="indefinite"/>
</text>
It will use the upper-left corner as an anchor-point. You can move it to the center using:
<text id="mytext" x='50' y='50' text-anchor="middle">My Text
<animateTransform attributeName="transform" begin="0s" dur="5s" type="rotate"
from="0 50 50" to="360 50 50" repeatCount="indefinite"/>
</text>
And now it will rotate anchored in the center: see this JSFiddle.
You might also want to experiment with other anchor and baseline properties, if this is not exactly what you want. See: SVG Spec - 10 Text - 10.9 Alignment properties
EDIT
Sorry for my original answer below...I had not yet had my first cup of coffee;)
Try adding text-anchor="end" and dy="1"
IGNORE BELOW
Try the following:
Text rotate attribute is an object that specifies or receives a list of individual character rotation values. The last rotation value rotates all following characters.
myText.setAttribute("rotate","deg1, deg2, deg3, deg4, ...")
rotationNumbs=myText.getAttribute("rotate")
~ or ~
[ NumberList = ] myText.rotate [ = NumberList ]

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

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.

Categories