JoeBe
JoeBe

Reputation: 1290

How to rearrange the DOM order of svg elements based on their data values in a dynamically changing d3 vis?

I have a json dataset which I dynamically visualize in a similar way as this example using the d3.js library.

However, I would like the bars that change their position within the chart to a lower position to be located "behind" the ones that are at higher positions. For this I would like to set up a z-index based on the data (bars with low values have lower z-index and thus would be located behind the ones with a larger value). Unfortunately, SVG uses document order as a z-index, which is why I would need to reorder the bars (the chartRow variable in the example) inside the document each time the chart changes.

Is there any way to achieve this goal?

Upvotes: 1

Views: 896

Answers (1)

Andrew Reid
Andrew Reid

Reputation: 38151

Reorder Elements in the DOM

To reorder elements in the DOM with d3 you can use selection.sort and selection.order:

// Sort & then order:
selection.sort(function(a,b) {
  return a.property - b.property; 
})
.order();

Selection.sort sorts a selection based on the bound data, a and b each represent a datum bound to a single element. The resulting behavior is essentially the same as Array.sort, but rather than using an array, you are sorting elements in a selection using their bound data. The content of the function provided to selection.sort (the compare function) determines sorting order:

The compare function, which defaults to ascending, is passed two elements’ data a and b to compare. It should return either a negative, positive, or zero value. If negative, then a should be before b; if positive, then a should be after b; otherwise, a and b are considered equal and the order is arbitrary. (docs)

Once we have a sorted selection we can apply selection.order(), which re-inserts each element into the DOM based on the selection's order. Remember that elements drawn first can be covered by elements drawn later - first in the selection means potentially underneath everything.


So if we had something like this, where overlapping bars are drawn sequentially based on the order of a data array:

enter image description here

var bars = [
  {value: 100},
  {value: 50},
  {value: 200},
  {value: 70},
  {value: 40},
  {value: 120}
];

var height = 200;
var width = 500;

var svg = d3.select("body")
  .append("svg")
  .attr("height",height)
  .attr("width",width);

var bars = svg.selectAll("rect")
  .data(bars)
  .enter()
  .append("rect")
  .attr("width", 45)
  .attr("height", function(d) { return d.value; })
  .attr("y", function(d) { return height-d.value; })
  .attr("x", function(d,i) { return i * 30 + 50; })
  .attr("fill", function(d,i){ return d3.schemeCategory10[i]; })
  .attr("stroke","black")
  .attr("stroke-width",1);


// Order the bars:
bars.sort(function(a,b) {
  return b.value - a.value;
}).order();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

We can pull up the small bars, say, for greater visibility with a sort and order:

enter image description here

var bars = [
  {value: 100},
  {value: 50},
  {value: 200},
  {value: 70},
  {value: 40},
  {value: 120}
];

var height = 200;
var width = 500;

var svg = d3.select("body")
  .append("svg")
  .attr("height",height)
  .attr("width",width);

var bars = svg.selectAll("rect")
  .data(bars)
  .enter()
  .append("rect")
  .attr("width", 45)
  .attr("height", function(d) { return d.value; })
  .attr("y", function(d) { return height-d.value; })
  .attr("x", function(d,i) { return i * 30 + 50; })
  .attr("fill", function(d,i){ return d3.schemeCategory10[i]; })
  .attr("stroke","black")
  .attr("stroke-width",1);

  
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>


Reposition based on order

As you can see above, this doesn't reset the x or y attribute of each bar to reflect the ordering on the z (as in your example). As in my basic example above, we can reset the x values based on i (this can be done by rescaling too, but is enough to be a separate question and answer):

var bars = [
  {value: 100},
  {value: 50},
  {value: 200},
  {value: 70},
  {value: 40},
  {value: 120}
];

var height = 200;
var width = 500;

var svg = d3.select("body")
  .append("svg")
  .attr("height",height)
  .attr("width",width);

var bars = svg.selectAll("rect")
  .data(bars)
  .enter()
  .append("rect")
  .attr("width", 45)
  .attr("height", function(d) { return d.value; })
  .attr("y", function(d) { return height-d.value; })
  .attr("x", function(d,i) { return i * 30 + 50; })
  .attr("fill", function(d,i){ return d3.schemeCategory10[i]; })
  .attr("stroke","black")
  .attr("stroke-width",1)
  
bars = bars.sort(function(a,b) {
  return a.value - b.value;
}).order();

bars.transition()
  .attr("x", function(d,i) { return i * 30 + 50; })
  .delay(500)
  .duration(2000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>


See also: This question and answer which has a lot of similarities to the example you have: sorting and ordering are used while transitioning both length of bars and bar order.

Upvotes: 3

Related Questions