How to move some tags inside another tag with d3? - d3.js

I have svg like this:
<svg id="mySvg">
<path></path>
<path></path>
<path></path>
</svg>
But I would like to group all paths in this svg inside the g tag, so it would look like that:
<svg id="mySvg">
<g>
<path></path>
<path></path>
<path></path>
</g>
</svg>
How should I do it?
d3.select("#mySvg").append("g") // -> and move all paths inside g

You can remove elements with selection.remove(). This method will return a selection of the removed elements.
You can also use selection.append() to append those elements. However, selection.append only takes a function or a string. If you supply a function it should return a (single) element/node. We can access the element/node of a selection of one element with selection.node()
This gives us the pattern:
let svg = d3.select('svg')
let path = svg.selectAll('path').remove();
svg.append('g').append(()=>path.node());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<svg>
<path d="M 30 30 C 60 30 50 50 80 50" stroke="black" stroke-width="1" fill="none"></path>
</svg>
However, append() is intended for a single element. If you have many elements, instead you could use selection.each() to add each selected element to a parent g:
let svg = d3.select('svg')
let path = svg.selectAll('path').remove();
let g = svg.append('g');
path.each(function() {
g.append(()=>this);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<svg>
<path d="M 30 30 C 60 30 50 50 80 50" stroke="black" stroke-width="1" fill="none"></path>
<path d="M 30 130 C 60 130 50 150 80 150" stroke="black" stroke-width="1" fill="none"></path>
</svg>
The use of function() and fat arrow notation is intended to ensure the correct this, which is the element we want to add to the parent g
This second snippet will work regardless of whether one or more nodes are selected.

In the past years I laid out some approaches similar to the one proposed by Andrew Reid in his answer: "Can I move an SVG element between SVG groups, in d3.js" and "D3.js how to embed selection into a new element". However, digging deeper into this issue, I found a solution that uses VanillaJS DOM methods and does the whole shebang in a single method call.
The API of an Element knows some methods for adding multiple elements to the DOM at the same time. The element.append() method can be used to elegantly and easily append your path elements to the new group in a single run.
const path = d3.selectAll("path");
d3.select("svg")
.insert("g") // insert the new g at the first position
.node() // get the DOM node
.append( // use the DOM's append, not the D3 one
...path.nodes() // add the nodes from the path selection
);
<script src="https://d3js.org/d3.v7.js"></script>
<svg id="mySvg">
<path></path>
<path></path>
<path></path>
</svg>

Related

Selecting last path of svg with d3.js

I am fairly new in D3.js I want to build a real time collaborating blackboard. One admin client draw paths on svg and other clients receive them. I'm trying to pick last path element from clients side. I walked around last few hours finding several posts as How can I select :last-child in d3.js? but cannot make it work. When i ask:
console.log('svg ='+svg.selectAll("path")
having drawn two paths i get
svg ={"_groups":[{"0":{},"1":{}}],"_parents":[{"__on":[{"type":"click","name":"","capture":false},{"type":"mousedown","name":"drag","capture":false},{"type":"touchstart","name":"drag","capture":false},{"type":"touchmove","name":"drag","capture":false},{"type":"touchend","name":"drag","capture":false},{"type":"touchcancel","name":"drag","capture":false}]}]}
EDIT: my DOM structure
<svg id="myCanvas" width="960" height="500">
<rect fill="#F2EDE4" width="100%" height="100%"></rect>
<path fill="none" stroke="black" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" d="M223.64999389648438,304.25L223.98332722981772,304.5833333333333C224.31666056315103,304.9166666666667,224.98332722981772,305.5833333333333,226.31666056315103,306.75C227.64999389648438,307.9166666666667,229.64999389648438,309.5833333333333,231.14999389648438,310.4166666666667C232.64999389648438,311.25,233.64999389648438,311.25,234.98332722981772,310.4166666666667C236.31666056315103,309.5833333333333,237.98332722981772,307.9166666666667,238.81666056315103,307.0833333333333L239.64999389648438,306.25"></path><path></path></svg>
Thanks in advance
Have you tried something like this?
const lastPath = d3.select('svg>path:last-child')
You should be able to use a string like :last-child in the d3 selection api.
Peter's answer is working.
You get a {"_groups":[[{}]],"_parents":[{}]}, as you said in your comment, because that is a D3 selection, and that's the expected object.
If you want to get the DOM element, simply use the node() function:
const lastPath = d3.select('svg>path:last-child').node();
//getting the DOM element -------------------------^
Here is a demo using your SVG, it will log that empty path, which is the last one (since S.O. snippets freeze — at least in my machine, using Chrome — when trying to log D3 selections, here is a fiddle with the same code):
const lastPath = d3.select('svg>path:last-child').node();
console.log(lastPath)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg id="myCanvas" width="960" height="500">
<rect fill="#F2EDE4" width="100%" height="100%"></rect>
<path fill="none" stroke="black" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" d="M223.64999389648438,304.25L223.98332722981772,304.5833333333333C224.31666056315103,304.9166666666667,224.98332722981772,305.5833333333333,226.31666056315103,306.75C227.64999389648438,307.9166666666667,229.64999389648438,309.5833333333333,231.14999389648438,310.4166666666667C232.64999389648438,311.25,233.64999389648438,311.25,234.98332722981772,310.4166666666667C236.31666056315103,309.5833333333333,237.98332722981772,307.9166666666667,238.81666056315103,307.0833333333333L239.64999389648438,306.25"></path>
<path></path>
</svg>
This is what solved this problem for me. According to d3.selections docs, this is what you should do:
var lastPath = d3.selectAll('svg>path').filter(':last-child');

D3 remove parents of elements returned by exit

The following code will remove old paths:
var join = svg.selectAll('path').data(data, function (d) {
return d.label;
});
join.exit().remove();
Each path element that I am removing is contained within a g element containing everything I want to remove.
<g class="item">
<text class="line-text">Label</text>
<path class="line" d="..." style="stroke: #2ca02c;"></path>
</g>
So what I really want to do here is remove the g element, but calling .remove() only removes the path element. How can I remove the entire g element?
The proper way to do this would be to bind the data to the g elements so that you operate on them. The quick and dirty way to do what you're asking for would be the following.
join.exit().each(function() { d3.select(this.parentNode).remove(); });

Using animateMotion along with keyTimes/keyPoints

I am trying to use non-linear animation rate on an SVG <animateMotion> by using the keyTimes="…" and keyPoints="…" attributes. It does not appear to be working: the animation motion is as linear as can be.
Here's the test file try it!
<svg xmlns="http://www.w3.org/2000/svg" xmlns:x="http://www.w3.org/1999/xlink"
viewBox="0 0 300 200">
<style>
path { stroke:#999 }
circle { fill-opacity:0.5; stroke:black }
</style>
<path id="p" d="M30,160 L270,40" />
<circle id="c" r="5" />
<animateMotion x:href="#c" fill="freeze"
dur="10s"
keyTimes="0;0.1;1"
keyPoints="0;0.9;1">
<mpath x:href="#p" />
</animateMotion>
</svg>
When working the ball should move 90% along the path in the first second, and move the final 10% in the remaining 9 seconds. What do I need to change to get this to work?
I've found another example online that is working correctly, so that I know it's not my OS/browser/version at fault.
(FWIW: Win7x64, Chrome30)
I found my mistake. Even though the default value for calcMode is linear—which is what I want—I didn't read far enough into the spec to see that it's a different default value for <animateMotion> elements.
Adding an explicit calcMode="linear" fixes the problem.
The default calcmode Value for animate Motion is paced not linear;
http://www.w3.org/TR/SVG/animate.html#AnimateMotionElement
And, if calcmode = "paced" is specified, any ‘keyTimes’ or ‘keySplines’ will be ignored.
http://www.w3.org/TR/SVG/animate.html#CalcModeAttribute;
That is why you have not got the the desired output...

Append an Entire SVG Element or Series of Elements

Using d3 what is the best way to append a whole series of svg elements to a particular selection. For example...First i append a rect.
tb.append("svg:rect")
.attr("width", "18")
.attr("height", "18")
.attr("y", "9")
.attr("x", (i * 20) + 5)
.attr("rx", "4")
.attr("ry", "4");
Now dependent on options i want to append an icon. The icon could be a series of paths or rects or polygons. but is dynamic and based on options. So i would like to do something like:
tb.append('<path d="M9.826,4.703C8.863,3.057,7.014,2.032,5.001,2.032c-2.012,0-3.861,1.024-4.829,2.671L0,5l0.173,0.299C1.141,6.944,2.99,7.968,5.001,7.968c2.012,0,3.862-1.023,4.825-2.669L10,5L9.826,4.703z M8.451,5.189C7.764,6.235,6.441,6.888,5,6.888c-1.438,0-2.762-0.652-3.453-1.698l-0.124-0.19l0.124-0.188C2.239,3.764,3.562,3.112,5,3.112c1.441,0,2.763,0.651,3.451,1.698l0.125,0.188L8.451,5.189z"/><circle cx="4.999" cy="5" r="1.905"/>');
or
tb.append('<polygon points="8.272,0 5.8,4.166 9,4.166 1.615,10 3.461,5 1,5 3.743,0.007"/>');
But i don't know the element type or varying attributes as it is fed in. Is there a pattern to just insert an entire element as a string? or a better pattern?
EDIT: I just typed up the answer below, but, upon re-reading your question, I'm not sure if, by "dynamic and based on options" you actually meant that the icon type is dependent on the data. Let me know if I misinterpreted, and I'll delete this answer....
Let's say you have dataArray which is an array of objects making up your data.
And then you have:
var selection = d3.selectAll('.icon').data(dataArray)
Then, what it sounds like you're asking is to conditionally create elements of, say, either <rect> or <circle>, depending on your data. Ideally, what you would want –– BUT d3 won't let you do it –– is:
selection.enter()
.append(function(d) { // <-- doesn't work :(
return d.type == 'round' ? 'circle' : 'rect';
})
.attr('class', 'icon');
I.e, the .append() method doesn't accept a function. So, instead, what you could do is this:
selection.enter()
.append('g')
.each(function(d, i) {
var g = d3.select(this); // That's the svg:g we've just created
if(d.type == 'round')
g.append('circle')
else
g.append('rect')
})
.attr('class', 'icon');
What you get in the DOM is something like this (depending of course on what's inside dataArray):
<svg>
<g class="icon">
<circle>
</g>
<g class="icon">
<rect>
</g>
<g class="icon">
<rect>
</g>
<g class="icon">
<circle>
</g>

Calculate path center after transformation matrices have been applied

I made a logo in Inkscape. For learning I wanted to make a wheel shape in the logo rotate by the animation support in SVG.
It was easy to implement the rotation, but it was difficult for me to be able to specify the correct axis of rotation. The shape was a cog wheel and I wanted it to rotate around its center. Trial and error gave that the xy-coordinate (47.1275, 1004.17) (whose components are strangely asymmetric, but I guess that has to do with the transformation matrices Inkscape applies) was a good approximation (see animateTransform tag below), but how would I get that from first principles?
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 321.281 150.799" xmlns:dc="http://purl.org/dc/elements/1.1/">
<g transform="translate(-9.9178912,-891.57237)">
<g transform="matrix(1.9522781,0,0,1.9522781,4.6434311,-1008.1558)">
<animateTransform attributeType="xml" attributeName="transform" type="rotate" from="0 47.1275 1004.17" to="45 47.1275 1004.17" dur="2s" fill="freeze" additive="sum" repeatCount="indefinite" />
<g transform="matrix(0.65043772,0,0,0.65043772,-143.67477,980.4256)" stroke="#666" stroke-miterlimit="4" stroke-dasharray="none" stroke-width="7.68713093" fill="none">
<path stroke-linejoin="miter" d="m293.404-3.51576c-2.73916,0-5.41514,0.287192-8,0.8125v6.1875c-3.47484,0.838872-6.7198,2.18462-9.6875,4l-4.375-4.375c-2.24264,1.48612-4.29226,3.22977-6.1875,5.125s-3.63888,3.94486-5.125,6.1875l4.375,4.375c-1.81538,2.9677-3.16112,6.21265-4,9.6875h-6.1875c-0.5253,2.58486-0.8125,5.26083-0.8125,8s0.2872,5.41515,0.8125,8h6.1875c0.83888,3.47485,2.18462,6.7198,4,9.6875l-4.375,4.375c1.48612,2.24264,3.22976,4.29227,5.125,6.1875s3.94486,3.63888,6.1875,5.125l4.375-4.375c2.9677,1.81538,6.21266,3.16113,9.6875,4v6.1875c2.58486,0.525308,5.26082,0.8125,8,0.8125,2.73916,0,5.41514-0.287192,8-0.8125v-6.1875c3.47484-0.838872,6.7198-2.18462,9.6875-4l4.375,4.375c2.24264-1.48612,4.29226-3.22977,6.1875-5.125s3.63888-3.94486,5.125-6.1875l-4.375-4.375c1.81538-2.9677,3.16112-6.21266,4-9.6875h6.1875c0.5253-2.58485,0.8125-5.26083,0.8125-8s-0.2872-5.41515-0.8125-8h-6.1875c-0.83888-3.47485-2.18462-6.7198-4-9.6875l4.375-4.375c-1.48612-2.24264-3.22976-4.29227-5.125-6.1875s-3.94486-3.63888-6.1875-5.125l-4.375,4.375c-2.9677-1.81538-6.21266-3.16113-9.6875-4v-6.1875c-2.58486-0.525308-5.26084-0.8125-8-0.8125z" stroke-dashoffset="162" stroke="#666" stroke-linecap="butt" stroke-miterlimit="4" stroke-dasharray="none" stroke-width="7.68713093" fill="none"/>
</g>
</g>
</g>
</svg>
From what I've read in the specification I would say the transformation matrices applied are
1.9522781 0 4.6434311
0 1.9522781 -1008.1558
0 0 1
and
0.65043772 0 -143.67477
0 0.65043772 980.4256
0 0 1
Are they applied on the xyz-coordinate (-9.9178912,-891.57237,0) after the translation transformation?
I guess a correct analysis above would get me the top left point of the path described, or maybe the coordinate for the first handle. After that, does one have to parse the path to decide the bounding-box and thereby the center (since it concerns a somewhat circular object) of the path?
Is it all a lesson in not trying to manually do animation on freely created shapes?
I think the transformations will be applied from the innermost outward, so transform="translate(-9.9178912,-891.57237)" will be done last. But you can ignore the other transformations if you put your animation in the innermost region, i.e. within the path itself:
<g transform1>
<g transform2>
<g transform3>
<path d="coordinates">
<animateTransform your transformation here>
</path>
</g>
</g>
</g>
Then you just need to find the centre of your path, which is easy to do in Inkscape, but tricky to do on-the-fly (related question here: programmatically How to get shape width in SVG document using java).
Personally, I'd use a script within the svg so you can use getBBox to find the bounding box of your shape. If you add the following element into your SVG you can make any element with the id="cog" turn about its centre:
<script type="text/ecmascript"><![CDATA[
var svgNS = "http://www.w3.org/2000/svg";
function init(evt)
{
if ( window.svgDocument == null )
{
svgDocument = evt.target.ownerDocument;
}
addRotateTransform('cog');
}
function addRotateTransform(target_id)
{
var element_to_rotate = svgDocument.getElementById(target_id);
var my_transform = svgDocument.createElementNS(svgNS, "animateTransform");
var bb = element_to_rotate.getBBox();
var cx = bb.x + bb.width/2;
var cy = bb.y + bb.height/2;
my_transform.setAttributeNS(null, "attributeName", "transform");
my_transform.setAttributeNS(null, "attributeType", "XML");
my_transform.setAttributeNS(null, "type", "rotate");
my_transform.setAttributeNS(null, "dur", "4s");
my_transform.setAttributeNS(null, "repeatCount", "indefinite");
my_transform.setAttributeNS(null, "from", "0 "+cx+" "+cy);
my_transform.setAttributeNS(null, "to", "360 "+cx+" "+cy);
element_to_rotate.appendChild(my_transform);
my_transform.beginElement();
}
]]></script>
You also need to add onload="init(evt)" as an attribute to the SVG tag. e.g.
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 321.281 150.799"
xmlns:dc="http://purl.org/dc/elements/1.1/"
onload="init(evt)">
This will call the init() function when the SVG is first loaded. The init() function calls addRotateTransform() which find the element with a given id. It then finds the centre of that object using getBBox() and adds an animateTransform method with the relevant centres. You can change the dur attribute which determines the speed of a full rotation.
It might seem like a lot of code, but I think it's the easiest way to determine the centre of a path. It also means to can easily add other rotating elements by add addRotateTransform('whatever-id'); to the init() function.

Resources