Animated Bar Chart with SVG and CSS (D3 & svelte) - d3.js

I want to make an animated bar chart with scroller. I already made the scroll and the data interactivity, but I stuck at the transition.
I use the CSS transition on each <g> element. While it's animating and moving toward the right direction, they start at nowhere/or center(?), but not from the last position. I made the data key consectetur to be always the largest every time, so it is supposed to be always on top of the chart. But it's always animating from the center or the bottom, not staying on top.
<svg>
{#each data as d}
<!--translate Y reactive to data changes -->
<g class="gBar" transform="translate(0 {d.id * 10})">
<text>Lorem ipsum</text>
<!--rect width reactive to data changes -->
<rect x="0" y="0"
width={d.val/max * w * 0.5}
height="10"
fill="black"/>
</g>
{/each}
</svg>
<style>
/* add transition to the gBar class */
.gBar {
transition:transform 1s;
}
</style>
I tried the svelte transition directive, but a bit confused to trigger it with the index change by the scroller. (The example of transition directive is using boolean checkbox)
This is what I already made:
https://svelte.dev/repl/099ac842d7a54ab7a674545f8dc6d622?version=3.53.1

Related

How to move some tags inside another tag with d3?

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>

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');

Nested SVG viewBox and preserveAspectRatio to align <image>

I am constructing an svg using D3.js. Inside it I am using <image> to place two types of images img1 and img2 where both have same width W but img1 is shorter than img2 i.e. H1<H2.
They should be placed within the outer svg in a rectangle with dimensions WxH2 at position (x,y) and the shorter one (img1) should be aligned vertically at the bottom of this rectangle like this:
This sounds like a job for viewBox & preserveAspectRatio so I tried creating a nested <svg> element with dimensions WxH2 (the larger of the 2 heights) at position (x,y) and depending on the image dynamically added each time, the layout (here shown with both images) would be:
Sample code with W=35, H1=19, H2=88, x=100, y=200:
```
<svg...> <!--outer SVG-->
<svg width="35" height="88" x="100" y="200" viewBox="0 0 35 88" preserveAspectRatio="xMidYMax meet"> <!-- nested SVG-->
<image xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="img1_OR_img2" width="35" height="CORRESPONDING_img_HEIGHT"></image>
</svg> <!-- end of nested SVG-->
</svg> <!-- end of outer SVG-->
```
The problem is that the height of the nested svg is ignored and it's the height of the <image> that defines the height of the rectangle. So no matter what vertical alignment I ask for through preserveAspectRatio in the nested svg, in the case of the shorter image img1 it will not make a difference since the rectangle is already the height of the img1. If I have <image> always have the larger height H2, then the smaller image img1 appears vertically aligned in the middle.
Should I be using a whole different substructure instead of a nested svg or am I misusing the viewBox/preserveAspectRatio combo?
Note: regarding the topic viewBox & preserveAspectRatio, I used this article which is the best I've found online so far (kudos to Sara for an amazing article).
Your problem is that you are changing the heights. In both cases you should be using 88 for "CORRESPONDING_img_HEIGHT". You need to use the same viewport for both images and let SVG position the image within the viewport according to the preserveAspectRatio setting.
In fact, you don't need to be using nested SVGs. The <image> element also has a preserveAspectRatio attribute.
<svg xmlns:xlink="http://www.w3.org/1999/xlink">
<image x="100" y="200" width="35" height="88" xlink:href="img1" preserveAspectRatio="xMidYMax meet"/>
<image x="100" y="200" width="35" height="88" xlink:href="img2" preserveAspectRatio="xMidYMax meet"/>
</svg>

SVG animation does not set attributes as expected

I am trying to animate the changing of the viewport of an SVG element. When a particular ellipse in the SVG is clicked the viewport is changed so that it is zoomed in on the ellipse. From my interpretation of the W3C animate specification, when fill="freeze" is used, the value in the to attribute will stay. However, when I use console.log("after animation: "+canvas.getAttribute("viewBox")); the viewBox is the same as before (the animation does zoom in). For extra info, the <animate> is added when the ellipse is clicked. This is the code for how the animation is added to the <svg>;
var canvassnap=Snap(canvas);
var animation = '<animate id="smoothpan" attributeName="viewBox" begin="0s" dur="'+duration+'ms" from="0 0 1280 720" to="'+minX+' '+minY+' '+(maxX-minX)+' '+(maxY-minY)+'" fill="freeze" />';
var parse = Snap.parse(animation);
canvassnap.add(parse);
Do I not understand the specification properly?
SVG has two values for each attribute, the base value which you can get either with getAttribute or via element.viewBox.baseVal and the animated value which you can get via element.viewBox.animVal.
If an element is not the subject of SMIL animation then animVal == baseVal.
SMIL animation only affects the animVal and it's the animVal that is used for rendering.

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