I'm trying to
import { transition } from 'd3-transition'
and add an update transition to this code, but I can't figure it out how to use the transition library separately.
import { select } from "d3-selection";
//...
const binding = select(host.svg)
.select("#points")
.selectAll("circle.point")
.data(data);
const enter = binding.enter();
enter
.append("circle")
.attr("class", "point")
.attr("cx", (d) => projection(d)[0])
.attr("cy", (d) => projection(d)[1])
.attr("r", (d) => radius(d.count));
I realize I can just import the whole d3 library, but I'm doing it this way intentionally.
There are two questions here:
how to import transition with ES6?
how to add an update transition?
To import d3-transition, your import is correct:
import { transition } from 'd3-transition'
To add an update transition, you can use selection.join(). selection.join() takes a function per selection: one for the enter, update and exit selection.
To track update changes, note that you need to provide a key function to D3, otherwise it won't be able to tell what changed. A key function will identify uniquely each of your data.
Finally, use selection.call() to avoid breaking the method chain.
You can see the full example at the link.
svg
.selectAll("rect")
// Key function to track updates
.data(data, d => d)
// https://observablehq.com/#d3/selection-join
.join(
// Enter selection handler
enter =>
enter
.append("rect")
.attr("width", 100)
.attr("height", d => d.value)
.attr("x", (d, i) => 150 * i) // spacing
.attr("fill", "red"),
// Update selection handler
update =>
update.attr("fill", "green").call(update =>
update
.transition()
.duration(3000)
.attr("height", d => d.value)
)
);
Example on CodeSandbox. Make sure to refresh the browser to see the transition.
Related
I am creating a line chart with D3.js. The line should appear over time while new data points are calculated 'on the fly' (i.e. the data array constantly grows). I have the function that does the data calculation, as well as the updateLine() function you see below, in a setInterval(). My problem is, that this function creates a new svg path for every newly added data point, resulting in a huge number of <path> elements.
function updateLine() {
canvas
.append('path')
.datum(data)
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1.5)
.attr('d', d3.line()
.x(function(d, i) {return xSc(d.x)})
.y(function(d, i) {return ySc(d.z)})
)
}
How can I 'extend' the existing path with the new data points?
I found an answer:
Clearly, the code above appends a new <path> every time the function is called. To avoid a <path> 'flood' in the DOM, I found two options:
Option 1
Before appending a new path, select the old path, that lacked the new data points, and remove it by calling remove(). In order to avoid selecting the wrong path, I used an ID selector.
function updateLine() {
canvas
.selectAll('#myPath').remove();
canvas
.append('path')
.datum(data)
.attr('id', '#myPath')
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1.5)
.attr('d', d3.line()
.x(function(d, i) {return xSc(d.x)})
.y(function(d, i) {return ySc(d.z)})
)
}
Option 2 (more elegant, I think)
Use a selectAll() to select the path, bind the data to the selection and call a join() on the selection. In the first call of updateLine(), the path is created (no SVG path exists and we have a nonempty enter selection to which a SVG path is appended and all attributes are set). In the following calls, the path exists in the DOM and the newly updated data array is bound to it. Thus the enter selection is empty and the update selection gets relevant, where we update the path with the new data.
function updateLine() {
canvas.selectAll('#myPath')
.data([data])
.join(
function(enter) {
console.log('Enter selection:');
console.log(enter);
return enter
.append('path')
.attr('id', 'myPath')
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1.5)
.attr('d', d3.line()
.x(function(d, i) {return xSc(d.x)})
.y(function(d, i) {return ySc(d.z)})
);
},
function(update) {
console.log('Update selection:');
console.log(update);
return update
.attr('d', d3.line()
.x(function(d, i) {return xSc(d.x)})
.y(function(d, i) {return ySc(d.z)})
);
}
);
}
A couple notes regarding the code of option 2:
It is important to use a selectAll() and not just a select() here,
since in the first call of the function, no <path> exists. select() would select the first match which remains empty in this case.
I call data([data]) and thus perform a join of data points in the data array with SVG elements. datum() would, to my understanding, not perform a join, however, this is important here, as we rely on the update selection.
Passing the data array as an array again to data([data]) causes a data bind of all data points to the one path element, which is exactly what we want here.
Could anyone please help me with the update pattern? For now this is what I get when I give my graph React component new data... the old one stays...
This is where I have my code https://github.com/danieltkach/react-d3-integration
I've been reading about it an I understand I have to do "stuff" in here... replace enter with join() ? And then do the 3 update functions inside? ...
// SVGs creation ---
const linkSvg = svgRef.current
.selectAll('path')
.data(LINKS, d=>d.agentName)
// .enter()
.join(
enter => enter.append('path')
.attr('stroke', d => !d.weight ? "gray" : linkStyles.onlineColor)
.attr('stroke-width', d => linkWeightScale(d.weight))
.attr('stroke-dasharray', d => {
if (!d.weight) return linkDashedScale(d.weight)
}),
update => update,
exit => exit.call(exit=>exit).remove()
)
I think your problem is here on GraphGenerator.jsx:
svgRef.current = d3.select('#viz')
.attr('width', svgWidth)
.attr('height', svgHeight)
.append('g') // a new group is appended
You're appending a new g element each time. That new element has no data, so nothing is removed or updated in the join. If you remove the .append('g') it should work.
EDIT:
Furthur down in your node creation you use:
const circles = svgRef.current
.selectAll()
.data(NODES)
.join("g");
You aren't selecting anything, so nothing can be updated. Use this there:
const circles = svgRef.current
.selectAll('.node')
.data(NODES)
.join("g")
.classed('node',true)
I am doing a number of transitions when my bar chart renders.
After these transitions have completed, I would like the values to render.
I am trying to use d3 transition.end but it looks like the code has changed from previous versions - I am using v6.
The code below runs without any delay - it doesn't wait for the transition to complete before invoking the function.
I have also tried .end(renderValuesInBars(data, metric, countryID, measurements) ) but the same thing happens.
Where am I going wrong?
function renderVerticalBars(data, measurements, metric, countryID) {
let selectDataForBarCharts = d3.select("svg")
.selectAll("rect")
.data(data, d => d[countryID])
selectDataForBarCharts
.enter()
.append("rect")
.attr('width', measurements.xScale.bandwidth())
.attr("height", 0)
.attr('y', d => measurements.yScale(0))
.merge(selectDataForBarCharts)
.transition().delay(500)
.attr("transform", `translate(0, ${measurements.margin.top})`)
.attr('width', measurements.xScale.bandwidth())
.attr('x', (d) => measurements.xScale(d[countryID]))
.transition()
.ease(d3.easeLinear)
.duration(setSpeed())
.attr("height", d => measurements.innerHeight - measurements.yScale(d[metric]))
.attr("y", (d) => measurements.yScale(d[metric]))
.attr("fill", d => setBarColor(d))
.on("end", renderValuesInBars(data, metric, countryID, measurements) )
selectDataForBarCharts.exit()
.transition().duration(500).attr("height", 0).attr("y", d => measurements.yScale(0)).remove()
}
Note that the .on("end", ...) method takes a callback for the second argument, which is executed when the transition ends. The code you posted is not passing a callback, but already evaluating the renderValuesInBars function at the moment of declaration. Instead, you want to pass a callback that tells d3 that the evaluation should occur at a later time (in that case, after the transition)
Instead of:
.on("end", renderValuesInBars(data, metric, countryID, measurements))
You can pass a callback that evaluates the function:
on("end", ( ) => renderValuesInBars(data, metric, countryID, measurements))
That way you're passing a callback that says "at the end of the transition, evaluate renderValuesInBars"
Has been changed from version 4.0
Previously it was:
element.each("end", callback)
Now it is:
element.on("end", callback)
A D3 bubblechart. Group and position svg:circles and svg:text elements
the function render() creates an svg element, circle and text as usual. This function includes .exit.remove update patterns.
runSimulation() is executed after page opening and a createChart() function.
click on a circle executes runSimulation() again, removing the circle
with .exit().remove() etc.
Simplified code:
fundtion render (){
const nodeEnter = this.nodeElements
.enter()
.append('svg:svg');
nodeEnter
.append('circle')
.on('click',runSimulation);
const textEnter = this.textElements
.enter()
.append('svg:text');
}
this.runSimulation(){
this.render();
function ticked(){
this.nodeElements
.attr('cx', cxValue)
.attr('cy', cyValue):
}
this.simulation.nodes.on.('tick',ticked);
}
On the first run the cx and cy attributes are appended to the svg:svg while the circles do not have the attributes and everything is rendered in the top left corner ( also with using svg:g)
on the click action the runSimulation is executed a second time; now the circle gets the cx and cy attributes attached and all elements move into the expected position.
-I am looking for a way to get the cx cy attributes to the circle on the first rendering so that the parent elements do not cluster at x=0 y =0, or to get x and y to svg:svg; the shown pattern is not working and I appreciate your help.
this.nodeElements = this.nodeGroup.selectAll('g')
.data(this.data, node => node.nodeName);
this.nodeElements.exit().remove();
const nodeEnter = this.nodeElements
.enter()
.append('g');
nodeEnter
.append('circle')
.attr('fill', node => this.color(node.familyType))
.attr('r', function (node) {
return (node.nodeName.length > 10) ? "75" : node.nodeName.length*6;
})
.on('click', this.runSimulation);
let wrap = (text) => {
text.each(function() {
let text = d3.select(this),
const textnode = nodeEnter
.append('text')
.attr('id', 'text')
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('dy', '0.1em')
.attr('font-size', '1.2em');
this.settings.getValue('option1').then((option) => {
if (option === true) {
let type = node => node.nodeName;
textnode
.text(type);
}
if (option === false) {
let type = node => node.otherName;
textnode
.text(type);
}
})
Thank you for your answer, Steven. runSimulation executes a tick function that does add the proper d.x and d.y now. I am not sure why it did not work - ionic lifecycle events ?
The effect I'm going for in a map based visualisation is:
Red circles on a map represent data (things at locations)
When something happens at a location, we briefly (2 seconds) show another, blue circle fading out over the top.
I'm trying to work out how to do this in the D3 paradigm. Specifically: how do you represent the same thing twice?
The problem I run into is that when I try to add the same dataset twice to a given SVG canvas group, nothing gets added. That is, using code like this:
g = svg.append("g");
var feature = g.selectAll("circle")
.data(stations)
.enter().append("circle")
.style("stroke", "black")
.style("opacity", .6)
.style("fill", "red")
.attr("r", function(d, i) { return d.free_bikes; });
var emphasis = g.selectAll("circle")
.data(stations)
.enter().append("circle")
.style("stroke", "black")
.style("opacity", .6)
.style("fill", "blue")
.attr("r", function(d, i) { return d.free_bikes; });
This workaround is ok, but kludgy and potentially limiting:
g2 = svg.append("g");
var emphasis = g2.selectAll("circle")
That is, adding the second group of elements to a different SVG group.
The proper way to do this is to use classes to select the circles (and applying that class when you create them). So you create the features like so:
var feature = g.selectAll("circle.feature")
.data(stations, function (d) { return d.id; } )
.enter().append("circle")
.attr("class", "feature") // <- assign the class
....
Similarly, for the emphasis:
var feature = g.selectAll("circle.emphasis")
.data(stations, function (d) { return d.id; } )
.enter().append("circle")
.attr("class", "emphasis") // <- assign the class
....
I've finally (sort of) figured it out. The two sets of data are treated as one because they share the same key, according to the rules of D3 constancy. So an easy way around is to give each set a key that can't overlap:
var feature = g.selectAll("circle")
.data(stations, function (d) { return d.id; } )
.enter().append("circle")
.style("stroke", "black")
.style("opacity", .6)
.style("fill", "red")
.attr("r", function(d, i) { return d.free_bikes * 1; });
var emphasis = g.selectAll("notathing")
.data(stations, function (d) { return d.id + " cannot possibly overlap"; } )
.enter().append("circle")
.style("stroke", "black")
.style("opacity", .6)
.style("fill", "blue")
.attr("r", function(d, i) { return d.free_bikes * 1; });
The only slight quirk is I have to modify the second selector (g.selectAll("notathing")) so it doesn't match any of the circles created by the first one.