SVG : How to convert meters to points ratio - svg

I have a building floor map in SVG whith these size attributes:
width="594.75pt" height="841.5pt"
The size of the map, is in meters : 40x52.
What is the correct way to convert meters to points ?
Here is what I've tried so far :
XmlTextReader reader = new XmlTextReader(pathToSvg);
while (reader.Read() && string.IsNullOrEmpty(width) && string.IsNullOrEmpty(height))
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
if (reader.Name == #"svg")
{
width = reader.GetAttribute(#"width");
height = reader.GetAttribute(#"height");
}
break;
}
}
// Remove pt from width and height strings
width = width.Replace("pt", string.Empty);
height = height.Replace("pt", string.Empty);
// Convert to double values
double widthInPoint = double.Parse(width, CultureInfo.InvariantCulture);
double heightInPoint = double.Parse(height, CultureInfo.InvariantCulture);
// compute the ratio meters/points in both dimensions <=== Is this section right ??
// 594.75pt => 40 meters
// 1pt => X meters
double ratioX = mapHorizontalMeterSize / widthInPoint;
double ratioY = mapVerticalMeterSize / heightInPoint;
// Compute the Beacon position in points
double radiusInPoint = Math.Round(radius / ratioX, 2);
double beaconXPositionOnMapInPt = Math.Round((customMapX / ratioX) - (radius / ratioX), 2);
double yPos = Math.Round((customMapY / ratioY) - (radius / ratioY), 2);
// SVG positioning is top left corner by default, we are bottom left (originCornerId == 0)
double beaconYPositionOnMapInPt = originCornerId == 0 ? heightInPoint - yPos : yPos;
string tmpPath;
var reaaderSettings = new XmlReaderSettings();
reaaderSettings.DtdProcessing = DtdProcessing.Parse;
using (var svgReader = XmlReader.Create(path, reaaderSettings))
{
XDocument doc = XDocument.Load(svgReader);
var xmlns = doc.Root.GetDefaultNamespace();
var xlinkns = doc.Root.GetNamespaceOfPrefix("xlink");
// Add the circle
doc.Root.Add(new XElement(xmlns + "circle",
new XAttribute("stroke", "blue"),
new XAttribute("stroke-width", "3"),
new XAttribute("cx", $"{beaconXPositionOnMapInPt.ToString(CultureInfo.InvariantCulture)}pt"),
new XAttribute("cy", $"{beaconYPositionOnMapInPt.ToString(CultureInfo.InvariantCulture)}pt"),
new XAttribute("r", $"{radiusInPoint.ToString(CultureInfo.InvariantCulture)}"),
new XAttribute("fill-opacity", "0.1")
));
// Add the beacon image
//XNamespace xlinkns = "https://www.w3.org/1999/xlink";
doc.Root.Add(new XElement(xmlns + "image",
new XAttribute("x", $"{beaconXPositionOnMapInPt.ToString(CultureInfo.InvariantCulture)}pt"),
new XAttribute("y", $"{yPos.ToString(CultureInfo.InvariantCulture)}pt"),
new XAttribute(xlinkns + "href", $"data:image/{iconFormat};base64,{icon}")
));
tmpPath = FileHelpers.NextAvailableFilename(path);
doc.Save(tmpPath);
}
The result is absolutely not what I'm expecting.
The svg file is almost 3Mb in size and I can't show it here.

If you need to add new elements with dimensions specified in meters you could use a conversion helper function to find the right scaling multiplier/divisor
If your svg's dimensions are 594.8 × 841.5 user units
594.8 × 841.5 pt (real life print format - A4)
containing a map that is 40×52m in real life:
Meter to point multiplier: 2834.64388 (for converting meters to points/user units)
Scaling divisor: 40*2834.64388 / 594.8
Js example
I'm adding a rectangle at x/y=20m; width/height=10m; using my helper function
m2UserUnits('20m', scaleDivisor) that will convert meter to user units.
let svg = document.querySelector('svg');
let realWidth = '40m';
let userWidth = '594.8pt';
let m2PtMultiplier = 2834.64388;
let scaleDivisor = parseFloat(realWidth)*m2PtMultiplier / parseFloat(userWidth);
//append to svg
let ns ='http://www.w3.org/2000/svg';
let rect = document.createElementNS(ns, 'rect');
rect.setAttribute('x', m2UserUnits('20m', scaleDivisor) );
rect.setAttribute('y', m2UserUnits('20m', scaleDivisor) );
rect.setAttribute('width', m2UserUnits('10m', scaleDivisor) );
rect.setAttribute('height', m2UserUnits('10m', scaleDivisor) );
svg.appendChild(rect)
let circle = document.createElementNS(ns, 'circle');
circle.setAttribute('cx', m2UserUnits('5m', scaleDivisor) );
circle.setAttribute('cy', m2UserUnits('5m', scaleDivisor) );
circle.setAttribute('r', m2UserUnits('5m', scaleDivisor) );
svg.appendChild(circle)
//unit conversion
function m2UserUnits(val, scaleDivisor){
let valNum = parseFloat(val);
if(val.indexOf('m')!==-1){
valNum *= 2834.64388;
}
return valNum/scaleDivisor;
}
svg{
border: 1px solid red;
width:40%;
}
text{
font-size:32px
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 594.8 841.5" >
<rect id="bg-A4" fill="#FFFFFF" width="594.8" height="841.5"/>
<rect id="map-40x52m" fill="#EEEEEE" width="594.8" height="773.2"/>
<text x="50%" y="90%" text-anchor="middle">Map area: 40×52m (real life)</text>
<text x="50%" y="98%" text-anchor="middle">svg viewport: 594.8 × 841.5 user units</text>
</svg>

Related

html canvas clip but with an image

I have been working with html canvas compositing trying to clip a pattern with a mask.
The main issue that I have is that the mask I have comes from an svg with transparencies within the outer most border. I want the entire inside from the outer most border to be filled with the pattern.
Take this SVG for example you can see that there is a single pixel border, then some transparency, and then an opaque red inner blob. The compositing I have done works as the documentation says it should, the single pixel border and the red inner portion pick up the pattern that I want to mask into this shape. The problem is that I want to mask the entire innards starting from the single pixel border.
This is where I think clip might help. But it seems clip only works with manually drawn paths, not paths from an svg (at least that I am aware of).
Is there a way to accomplish what I am trying to do?
Regards,
James
The Path2D constructor accepts an SVG path data argument, that it will parse as the d attribute of an SVG <path> element.
You can then use this Path2D object with the clip() method:
(async () => {
// fetch the svg's path-data
const markup = await fetch("https://upload.wikimedia.org/wikipedia/commons/7/76/Autism_spectrum_infinity_awareness_symbol.svg").then(resp => resp.ok && resp.text());
const doc = new DOMParser().parseFromString(markup, "image/svg+xml");
const pathData = doc.querySelector("[d]").getAttribute("d");
// build our Path2D object and use it
const path = new Path2D(pathData);
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.clip(path);
// draw something that will get clipped
const rad = 30;
for(let y = 0; y < canvas.height; y += rad * 2 ) {
for(let x = 0; x < canvas.width; x += rad * 2 ) {
ctx.moveTo(x+rad, y);
ctx.arc(x, y, rad, 0, Math.PI*2);
}
}
ctx.fillStyle = "red";
ctx.fill();
})().catch(console.error);
<canvas width="792" height="612"></canvas>
If you need to transform this path-data (e.g scale, or rotate), then you can create a second Path2D object, and use its .addPath(path, matrix) method to do so:
// same as above, but smaller
(async () => {
const markup = await fetch("https://upload.wikimedia.org/wikipedia/commons/7/76/Autism_spectrum_infinity_awareness_symbol.svg").then(resp => resp.ok && resp.text());
const doc = new DOMParser().parseFromString(markup, "image/svg+xml");
const pathData = doc.querySelector("[d]").getAttribute("d");
const originalPath = new Path2D(pathData);
const path = new Path2D();
// scale by 0.5
path.addPath(originalPath, { a: 0.5, d: 0.5 });
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.clip(path);
// draw something that will get clipped
const rad = 15;
for(let y = 0; y < canvas.height; y += rad * 2 ) {
for(let x = 0; x < canvas.width; x += rad * 2 ) {
ctx.moveTo(x+rad, y);
ctx.arc(x, y, rad, 0, Math.PI*2);
}
}
ctx.fillStyle = "red";
ctx.fill();
})().catch(console.error);
<canvas width="396" height="306"></canvas>

How to add non linear curve between any two points of area chart in d3.js

I recently started with d3.js.I am working on a stacked area chart in d3 which looks similar to the below chart,
const stack = d3.stack().keys(["aData", "bData"]);
const stackedValues = stack(data);
const stackedData = [];
stackedValues.forEach((layer, index) => {
const currentStack = [];
layer.forEach((d, i) => {
currentStack.push({
values: d,
year: data[i].year
});
});
stackedData.push(currentStack);
});
const yScale = d3
.scaleLinear()
.range([height, 0])
.domain([0, d3.max(stackedValues[stackedValues.length - 1], dp => dp[1])]);
const xScale = d3
.scaleLinear()
.range([0, width])
.domain(d3.extent(data, dataPoint => dataPoint.year));
const area = d3
.area()
.x(dataPoint => xScale(dataPoint.year))
.y0(dataPoint => yScale(dataPoint.values[0]))
.y1(dataPoint => yScale(dataPoint.values[1]));
const series = grp
.selectAll(".series")
.data(stackedData)
.enter()
.append("g")
.attr("class", "series");
series
.append("path")
.attr("transform", `translate(${margin.left},0)`)
.style("fill", (d, i) => color[i])
.attr("stroke", "steelblue")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", strokeWidth)
.attr("d", d => area(d));
I have a requirement to be able to add non linear curve between any two points. I have made a very basic outline chart just to explain my point.
I tried using the curve function but it changes the whole line to the provided curve (here is the example code https://codepen.io/saif_shaik/pen/VwmqxMR), I just need to add a non linear curve between two points. is there any way to achieve this?
I simplied your path by removing precision in: https://yqnn.github.io/svg-path-editor/
You can use that editor to play with d-path, and learn where/how you want to change that d-path String.
Copy the d-path below and paste it in: https://yqnn.github.io/svg-path-editor/
<svg height="300" width="600"><g transform="translate(30,0)">
<g transform="translate(-28.5,-90)">
<g class="series">
<path stroke="steelblue" stroke-linejoin="round"
stroke-linecap="round" stroke-width="1.5"
d="M0 257 15 250C30 242 61 227 91 216C122 205 152 197 182 199C213 200 243 211 274 208
C304 205 334 188 365 169C395 151 425 129 456 116C486 102 517 96 532 93
L547 90 547 280 532 280C517 280 486 280 456 280C425 280 395 280 365 280
C334 280 304 280 273 280C243 280 213 280 182 280C152 280 122 280 91 280
C61 280 30 280 15 280L0 280Z"
style="fill: lightgreen;">
</path></g></g></g></svg>
You could create a custom curve generator. This could take a number of different forms. I'll recycle a previous example by tweaking one of the existing d3 curves and using its point method to create a custom curve.
Normally a custom curve applies the same curve between all points, to allow different types of lines to connect points, I'll keep track of the current point's index in the snippet below.
The custom curve in the snippet below is returned by a parent function that takes an index value. This index value indicates which data point should use a different curve between it and the next data point. The two types of curves are hand crafted - some types curves will present more challenges than others.
This produces a result such as:
function generator(i,context) {
var index = -1;
return function(context) {
var custom = d3.curveLinear(context);
custom._context = context;
custom.point = function(x,y) {
x = +x, y = +y;
index++;
switch (this._point) {
case 0: this._point = 1;
this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y);
this.x0 = x; this.y0 = y;
break;
case 1: this._point = 2;
default:
// curvy mountains between values if index isn't specified:
if(index != i+1) {
var x1 = this.x0 * 0.5 + x * 0.5;
var y1 = this.y0 * 0.5 + y * 0.5;
var m = 1/(y1 - y)/(x1 - x);
var r = -100; // offset of mid point.
var k = r / Math.sqrt(1 + (m*m) );
if (m == Infinity) {
y1 += r;
}
else {
y1 += k;
x1 += m*k;
}
this._context.quadraticCurveTo(x1,y1,x,y);
// always update x and y values for next segment:
this.x0 = x; this.y0 = y;
break;
}
// straight lines if index matches:
else {
// the simplest line possible:
this._context.lineTo(x,y);
this.x0 = x; this.y0 = y;
break;
}
}
}
return custom;
}
}
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 300);
var data = d3.range(10).map(function(d) {
var x = d*40+40;
var y = Math.random() * 200 + 50;
return { x:x, y:y }
})
var line = d3.line()
.curve(generator(3)) // striaght line between index 3 and 4.
.x(d=>d.x)
.y(d=>d.y)
svg.append("path")
.datum(data)
.attr("d",line)
.style("fill","none")
.style("stroke-width",3)
.style("stroke","#aaa")
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx",d=>d.x)
.attr("cy",d=>d.y)
.attr("r", 2)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
This line will work with canvas as well if you specify a context as the second argument of generator(). There are all sorts of refinements that could be made here - the basic principle should be fairly adaptable however.
Just change the curve type. The curveBasis approximates a curve between points but it doesn't cross them. So use either the usually used "curveCardinal" type or maybe even "curveCatmullRom" that are curves passing the data points.
// Fake data
const data = [
{
year: 2000,
aData: 50,
bData: 300
},
{
year: 2001,
aData: 150,
bData: 50
},
{
year: 2002,
aData: 200,
bData: 100
},
{
year: 2003,
aData: 130,
bData: 50
},
{
year: 2004,
aData: 240,
bData: 80
},
{
year: 2005,
aData: 380,
bData: 10
},
{
year: 2006,
aData: 420,
bData: 200
}
];
const color = ["lightgreen", "lightblue"];
// Create SVG and padding for the chart
const svg = d3
.select("#chart")
.append("svg")
.attr("height", 300)
.attr("width", 600);
const strokeWidth = 1.5;
const margin = { top: 0, bottom: 20, left: 30, right: 20 };
const chart = svg.append("g").attr("transform", `translate(${margin.left},0)`);
const width = +svg.attr("width") - margin.left - margin.right - strokeWidth * 2;
const height = +svg.attr("height") - margin.top - margin.bottom;
const grp = chart
.append("g")
.attr("transform", `translate(-${margin.left - strokeWidth},-${margin.top})`);
// Create stack
const stack = d3.stack().keys(["aData", "bData"]);
const stackedValues = stack(data);
const stackedData = [];
// Copy the stack offsets back into the data.
stackedValues.forEach((layer, index) => {
const currentStack = [];
layer.forEach((d, i) => {
currentStack.push({
values: d,
year: data[i].year
});
});
stackedData.push(currentStack);
});
// Create scales
const yScale = d3
.scaleLinear()
.range([height, 0])
.domain([0, d3.max(stackedValues[stackedValues.length - 1], dp => dp[1])]);
const xScale = d3
.scaleLinear()
.range([0, width])
.domain(d3.extent(data, dataPoint => dataPoint.year));
const area = d3
.area()
.x(dataPoint => xScale(dataPoint.year))
.y0(dataPoint => yScale(dataPoint.values[0]))
.y1(dataPoint => yScale(dataPoint.values[1]))
//.curve(d3.curveBasis)
.curve(d3.curveCardinal)
//.curve(d3.curveCatmullRom.alpha(0.5))
;
const series = grp
.selectAll(".series")
.data(stackedData)
.enter()
.append("g")
.attr("class", "series");
series
.append("path")
.attr("transform", `translate(${margin.left},0)`)
.style("fill", (d, i) => color[i])
.attr("stroke", "steelblue")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", strokeWidth)
.attr("d", d => area(d));
const dotsGreen = chart
.selectAll(".gdot")
.data(data)
.enter()
.append("circle")
.attr("class", "gdot")
.attr("cx", function(d) {
return xScale(d.year)
})
.attr("cy", d => yScale(d.aData))
.attr("r", 4)
.attr("fill", "green");
const dotsBlue = chart
.selectAll(".bdot")
.data(data)
.enter()
.append("circle")
.attr("class", "bdot")
.attr("cx", function(d) {
return xScale(d.year)
})
.attr("cy", d => yScale(d.aData+d.bData))
.attr("r", 4)
.attr("fill", "blue");
// Add the X Axis
chart
.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale).ticks(data.length));
// Add the Y Axis
chart
.append("g")
.attr("transform", `translate(0, 0)`)
.call(d3.axisLeft(yScale));
#chart {
text-align: center;
margin-top: 40px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="chart"></div>
The working codePen forked from your code is also here:
changed CodePen pen
Note: I've added the code for two circle groups (not SVG G element) that you can simply remove. They just serve for a proof where curves are drawn near data points based on curve type scripted.
Your sketched chart looks like you want to have a curve only between two given points. For that you would have to change the curve call to use the running index (e.g. (d,i)) in a function that would return different curve type based on chosen index (or indeces).
ADDED: you can play with different D3.js curve types here:
D3 curve explorer

Transform <g> attribute on map of d3.js

I have prepared one example of map in d3.js. I wanted to implement zoom on map with and node(contains circle, smiley and text. as of now i putted circle and smiley) on map shows the city of different countries. When i zoom over map i could not able to transform the tag so smiley got misplace as per my logic. so how to transform only g tags on map. I don't want to transform shape(circle, images) inside tag.
My Jsfiddle link
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll(".node")
.attr("width", function(){
var self = d3.select(this);
var r = 28 / d3.event.scale; // set radius according to scale
self.style("stroke-width", r < 4 ? (r < 2 ? 0.5 : 1) : 2); // scale stroke-width
return r;
});
g.selectAll(".circle")
.attr("r", function(){
var self = d3.select(this);
var r = 8 / d3.event.scale; // set radius according to scale
self.style("stroke-width", r < 4 ? (r < 2 ? 0.5 : 1) : 2); // scale stroke-width
return r;
});
});
Please anybody help me to solve my issue.
To do semantic zooming, one would need to adjust the width and height of the smiley faces as well as adjust the x and y locations since the adjustments will change relative to the width/height:
g.selectAll(".node")
.attr("x", function(d) { return (projection([d.lon, d.lat])[0]) - (8 / d3.event.scale); })
.attr('y', function(d) { return (projection([d.lon, d.lat])[1]) - (8 / d3.event.scale); })
.attr('width', function () { return 20 / d3.event.scale; })
.attr('height', function () { return 20 / d3.event.scale; })
Demo: http://jsfiddle.net/ktee4dLp/

How to keep border width even when object is expanded

My objective in this jsfiddle is to have a Fabric object (for example, a rect) that will "visually" keep its original border width even when it is scaled/expanded. For example, let's say that strokeWidth = 3px; usually when a rect is expanded the user will see the border wider. In my case I need the border to keep its width in screen pixels.
To achieve that, I calculate a factor factorHeight = origHeight / obj.getHeight() every time the object is scaled, and multiply strokeWidth by this factor. This works well when the object is scaled proportionally (try it in the jsfiddle).
However, when the object is not scaled proportionally, the problem is that the top/bottom border widths are different from the right/left border widths. Since there's a single strokeWidth for all these borders I cannot change this behavior. Any ideas?
Javascript:
var canvas = new fabric.Canvas('c' );
var origWidth = 40;
var origHeight = 100;
var origStrokeWidth = 3;
var rect = new fabric.Rect({
top: 30,
left: 30,
width: origWidth,
height: origHeight,
fill: 'rgba(255,255,255,0)',
strokeWidth: origStrokeWidth,
stroke: '#000000'
});
canvas.add(rect);
canvas.on('object:scaling', onObjectScaled);
function onObjectScaled(e) {
var obj = e.target;
var factorHeight = origHeight / obj.getHeight();
var factorWidth = origWidth / obj.getWidth();
var factor;
if (factorHeight < factorWidth)
factor = factorHeight;
else
factor = factorWidth;
var newStrokeWidth = origStrokeWidth * factor;
obj.setStrokeWidth(newStrokeWidth);
canvas.renderAll();
}
I had the same problem, this is the solution I found somewhere here on SO, but I cant provide you with the link to question, but here is the code that solved it:
canvas.on({
'object:scaling': function(e) {
var obj = e.target;
obj.KeepStrokeWidth = 5;
if(obj.KeepStrokeWidth){
var newStrokeWidth = obj.KeepStrokeWidth / ((obj.scaleX + obj.scaleY) / 2);
obj.set('strokeWidth',newStrokeWidth);
}
}
});
Hope it will work for you too.

Trimming text to a given pixel width in SVG

I'm drawing text labels in SVG. I have a fixed width available (say 200px). When the text is too long, how can I trim it ?
The ideal solution would also add ellipsis (...) where the text is cut. But I can also live without it.
Using d3 library
a wrapper function for overflowing text:
function wrap() {
var self = d3.select(this),
textLength = self.node().getComputedTextLength(),
text = self.text();
while (textLength > (width - 2 * padding) && text.length > 0) {
text = text.slice(0, -1);
self.text(text + '...');
textLength = self.node().getComputedTextLength();
}
}
usage:
text.append('tspan').text(function(d) { return d.name; }).each(wrap);
One way to do this is to use a textPath element, since all characters that fall off the path will be clipped away automatically. See the text-path examples from the SVG testsuite.
Another way is to use CSS3 text-overflow on svg text elements, an example here. Opera 11 supports that, but you'll likely find that the other browsers support it only on html elements at this time.
You can also measure the text strings and insert the ellipsis yourself with script, I'd suggest using the getSubStringLength method on the text element, increasing the nchars parameter until you find a length that is suitable.
Implementing Erik's 3rd suggestion I came up with something like this:
//places textString in textObj, adds an ellipsis if text can't fit in width
function placeTextWithEllipsis(textObj,textString,width){
textObj.textContent=textString;
//ellipsis is needed
if (textObj.getSubStringLength(0,textString.length)>=width){
for (var x=textString.length-3;x>0;x-=3){
if (textObj.getSubStringLength(0,x)<=width){
textObj.textContent=textString.substring(0,x)+"...";
return;
}
}
textObj.textContent="..."; //can't place at all
}
}
Seems to do the trick :)
#user2846569 show me how to do it ( yes, using d3.js ). But, I have to make some little changes to work:
function wrap( d ) {
var self = d3.select(this),
textLength = self.node().getComputedTextLength(),
text = self.text();
while ( ( textLength > self.attr('width') )&& text.length > 0) {
text = text.slice(0, -1);
self.text(text + '...');
textLength = self.node().getComputedTextLength();
}
}
svg.append('text')
.append('tspan')
.text(function(d) { return d; })
.attr('width', 200 )
.each( wrap );
The linearGradient element can be used to produce a pure SVG solution. This example fades out the truncated text (no ellipsis):
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="0" x2="200" y1="0" y2="0" id="truncateText">
<stop offset="90%" stop-opacity="1" />
<stop offset="100%" stop-opacity="0" />
</linearGradient>
<linearGradient id="truncateLegendText0" gradientTransform="translate(0)" xlink:href="#truncateText" />
<linearGradient id="truncateLegendText1" gradientTransform="translate(200)" xlink:href="#truncateText" />
</defs>
<text fill="url(#truncateLegendText0)" font-size="50" x="0" y="50">0123456789</text>
<text fill="url(#truncateLegendText1)" font-size="50" x="200" y="150">0123456789</text>
</svg>
(I had to use linear gradients to solve this because the SVG renderer I was using does not support the textPath solution.)
Try this one, I use this function in my chart library:
function textEllipsis(el, text, width) {
if (typeof el.getSubStringLength !== "undefined") {
el.textContent = text;
var len = text.length;
while (el.getSubStringLength(0, len--) > width) {}
el.textContent = text.slice(0, len) + "...";
} else if (typeof el.getComputedTextLength !== "undefined") {
while (el.getComputedTextLength() > width) {
text = text.slice(0,-1);
el.textContent = text + "...";
}
} else {
// the last fallback
while (el.getBBox().width > width) {
text = text.slice(0,-1);
// we need to update the textContent to update the boundary width
el.textContent = text + "...";
}
}
}
There is several variants using d3 and loops for search smaller text that fit. This can be achieved without loops and it work faster. textNode - d3 node.
clipText(textNode, maxWidth, postfix) {
const textWidth = textNode.getComputedTextLength();
if (textWidth > maxWidth) {
let text = textNode.textContent;
const newLength = Math.round(text.length * (1 - (textWidth - maxWidth) / textWidth));
text = text.substring(0, newLength);
textNode.textContent = text.trim() + postfix;
}
}
My approach was similar to OpherV's, but I tried doing this using JQuery
function getWidthOfText(text, fontSize, fontFamily) {
var span = $('<span></span>');
span.css({
'font-family': fontFamily,
'font-size' : fontSize
}).text(text);
$('body').append(span);
var w = span.width();
span.remove();
return w;
}
function getStringForSize(text, size, fontSize, fontFamily) {
var curSize = getWidthOfText(text, fontSize, fontFamily);
if(curSize > size)
{
var curText = text.substring(0,text.length-5) + '...';
return getStringForSize(curText, size, fontSize, fontFamily);
}
else
{
return text;
}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Now when calling getStringForSize('asdfasdfasdfasdfasdfasdf', 110, '13px','OpenSans-Light') you'll get "asdfasdfasdfasd..."

Resources