3

I am trying to create two D3 trees with what appears to be a share central root. The trees should expand to the right and left as the user clicks on my nodes.
I came across an example here on stackoverflow.

Here is the link to that SO: Tree with children towards multiple side in d3.js (similar to family tree)

I even put together a bl.ocks.org version of this code here: https://bl.ocks.org/redcricket/de324f83aa6c84db2588c1a1f53cc5e3

The above examples are D3 v3. I am adapting the above example to D3 v4 and integrating it into an angular component, but I have run into a problem in that I can only display only one tree at a time.

The entry point into my code is this angular component and its service component:

The component:

import { Component, OnInit, OnChanges, ViewChild, ElementRef, Input, Output, EventEmitter} from '@angular/core';
import { AngularD3TreeLibService } from './custom-d3-tree.service';

@Component({
  selector: 'custom-angular-d3-tree-lib',
  template: `<div class="d3-chart" #chart></div> `,
  styleUrls: ['./custom-d3-tree.component.css']
})
export class AngularD3TreeLibComponent implements OnInit, OnChanges {
  @ViewChild('chart') private chartContainer: ElementRef;
  @Input() treeData: any = [];
  @Output() onNodeChanged: EventEmitter<any>= new EventEmitter();
  @Output() onNodeSelected: EventEmitter<any>= new EventEmitter();

  constructor( private treeService: AngularD3TreeLibService ) {
    treeService.setNodeChangedListener((node)=>{ this.onNodeChanged.emit(node); })
    treeService.setNodeSelectedListener((node)=>{ this.onNodeSelected.emit(node); })
  }

  ngOnInit() {}
  ngOnChanges(changes: any) { this.seedTree(); }

  seedTree(){
    if(!!this.treeData){
      this.treeService.createChart(this.chartContainer, this.treeData);
      this.treeService.update();
    }
  }
}

The service:

import { Injectable } from '@angular/core';
import { TreeModel } from './tree.dendo.model';

@Injectable({
  providedIn: 'root'
})
export class AngularD3TreeLibService {
  treeModel: TreeModel= new TreeModel();

  constructor() { }

  createChart(chartContainer: any, treeData: any): void {
    let element = chartContainer.nativeElement;
    element.innerHTML= "";
    this.treeModel.addSvgToContainer(chartContainer);
    this.treeModel.createLayout();
    this.treeModel.createTreeData(treeData);
  }

  update(){
    this.treeModel.rightTreeUpdate(this.treeModel.rroot);
    this.treeModel.leftTreeUpdate(this.treeModel.lroot);
  }
}

Note in the AngularD3TreeLibService.update() method I call rightTreeUpdate before I call leftTreeUpdate. This results in only my left tree being visible.

left tree

In my TreeModel code I am able to display the right tree but not the left by calling leftTreeUpdate before rightTreeUpdate in my click() function.

enter image description here

I suspect I am doing something wrong in my setNodes() and setLinks() methods as I really do not understand the purpose of things like nodeEnter, nodeUpdate and nodeExit.

Here is an edited (for brevity) version of my TreeModel.

import * as d3 from 'd3';

export class TreeModel {

  rroot: any; // right root
  lroot: any; // left root
  treeLayout: any;
  svg: any;
  N: number = 10;
  treeData: any;

  rect_width: number = 125;
  rect_height: number = 42;

  height: number;
  width: number;
  margin: any = { top: 200, bottom: 90, left: 100, right: 90};
  duration: number= 750;
  nodeWidth: number = 1;
  nodeHeight: number = 1;
  nodeRadius: number = 5;
  horizontalSeparationBetweenNodes: number = 1;
  verticalSeparationBetweenNodes: number = 10;

  selectedNodeByDrag: any;

  selectedNodeByClick: any;
  previousClickedDomNode: any;

  ... omitted for brevity ...

  constructor(){}

  addSvgToContainer(chartContainer: any){
    let element = chartContainer.nativeElement;

    this.width = element.offsetWidth - this.margin.left - this.margin.right;
    this.height = element.offsetHeight - this.margin.top - this.margin.bottom;

    this.svg = d3.select(element).append('svg')
      .attr('width', element.offsetWidth)
      .attr('height', element.offsetHeight)
      .append("g")
      .attr("transform", "translate("
            + this.margin.left + "," + this.margin.top + ")");
    this.svg = this.svg.append("g");

    ... omitted for brevity ...
  }

  // zoom stuff
  ... omitted for brevity ...
  // end zoom stuff

  createLayout(){
    this.treeLayout = d3.tree()
      .size([this.height, this.width])
      .nodeSize([this.nodeWidth + this.horizontalSeparationBetweenNodes, this.nodeHeight + this.verticalSeparationBetweenNodes])
      .separation((a,b)=>{return a.parent == b.parent ? 50 : 200});
  }

  getRandomColor() {
    ... omitted for brevity ...
  }

  chunkify(a, n, balanced) {
    ... omitted for brevity ...
  }

  twoTreeBuildCenterNodesChildren(children:any) {
    // this routine is suppose to build a json/tree object that represent the children of the center node.
    // if there are more than N number of nodes on any level we need to create an additional level to
    // accommodate these nodes.
    ... omitted for brevity ...
  }


  compare(a,b) {
    ... omitted for brevity ...
  }

  buildTwoTreeData(apiJson:any) {
    var componentType = Object.keys(apiJson)[0];
    var centerNodeLeft = {'component_type': componentType, "name": apiJson[componentType].name, "color": "#fff", "children": []};
    var centerNodeRight = {'component_type': componentType, "name": apiJson[componentType].name, "color": "#fff", "children": []};
    var tmp_leftNodes = [];
    for ( var i=0; i < apiJson[componentType].multiparent.length; i++ ) {
      var c = apiJson[componentType].multiparent[i];
      c['color'] = this.getRandomColor();
      c['name'] = c.parent.name;
      tmp_leftNodes.push(c);
    }
    var leftNodes = tmp_leftNodes.sort(this.compare);
    var rightNodes = apiJson[componentType].children.sort(this.compare);
    var right_center_node_children = this.twoTreeBuildCenterNodesChildren(rightNodes.sort(this.compare));
    var left_center_node_children = this.twoTreeBuildCenterNodesChildren(leftNodes.sort(this.compare));
    centerNodeLeft.children = left_center_node_children;
    centerNodeRight.children = right_center_node_children;
    return[centerNodeLeft, centerNodeRight];

  }

  translateJson(apiJson:any){ return this.buildTwoTreeData(apiJson); }

  createTreeData(rawData: any){
    var parsedData = this.translateJson(rawData);
    this.lroot = d3.hierarchy(parsedData[0]);
    this.lroot.x0 = this.height / 2;
    this.lroot.y0 = 0;
    this.lroot.children.map((d)=>this.collapse(d));
    this.rroot = d3.hierarchy(parsedData[1]);
    this.rroot.x0 = this.height / 2;
    this.rroot.y0 = 0;
    this.rroot.children.map((d)=>this.collapse(d));
  }

  collapse(d) {
    if(d.children) {
      d._children = d.children
      d._children.map((d)=>this.collapse(d));
      d.children = null
    }
  }

  expand_node(d) {
    if (d.children) { d._children = d.children; d.children = null; } else { d.children = d._children; d._children = null; }
  }

  expand(d) {
    if(d._children) {
      d.children = d._children
      d.children.map((d)=>this.expand(d));
      d.children = null
    }
  }

  rightTreeUpdate(source) {
    const treeData = this.treeLayout(this.rroot);
    this.setNodes(source, treeData, 'right');
    this.setLinks(source, treeData, 'right');
  }

  leftTreeUpdate(source) {
    const treeData = this.treeLayout(this.lroot);
    this.setNodes(source, treeData, 'left');
    this.setLinks(source, treeData, 'left');
  }

  setNodes(source:any, treeData: any, side: string){
    let nodes = treeData.descendants();
    let treeModel= this;
    if ( side === 'left') {
      let width = this.width;
      nodes.forEach(function (d) { d.y = (d.depth * -180) });
    } else {
      // this draws everything to the right.
      nodes.forEach(function(d){ d.y = d.depth * 180});
    }

    var node = this.svg.selectAll('g.node')
        .data(nodes, function(d) { return d.id || (d.id = ++this.i); });
    var nodeEnter = node.enter().append('g')
        .attr('class', 'node')
        .attr("transform", function(d) {
            return "   translate(" + source.y0 + "," + source.x0 + ")";
        });

    nodeEnter.append('rect')
      .attr('class', 'node-rect')
      .attr('x', 0)
      .attr('y', 0)
      .attr('rx', 6)
      .attr('ry', 6)
      .attr('width', this.rect_width)
      .attr('height', this.rect_height)
      .attr('stroke', 'black')
      .style("fill", function(d) {
        return d.data.color;
      });

    nodeEnter.append('text')
      .attr('y', 20)
      .attr('x', 40)
      .attr("text-anchor", "middle")
      .text(function(d){
          return (d.data.name || d.data.description || d.id);
      });

    var nodeUpdate = nodeEnter.merge(node);
    nodeUpdate.transition()
      .duration(this.duration)
      .attr("transform", function(d) {
        return "translate(" + d.y + "," + d.x  + ")";
       });

    var 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);

    // Store the old positions for transition.
    nodes.forEach(function(d){
      d.x0 = d.x;
      d.y0 = d.y;
    });
    // On exit reduce the opacity of text labels
    nodeExit.select('text')
      .style('fill-opacity', 1e-6);

    nodeEnter
      .on('click', function(d){
        treeModel.click(d, this);
        //treeModel.update(d);
        // treeModel.rightTreeUpdate(d);
      });
  }

    ... omitted for brevity ...

  setLinks( source: any, treeData: any, side: string){
    let links = treeData.descendants().slice(1);
    var link = this.svg.selectAll('path.link')
      .data(links, function(d) { return d.id; });

    // Enter any new links at the parent's previous position.
    var linkEnter = link.enter().insert('path', "g")
      .attr("class", "link")
      .attr('fill', 'none')
      .attr('stroke', 'black')
      .attr('d', (d)=>{
        var o = {x: source.x0, y: source.y0}
        return this.rdiagonalCurvedPath(o, o)
      });

    var linkUpdate = linkEnter.merge(link);

    linkUpdate.transition()
      .duration(this.duration)
      .attr('d', (d)=>{return this.rdiagonalCurvedPath(d, d.parent)});

    var linkExit = link.exit().transition()
      .duration(this.duration)
      .attr('d', (d) => {
        var o = {x: source.x, y: source.y}
        return this.rdiagonalCurvedPath(o, o)
      })
      .remove();
  }

  click(d, domNode) {
    if( d._children ) {
      this.expand_node(d);
    } else if ( d.children) {
      this.collapse(d);
    } else {
      console.log('click() skipping load of new data for now ');
    }
    // HERE IS WHERE I CALL
    // rightTreeUpdate() after leftTreeUpdate() which displays the right tree, but not the left tree.
    this.leftTreeUpdate(this.lroot);
    this.rightTreeUpdate(this.rroot);
  }

    ... omitted for brevity ...
}
Red Cricket
  • 9,762
  • 21
  • 81
  • 166
  • 1
    You are selecting the same nodes for left and right: `var node = this.svg.selectAll('g.node')` and `.attr("class","node")`: this applies the update/exit/enter cycle on the nodes that already exist. If you differentiate the left and right nodes' class names and select either right or left nodes rather than all nodes, you should be able to fix this. – Andrew Reid Mar 29 '19 at 22:11
  • Thanks Andrew. Should I put some information into my raw data indicating that whether the node is a left node or right node or should I assign left and right nodes different "classes" to make it easier to determine what nodes I want to select? – Red Cricket Mar 29 '19 at 22:20
  • 1
    assigning different class names for left/right nodes is probably easiest, as it only requires you to change two lines (at a glance): `svg.selectAll(".node"+side)`, `nodeEnter.attr("class","node"+side)` – Andrew Reid Mar 29 '19 at 23:25
  • 1
    Boooyah! You're the man. That did the trick. I imagine that I need to do the same sort thing for the links. – Red Cricket Mar 30 '19 at 01:59

1 Answers1

2

After following Andrew Ried's suggestion I was able to draw both trees by changing just four lines of code.

In my setNodes() method I now have this:

    // var node = this.svg.selectAll('g.node')
    var node = this.svg.selectAll('g.node'+side)
        .data(nodes, function(d) { return d.id || (d.id = ++this.i); });
    var nodeEnter = node.enter().append('g')
        // .attr('class', 'node')
        .attr('class', 'node'+side)
        .attr("transform", function(d) {
            return "   translate(" + source.y0 + "," + source.x0 + ")";
        });

and in my setLinks method a similar change:

    var link = this.svg.selectAll('path.link'+side)
      .data(links, function(d) { return d.id; });

    // Enter any new links at the parent's previous position.
    var linkEnter = link.enter().insert('path', "g")
      .attr("class", "link"+side)
      .attr('fill', 'none')
      .attr('stroke', 'black')
      .attr('d', (d)=>{
        var o = {x: source.x0, y: source.y0}
        return this.rdiagonalCurvedPath(o, o)
      });

Here's what my two trees look like now.

enter image description here

I still need to work on correctly drawing the links on the left, but that's another issue. Thanks Andrew!

Red Cricket
  • 9,762
  • 21
  • 81
  • 166