D3.js v4+ : truncate text to fit in fixed space - javascript
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);
});
Related
How to get the exact BBox for svg <tspan>
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>
Add text to SVG path dynamically
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>
SVG.js TextPath not aligned with path (Verticaly)
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)
Paintcode and Snap SVG
I am trying to bridge examples from Paintcode and Snap SVG. Here is a simple project that has the gears that spin when a slider is dragged. This is great but I would like to do the interaction with the mouse instead. This is an example of the gears as svg: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="240" height="200" xml:space="preserve" id="gears"> <!-- Generated by PaintCode - http://www.paintcodeapp.com --> <path id="gears-largeGear" stroke="none" fill="rgb(69, 131, 212)" d="M 2.78,-67.94 C 3.71,-63.88 4.74,-59.43 5.59,-55.72 7.73,-55.51 9.84,-55.18 11.9,-54.73 13.87,-58 16.21,-61.91 18.36,-65.49 20.15,-64.99 21.91,-64.42 23.64,-63.78 23.27,-59.62 22.87,-55.08 22.54,-51.28 24.5,-50.42 26.4,-49.45 28.23,-48.37 31.11,-50.87 34.55,-53.87 37.69,-56.6 39.24,-55.58 40.73,-54.49 42.18,-53.34 40.55,-49.5 38.77,-45.3 37.28,-41.79 38.87,-40.37 40.37,-38.87 41.79,-37.28 45.3,-38.77 49.5,-40.55 53.34,-42.18 54.49,-40.73 55.58,-39.24 56.6,-37.69 53.87,-34.55 50.87,-31.11 48.37,-28.23 49.45,-26.4 50.42,-24.5 51.28,-22.54 55.08,-22.87 59.62,-23.27 63.78,-23.64 64.42,-21.91 64.99,-20.15 65.49,-18.36 61.91,-16.21 58,-13.87 54.73,-11.9 55.18,-9.84 55.51,-7.73 55.72,-5.59 59.43,-4.74 63.88,-3.71 67.94,-2.78 67.98,-1.86 68,-0.93 68,0 68,0.93 67.98,1.86 67.94,2.78 63.88,3.71 59.43,4.74 55.72,5.59 55.51,7.73 55.18,9.84 54.73,11.9 58,13.87 61.91,16.21 65.49,18.36 64.99,20.15 64.42,21.91 63.78,23.64 59.62,23.27 55.08,22.87 51.28,22.54 50.42,24.5 49.45,26.4 48.37,28.23 50.87,31.11 53.87,34.55 56.6,37.69 55.58,39.24 54.49,40.73 53.34,42.18 49.5,40.55 45.3,38.77 41.79,37.28 40.37,38.87 38.87,40.37 37.28,41.79 38.77,45.3 40.55,49.5 42.18,53.34 40.73,54.49 39.24,55.58 37.69,56.6 34.55,53.87 31.11,50.87 28.23,48.37 26.4,49.45 24.5,50.42 22.54,51.28 22.87,55.08 23.27,59.62 23.64,63.78 21.91,64.42 20.15,64.99 18.36,65.49 16.21,61.91 13.87,58 11.9,54.73 9.84,55.18 7.73,55.51 5.59,55.72 4.74,59.43 3.71,63.88 2.78,67.94 1.86,67.98 0.93,68 -0,68 -0.93,68 -1.86,67.98 -2.78,67.94 -3.71,63.88 -4.74,59.43 -5.59,55.72 -7.73,55.51 -9.84,55.18 -11.9,54.73 -13.87,58 -16.21,61.91 -18.36,65.49 -20.15,64.99 -21.91,64.42 -23.64,63.78 -23.27,59.62 -22.87,55.08 -22.54,51.28 -24.5,50.42 -26.4,49.45 -28.23,48.37 -31.11,50.87 -34.55,53.87 -37.69,56.6 -39.24,55.58 -40.73,54.49 -42.18,53.34 -40.55,49.5 -38.77,45.3 -37.28,41.79 -38.87,40.37 -40.37,38.87 -41.79,37.28 -45.3,38.77 -49.5,40.55 -53.34,42.18 -54.49,40.73 -55.58,39.24 -56.6,37.69 -53.87,34.55 -50.87,31.11 -48.37,28.23 -49.45,26.4 -50.42,24.5 -51.28,22.54 -55.08,22.87 -59.62,23.27 -63.78,23.64 -64.42,21.91 -64.99,20.15 -65.49,18.36 -61.91,16.21 -58,13.87 -54.73,11.9 -55.18,9.84 -55.51,7.73 -55.72,5.59 -59.43,4.74 -63.88,3.71 -67.94,2.78 -67.98,1.86 -68,0.93 -68,-0 -68,-0.93 -67.98,-1.86 -67.94,-2.78 -63.88,-3.71 -59.43,-4.74 -55.72,-5.59 -55.51,-7.73 -55.18,-9.84 -54.73,-11.9 -58,-13.87 -61.91,-16.21 -65.49,-18.36 -64.99,-20.15 -64.42,-21.91 -63.78,-23.64 -59.62,-23.27 -55.08,-22.87 -51.28,-22.54 -50.42,-24.5 -49.45,-26.4 -48.37,-28.23 -50.87,-31.11 -53.87,-34.55 -56.6,-37.69 -55.58,-39.24 -54.49,-40.73 -53.34,-42.18 -49.5,-40.55 -45.3,-38.77 -41.79,-37.28 -40.37,-38.87 -38.87,-40.37 -37.28,-41.79 -38.77,-45.3 -40.55,-49.5 -42.18,-53.34 -41.55,-53.84 -40.91,-54.33 -40.26,-54.81 -39.42,-55.43 -38.56,-56.03 -37.69,-56.6 -34.55,-53.87 -31.11,-50.87 -28.23,-48.37 -26.4,-49.45 -24.5,-50.42 -22.54,-51.28 -22.87,-55.08 -23.27,-59.62 -23.64,-63.78 -21.91,-64.42 -20.15,-64.99 -18.36,-65.49 -16.21,-61.91 -13.87,-58 -11.9,-54.73 -9.84,-55.18 -7.73,-55.51 -5.59,-55.72 -4.74,-59.43 -3.71,-63.88 -2.78,-67.94 -1.93,-67.98 -1.08,-68 -0.23,-68 L 0,-68 C 0.93,-68 1.86,-67.98 2.78,-67.94 Z M 0,-36 C -6.22,-36 -12.07,-34.42 -17.18,-31.65 -28.39,-25.55 -36,-13.66 -36,-0 -36,19.88 -19.88,36 0,36 19.88,36 36,19.88 36,-0 36,-19.88 19.88,-36 0,-36 Z M 0,-36" transform="translate(150, 96) rotate(-0.5)" /> <path id="gears-smallGear" stroke="none" fill="rgb(115, 152, 218)" d="M 2.12,-37.94 L 2.45,-37.92 C 3.29,-34.48 4.2,-30.71 4.97,-27.56 6.23,-27.34 7.46,-27.03 8.65,-26.64 9.87,-26.24 11.04,-25.77 12.18,-25.22 14.65,-27.32 17.61,-29.83 20.31,-32.12 21.69,-31.25 23.02,-30.28 24.27,-29.24 22.92,-25.96 21.45,-22.37 20.22,-19.37 21.97,-17.55 23.47,-15.49 24.68,-13.24 27.91,-13.49 31.78,-13.78 35.32,-14.05 35.92,-12.54 36.42,-10.99 36.83,-9.39 33.81,-7.53 30.5,-5.49 27.75,-3.79 27.91,-2.55 28,-1.29 28,-0 28,1.29 27.91,2.55 27.75,3.79 30.51,5.49 33.81,7.53 36.83,9.39 36.42,10.99 35.92,12.54 35.32,14.05 31.78,13.78 27.91,13.49 24.68,13.24 23.47,15.49 21.97,17.55 20.22,19.37 21.45,22.37 22.92,25.96 24.27,29.24 23.02,30.28 21.69,31.25 20.31,32.12 17.61,29.83 14.65,27.32 12.18,25.22 9.93,26.31 7.51,27.11 4.97,27.56 4.2,30.71 3.29,34.48 2.45,37.92 1.64,37.97 0.82,38 -0,38 -0.82,38 -1.64,37.97 -2.45,37.92 -3.29,34.48 -4.2,30.71 -4.97,27.56 -7.51,27.11 -9.93,26.31 -12.18,25.22 -14.65,27.32 -17.61,29.83 -20.31,32.12 -21.69,31.25 -23.02,30.28 -24.27,29.24 -22.92,25.96 -21.45,22.37 -20.22,19.37 -21.97,17.55 -23.47,15.49 -24.68,13.24 -27.91,13.49 -31.78,13.78 -35.32,14.05 -35.92,12.54 -36.42,10.99 -36.83,9.39 -33.81,7.53 -30.5,5.49 -27.75,3.79 -27.91,2.55 -28,1.29 -28,-0 -28,-1.29 -27.91,-2.55 -27.75,-3.79 -30.51,-5.49 -33.81,-7.53 -36.83,-9.39 -36.42,-10.99 -35.92,-12.54 -35.32,-14.05 -31.78,-13.78 -27.91,-13.49 -24.68,-13.24 -23.47,-15.49 -21.97,-17.55 -20.22,-19.37 -21.45,-22.37 -22.92,-25.96 -24.27,-29.24 -23.02,-30.28 -21.69,-31.25 -20.31,-32.12 -17.61,-29.83 -14.65,-27.32 -12.18,-25.22 -9.93,-26.31 -7.51,-27.11 -4.97,-27.56 -4.2,-30.71 -3.29,-34.48 -2.45,-37.92 -1.64,-37.97 -0.82,-38 0,-38 0.71,-38 1.42,-37.98 2.12,-37.94 Z M 0,-14 C -7.73,-14 -14,-7.73 -14,-0 -14,7.73 -7.73,14 0,14 7.73,14 14,7.73 14,-0 14,-5.48 10.85,-10.22 6.27,-12.52 4.38,-13.47 2.25,-14 0,-14 Z M 0,-14" transform="translate(62, 137) rotate(1)" /> </svg> What I am trying to sort out is how to click inside either gears-largeGear or gears-smallGear one or the other gear and drag the mouse to cause them to rotate similar to how the slider works. Searching around it seems like Snap SVG is a good way to do this but I've struggled to bridge the SVG and the Snap documentation together. In Snap I see how to load the SVG but I don't quite see how to get the path or group to setup the interaction and rotation. If I used groups like this gears-largeGroup and gears-smallGroup would it be the same sort of code as if it were just paths being animated? <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="240" height="200" xml:space="preserve" id="gears"> <!-- Generated by PaintCode - http://www.paintcodeapp.com --> <g id="gears-largeGroup" transform="translate(149.59, 96.41)" > <path id="gears-largeGear" stroke="none" fill="rgb(69, 131, 212)" d="M 2.78,-67.94 C 3.71,-63.88 4.74,-59.43 5.59,-55.72 7.73,-55.51 9.84,-55.18 11.9,-54.73 13.87,-58 16.21,-61.91 18.36,-65.49 20.15,-64.99 21.91,-64.42 23.64,-63.78 23.27,-59.62 22.87,-55.08 22.54,-51.28 24.5,-50.42 26.4,-49.45 28.23,-48.37 31.11,-50.87 34.55,-53.87 37.69,-56.6 39.24,-55.58 40.73,-54.49 42.18,-53.34 40.55,-49.5 38.77,-45.3 37.28,-41.79 38.87,-40.37 40.37,-38.87 41.79,-37.28 45.3,-38.77 49.5,-40.55 53.34,-42.18 54.49,-40.73 55.58,-39.24 56.6,-37.69 53.87,-34.55 50.87,-31.11 48.37,-28.23 49.45,-26.4 50.42,-24.5 51.28,-22.54 55.08,-22.87 59.62,-23.27 63.78,-23.64 64.42,-21.91 64.99,-20.15 65.49,-18.36 61.91,-16.21 58,-13.87 54.73,-11.9 55.18,-9.84 55.51,-7.73 55.72,-5.59 59.43,-4.74 63.88,-3.71 67.94,-2.78 67.98,-1.86 68,-0.93 68,0 68,0.93 67.98,1.86 67.94,2.78 63.88,3.71 59.43,4.74 55.72,5.59 55.51,7.73 55.18,9.84 54.73,11.9 58,13.87 61.91,16.21 65.49,18.36 64.99,20.15 64.42,21.91 63.78,23.64 59.62,23.27 55.08,22.87 51.28,22.54 50.42,24.5 49.45,26.4 48.37,28.23 50.87,31.11 53.87,34.55 56.6,37.69 55.58,39.24 54.49,40.73 53.34,42.18 49.5,40.55 45.3,38.77 41.79,37.28 40.37,38.87 38.87,40.37 37.28,41.79 38.77,45.3 40.55,49.5 42.18,53.34 40.73,54.49 39.24,55.58 37.69,56.6 34.55,53.87 31.11,50.87 28.23,48.37 26.4,49.45 24.5,50.42 22.54,51.28 22.87,55.08 23.27,59.62 23.64,63.78 21.91,64.42 20.15,64.99 18.36,65.49 16.21,61.91 13.87,58 11.9,54.73 9.84,55.18 7.73,55.51 5.59,55.72 4.74,59.43 3.71,63.88 2.78,67.94 1.86,67.98 0.93,68 -0,68 -0.93,68 -1.86,67.98 -2.78,67.94 -3.71,63.88 -4.74,59.43 -5.59,55.72 -7.73,55.51 -9.84,55.18 -11.9,54.73 -13.87,58 -16.21,61.91 -18.36,65.49 -20.15,64.99 -21.91,64.42 -23.64,63.78 -23.27,59.62 -22.87,55.08 -22.54,51.28 -24.5,50.42 -26.4,49.45 -28.23,48.37 -31.11,50.87 -34.55,53.87 -37.69,56.6 -39.24,55.58 -40.73,54.49 -42.18,53.34 -40.55,49.5 -38.77,45.3 -37.28,41.79 -38.87,40.37 -40.37,38.87 -41.79,37.28 -45.3,38.77 -49.5,40.55 -53.34,42.18 -54.49,40.73 -55.58,39.24 -56.6,37.69 -53.87,34.55 -50.87,31.11 -48.37,28.23 -49.45,26.4 -50.42,24.5 -51.28,22.54 -55.08,22.87 -59.62,23.27 -63.78,23.64 -64.42,21.91 -64.99,20.15 -65.49,18.36 -61.91,16.21 -58,13.87 -54.73,11.9 -55.18,9.84 -55.51,7.73 -55.72,5.59 -59.43,4.74 -63.88,3.71 -67.94,2.78 -67.98,1.86 -68,0.93 -68,-0 -68,-0.93 -67.98,-1.86 -67.94,-2.78 -63.88,-3.71 -59.43,-4.74 -55.72,-5.59 -55.51,-7.73 -55.18,-9.84 -54.73,-11.9 -58,-13.87 -61.91,-16.21 -65.49,-18.36 -64.99,-20.15 -64.42,-21.91 -63.78,-23.64 -59.62,-23.27 -55.08,-22.87 -51.28,-22.54 -50.42,-24.5 -49.45,-26.4 -48.37,-28.23 -50.87,-31.11 -53.87,-34.55 -56.6,-37.69 -55.58,-39.24 -54.49,-40.73 -53.34,-42.18 -49.5,-40.55 -45.3,-38.77 -41.79,-37.28 -40.37,-38.87 -38.87,-40.37 -37.28,-41.79 -38.77,-45.3 -40.55,-49.5 -42.18,-53.34 -41.55,-53.84 -40.91,-54.33 -40.26,-54.81 -39.42,-55.43 -38.56,-56.03 -37.69,-56.6 -34.55,-53.87 -31.11,-50.87 -28.23,-48.37 -26.4,-49.45 -24.5,-50.42 -22.54,-51.28 -22.87,-55.08 -23.27,-59.62 -23.64,-63.78 -21.91,-64.42 -20.15,-64.99 -18.36,-65.49 -16.21,-61.91 -13.87,-58 -11.9,-54.73 -9.84,-55.18 -7.73,-55.51 -5.59,-55.72 -4.74,-59.43 -3.71,-63.88 -2.78,-67.94 -1.93,-67.98 -1.08,-68 -0.23,-68 L 0,-68 C 0.93,-68 1.86,-67.98 2.78,-67.94 Z M 0,-36 C -6.22,-36 -12.07,-34.42 -17.18,-31.65 -28.39,-25.55 -36,-13.66 -36,-0 -36,19.88 -19.88,36 0,36 19.88,36 36,19.88 36,-0 36,-19.88 19.88,-36 0,-36 Z M 0,-36" transform="rotate(-38)" /> </g> <g id="gears-smallGroup" transform="translate(62, 137)" > <path id="gears-smallGear" stroke="none" fill="rgb(115, 152, 218)" d="M 2.12,-37.94 L 2.45,-37.92 C 3.29,-34.48 4.2,-30.71 4.97,-27.56 6.23,-27.34 7.46,-27.03 8.65,-26.64 9.87,-26.24 11.04,-25.77 12.18,-25.22 14.65,-27.32 17.61,-29.83 20.31,-32.12 21.69,-31.25 23.02,-30.28 24.27,-29.24 22.92,-25.96 21.45,-22.37 20.22,-19.37 21.97,-17.55 23.47,-15.49 24.68,-13.24 27.91,-13.49 31.78,-13.78 35.32,-14.05 35.92,-12.54 36.42,-10.99 36.83,-9.39 33.81,-7.53 30.5,-5.49 27.75,-3.79 27.91,-2.55 28,-1.29 28,-0 28,1.29 27.91,2.55 27.75,3.79 30.51,5.49 33.81,7.53 36.83,9.39 36.42,10.99 35.92,12.54 35.32,14.05 31.78,13.78 27.91,13.49 24.68,13.24 23.47,15.49 21.97,17.55 20.22,19.37 21.45,22.37 22.92,25.96 24.27,29.24 23.02,30.28 21.69,31.25 20.31,32.12 17.61,29.83 14.65,27.32 12.18,25.22 9.93,26.31 7.51,27.11 4.97,27.56 4.2,30.71 3.29,34.48 2.45,37.92 1.64,37.97 0.82,38 -0,38 -0.82,38 -1.64,37.97 -2.45,37.92 -3.29,34.48 -4.2,30.71 -4.97,27.56 -7.51,27.11 -9.93,26.31 -12.18,25.22 -14.65,27.32 -17.61,29.83 -20.31,32.12 -21.69,31.25 -23.02,30.28 -24.27,29.24 -22.92,25.96 -21.45,22.37 -20.22,19.37 -21.97,17.55 -23.47,15.49 -24.68,13.24 -27.91,13.49 -31.78,13.78 -35.32,14.05 -35.92,12.54 -36.42,10.99 -36.83,9.39 -33.81,7.53 -30.5,5.49 -27.75,3.79 -27.91,2.55 -28,1.29 -28,-0 -28,-1.29 -27.91,-2.55 -27.75,-3.79 -30.51,-5.49 -33.81,-7.53 -36.83,-9.39 -36.42,-10.99 -35.92,-12.54 -35.32,-14.05 -31.78,-13.78 -27.91,-13.49 -24.68,-13.24 -23.47,-15.49 -21.97,-17.55 -20.22,-19.37 -21.45,-22.37 -22.92,-25.96 -24.27,-29.24 -23.02,-30.28 -21.69,-31.25 -20.31,-32.12 -17.61,-29.83 -14.65,-27.32 -12.18,-25.22 -9.93,-26.31 -7.51,-27.11 -4.97,-27.56 -4.2,-30.71 -3.29,-34.48 -2.45,-37.92 -1.64,-37.97 -0.82,-38 0,-38 0.71,-38 1.42,-37.98 2.12,-37.94 Z M 0,-14 C -7.73,-14 -14,-7.73 -14,-0 -14,7.73 -7.73,14 0,14 7.73,14 14,7.73 14,-0 14,-5.48 10.85,-10.22 6.27,-12.52 4.38,-13.47 2.25,-14 0,-14 Z M 0,-14" transform="rotate(-3)" /> </g> </svg> Would it be easier to do the mouse hit-test and interaction in Javascript instead of using Snap?
EDIT: SEE LOWER DOWN FOR IMPROVED METHOD Firstly, it may be easier to move the transform from the group in the 2nd example to the path. Then you can add any rotation transforms to the group without getting too messy. Eg. <g id="gears-largeGroup" > <path transform="translate(149.59, 96.41)" id="gears-largeGear"..... Once you have your SVG, you can select it in Snap with the Snap.select() method. This takes a css selector. So we can find one with Snap.select('#gears-smallGroup') Now we have our gear, we can add a drag handler to it. Snap.select('#gears-smallGroup').drag( dragRotate, dragStart ) So we just need to write our handler. Firstly we want to store the start of the drag (see updated solution at end, which is better), so we can write our startRotate function. This just stores an x,y object alongside the element. function dragRotateStart( x, y ) { this.data('oxy', { x: x, y: y }) } Then we can write the main drag rotation handler. This takes the original start position oxy stored above, and adds it to the drags delta increments that is passes over. Then we uses Snaps angle() method to calculate the angle between two points. One the x,y we just had, and the other the center of the element. Now we have the angle, we can just do a rotation transform. Snap can use a shortform for a transform (r=rotation, t=translate, s=scale and r&s with Snap will transform from their centers unless specified). So this becomes function dragRotate( dx, dy, x, y ) { this.transform('r' + Snap.angle( this.getBBox().cx, this.getBBox().cy, dx + this.data('oxy').x, dy + this.data('oxy').y ) ); } So whole code... Snap.select('#gears-smallGroup').drag( dragRotate, dragRotateStart ) Snap.select('#gears-largeGroup').drag( dragRotate, dragRotateStart ) function dragRotate( dx, dy, x, y ) { this.transform('r' + Snap.angle( this.getBBox().cx, this.getBBox().cy, dx + this.data('oxy').x, dy + this.data('oxy').y ) ); } function dragRotateStart( x, y ) { this.data('oxy', { x: x, y: y }) } jsfiddle Now you have this, you can fiddle with it, so that one will rotate the other. You may also have to check the starting points after a redrag depending on where your new drag starts from. (quick stab at making smaller gear rotate bigger one jsfiddle) UPDATED AND IMPROVED VERSION: Extending this further, you will need to adapt it, to take into account both the starting rotation (also if it's pre-rotated) and the starting angle in the drag, we only want the difference in angles, so it doesn't reset. So we can store out starting bits. What angle was the start of the drag at (I've set default to 10 as the image dot was offset by about 10deg). What was the old rotation of the element. function dragRotateStart(x, y) { this.data('startingAngle', Snap.angle(this.getBBox().cx, this.getBBox().cy, x, y)); this.data('startingRotation', this.data('rotation')||10) } And then calculate the correct rotation from the starting rotation, plus the angle difference, storing it as we go (so if we drag again later, we know what its last rotation was)... function dragRotate(dx, dy, x, y) { var angleDiff = Snap.angle(this.getBBox().cx, this.getBBox().cy, x, y) - this.data('startingAngle') var newRotation = angleDiff + +this.data('startingRotation'); this.data('rotation', newRotation) this.transform('r' + newRotation); } jsfiddle
Can a path have 2 different fill colours at a certain point?
I have an area chart. I would like it to change the fill colour at a certain point along the x axis. Example: If a value is greater than a certain value, change the fill colour of the path from this point on. I have been trying the following: .attr("fill", function (d, i) { if (d.timeFrom < d.beforePredictedDate ){ d3.select(this).style("fill", function (d,i) { return "purple" }); } else{ d3.select(this).style("fill", function (d,i) { return "green" }); } } The ideal outcome would produce something that enables the following:
As Robert said, you can make a fill like that using a linearGradient that starts and ends at the colour boundary. <svg> <defs> <linearGradient id="grad"> <stop offset="70%" stop-color="black"/> <stop offset="70%" stop-color="limegreen"/> </linearGradient> </defs> <circle cx="150" cy="75" r="75" fill="url(#grad)"/> </svg> But for general paths, working out where to position the gradient stops may end up being a pain. So it is probably simpler and better in most cases to use two paths - as Robert said.