Как я могу отобразить два дерева с центральным корнем? - PullRequest
2 голосов
/ 30 марта 2019

Я пытаюсь создать два дерева D3 с центральным корнем общего ресурса. Деревья должны расширяться вправо и влево, когда пользователь нажимает на мои узлы.
Я натолкнулся на пример здесь на stackoverflow.

Вот ссылка на эту SO: Дерево с потомками в сторону нескольких сторон в d3.js (аналогично генеалогическому дереву)

Я даже собрал версию этого кода на bl.ocks.org: https://bl.ocks.org/redcricket/de324f83aa6c84db2588c1a1f53cc5e3

Приведенные выше примеры - D3 v3. Я адаптирую приведенный выше пример к D3 v4 и интегрирую его в угловой компонент, но я столкнулся с проблемой, заключающейся в том, что я могу одновременно отображать только одно дерево.

Точкой входа в мой код является этот угловой компонент и его служебный компонент:

Компонент:

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();
    }
  }
}

Услуга:

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

Обратите внимание, в методе AngularD3TreeLibService.update() я вызываю rightTreeUpdate, прежде чем вызвать leftTreeUpdate. В результате видно только мое левое дерево.

left tree

В моем коде TreeModel я могу отобразить правильное дерево, но не левое, вызвав leftTreeUpdate до rightTreeUpdate в моей click() функции.

enter image description here

Я подозреваю, что я делаю что-то не так в моих методах setNodes() и setLinks(), поскольку я действительно не понимаю цели таких вещей, как nodeEnter, nodeUpdate и nodeExit.

Вот отредактированная (для краткости) версия моей 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 ...
}

1 Ответ

2 голосов
/ 30 марта 2019

Следуя предложению Эндрю Рида, я смог нарисовать оба дерева, изменив всего четыре строки кода.

В моем setNodes() методе у меня теперь есть:

    // 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 + ")";
        });

и в моем setLinks методе аналогичное изменение:

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

Вот как теперь выглядят мои два дерева.

enter image description here

Мне все еще нужно поработать над правильным отображением ссылок слева, но это другая проблема. Спасибо Андрей!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...