d3 / d3-selection

Transform the DOM by selecting elements and joining to data.

Home Page:https://d3js.org/d3-selection

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Inconsistent element iteration on hierarchy during data binding update

bjd183 opened this issue · comments

I am seeing inconsistent/incomplete element iteration after a data binding update. I have reduced the issue to the following code and the console log output that demonstrates the offending behavior. This is based on the Zoomable Circle Pack example but modified to have circle (and text) elements contained in g groups that are transformed rather than each and every potential child element. The child elements, however, are also modified. In this case circle attributes radius and class need updating.

As can be seen in the console log output,

  1. The children property of the data item in the accessor method in a merged circle remains undefined when it should be populated in the second and third passes.
  2. The data item with id=2 is skipped altogether on the final pass.

It occurs to me that I may have an issue with the data join or selection approach. I have experimented with a number of variations based on the docs or other issues but to no avail.

<!doctype html>

<html lang="en">
<head>
  <title>D3 Select</title>
  <meta charset="utf-8"/>
  <style>
  .node-leaf {
    fill: white;
  }

  svg circle {
    fill: indianred;
    opacity: 0.5;
    stroke: white;
  }
  </style>
</head>
<body>

<div id="content">
  <svg height="600" width="600">
    <g transform='translate(0, 0)' style='background: rgb(131, 38, 129)'>
    </g>
  </svg>
</div>

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

<script>

function toJSON(d) {
  var o = {id: d.id, parent: d.data.parent, children: Boolean(d.children), name: d.data.name};
  return JSON.stringify(o);
}

function nodesToJSON(nodes) {
  for (let i = 0; i < nodes.length; i++) {
    console.log('hierarchy node: ' + toJSON(nodes[i]));
  }
}

var stratify = d3.stratify()
  .id(function(d) {return d.id;})
  .parentId(function(d) {return d.parent;});

const diameter = 600;

function updatePack(nodes) {
  console.log('updatePack');
  var root = stratify(nodes)
    .sum(function (d) {return 1;});
  var pack = d3.pack()
    .size([diameter, diameter])
    .padding(15);
  var packroot = pack(root);
  var descendants = packroot.descendants();
  nodesToJSON(descendants);
  var g = d3.select('#content svg g').selectAll('g')
    .data(descendants, function(d) {return d.id;});

  var circle = g.selectAll('circle');

  var gEnter = g.enter()
    .append('g');

  var circleEnter = gEnter.append('circle');

  var gMerge = gEnter.merge(g)
    .attr('transform', function(d) {
      console.log('merge g: ' + toJSON(d));
      return 'translate(' + [d.x, d.y] + ')';
    });

  var circleMerge = circleEnter.merge(circle)
    .attr('class', function(d) {
      //TODO 2nd of 4 elements skipped
      //TODO d.children always undefined (not updated)
      console.log('merge circle: ' + toJSON(d));
      var value =  d.parent ? d.children ? 'node' : 'node node-leaf' : 'node node-root';
      return value;
    })
    .attr('r', function(d) {return d.r;});
}

const data = [];

data[0] = {id:1, parent:null, name:'A1'};
updatePack(data);
data[1] = {id:2, parent:1, name:'A1.B1'};
updatePack(data);
data[2] = {id:3, parent:2, name:'A1.B1.C1'};
data[3] = {id:4, parent:2, name:'A1.B1.C2'};
updatePack(data);

</script>

</body>
</html>

updatePack
d3select-fail.html:40 hierarchy node: {"id":"1","parent":null,"children":false,"name":"A1"}
d3select-fail.html:72 merge g: {"id":"1","parent":null,"children":false,"name":"A1"}
d3select-fail.html:78 merge circle: {"id":"1","parent":null,"children":false,"name":"A1"}
d3select-fail.html:51 updatePack
d3select-fail.html:40 hierarchy node: {"id":"1","parent":null,"children":true,"name":"A1"}
d3select-fail.html:40 hierarchy node: {"id":"2","parent":1,"children":false,"name":"A1.B1"}
d3select-fail.html:72 merge g: {"id":"1","parent":null,"children":true,"name":"A1"}
d3select-fail.html:72 merge g: {"id":"2","parent":1,"children":false,"name":"A1.B1"}
d3select-fail.html:78 merge circle: {"id":"1","parent":null,"children":false,"name":"A1"}
d3select-fail.html:78 merge circle: {"id":"2","parent":1,"children":false,"name":"A1.B1"}
d3select-fail.html:51 updatePack
d3select-fail.html:40 hierarchy node: {"id":"1","parent":null,"children":true,"name":"A1"}
d3select-fail.html:40 hierarchy node: {"id":"2","parent":1,"children":true,"name":"A1.B1"}
d3select-fail.html:40 hierarchy node: {"id":"3","parent":2,"children":false,"name":"A1.B1.C1"}
d3select-fail.html:40 hierarchy node: {"id":"4","parent":2,"children":false,"name":"A1.B1.C2"}
d3select-fail.html:72 merge g: {"id":"1","parent":null,"children":true,"name":"A1"}
d3select-fail.html:72 merge g: {"id":"2","parent":1,"children":true,"name":"A1.B1"}
d3select-fail.html:72 merge g: {"id":"3","parent":2,"children":false,"name":"A1.B1.C1"}
d3select-fail.html:72 merge g: {"id":"4","parent":2,"children":false,"name":"A1.B1.C2"}
d3select-fail.html:78 merge circle: {"id":"1","parent":null,"children":false,"name":"A1"}
<id 2 missing here>
d3select-fail.html:78 merge circle: {"id":"3","parent":2,"children":false,"name":"A1.B1.C1"}
d3select-fail.html:78 merge circle: {"id":"4","parent":2,"children":false,"name":"A1.B1.C2"}

Do I need to join data to the parent g as well as both the child circle and text elements?

The answer seems to be found here. Is this the correct approach?

Yes, the linked answer in SO is correct.