I am trying to figure out why getBBox() for tspan element of a svg does not return the dimension.
To demonstrate this with an example, if I run BBox on both tsp1 and rect1, it returns the correct dimension for rect1 but not for tsp1
var tsp = document.getElementById('tsp1');
var tspBBox = tsp.getBBox();
var rect = document.getElementById('rect1');
var rectBBox = rect.getBBox();
console.log(tspBBox);
console.log(rectBBox);
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<text class="t1" id="t1" font-size="20" font-family="PT Mono" text-decoration="underline">
<tspan class="tsp1" id="tsp1" x="10.23" y="135.05">Abc ef ghi</tspan>
</text>
<rect class="rect1" id="rect1" x="9.23" y="112.73368530273439" height="31.546314697265625" width="1" fill="orange" />
</svg>
I was expecting BBox to return the exact x and y for tsp1 but it does not.
I don't know why. I need to pass on the exact values to the succeeding class dynamically.
How can javascript return the exact dimension for the tspan element?
There are a number of methods for measuring text, and they are a bit more complex than defining a simple box. This is because with the dx, dy and rotate attributes, each addressable character can be be positioned individually - moved and rotated in every direction. Therefore, it makes more sense to answer the question where a single character is positioned, and where, after completing one sequence, the next character would be positioned.
In your case none of the above attributes are set ( on the <tspan> or <text> element). In this case is is possible to retrieve the start position of the <tspan> with .getStartPositionOfChar(0) and the horizontal width with .getComputedTextLength().* The height according to the font metrics is the same for all characters in the tspan, so it is enough to return one .getExtentOfChar(0) - 0 refers to the first character within the sequence of addressable characters.
As chrwahl pointed out in his answer, the start position refers to the font-specific baseline and normally will not be identical to the top left corner of a bounding box.
*There is a subtle trick here: if the letter-spacing or word-spacing CSS properties were defined, the "length" returned would not only return the width from the start of the first character to the end of the last, but also would add (or subtract) a spacing value that is defined after the end of the string. In other words: despite its name, the method returns the relative horizontal start position of the next character after the string examined.
var tsp = document.getElementById('tsp1');
var tspPos = tsp.getStartPositionOfChar(0);
console.log('start position', tspPos.x, tspPos.y);
console.log('horizontal advance', tsp.getComputedTextLength());
console.log('vertical extent', tsp.getExtentOfChar(0).height);
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<text class="t1" id="t1" font-size="20" font-family="PT Mono" text-decoration="underline">
<tspan class="tsp1" id="tsp1" x="10.23" y="135.05">Abc ef ghi</tspan>
</text>
</svg>
It is all about the dominant-baseline. So, there is a differences between where the text is placed according to the dominant-baseline and the box that the text takes up. The value text-before-edge will place the text according to the upper left corner of the box.
var tsp = document.getElementById('tsp1');
var tspBBox = tsp.getBBox();
var rect = document.getElementById('rect1');
var rectBBox = rect.getBBox();
console.log('tspBBox', tspBBox.x, tspBBox.y);
console.log('rectBBox', rectBBox.x, rectBBox.y);
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 100 400 200">
<text class="t1" id="t1" font-size="20" font-family="PT Mono"
text-decoration="underline" dominant-baseline="text-before-edge">
<tspan class="tsp1" id="tsp1" x="10.23" y="135.05">Abc ef ghi</tspan>
</text>
<rect class="rect1" id="rect1" x="9.23" y="112.73368530273439" height="31.546314697265625" width="1" fill="orange" />
</svg>
Related
Helo I have a problem. I am using npm package which provides pack of weather icons. The problem is the SVG box is big and it breaks positioning that is a lot of white space on website, it does not look nice. Is negative margin the way>
import {wiCloudy} from 'weather-icons-react'
const WeatherDataBox = () => {
return (
icon=<WiCloudy size={256} />
)}
<svg stroke="currentColor" fill="currentColor"
stroke-width="0" viewBox="0 0 30 30"
attr="[object Object]" size="256" height="256" width="256">
<path>*long code*</path>
</svg>
I changed viewbox and it reduced empty background
let ICON_SIZE = 256
let VIEWBOX = "5 5 20 20"
<WiCloudy viewBox={VIEWBOX} size={ICON_SIZE} />
I have an SVG exported from Adobe Illustrator with several paths like this, which produces a small polygon I intend to use as a text box
<svg viewbox="387 390 74 20">
<g>
<path class="st37" d="M452,408h-56c-4.42,0-8-3.58-8-8l0,0c0-4.42,3.58-8,8-8h56c4.42,0,8,3.58,8,8l0,0 C460,404.42,456.42,408,452,408z" />
</g>
</svg>
I'd like to dynamically add text to it. I've seen many similar questions here, but most of them involed specifying a x and y property for a text element based on the x and y property the path element. My path, however, does not have such properties.
I've tried to use a textPath element with xlink:href pointing to my path. I gets attached to the path, but the text wraps my path element instead of being inside it.
So, is there a way to achieve this? I'm open to different solutions here. My dream would be to use javascript to get the path element and add the text from there. But I could also edit the base SVG file to add a text or any other relevant element and attributes to make this work, as long as I can change the text dynamically from javascript later. And since this SVG is produced by Illustrator, I could also try different export options there in order to get a proper output for my goal.
Here's some sample code that takes a label path and adds a <text> element after it with whatever text you choose.
let label1 = document.querySelector("#label1");
addLabelText(label1, "Something");
function addLabelText(bgPath, labelText)
{
let bbox = bgPath.getBBox();
let x = bbox.x + bbox.width / 2;
let y = bbox.y + bbox.height / 2;
// Create a <text> element
let textElem = document.createElementNS(bgPath.namespaceURI, "text");
textElem.setAttribute("x", x);
textElem.setAttribute("y", y);
// Centre text horizontally at x,y
textElem.setAttribute("text-anchor", "middle");
// Give it a class that will determine the text size, colour, etc
textElem.classList.add("label-text");
// Set the text
textElem.textContent = labelText;
// Add this text element directly after the label background path
bgPath.after(textElem);
}
.st37 {
fill: linen;
}
.label-text {
font-size: 10px;
fill: rebeccapurple;
transform: translate(0, 3px); /* adjust vertical position to centre text */
}
<svg viewbox="387 390 74 20">
<g>
<path id="label1" class="st37" d="M452,408h-56c-4.42,0-8-3.58-8-8l0,0c0-4.42,3.58-8,8-8h56c4.42,0,8,3.58,8,8l0,0 C460,404.42,456.42,408,452,408z" />
</g>
</svg>
Since you can edit your base SVG align
create a proper SVG to work with
Your path is a background label starting (red circle) at a large offset x=452 y=408
I would recreate it,
starting at the green circle, (editor: https://yqnn.github.io/svg-path-editor/)
in a viewBox="0 0 80 20"
To get single coordinates for both backgroundlabel and (blue) textPath line
after that use JavaScript to add text dynamically.
No need for text x,y calculations, pathLength and startoffset do the work
Or if you go fancy you can create the blue line dynamically from getBBox()
That will also work with your current path; just more calculations required
It is all about coordinates,
and positioning the blue line (with stroke="transparent" then):
playground:
<svg viewbox="387 390 74 20">
<path fill="lightgreen" d="M452,408h-56c-4.42,0-8-3.58-8-8l0,0c0-4.42,3.58-8,8-8h56c4.42,0,8,3.58,8,8l0,0C460,404.42,456.42,408,452,408z" />
<circle cx="452" cy="408" r="2" fill="red"/>
<circle cx="388" cy="400" r="2" fill="green"/>
<path id="P" pathLength="100" d="M388 400h72" stroke="blue"/>
<text>
<textPath href="#P" startoffset="50" text-anchor="middle" dominant-baseline="middle"
fill="green" font-size="14px">My Text</textPath>
</text>
</svg>
Thanks for the answers! I end up using a tweaked version of Paul LeBeau's function to take into account the structure suggested by Danny '365CSI' Engelman so I don't have to use translate to center the text vertically.
let label = document.querySelector("#mylabel");
addLabelTextPath(label, "Something");
function addLabelTextPath(bgPath, labelText) {
let bbox = bgPath.getBBox();
let x = bbox.x + bbox.width / 2;
let y = bbox.y + bbox.height / 2;
// Create the path line
let pathElem = document.createElementNS(bgPath.namespaceURI, "path");
pathElem.setAttribute("pathLength", 100);
pathElem.setAttribute("d", `M${bbox.x} ${y}h${bbox.width}`);
pathElem.id = `baseline-${bgPath.id}`;
// Create a <text> element
let textElem = document.createElementNS(bgPath.namespaceURI, "text");
// Create a <textPath> element
let textPath = document.createElementNS(bgPath.namespaceURI, "textPath");
textPath.setAttribute("href", `#${pathElem.id}`);
//Center text
textPath.setAttribute("dominant-baseline", "Middle");
textPath.setAttribute("startOffset", 50);
textPath.setAttribute("text-anchor", "middle");
// Give it a class that will determine the text size, colour, etc
textPath.classList.add("label-text");
// Set the text
textPath.textContent = labelText;
// Add the elements directly after the label background path
bgPath.after(pathElem);
pathElem.after(textElem);
textElem.appendChild(textPath);
}
.st37 {
fill: lightblue;
}
.label-text {
font-size: 10px;
fill: darkblue;
}
<svg viewbox="387 390 74 20">
<g>
<path id="mylabel" class="st37" d="M452,408h-56c-4.42,0-8-3.58-8-8l0,0c0-4.42,3.58-8,8-8h56c4.42,0,8,3.58,8,8l0,0 C460,404.42,456.42,408,452,408z" />
</g>
</svg>
I'm trying to place a text onto a path with svg.js
Here is my fiddle:
https://jsfiddle.net/Byteschmiede/ytz67egn/1/
var draw = SVG().addTo('body').size(500, 500)
draw.svg(`<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="100%" height="100%" viewBox="0 0 4725 2363" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path id="text1326" d="M2362.2,952.109C2548.36,773.615 2727.44,622.128 2899.46,497.647C3071.47,370.823 3216.39,286.272 3334.21,243.993C3452.02,201.721 3580.45,180.583 3719.47,180.58C4011.66,180.583 4247.3,278.051 4426.39,472.986C4607.82,665.577 4698.54,899.267 4698.55,1174.06C4698.54,1361.95 4658.48,1535.75 4578.37,1695.46C4498.25,1855.16 4383.97,1976.12 4235.52,2058.32C4089.42,2140.53 3920.94,2181.63 3730.08,2181.62C3482.66,2181.63 3265.87,2128.78 3079.72,2023.09C2895.92,1917.4 2656.75,1717.77 2362.2,1424.19C2055.87,1727.16 1811.99,1929.15 1630.55,2030.14C1449.11,2131.13 1238.21,2181.63 997.866,2181.62C691.537,2181.63 452.365,2086.51 280.351,1896.27C110.692,1706.03 25.862,1465.29 25.863,1174.06C25.862,901.616 115.404,667.926 294.489,472.986C475.929,278.051 712.744,180.583 1004.94,180.58C1146.32,180.583 1275.92,201.721 1393.74,243.993C1511.55,286.272 1655.29,370.823 1824.95,497.647C1996.97,622.128 2176.05,773.615 2362.2,952.109M2591.95,1170.53C2841.72,1417.14 3046.73,1585.07 3206.96,1674.32C3369.55,1761.22 3530.96,1804.67 3691.2,1804.67C3891.49,1804.67 4048.18,1745.95 4161.29,1628.52C4274.4,1508.74 4330.95,1364.3 4330.95,1195.19C4330.95,1009.65 4274.4,856.992 4161.29,737.208C4050.54,615.082 3903.27,554.017 3719.47,554.014C3615.79,554.017 3515.64,572.806 3419.04,610.382C3322.42,645.614 3206.96,709.027 3072.65,800.622C2938.33,889.873 2778.1,1013.18 2591.95,1170.53M2132.46,1170.53C1962.8,1027.27 1810.81,909.836 1676.5,818.237C1542.19,724.294 1424.37,657.357 1323.05,617.428C1221.72,577.503 1110.97,557.54 990.797,557.537C818.78,557.54 676.22,617.43 563.115,737.208C450.009,856.992 393.456,1009.65 393.457,1195.19C393.456,1324.37 422.911,1433.58 481.82,1522.83C540.729,1612.08 612.598,1681.37 697.428,1730.68C784.613,1780.01 893.006,1804.67 1022.61,1804.67C1192.27,1804.67 1357.21,1760.05 1517.45,1670.8C1677.68,1581.55 1882.68,1414.79 2132.46,1170.53" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:1px;"/>
<path id="textCurve3" d="M294.489,472.986C475.929,278.051 712.744,180.583 1004.94,180.58C1146.32,180.583 1275.92,201.721 1393.74,243.993C1511.55,286.272 1655.29,370.823 1824.95,497.647" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:1px;"/>
<path id="textCurve2" d="M2899.46,497.647C3071.47,370.823 3216.39,286.272 3334.21,243.993C3452.02,201.721 3580.45,180.583 3719.47,180.58C4011.66,180.583 4247.3,278.051 4426.39,472.986" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:1px;"/>
<path id="textCurve1" d="M564.216,1310.73C609.732,1475.4 770.154,1619.22 909.116,1635.49C993.881,1645.42 1152.78,1644.6 1262.96,1605.82C1479.72,1529.53 1626.78,1408.31 1802.89,1258.49" style="fill:none;fill-rule:nonzero;stroke:rgb(177,0,52);stroke-width:1px;"/>
</svg>
`)
let textCurve1 = draw.find("#textCurve1")[0]
let textPath = textCurve1.text("Forever").font({
size: textCurve1.height()
})
.attr({
startOffset: '50%',
'text-anchor': 'middle'
})
.fill({
opacity: 0
}).stroke({
color: "#000",
width: 1
})
The red line is the target path.
The Text needs to follow the path.
As you can see, the text curve is correct but the placement is way to low on the y axis.
I'm not sure how to solve this, since there aren't much opportunities.
I tried attributes like dominant-baseline and baseline-alignment but nothing workend
Here is a picture of the svg
You can use a tspan with a dx attribute to move specific text up or down.
const text = draw.text((text) => {
text.tspan('Some Text').dx(10)
}).path(textCurve1)
It is not uncommon to require text elements in the SVG we are manipulating via d3 e.g. categorical tick labels. This is somewhat unfortunate as the <text> element in SVG is not the best... The size of fonts rendered is often slightly larger than that of how much one thinks the font should take. For example, if choosing a mono-space font with a width / height ratio of 0.6 (e.g. if font size is 12px then the width of a character should be 7.2px), the element's computed bounding rectangle might be 14.2px by n*8px where n is the number of characters.
Further complicating the issue is the fact that more often than not, people use fonts which are not monospaced.
It is easy enough to truncate a string which is "too long" by
string.slice(0, numChars-3)+'...'
but knowing the correct number of characters to fit within a fixed width seems non trivial.
function truncateText(t, text, space) {
// make sure it is a string
text = String(text)
// set text
t.text(text)
// get space it takes up
var rect = t.node().getBoundingClientRect()
while (Math.max(rect.width, rect.height) > space) {
text = text.slice(0, text.length - 1)
t.text(text + '...')
rect = t.node().getBoundingClientRect()
if (text.length == 0) break
}
}
the above function takes a d3.selection, the text and the space in which the text should fit in. By constantly manipulating the DOM, we can get perfect fit, however this is computationally very expensive.
To clarify, to fit text in a fix space, I mean that if I have a string var string = "this is my very long string", I want the direction of the string being rendered (left to right, i.e. we are looking at string length) to fit within a fixed space (e.g. var fixedSpace = 100 //px)
The above truncate text function works well for just a few strings, but if there are many strings that call this function, it gets laggy.
Of course we could optimized by just picking the longest string, calculate truncateText on that string, and take the number of characters as a result (although this is still somewhat buggy as not all characters have same width).
Is there a more efficient way to truncate text into a fixed space with d3
I agree that the approach you've suggested is computationally expensive, but it's about the only one I can think of. However if you're only running it occasionally (i.e. just on page load, rather than on mouseover), then it shouldn't be too bad, depending on how many text elements you're applying it to.
You might want to try comparing the performance of your approach to the one in this example by Mike Bostock, which uses node().getComputedTextLength() instead of node().getBoundingClientRect() and breaks up the text by word:
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
PS/ There's a CSS technique for truncating text with an ellipsis, but unfortunately it doesn't work in SVG :(
Another option would be to use a 100px wide rect as the clipPath for your text element -- something like so:
d3.selectAll("text.label")
.attr("x", function(t) {
return Math.max(0, 100-this.textLength.baseVal.value);
});
#text-box {
stroke: green;
fill: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg>
<defs>
<rect id="text-box" x="0" y="0" width="100" height="1.2em" />
<clipPath id="clip-box">
<use href="#text-box" />
</clipPath>
</defs>
<g transform="translate(0, 0)">
<use x="0" y="0" href="#text-box" />
<text x="0" y="1em" class="label" clip-path="url(#clip-box)">Long text that should be clipped</text>
</g>
<g transform="translate(0, 50)">
<use x="0" y="0" href="#text-box" />
<text x="0" y="1em" class="label" clip-path="url(#clip-box)">Another long string</text>
</g>
<g transform="translate(0, 100)">
<use x="0" y="0" href="#text-box" />
<text x="0" y="1em" class="label" clip-path="url(#clip-box)">Short text</text>
</g>
</svg>
Update: After the labels are rendered, use this function to get their lengths, and adjust the "x" attribute if the text is shorter than the clipping width:
d3.selectAll("text.label")
.attr("x", function(t) {
return Math.max(0, 100-this.textLength.baseVal.value);
});
First of all I looked at all possible related answers here but none of them seem to bring the answer I need so here I am.
Given a svg text path:
<svg viewBox="0 0 900 900"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
id="mysvg"
>
<defs>
<path id="myPath" d="M70 110 C 70 140, 110 140, 110 110" stroke="black" fill="transparent"/>
</defs>
<use xlink:href="#myPath" fill="none" stroke="red" />
<text id="names" font-family="Verdana" font-size="10" text-anchor="middle" >
<textPath xlink:href="#myPath" startOffset="50%">
My text is going to exceed at some point…
</textPath>
</text>
</svg>
At this point the text exceeds the textpath
I can't find a way to check for possible overflow through jquery. This command won't actually return undefined:
alert($("text#names").attr("textLength") );
I am trying to check for overflows in order to fit the text to the maximum length or so.
I had the same problem when adjusting font size so that the given text will be drawn with the largest possible font size without overflow. Its quite simple using plain JS.
1) Determine the np. of characters in the text element with a minimum font size:
textElement.css('font-size', 1);
var allCharCount = textElement[0].getNumberOfChars();
2) Then set font size to any value and determine the length again
var hasOverflow = allCharCount != textElement[0].getNumberOfChars();
getNumberOfChars() will only return the no. of chars drawn. If there is an overflow this number will be smaller then from the original whole string.
It looks like text.getNumberOfChars() has changed since the other answer was written, and now returns the total number of characters in the string, regardless of if they're rendered or not.
My approach to this problem is to:
Change the <textPath> element to draw on a much longer path, then calculate the text length using text.getComputedLength()
Change the <textPath> back to the original path and calculate length again
If the length on the original path is shorter than the length on the longer path, you know there's an overflow.
const textPath = document.querySelector('textPath');
const checkClipped = () => {
textPath.setAttribute('xlink:href', '#fullWidthPath');
const fullLength = textPath.getComputedTextLength();
textPath.setAttribute('xlink:href', '#myPath');
const curvedLength = textPath.getComputedTextLength();
return fullLength > curvedLength;
}
const findLongestString = () => {
const text = textPath.innerHTML;
if (checkClipped()) {
const newText = text.substring(0, text.length - 1);
textPath.innerHTML = newText;
return findLongestString(newText);
} else {
return text;
}
}
console.log(findLongestString())
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="mysvg">
<defs>
<path id="myPath" d="M70 110 C 70 140, 110 140, 110 110" stroke="black" fill="transparent"/>
<path id="fullWidthPath" d="M 0 0 L 0 10000" />
</defs>
<use xlink:href="#myPath" fill="none" stroke="red" />
<text id="names" font-family="Verdana" font-size="10" text-anchor="middle" >
<textPath xlink:href="#myPath" startOffset="50%">
My text is going to exceed at some point…
</textPath>
</text>
</svg>