D3: Separating data exit/remove/merge from drawing of elements - javascript

I am drawing some complex interactive SVGs with D3 v4 and running into some problems. My goals are:
Each data element corresponds to a group with multiple SVG shape elements (e.g. <g><circle></circle><circle></circle></g>)
The multiple SVG shape elements have to be drawn in a certain order (because they overlap)
Certain shape elements are updated without data elements being added or removed (e.g. when clicking on a shape, change the shape color)
I am running into trouble because the .data() -> .exit().remove() -> .enter() -> .merge() process requires a specific order and that order conflicts with the necessary draw order as well as the ability to update styles on the fly. This is what I started with, which does not work because of draw order:
function updateGraph() {
let eachNodeG = allNodesG
.selectAll('.eachNodeG')
.data(graphData._nodes, function (d) {
return d._id;
})
eachNodeG.exit().remove();
let eachNodeGEnter = eachNodeG.enter()
.append('g')
.attr("class", "eachNodeG")
eachNodeGEnter
.append('circle')
.classed('interactivecircle', true)
.on('click', function (d) {...})
let eachNodeG = eachNodeGEnter
.merge(eachNodeG)
.style('fill', function (d) {...}) //this is here b/c it needs to change
// when data change (without them being added/removed)
// this must be separate because the background circle needs to change even
// when nodes are not added and removed; but this doesn't work here because
// the circle needs to be in the background
eachNodeG
.append('circle')
.classed('bgcircle', true)
}
I thought maybe I could separate the data update process from the data drawing process entirely, by doing enter() exit() merge() just on the groups containing the data and then drawing everything afterward. But here I run into a different problem: either I remove and re-add all of the shapes on every update (which makes double-clicking difficult and seems like a waste of processing power), or I have to figure out some way to update only the shapes that have changed. Does it using the remove and re-add method looks like this:
// add/remove individual groups based on updated data
let eachNodeG = allNodesG
.selectAll('.eachNodeG')
.data(graphData._nodes)
eachNodeG.exit().remove();
let eachNodeGEnter = eachNodeG.enter()
.append('g')
.attr("class", "eachNodeG")
eachNodeG = eachNodeGEnter
.merge(eachNodeG)
// draw (or remove and re-draw) elements within individual groups
d3.selectAll('.bgcircle').remove()
eachNodeG.append('circle')
.classed('bgcircle', true)
d3.selectAll('.interactivecircle').remove()
eachNodeG.append('circle')
.classed('interactivecircle', true)
.style('fill', function (d) {...})
.on('click',function(d){...})
})
Is there a better way to draw the shapes in order while keeping them updateable?

You could use selection.raise or selection.lower to move circles after they have been created.

Related

Multiline chart, can't get line data

I have a demo here
I have a responsive line chart with multi lines from one data source.
The lines also have circles at each data point.
When the page is resized I want to redraw the graph to change the width.
The circles are working but I can't get the same data for the lines.
I'm creating the actual paths but cant seem to add the data to draw the line.
const dataLine = d3.line()
.x((d) => this.x(d.date) + 0.5 * this.x.bandwidth())
.y((d) => this.y(d.value));
let lines = this.chart.append('g')
.classed('lines', true)
lines.selectAll('.line-group')
.data(data).enter()
.append('g')
.classed('line-group', true)
let linesUpdate = d3.selectAll('.line-group').selectAll("path")
.data(d => d.values)
linesUpdate.enter()
.append('path')
.classed('line', true)
.merge(linesUpdate)
.attr("d", dataLine);
How can I add the data to draw the line
This part of D3's data binding process may be a bit counterintuitive: you have to return an array for the data() method, that's sure... but then each object in the data array will be treated as an individual data! That's why you see no path, because you're actually appending a dozen paths in that group.
The solution is just wrapping the array inside another array. So turn this...
data(d => d.values)
... into this:
data(d => [d.values])
That way, each array will be treated as an individual data point. Then, that data point, which is actually an array of objects, gets passed to the line generator.
Here is your updated code: https://stackblitz.com/edit/multiline-responsive-xbtqzc-fldp7c?file=src/app/bar-chart.ts

Append elements into different containers depending on conditions with D3

I have an array, looking as simple as
var labels = ['label 1', 'label2', ..., 'label n']
Now if I want to put them as visual elements in the chart, I can do like this:
var legendItem = legendArea
.selectAll('g')
.data(labels)
.enter()
.append('g')
.attr({class: 'legend-element'});
legendArea becomes a parent for all the labels now. But, I have a more complex scenario, where I need to put labels not in legendArea directly but create a wrapper g element first inside legendArea, which will then contain a set of labels, depending on some criteria that I get from each label.
As a result, I will have a number of g elements with a set of labels inside of them, one can have 5, another can have 8, any number, as they are not spread evenly.
What I see now, is I need to run a loop through all labels array elements, check if current labels conforms to criteria, create a new wrapper element if needed and then append. But this solution seems to be not D3-style, as in most cases it's possible to do functional style code with D3, not for..loop.
I suspect I can do something more custom here, something like:
var legendItem = legendArea
.selectAll('g')
.data(labels)
.enter()
// Do some unknown D3 magic here to create a new wrapper element and append the label to it.
.attr({class: 'legend-element'});
Please advise on how to do it in D3 fashion.
I would get a list of categories, use that as data for the g elements, and then add the rest with filters:
var cats = svg.selectAll("g").data(categories)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(10," + ((i+1) * 30) + ")"; });
cats.selectAll("text")
.data(function(d) { return data.filter(function(e) { return e.category == d; }); })
.enter().append("text")
.text(function(d) { return d.label; });
This works similar to nested selections, except that the nesting isn't in the data itself (although you can modify the data structures for that as well), but by referencing different data structures.
The first block of code simply adds a g element for each category. The second block of code does a nested selection (and is hence able to reference the data bound to the g element just before). In the .data() function, we take all the data and filter out the items that match the current category. The rest is bog-standard D3 again.
Complete demo here.

d3 Force: Making sense of data binding

I can recreate the following 1000 times and have enough of an understanding to do so. But I'm trying to get my head around a few specific bits that I just 'do', rather than understand:
var w = 900,
h = 500;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.attr("style", "border: 1px solid grey;")
.on("mousemove", fn)
var force = d3.layout.force()
.size([w, h])
.on("tick", tick)
.gravity(0)
.charge(0)
.start()
function fn() {
var m = d3.mouse(this);
var point = {x: m[0], y: m[1]};
d3.select("#output").text(force.nodes().length)
var node = svg
.append("circle")
.data([point])
.attr("cx", function(d) {return d.x})
.attr("cy", function(d) {return d.y})
.attr("r", 0.1)
.transition().ease(Math.sqrt)
.attr("r", 5)
.transition().delay(1000)
.each("end", function() {
force.nodes().shift()
})
.remove()
force.nodes().push(point)
force.start()
}
function tick() {
svg.selectAll("circle")
.attr("cx", function(d) {return d.x})
.attr("cy", function(d) {return d.y})
}
In particular it's the data binding part I'm not sure about.
In function fn() (on mousemove of svg space) we define a new point and we need to do two things with it; push it into force.nodes() so that the x and y coordinates of the point can be manipulated by forces configured in the force layout, and we need to use the coordinates of the point to create and manipulate the visualisation.
So we create the point first off. We then build a circle to represent this point. We push the point into force.nodes() and after a short delay, we remove both the visualisation and the point from the force.nodes() array.
The bit I don't understand is how the visualisation and the point in the array stay "connected"?
Conjecture: The data point is an object which the force layout is constantly updating the x and y properties of. There is a "link" to this object bound to the circle element. The object is therefore easily accessed and used by the circle object, but not without us controlling that process. The circle is defined as having a cx and cy at point of its creation, but we need to keep accessing the underlying data to update its cx and cy?
If that's the case, how is the object "shared" by both force.nodes() and the circle element?
Or am I miles off the mark?
Also I have read a lot of documentation on this but I feel this is something more intrinsic to javascript rather than d3 necessarily, so it's not elaborated on in any literature I've so far read.
The link between the data structures that the force layout updates and the visualization (i.e. the DOM elements) is the tick event handler function. The tick event is generated by the force layout to signify that the force simulation has progressed another step (i.e. tick) and its internal state has changed. This signals that the visualization needs to be updated.
There are two parts to making this link happen. First, the data operated on by the force layout (i.e. the links and nodes) needs to be bound to DOM elements. This is done using the usual .selectAll().data().enter().append() pattern, usually in the initialisation code, sometimes in the tick event handler function. This establishes the link between data and DOM elements.
The second part to this is the code that updates the DOM elements when the force layout changes their positions. This is what happens in the tick event handler function. If you're not adding or removing elements, there's usually no need to rebind data and often you won't see the .selectAll().data() pattern, but only the code that actually updates the positions based on the data already bound to the elements (in your case this works even though you're changing the elements because the data binding happens in the function that updates the data for the force layout as well).
As an experiment, take an arbitrary force layout example and delete the tick event handler function -- you'll see that nothing happens at all even though the force layout is running.

Updating a pie chart with a new data set with more values

I'm relatively new to D3 and have been following a few pie chart tutorials.
Namely the Mike Bostock Tutorials. http://bl.ocks.org/mbostock/1346410
But I have question about a donut chart updating from one data set to another with the 2nd data set having much more values than the first.
I have attempted this numerous times through an update function but no luck, I'll keep it simple and give a hypothetical example , lets say my first data set had 5 values
[1,2,3,4,5]
and my second data set had 10 values
[1,2,3,4,5,6,7,8,9,10]
only 5 values of the new data set would be depicted on the arcs after the dynamic update. It's like the pie is fixed with only 5 arc sections being able to display 5 values of the new dataset.
Any help would be appreciated as its been stumbling around with the idea for awhile!
The key to making it work with data of different size is to handle the .enter() and .exit() selections. This tutorial goes into more detail, but briefly the enter selection represents data for which no DOM elements exist (e.g. in the case where you pass in more data), the update selection (which you're already handling) represents data for which DOM elements exist and the exit selection DOM elements for which no data exists anymore (e.g. when you have more elements to start with).
So in your change function, you would need to do something like this.
function change() {
clearTimeout(timeout);
var path = svg.datum(data).selectAll("path")
.data(pie);
path.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { this._current = d; }); // add the new arcs
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
path.exit().remove(); // remove old arcs
}
This assumes that you're updating your data variable as you suggest above instead of getting a different value from the existing data structure as in the example.
Here I made a simple update that triggers when you click the text above the pie chart: JsFiddle
The main thing happening is all the data is updated when the .on("click") event triggers, so the chart gets updated like so:
d3.select("#update")
.on("click", function (d) {
data = [1,2,3,4,5,6,7,8,9,10];
vis.data([data]);
arc = d3.svg.arc().outerRadius(r);
pie = d3.layout.pie().value(function(d){return d; });
arcs.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice")
.append("svg:path")
.attr("fill", function(d, i){return color(i);}).attr("d", arc);
});

d3: confusion about selectAll() when creating new elements

I am new to d3 and am using 'Interactive Data Visualization for the Web' by Scott Murray (which is great btw) to get me started. Now everything I saw so far works as described but something got me confused when looking at the procedure to create a new element. Simple example (from Scott Murray):
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle");
The name "circle" is used for the selectAll which returns an empty selection (which is ok as I learned). Then circles are appended by putting the same name into the .append. Great!
Now what got me confused was what happens when you want to do the same thing again. So you have a second dataset and want to generate new circles in the same way. Using the same code just replacing the dataset will obviously not work as the selectAll("circle") will not return an empty selection anymore. So I played around and found out that I can use any name in the selectAll and even leave it empty like this: selectAll()
Scott Murrays examples always just use one type (circle, text, etc.) per dataset. Finally I found in the official examples something like
svg.selectAll("line.left")
.data(dataset)
.enter()
.append("line")
.attr ...
svg.selectAll("line.right")
.data(dataset)
.enter()
.append("line");
.attr ...
Now my question: How is this entry in selectAll("ENTRY") really used? Can it be utilized later to again reference those elements in any way or is it really just a dummy name which can be chosen in any way and just needs to return an empty selection? I could not find this entry anywhere in the resulting DOM or object structure anymore.
Thank you for de-confusing me.
What you put in the selectAll() call before the call to .data() really only matters if you're changing/updating what's displayed. Imagine that you have a number of circles already and you want to change their positions. The coordinates are determined by the data, so initially you would do something like
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d; })
.attr("cy", function(d) { return d; });
Now your new data has the same number of elements, but different coordinates. To update the circle positions, all you need to do is
svg.selectAll("circle")
.data(newData)
.attr("cx", function(d) { return d; })
.attr("cy", function(d) { return d; });
What happens is that D3 matches the elements in newData to the existing circles (what you selected in selectAll). This way you don't need to append the circles again (they are there already after all), but only update their coordinates.
Note that in the first call, you didn't technically need to select circles. It is good practice to do so however just to make clear what you're trying to do and to avoid issues with accidentally selecting other elements.
You can find more on this update pattern here for example.

Categories

Resources