Quantcast
Channel: User i alarmed alien - Stack Overflow
Viewing all articles
Browse latest Browse all 42

Answer by i alarmed alien for Do d3 elements need to be redefined with repeated code?

$
0
0

The simple answer to your question is "no, elements do not need to be redefined with repeated code." The longer answer (which I will try to keep short) concerns the d3 enter / update / exit paradigm and object constancy.

There is already a lot of documentation about d3's data binding paradigm; by thinking about data bound to DOM elements, we can identify the enter selection, new data/elements; the update selection, existing data/elements that have changed; and the exit selection, data/elements to be deleted. Using a key function to uniquely identify each datum when joining it to the DOM allows d3 to identify whether it is new, updated, or has been removed from the data set. For example:

var data = [{size: 8, id: 1}, {size: 10, id: 2}, {size: 24, id: 3}];var nodes = svg.selectAll(".node").data(data, function (d) { return d.id });// deal with enter / exit / update selections, etc.// later onvar updated = [{size: 21, id: 1}, {size: 10, id: 4}, {size: 24, id: 3}];var nodes_now = svg.selectAll(".node").data(updated, function (d) { return d.id });// nodes_now.enter() will contain {size:10, id: 4}// nodes_now.exit() will contain {size:10, id: 2}

Again, there is a lot of existing information about this; see the d3 docs and object constancy for more details.

If there are no data/elements in the chart being updated--e.g. if the visualisation is only being drawn once and animation is not desired, or if the data is being replaced each time the chart is redrawn, there is no need to do anything with the update selection; the appropriate attributes can be set directly on the enter selection. In your example, there is no key function so every update dumps all the old data from the chart and redraws it with the new data. You don't really need any of the code following the transformations that you perform on the enter selection because there is no update selection to work with.

The kinds of examples that you have probably seen are those where the update selection is used to animate the chart. A typical pattern is

// bind data to elementsvar nodes = d3.selectAll('.node').data( my_data, d => d.id )// delete extinct datanodes.exit().remove()// add new data itemsvar nodeEnter = nodes.enter().append(el) // whatever the element is.classed('node', true).attr(...) // initialise attributes// merge the new nodes into the existing selection to create the enter+update selection// turn the selection into a transition so that any changes will be animatedvar nodeUpdate = nodes.merge(nodesEnter).transition().duration(1000)// now set the appropriate values for attributes, etc.nodeUpdate.attr(...)

The enter+update selection contains both newly-initialised nodes and existing nodes that have changed value, so any transformations have to cover both these cases. If we wanted to use this pattern on your code, this might be a way to do it:

  // use the node size as the key function so we have some data persisting between updates  var node = svg.selectAll(".node").data(data, d => d.size)  // fade out extinct nodes  node    .exit()    .transition()    .duration(1000)    .attr('opacity', 0)    .remove()  // save the enter selection as `nodeEnter`  var nodeEnter = node    .enter()    .append("g")    .classed("node", true)    .attr("opacity", 0)    // set initial opacity to 0    // transform the group element, rather than each bit of the group    .attr('transform', d => 'translate('+ d.x +','+ d.y +')')  nodeEnter   .append("circle")    .classed("outer", true)    .attr("opacity", 0.5)    .attr("fill", d => color(d.size))    .attr("r", 0)                     // initialise radius to 0  .select(function() { return this.parentNode; })   .append("circle")    .classed("inner", true)    .attr("fill", d => color(d.size))    .attr("r", 0)                     // initialise radius to 0  .select(function() { return this.parentNode; })   .append("text")    .attr("dy", '0.35em')    .attr("text-anchor", "middle")    .text(d => d.size) // merge enter selection with update selection // the following transformations will apply to new nodes and existing nodes node = node    .merge(nodeEnter)    .transition()    .duration(1000)  node    .attr('opacity', 1)  // fade into view    .attr('transform', d => 'translate('+ d.x +','+ d.y +')')  // move to appropriate location  node.select("circle.inner")    .attr("r", d => d.size)      // set radius to appropriate size  node.select("circle.outer")    .attr("r", d => d.size * 2)  // set radius to appropriate size

Only elements and attributes that are being animated (e.g. the circle radius or the opacity of the g element of new nodes) or that rely on aspects of the datum that may change (the g transform, which uses d.x and d.y of existing nodes) need to be updated, hence the update code is far more compact than that for the enter selection.

Full demo:

var svg = d3.select("svg");d3.select("button").on("click", update);let color = d3.scaleOrdinal().range(d3.schemeAccent);let data;update();function update() {  updateData();  updateNodes();}function updateData() {  let numNodes = ~~(Math.random() * 4 + 10);  data = d3.range(numNodes).map(function(d) {    return {      size: ~~(Math.random() * 20 + 3),      x: ~~(Math.random() * 600),      y: ~~(Math.random() * 200)    };  });}function updateNodes() {  var node = svg.selectAll(".node").data(data, d => d.size)  node    .exit()    .transition()    .duration(1000)    .attr('opacity', 0)    .remove()  var nodeEnt = node    .enter()    .append("g")    .classed("node", true)    .attr("opacity", 0)    .attr('transform', d => 'translate('+ d.x +','+ d.y +')')  nodeEnt    .append("circle")    .classed("outer", true)    .attr("opacity", 0)    .attr("fill", d => color(d.size))    .attr("r", d => 0)   .select(function() { return this.parentNode; }) //needs an old style function for this reason:  https://stackoverflow.com/questions/28371982/what-does-this-refer-to-in-arrow-functions-in-es6 .select(()=> this.parentNode) won't work    .append("circle")    .classed("inner", true)    .attr("fill", d => color(d.size))    .attr("r", 0)   .select(function() { return this.parentNode; })    .append("text")    .attr("dy", '0.35em')    .attr("text-anchor", "middle")    .text(d => d.size) node = node    .merge(nodeEnt)    .transition()    .duration(1000)  node    .attr('opacity', 1)    .attr('transform', d => 'translate('+ d.x +','+ d.y +')')  node.select("circle.inner")    .attr('opacity', 1)    .attr("r", d => d.size)  node    .select("circle.outer")    .attr("opacity", 0.5)    .attr("r", d => d.size * 2)}
<script src="https://d3js.org/d3.v5.min.js"></script><button>Update</button><br><svg width="600" height="200"></svg>

It is worth noting that there are plenty of d3 examples that have a lot of redundant code in them.

So much for keeping this short...


Viewing all articles
Browse latest Browse all 42

Trending Articles