How to drag svgs inserted by button click? - svg

What I'm trying to do is insert svg circles by clicking button to the workspace. Beside that, I want to free drag all those circles.
Can you help me the code?
document.getElementById('draw').addEventListener('click', function(){
document.getElementById('here').innerHTML =
'<svg height="100" width="100">' +
'<circle cx="50" cy="50" r="40" stroke="black" stroke-width="1" fill="rgba(130,130,130,0.6)">' +
'</svg>';
});
<button id="draw">Draw Circle</button>
<div id="here"></div>

I was amazed that creating an SVG like this would work, and it works! (on IE too). However it creates problems when trying to work with events. I prefer to create the SVG element and the circle element using createElementNS and use appendChild to append them to the DOM
const SVG_NS = 'http://www.w3.org/2000/svg';
const SVG_XLINK = "http://www.w3.org/1999/xlink";
/*let innerSVG = '<svg height="100" width="100">' +
'<circle cx="50" cy="50" r="40" stroke="black" stroke-width="1" fill="rgba(130,130,130,0.6)">' +
'</svg>';*/
let svgdata = {width:100,height:100}
let circledata = {cx:50,cy:50,r:40}
// creating a new SVG element using the data
let svg = newSVG(svgdata);
// creating a new circle element using the data and appending it to the svg
let circle = drawCircle(circledata, svg);
// the offset between the click point on the SVG and the left upper corner of the SVG
let offset={}
// a flag to control the dragging
let dragging = false;
// the mouse position
let m;
document.getElementById('draw').addEventListener('click', function(){
here.appendChild(svg)});
// events
here.addEventListener("mousedown",(evt)=>{
dragging = true;
offset = oMousePos(svg, evt);
})
here.addEventListener("mousemove",(evt)=>{
if(dragging){
m = oMousePos(here, evt);
svg.style.top = (m.y - offset.y)+"px";
svg.style.left = (m.x - offset.x)+"px";
}
})
here.addEventListener("mouseup",(evt)=>{
dragging = false;
})
function drawCircle(o, parent) {
var circle = document.createElementNS(SVG_NS, 'circle');
for (var name in o) {
if (o.hasOwnProperty(name)) {
circle.setAttributeNS(null, name, o[name]);
}
}
parent.appendChild(circle);
return circle;
}
function newSVG(o) {
let svg = document.createElementNS(SVG_NS, 'svg');
for (var name in o) {
if (o.hasOwnProperty(name)) {
svg.setAttributeNS(null, name, o[name]);
}
}
return svg;
}
function oMousePos(elmt, evt) {
let ClientRect = elmt.getBoundingClientRect();
return {
x: Math.round(evt.clientX - ClientRect.left),
y: Math.round(evt.clientY - ClientRect.top)
}
}
svg{border:1px solid;position:absolute;}
circle{
stroke:black;stroke-width:1;fill:rgba(130,130,130,0.6);
}
#here{width:100%; height:100vh; background-color:#efefef;margin:0; padding:0; position:relative}
<button id="draw">Draw Circle</button>
<div id="here"></div>

Related

SVG : How to convert meters to points ratio

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>

How do you load and display SVG graphics in three.js?

Consider, I have an SVG vector graphics file (logotype), which I want to load and display in three.js (with WebGL renderer).
What would be the recommended way to approach this?
It seems like I need to load the image and to create a geometry and a mesh from it.
I've managed to load the SVG document using the THREE.SVGLoader, but I can't find any relevant information on how to create a geometry/mesh from it further down the line.
function preload () {
const svgLoader = new THREE.SVGLoader();
svgLoader.load('images/logo.svg', svgDocument => {
// #todo: create a geometry/mesh from svgDocument?
// #todo: scene.add(logoMesh);
});
}
Texture
If you need svg only for texture purposes:
Render svg image to canvas
use that canvas as source of texture
use texture in your scene...
Disclaimer I'm not the author of this code, I just fixed jsfiddle that I've found
window.onload = () => {
var mesh;
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(50, 500 / 400, 0.1, 1000);
camera.position.z = 10;
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(500, 400);
document.body.appendChild(renderer.domElement);
var svg = document.getElementById("svgContainer").querySelector("svg");
var svgData = (new XMLSerializer()).serializeToString(svg);
var canvas = document.createElement("canvas");
var svgSize = svg.getBoundingClientRect();
canvas.width = svgSize.width;
canvas.height = svgSize.height;
var ctx = canvas.getContext("2d");
var img = document.createElement("img");
img.setAttribute("src", "data:image/svg+xml;base64," + window.btoa(unescape(encodeURIComponent(svgData))) );
img.onload = function() {
ctx.drawImage(img, 0, 0);
var texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
var geometry = new THREE.SphereGeometry(3, 50, 50, 0, Math.PI * 2, 0, Math.PI * 2);
var material = new THREE.MeshBasicMaterial({ map: texture });
material.map.minFilter = THREE.LinearFilter;
mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
};
var render = function () {
requestAnimationFrame(render);
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
};
render();
}
<script src="https://threejs.org/build/three.min.js"></script>
<div id="svgContainer" style="width: 222px; height: 222px;">
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg" version="1.1">
<rect width="200" height="200" fill="lime" stroke-width="4" stroke="pink" />
<circle cx="125" cy="125" r="75" fill="orange" />
<polyline points="50,150 50,200 200,200 200,100" stroke="red" stroke-width="4" fill="none" />
<line x1="50" y1="50" x2="200" y2="200" stroke="blue" stroke-width="4" />
</svg>
</div>
Mesh
If you would like to render svg as geometry I'll would suggest use of some libraries e.g. svg-mesh-3d
Example from docs of svg-mesh-3d
var loadSvg = require('load-svg')
var parsePath = require('extract-svg-path').parse
var svgMesh3d = require('svg-mesh-3d')
loadSvg('svg/logo.svg', function (err, svg) {
if (err) throw err
var svgPath = parsePath(svg)
var mesh = svgMesh3d(svgPath, {
delaunay: false,
scale: 4
})
})
Blender
Alternative options is to use blender to import svg, (optionally) tune geometry and export it to Three.js using Three.js Blender Export

YES/NO - is there a way to improve mouse dragging with pure SVG tools?

So I was spending some time playing around with pure (no external libraries) SVG elements dragging.
In general all works, but there is this nasty issue for fast moving mouse:
- when user mousedowns a draggable SVG element close to its edge
- then drags (mousemove) such draggable too fast
- the mouse "loses" the draggable
Here the issue is described in more details:
http://www.svgopen.org/2005/papers/AdvancedMouseEventModelForSVG-1/index.html#S3.2
Also here the author tried to fix UX by leveraging mouseout event:
http://nuclearprojects.com/blog/svg-click-and-drag-object-with-mouse-code/
I copied the above code snippet here: http://codepen.io/cmer41k/pen/zNGwpa
The question I have is:
Is there no other way (provided by pure SVG) to prevent such "loss" of SVG element while mouse moves too fast?
My attempt to solve this was:
- detect (somehow) that mouseout event happened without finishing the dragging.
- and if so (we sort of detected "disconnect") - reconnect the SVG element with current mouse position.
Is there a reason why this wouldn't work?
Code:
var click=false; // flag to indicate when shape has been clicked
var clickX, clickY; // stores cursor location upon first click
var moveX=0, moveY=0; // keeps track of overall transformation
var lastMoveX=0, lastMoveY=0; // stores previous transformation (move)
function mouseDown(evt){
evt.preventDefault(); // Needed for Firefox to allow dragging correctly
click=true;
clickX = evt.clientX;
clickY = evt.clientY;
evt.target.setAttribute("fill","green");
}
function move(evt){
evt.preventDefault();
if(click){
moveX = lastMoveX + ( evt.clientX – clickX );
moveY = lastMoveY + ( evt.clientY – clickY );
evt.target.setAttribute("transform", "translate(" + moveX + "," + moveY + ")");
}
}
function endMove(evt){
click=false;
lastMoveX = moveX;
lastMoveY = moveY;
evt.target.setAttribute("fill","gray");
}
The most important part of your code is missing, namely how or more specifically on which element you register the events.
What you basically do to prevent this problem is to register the mousemove and mouseup events on the outermost svg element, and not on the element you want to drag.
svg.addEventListener("mousemove", move)
svg.addEventListener("mouseup", endMove)
When starting the drag, register the events on the svg element, and when done unregister them.
svg.removeEventListener("mousemove", move)
svg.removeListener("mouseup", endMove)
you have to store the element you are currently dragging, so it is available in the other event handlers.
what i additionally do is to set pointer-events to "none" on the dragged
element so that you can react to mouse events underneath the dragged element (f.e. finding the drop target...)
evt.target.setAttribute("pointer-events", "none")
but don't forget to set it back to something sensible when dragging is done
evt.target.setAttribute("pointer-events", "all")
var click = false; // flag to indicate when shape has been clicked
var clickX, clickY; // stores cursor location upon first click
var moveX = 0,
moveY = 0; // keeps track of overall transformation
var lastMoveX = 0,
lastMoveY = 0; // stores previous transformation (move)
var currentTarget = null
function mouseDown(evt) {
evt.preventDefault(); // Needed for Firefox to allow dragging correctly
click = true;
clickX = evt.clientX;
clickY = evt.clientY;
evt.target.setAttribute("fill", "green");
// register move events on outermost SVG Element
currentTarget = evt.target
svg.addEventListener("mousemove", move)
svg.addEventListener("mouseup", endMove)
evt.target.setAttribute("pointer-events", "none")
}
function move(evt) {
evt.preventDefault();
if (click) {
moveX = lastMoveX + (evt.clientX - clickX);
moveY = lastMoveY + (evt.clientY - clickY);
currentTarget.setAttribute("transform", "translate(" + moveX + "," + moveY + ")");
}
}
function endMove(evt) {
click = false;
lastMoveX = moveX;
lastMoveY = moveY;
currentTarget.setAttribute("fill", "gray");
svg.removeEventListener("mousemove", move)
svg.removeEventListener("mouseup", endMove)
currentTarget.setAttribute("pointer-events", "all")
}
<svg id="svg" width="800" height="600" style="border: 1px solid black; background: #E0FFFF;">
<rect x="0" y="0" width="800" height="600" fill="none" pointer-events="all" />
<circle id="mycirc" cx="60" cy="60" r="22" onmousedown="mouseDown(evt)" />
</svg>
more advanced
there are still two things not so well with this code.
it does not work for viewBoxed SVGs nor for elements inside
transformed parents.
all the globals are bad coding practice.
here is how to fix those:
Nr. 1 is solved by converting mouse coordinates into local coordinates using the inverse of getScreenCTM (CTM = Current Transformation Matrix).
function globalToLocalCoords(x, y) {
var p = elem.ownerSVGElement.createSVGPoint()
var m = elem.parentNode.getScreenCTM()
p.x = x
p.y = y
return p.matrixTransform(m.inverse())
}
For nr. 2 see this implementation:
var dre = document.querySelectorAll(".draggable")
for (var i = 0; i < dre.length; i++) {
var o = new Draggable(dre[i])
}
function Draggable(elem) {
this.target = elem
this.clickPoint = this.target.ownerSVGElement.createSVGPoint()
this.lastMove = this.target.ownerSVGElement.createSVGPoint()
this.currentMove = this.target.ownerSVGElement.createSVGPoint()
this.target.addEventListener("mousedown", this)
this.handleEvent = function(evt) {
evt.preventDefault()
this.clickPoint = globalToLocalCoords(evt.clientX, evt.clientY)
this.target.classList.add("dragged")
this.target.setAttribute("pointer-events", "none")
this.target.ownerSVGElement.addEventListener("mousemove", this.move)
this.target.ownerSVGElement.addEventListener("mouseup", this.endMove)
}
this.move = function(evt) {
var p = globalToLocalCoords(evt.clientX, evt.clientY)
this.currentMove.x = this.lastMove.x + (p.x - this.clickPoint.x)
this.currentMove.y = this.lastMove.y + (p.y - this.clickPoint.y)
this.target.setAttribute("transform", "translate(" + this.currentMove.x + "," + this.currentMove.y + ")")
}.bind(this)
this.endMove = function(evt) {
this.lastMove.x = this.currentMove.x
this.lastMove.y = this.currentMove.y
this.target.classList.remove("dragged")
this.target.setAttribute("pointer-events", "all")
this.target.ownerSVGElement.removeEventListener("mousemove", this.move)
this.target.ownerSVGElement.removeEventListener("mouseup", this.endMove)
}.bind(this)
function globalToLocalCoords(x, y) {
var p = elem.ownerSVGElement.createSVGPoint()
var m = elem.parentNode.getScreenCTM()
p.x = x
p.y = y
return p.matrixTransform(m.inverse())
}
}
.dragged {
fill-opacity: 0.5;
stroke-width: 0.5px;
stroke: black;
stroke-dasharray: 1 1;
}
.draggable{cursor:move}
<svg id="svg" viewBox="0 0 800 600" style="border: 1px solid black; background: #E0FFFF;">
<rect x="0" y="0" width="800" height="600" fill="none" pointer-events="all" />
<circle class="draggable" id="mycirc" cx="60" cy="60" r="22" fill="blue" />
<g transform="rotate(45,175,75)">
<rect class="draggable" id="mycirc" x="160" y="60" width="30" height="30" fill="green" />
</g>
<g transform="translate(200 200) scale(2 2)">
<g class="draggable">
<circle cx="0" cy="0" r="30" fill="yellow"/>
<text text-anchor="middle" x="0" y="0" fill="red">I'm draggable</text>
</g>
</g>
</svg>
<div id="out"></div>

Make the svg follow a hierarchical structure

I'm new with d3 and I was wondering if it is possible to make the svg follow the hierarchical structure of a tree.
Let's say that I have a json like that :
{
"name": "root",
"children": [
{
"name" : "child1",
"children": [
{
"name": "child1_1"
},
{
"name": "child1_2"
}
]
},
{
"name": "child2",
"children": [
{
"name": "child2_1"
},
{
"name": "child2_2"
}
]
}
]
}
How can I have a svg structure like this :
<g class="root">
<g class="child1">
<g class="child1_1">
</g>
<g class="child1_2">
</g>
</g>
<g class="child2">
<g class="child2_1">
</g>
<g class="child2_2">
</g>
</g>
</g>
I've tried stuff like that :
var w = 960,
h = 2000,
i = 0,
root;
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var vis = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(40,0)");
d3.json(
'./small_tree.json',
function(json) {
json.x0 = 800;
json.y0 = 0;
update(root = json);
}
);
function update(source) {
var nodes = tree.nodes(root);
var child = vis.selectAll('g.child_node')
.data(nodes,function(d){return d.children;})
.enter()
.append('g')
.attr("class", "child_node");
}
And I'm getting a tag for the children of the root node, but I don't know how to recursively create nested group for children of children.
Anyone have an idea ?
Thanks,
Rem
D3 doesn't natively handle recursion, and it looks like the standard hierarchical layouts don't, in general, create nested structures. Instead, you need to recurse in your own code. Here's one implementation:
// simple offset, just to make things visible
var offset = 0;
// recursion function
function addNode(selection, depth) {
// set the current children as the data array
var nodeGroup = selection.selectAll('g.child_node')
.data(function(d) { return d.children })
.enter()
// add a node for each child
.append('g')
.attr("class", "child_node")
// very simple depth-based placement
.attr("transform", "translate(" + (depth * 30) + "," +
(++offset * 15) + ")");
nodeGroup.append("text")
.attr("dy", "1em")
.text(function(d) { return d.name });
// recurse - there might be a way to ditch the conditional here
nodeGroup.each(function(d) {
if (d.children) nodeGroup.call(addNode, depth + 1);
});
}
d3.json(
'./small_tree.json',
function(root) {
// kick off the recursive append
vis
.datum({ children: [root] })
.call(addNode, 0);
}
}
See fiddle: http://jsfiddle.net/nrabinowitz/ELYWa/

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