How to linebreak an svg text within javascript? - javascript

So here is what I have:
<path class="..." onmousemove="show_tooltip(event,'very long text
\\\n I would like to linebreak')" onmouseout="hide_tooltip()" d="..."/>
<rect class="tooltip_bg" id="tooltip_bg" ... />
<text class="tooltip" id="tooltip" ...>Tooltip</text>
<script>
<![CDATA[
function show_tooltip(e,text) {
var tt = document.getElementById('tooltip');
var bg = document.getElementById('tooltip_bg');
// set position ...
tt.textContent=text;
bg.setAttribute('width',tt.getBBox().width+10);
bg.setAttribute('height',tt.getBBox().height+6);
// set visibility ...
}
...
Now my very long tooltip text doesn't have a linebreak, even though if I use alert(); it shows me that the text actually DOES have two lines. (It contains a "\" though, how do I remove that one by the way?)
I can't get CDATA to work anywhere.

This is not something that SVG 1.1 supports. SVG 1.2 does have the textArea element, with automatic word wrapping, but it's not implemented in all browsers. SVG 2 does not plan on implementing textArea, but it does have auto-wrapped text.
However, given that you already know where your linebreaks should occur, you can break your text into multiple <tspan>s, each with x="0" and dy="1.4em" to simulate actual lines of text. For example:
<g transform="translate(123 456)"><!-- replace with your target upper left corner coordinates -->
<text x="0" y="0">
<tspan x="0" dy="1.2em">very long text</tspan>
<tspan x="0" dy="1.2em">I would like to linebreak</tspan>
</text>
</g>
Of course, since you want to do that from JavaScript, you'll have to manually create and insert each element into the DOM.

I suppese you alredy managed to solve it, but if someone is looking for similar solution then this worked for me:
g.append('svg:text')
.attr('x', 0)
.attr('y', 30)
.attr('class', 'id')
.append('svg:tspan')
.attr('x', 0)
.attr('dy', 5)
.text(function(d) { return d.name; })
.append('svg:tspan')
.attr('x', 0)
.attr('dy', 20)
.text(function(d) { return d.sname; })
.append('svg:tspan')
.attr('x', 0)
.attr('dy', 20)
.text(function(d) { return d.idcode; })
There are 3 lines separated with linebreak.

With the tspan solution, let's say you don't know in advance where to put your line breaks: you can use this nice function, that I found here:
http://bl.ocks.org/mbostock/7555321
That automatically does line breaks for long text svg for a given width in pixel.
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")) || 0,
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);
}
}
});
}

use HTML instead of javascript
limitation: the SVG renderer must support HTML rendering
for example, inkscape cannot render such SVG files
<html>
<head><style> * { margin: 0; padding: 0; } </style></head>
<body>
<h1>svg foreignObject to embed html</h1>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 300 300"
x="0" y="0" height="300" width="300"
>
<circle
r="142" cx="150" cy="150"
fill="none" stroke="#000000" stroke-width="2"
/>
<foreignObject
x="50" y="50" width="200" height="200"
>
<div
xmlns="http://www.w3.org/1999/xhtml"
style="
width: 196px; height: 196px;
border: solid 2px #000000;
font-size: 32px;
overflow: auto; /* scroll */
"
>
<p>this is html in svg 1</p>
<p>this is html in svg 2</p>
<p>this is html in svg 3</p>
<p>this is html in svg 4</p>
</div>
</foreignObject>
</svg>
</body></html>

I think this does what you want:
function ShowTooltip(evt, mouseovertext){
// Make tooltip text
var tooltip_text = tt.childNodes.item(1);
var words = mouseovertext.split("\\\n");
var max_length = 0;
for (var i=0; i<3; i++){
tooltip_text.childNodes.item(i).firstChild.data = i<words.length ? words[i] : " ";
length = tooltip_text.childNodes.item(i).getComputedTextLength();
if (length > max_length) {max_length = length;}
}
var x = evt.clientX + 14 + max_length/2;
var y = evt.clientY + 29;
tt.setAttributeNS(null,"transform", "translate(" + x + " " + y + ")")
// Make tooltip background
bg.setAttributeNS(null,"width", max_length+15);
bg.setAttributeNS(null,"height", words.length*15+6);
bg.setAttributeNS(null,"x",evt.clientX+8);
bg.setAttributeNS(null,"y",evt.clientY+14);
// Show everything
tt.setAttributeNS(null,"visibility","visible");
bg.setAttributeNS(null,"visibility","visible");
}
It splits the text on \\\n and for each puts each fragment in a tspan. Then it calculates the size of the box required based on the longest length of text and the number of lines. You will also need to change the tooltip text element to contain three tspans:
<g id="tooltip" visibility="hidden">
<text><tspan>x</tspan><tspan x="0" dy="15">x</tspan><tspan x="0" dy="15">x</tspan></text>
</g>
This assumes that you never have more than three lines. If you want more than three lines you can add more tspans and increase the length of the for loop.

I have adapted a bit the solution by #steco, switching the dependency from d3 to jquery and adding the height of the text element as parameter
function wrap(text, width, height) {
text.each(function(idx,elem) {
var text = $(elem);
text.attr("dy",height);
var 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 (elem.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);
}
}
});
}

Related

How did they do this with SVG's, a static image, hotspots and javascript

About half way down on the following site there is an image of a house with SVG animations and hotspots.
https://enphase.com/en-us/homeowners
I see all the individual elements but I don't understand how the they put it all together. The elements are positioned using percentages to 5 decimal places. I'm assuming they used some software to create the SVGs put more importantly, the layout. Any idea what that software is? There is now way they hand coded the layout and calculated the positioning.
You can have the same functionality using SVG stroke-dasharray and stroke-dashoffset attributes mainipulation with javascript animation timers, I usually use D3.js to do this type of animation/SVG manipulation, but you can also do it purely in javascript, here is a block example by Noah Veltman on bl.ocks.org:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style>
path {
fill: none;
stroke: #d3008c;
stroke-width: 2px;
}
#arrowhead {
fill: #d3008c;
stroke: none;
}
</style>
</head>
<body>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="960" height="500">
<path d="M636.5,315c-0.4-18.7,1.9-27.9-5.3-35.9
c-22.7-25-107.3-2.8-118.3,35.9c-7,24.4,20.6,37.2,16,71c-4,29.6-30.8,60.7-56.5,61.1c-30.8,0.4-32.9-43.8-81.7-70.2
c-50.9-27.6-110.1-12.9-125.2-9.2c-66.1,16.4-82.2,56.9-109.2,47.3c-38-13.6-55.9-112.1-19.8-143.5c39-34,121.2,27.7,148.1-3.8
c18-21.1,3.1-74.3-25.2-105.3c-31.1-34.1-70.1-32.4-105.3-76.3c-8.2-10.2-16.9-23.8-15.3-39.7c1.2-11.4,7.5-23.3,15.3-29
c33.8-25,101.6,62.6,193.1,59.5c40.1-1.3,38.7-18.5,99.2-38.9c126.2-42.6,242.4-4.9,297.7,13c54.7,17.7,105.4,35,129.8,82.4
c13,25.3,22.9,67.7,4.6,87c-11.6,12.3-25.1,5.1-46.6,20.6c-2.8,2-28.9,21.4-32.1,49.6c-3.1,27.4,18.7,35,29,70.2
c8.8,30.1,8.5,77.8-18.3,99.2c-32.3,25.8-87,0.6-100-5.3c-69.6-32-67.2-88.4-73.3-109.2z"/>
<defs>
<path id="arrowhead" d="M7,0 L-7,-5 L-7,5 Z" />
</defs>
</svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var path = document.querySelector("path"),
totalLength = path.getTotalLength(),
group = totalLength / 20,
start;
var arrowheads = d3.select("svg").selectAll("use")
.data(d3.range(20).map(function(d){ return d * group + 50; }))
.enter()
.append("use")
.attr("xlink:href", "#arrowhead");
path.style.strokeDasharray = "50," + (group - 50);
requestAnimationFrame(update);
function update(t) {
if (!start) {
start = t;
}
var offset = -group * ((t - start) % 900) / 900;
path.style.strokeDashoffset = offset;
arrowheads.attr("transform",function(d){
var l = d - offset;
if (l < 0) {
l = totalLength + l;
} else if (l > totalLength) {
l -= totalLength;
}
var p = pointAtLength(l);
return "translate(" + p + ") rotate( " + angleAtLength(l) + ")";
});
requestAnimationFrame(update);
}
function pointAtLength(l) {
var xy = path.getPointAtLength(l);
return [xy.x, xy.y];
}
// Approximate tangent
function angleAtLength(l) {
var a = pointAtLength(Math.max(l - 0.01,0)), // this could be slightly negative
b = pointAtLength(l + 0.01); // browsers cap at total length
return Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI;
}
</script>
And this is how the <path> looks like without animations:
path {
fill: none;
stroke: #d3008c;
stroke-width: 2px;
}
#arrowhead {
fill: #d3008c;
stroke: none;
}
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="960" height="500">
<path d="M636.5,315c-0.4-18.7,1.9-27.9-5.3-35.9
c-22.7-25-107.3-2.8-118.3,35.9c-7,24.4,20.6,37.2,16,71c-4,29.6-30.8,60.7-56.5,61.1c-30.8,0.4-32.9-43.8-81.7-70.2
c-50.9-27.6-110.1-12.9-125.2-9.2c-66.1,16.4-82.2,56.9-109.2,47.3c-38-13.6-55.9-112.1-19.8-143.5c39-34,121.2,27.7,148.1-3.8
c18-21.1,3.1-74.3-25.2-105.3c-31.1-34.1-70.1-32.4-105.3-76.3c-8.2-10.2-16.9-23.8-15.3-39.7c1.2-11.4,7.5-23.3,15.3-29
c33.8-25,101.6,62.6,193.1,59.5c40.1-1.3,38.7-18.5,99.2-38.9c126.2-42.6,242.4-4.9,297.7,13c54.7,17.7,105.4,35,129.8,82.4
c13,25.3,22.9,67.7,4.6,87c-11.6,12.3-25.1,5.1-46.6,20.6c-2.8,2-28.9,21.4-32.1,49.6c-3.1,27.4,18.7,35,29,70.2
c8.8,30.1,8.5,77.8-18.3,99.2c-32.3,25.8-87,0.6-100-5.3c-69.6-32-67.2-88.4-73.3-109.2z"/>
<defs>
<path id="arrowhead" d="M7,0 L-7,-5 L-7,5 Z" />
</defs>
</svg>

How do i animate the fill of an inline svg to change with mouse position (in parent div)

Is it possible to animate the fill of a inline svg path, to change as the mouse moves around the page ?
In the example in the code snippet below, the x and y positions of the mouse are monitored using the "mousemove" event and the screenX and screenY properties of the event object. Red and green colors values are set depending on those coordinates, and an rgb fill color string is constructed and imposed on an SVG path element.
var path = document.querySelector("path");
document.body.addEventListener("mousemove", function(evt) {
var xRatio = Math.min(1, evt.clientX / document.body.offsetWidth );
var yRatio = Math.min(1, evt.clientY / document.body.offsetHeight);
var red = parseInt(255 * xRatio);
var green = parseInt(255 * yRatio);
var color = "rgb(" + red + ", " + green + ", 128)";
document.querySelector("path").setAttribute("fill", color);
});
body {
border: solid black 1px;
padding: 0.5em;
}
p {
margin: 0;
}
<body>
<p>Move your mouse over the image to see the colour-change effect.</p>
<svg width="400" height="150">
<path d="M10,75 C200,-100 300,120 390,75 200,250 100,30 10,75" stroke="black" stroke-width="4" fill="none" />
</svg>
</body>

Preserve a Transform in an SVG element

I have a SVG shape (which is quite complex, I have made it simple for the sake of understanding) which I need to transform. My requirement is,
When mouseover, The group should be scaled to 2x
When mouseout, group should scale back to 1x
When dragging the group scale should be preseved
So far, I have managed to do all the parts, except one issue. After I drag the element when I try to mouseover the group it reverts to its original location. I cannot understand why it happens. Here is a working fiddle. Can anyone help me out?
index.html
<svg width="400" height="400" style="background-color: red">
<g id="op" class="operator" transform="translate(0,0)">
<circle class="head" cx="50" cy="50" r="20" style="fill: yellow"></circle>
<circle cx="40" cy="40" r="10" style="fill: blue"></circle>
<circle cx="60" cy="40" r="10" style="fill: blue"></circle>
</g>
</svg>
script.js
d3.selectAll('.operator')
.on('mouseenter', function () {
console.log('Mouse Enter');
var c = d3.select(this).select('.head');
var x = c.attr('cx');
var y = c.attr('cy');
var scale = 2;
var scaleX = -1 * x * (scale - 1);
var scaleY = -1 * y * (scale - 1);
d3.select(this).attr('transform', 'translate(' + scaleX + ',' + scaleY + ')' + 'scale(' + scale + ')');
})
.on('mouseleave', function () {
console.log('Mouse Leave');
var c = d3.select(this).select('.head');
var x = c.attr('cx');
var y = c.attr('cy');
var scale = 1;
var scaleX = -1 * x * (scale - 1);
var scaleY = -1 * y * (scale - 1);
d3.select(this).attr('transform', 'translate(' + scaleX + ',' + scaleY + ')' + 'scale(' + scale + ')');
})
.call(d3.behavior.drag()
.origin(function () {
var t = d3.select(this);
return {
x: d3.transform(t.attr("transform")).translate[0],
y: d3.transform(t.attr("transform")).translate[1]
};
})
.on('drag', function () {
var oldScale = d3.transform(d3.select(this).attr('transform')).scale;
d3.select(this).attr('transform', 'translate(' + d3.event.x + ',' + d3.event.y + ')scale(' + oldScale + ')');
}))
As #PhilAnderson said, you shouldn't be mixing translate and cx/cy. In fact, the way you are nesting elements, you should only be translating. Translate your g within the SVG and then translate your circles within the g. Correcting that, things get much simpler:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<svg width="400" height="400" style="background-color: red">
<g id="op" class="operator" transform="translate(50,50)">
<circle class="head" r="20" style="fill: yellow"></circle>
<circle transform="translate(-10,0)" r="10" style="fill: blue"></circle>
<circle transform="translate(10,0)" r="10" style="fill: blue"></circle>
</g>
</svg>
<script>
d3.select('.operator')
.on('mouseenter', function() {
console.log('Mouse Enter');
var self = d3.select(this),
xy = d3.transform(self.attr('transform')).translate,
scale = 2;
self.attr('transform', 'translate(' + xy[0] + ',' + xy[1] + ')' + 'scale(' + scale + ')');
})
.on('mouseleave', function() {
console.log('Mouse Leave');
var self = d3.select(this),
xy = d3.transform(self.attr('transform')).translate,
scale = 1;
self.attr('transform', 'translate(' + xy[0] + ',' + xy[1] + ')' + 'scale(' + scale + ')');
})
.call(d3.behavior.drag()
.on('drag', function() {
var self = d3.select(this),
oldScale = d3.transform(self.attr('transform')).scale;
self.attr('transform', 'translate(' + d3.event.x + ',' + d3.event.y + ')scale(' + oldScale + ')');
}))
</script>
</body>
</html>
It looks like you're confusing the cx and cy attributes with the translate.
In mouseenter and mouseleave you move the shape according to the values of cx and cy, but in the drag event you simply translate using the x and y values of the event.
One way of fixing this would be to set the cx and cy attributes in your drag event, although tbh it would be better to settle for one approach and stick to it throughout.

creating circles with svg and javascript

(UPDATED) I'm having some issues regarding svg and javascript. What I want to create is a series of circles on top of one another, with their radius (r) values increasing by one each time the loop goes round, so that it creates some sort of a pattern. Here is what I have so far(for loop values are from another forum post, I would rather do it with a while loop that would execute 10 times) -
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Dynamic SVG!</title>
</head>
<defs>
<svg height="10000" width="10000" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<circle id="cir1" cx="300" cy="300" r="40" stroke="yellow" stroke-width="" fill="none"/>
</svg>
</defs>
<script>
var svgns = "http://www.w3.org/2000/svg";
for (var x = 0; x < 5000; x += 50) {
for (var y = 0; y < 3000; y += 50) {
var circle = document.createElementNS(svgns, 'circle');
circle.setAttributeNS(null, 'x', x);
circle.setAttributeNS(null, 'y', y);
circle.setAttributeNS(null, 'height', '50');
circle.setAttributeNS(null, 'width', '50');
document.getElementById('cir1').appendChild(circle);
}
}
</script>
<body>
</body>
</html>
Any help out there?
Thanks.
Ok, so this is, what I had to fix in order to get your code working:
You append to the circle element, but should append to the svg-container. A circle element has no child elements.
You did not set any styles for the circles, so they were transparent.
The coordinates in a circle element are called cx and cy instead of x and y.
The <defs> element should be a child of the <svg> element. Also everything within it wont be rendered.
JavaScript
var svgns = "http://www.w3.org/2000/svg",
container = document.getElementById( 'cont' );
for (var x = 0; x < 500; x += 50) {
for (var y = 0; y < 300; y += 50) {
var circle = document.createElementNS(svgns, 'circle');
circle.setAttributeNS(null, 'cx', x);
circle.setAttributeNS(null, 'cy', y);
circle.setAttributeNS(null, 'r', 50);
circle.setAttributeNS(null, 'style', 'fill: none; stroke: blue; stroke-width: 1px;' );
container.appendChild(circle);
}
}
HTML
<svg id="cont" height="1000" width="1000" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<circle id="cir1" cx="300" cy="300" r="40" stroke="yellow" stroke-width="" fill="none" />
</svg>
Example Fiddle
I also adjusted your sizes as for a mere test, they were quite big.

why does appending a foreignObject into an SVG break jquery ui's resize?

I've got a piechart that's built with D3, the markup looks like:
<div class="display">
<svg height="300">
<g transform="translate(140,125)">
<g class="slice">
<path fill="#3182bd" d="M-103.92304845413268,-59.99999999999993A120,120 0 0,1 -1.2862432473281782e-13,-120L0,0Z"></path>
<text transform="translate(-30.000000000000075,-51.96152422706628)" text-anchor="middle">A</text>
</g>
<g class="slice">
<path fill="#6baed6" d="M7.347638122934264e-15,120A120,120 0 0,1 -103.92304845413268,-59.99999999999993L0,0Z"></path>
<text transform="translate(-51.961524227066306,30.00000000000002)" text-anchor="middle">B</text>
</g>
<g class="slice">
<path fill="#9ecae1" d="M7.347638122934264e-15,-120A120,120 0 1,1 7.347638122934264e-15,120L0,0Z"></path>
<text transform="translate(60,0)" text-anchor="middle">C</text>
</g>
</g>
</svg>
<div class="pieChartTooltip" style="position: absolute; z-index: 10; visibility: visible; top: 32px; left: 192px;">C: 3.00</div>
</div>
Ok nifty. Then I wanted to be able to resize the container that it's in (angular directive):
.directive("resizable", [ "debounce", (debounce) ->
restrict: "A"
link: (scope, element, attrs) ->
broadcastResize = (event, ui) ->
scope.$root.$broadcast "resize", ui.element.parents(".widget"), ui.size
debounceResize = debounce(broadcastResize, 800, false)
element.resizable
grid: 10
helper: "ui-resizable-helper"
stop: (event, ui) ->
debounceResize event, ui
])
All fine. Next I wanted to add an overlay for the pie chart that said "sample data" or "refreshed at 3:45" or other things like that. So I added this code to the object that builds the pie chart (coffee):
drawOverlay: (msg) =>
#logger.debug 'drawing overlay'
vis = d3.select(#selector + " svg ")
#logger.warn "no svg vis found" unless vis?
x = (#model.width / 2 ) - 75
y = (#model.height / 2 ) - 50
#jq(#selector + " svg foreignObject").remove()
text = vis.append("foreignObject")
.attr("class", "widgetOverlay")
.attr("x", x)
.attr("y", y)
.attr("width", 150)
.attr("height", 50)
.append("xhtml:body")
.html('<div>' + msg + '</div>')
Here's the problem: adding that overlay causes jquery ui's resize helper to get "stuck" on screen -- but only the first time it's drawn. Subsequent resizes work fine. I suspect it's because of the second body tag that gets inserted in the svg. As far as I know it is required -- taking it out caused the overlay to not be appended. If I put a return at the top of drawOverlay the resize directive works just fine. So it seems clear where the problem lies, I'm wondering if anyone can explain why this is happening -- and maybe offer a fix.
If adding a body tag confuses jquery.ui, then why not skip the body tag and append the div directly?
var msg = 'Hello World';
var x = 150, y = 150;
var svg = d3.select('body')
.append('svg')
.attr('width', 300)
.attr('height', 300)
.append("foreignObject")
.attr("class", "widgetOverlay")
.attr("x", x)
.attr("y", y)
.attr("width", 150)
.attr("height", 50)
.append('xhtml:div')
.text(msg);
Demo

Categories

Resources