d3.jstreenodesinterchange

D3.JS Tree graph - Order of clicking is messing up the data


I have a D3 tree graph which loads data from an API when the nodes are clicked. I am noticing that the order in which the nodes are clicked is impacting the display of data in the tree. The data is correct but the order of nodes changes (const nodes = treeData.descendants()) based on the order of clicks.

The code I have

node_clicked(d) {
      d.data.isCollapsed = !d.data.isCollapsed;

      this.service.linkedConcepts(d.data.id)
      .then(data => {
        if (data) {
          const items = data as any[];
          this.appendData(d.data.id, this.treeData, items);
          if (d.children) {
            d._children = d.children;
            d.children = null;
          } else {
            d.children = d._children;
            d._children = null;
          }
          this.root = d3.hierarchy(this.treeData, d => d.children);

          this.update(d);
        }
      })
      .catch(err => {
        this.removeFromDataBeingRetrieved(d.data.id);
        console.error(err);
      });
  }

appendData(parentid, node, items) {
    if (node.id === parentid) {
      items.forEach(item  => {
        node.children.push({
          name: item.Label,
          id: item.ConceptID,
          isCollapsed: true,
          isTopConcept: false,
          count: item.LinkedConceptsCount,
          children: []
        });
      });
      node.isCollapsed = false;
    } else if (node.children) {
      node.children.forEach(item => this.appendData(parentid, item, items));
    }
  }

update(source) {

    const treeData = this.treemap(this.root);

    // Compute the new tree layout.
    const nodes = treeData.descendants(),
        links = treeData.descendants().slice(1);

        // Normalize for fixed-depth.
    nodes.forEach(d =>  d.y = d.depth * 180);

    // ****************** Nodes section ***************************

    let i = 0;

    // Update the nodes...
    const node = this.svg.selectAll('g.node')
        .data(nodes, function(d) {return d.id || (d.id = ++i); });

        // Enter any new modes at the parent's previous position.
    const nodeEnter = node.enter().append('g')
        .attr('class', 'node')
        .attr('transform', d => 'translate(' + source.y0 + ',' + source.x0 + ')')
        .on('click', d => this.node_clicked(d));

        // Add Circle for the nodes
    nodeEnter.append('circle')
        .attr('class', 'node')
        .attr('r', 1e-6)
        .style('stroke', function(d) {
          return d.data.count > 0 ? '#fff' : 'steelblue'; })
        .style('fill', function(d) {
            return d.isCollapsed && d.data.count > 0 ? 'lightsteelblue' : '#fff';
        })
        .append('title')
        .text(d => d.data.count + ' linked articles');

        // Add labels for the nodes
    nodeEnter.append('text')
        .attr('dy', '.35em')
        .attr('x', function(d) {
            return d.children || d._children ? -13 : 13;
        })
        .attr('text-anchor', function(d) {
            return (d.data.isTopConcept === true) ? 'end' : 'start';
        })
        .text(function(d) { return d.data.name; })
        .call(d => this.wrap(d));


        // UPDATE
    const nodeUpdate = nodeEnter.merge(node);

    // Transition to the proper position for the node
    nodeUpdate.transition()
      .duration(this.duration)
      .attr('transform', function(d) {
          return 'translate(' + d.y + ',' + d.x + ')';
       });

       // Update the node attributes and style
    nodeUpdate.select('circle.node')
      .attr('r', 10)
      .style('fill', function(d) {
        if (d.data.count === 0) {
          return '#fff';
        } else if ( d.data.isCollapsed === true ) {
          return 'lightsteelblue';
        } else {
          return '#fff';
        }
      })
      .style('stroke-width', '3px')
      .style('stroke', function(d) {
        return d._children > 0 ? '#fff' : 'lightsteelblue'; })
      .attr('cursor', 'pointer');

    // Remove any exiting nodes
    const nodeExit = node.exit().transition()
        .duration(this.duration)
        .attr('transform', function(d) {
            return 'translate(' + source.y + ',' + source.x + ')';
        })
        .remove();

        // On exit reduce the node circles size to 0
    nodeExit.select('circle')
      .attr('r', 1e-6);

    // On exit reduce the opacity of text labels
    nodeExit.select('text')
      .style('fill-opacity', 1e-6);

      // ****************** links section ***************************

    // Update the links...
    const link = this.svg.selectAll('path.link')
        .data(links, d => d['id']);

        // Enter any new links at the parent's previous position.
    const linkEnter = link.enter().insert('path', 'g')
        .attr('class', 'link')
        .style('fill', 'none')
        .style('stroke', '#ccc')
        .style('stroke-width', '3px')
        .attr('d', d => this.diagonal({x: source.x, y: source.y},
                {x: source.x, y: source.y}));

        // UPDATE
    const linkUpdate = linkEnter.merge(link);

    // Transition back to the parent element position
    linkUpdate.transition()
        .duration(this.duration)
        .attr('d', d => this.diagonal(d, d.parent) );

        // Remove any exiting links
    const linkExit = link.exit().transition()
        .duration(this.duration)
        .attr('d', d => this.diagonal({x: source.x, y: source.y},
                {x: source.x, y: source.y}))
        .remove();

        // Store the old positions for transition.
    nodes.forEach(function(d){
      d.x0 = d.x;
      d.y0 = d.y;
    });

  }

An illustration of the problem: When I expand level 2 nodes before expanding level 3 nodes, I see this which is correct data - enter image description here

When I expand level 3 nodes before level 2, I see this which is incorrect data. The data displayed in level 4 is repeated in level 3 (highlighted) as well - enter image description here

Do I have to manually manage the order of treeData.descendants() to handle this issue? Is there any other inbuilt way?


Solution

  • This line of code was causing the issue

       const node = this.svg.selectAll('g.node')
            .data(nodes, function(d) {return d.id || (d.id = ++i); });

    I changed it to

        const node = this.svg.selectAll('g.node')
            .data(nodes, function(d) {return d.id || (d.id = d.data.id); });

    and it works perfectly now :)