Home > database >  Anguler D3 tree not collapse back into its parent
Anguler D3 tree not collapse back into its parent

Time:02-08

We were able to use this example for Anguler here(https://bl.ocks.org/d3noob/1a96af738c89b88723eb63456beb6510) and achieve the Collapsible tree diagram. But it's not collapse back into its parent or our click action is not working properly .

Here is my code https://stackblitz.com/edit/angular-ivy-acd2yd?file=src/app/app.component.ts

CodePudding user response:

Transform a code from JS to typeScript it's not only Copy Paste. We need go slower.

First, in typescript we use let or const to have a block-scope instead of var. "var" create a variable global to all the application

After, we needn't put all the code in ngOnInit. We should separate in functions all the code under ngOnInit. We can get off variables and declare outside the ngOnInit

  treeData:any={...}
  margin = { top: 0, right: 30, bottom: 0, left: 30 };
  duration = 750;

  width: number;
  height: number;
  svg: any;
  root: any;

  i = 0;
  treemap: any;

Also we need get off the functions, so we has the functions

  update(source:any){
      ...
  }
  collapse(d: any) {
    if (d.children) {
      d._children = d.children;
      d._children.forEach((d:any)=>this.collapse(d));
      d.children = null;
    }
  }

  click(d: any) {
    if (d.children) {
      d._children = d.children;
      d.children = null;
    } else {
      d.children = d._children;
      d._children = null;
    }
    this.update(d);
  }

  diagonal(s: any, d: any) {
    const path = `M ${s.y} ${s.x}
            C ${(s.y   d.y) / 2} ${s.x},
              ${(s.y   d.y) / 2} ${d.x},
              ${d.y} ${d.x}`;

    return path;
  }

And transfor all the functions use the flat arrow sintax, so

    //in stead of use 
    .attr('transform', function (d: any) {
      return 'translate('   source.y0   ','   source.x0   ')';
    })

    //we use
    .attr('transform', (d: any) => {
      return 'translate('   source.y0   ','   source.x0   ')';
    })

And use this. to make reference to the variables of the component.

After all of this, Out ngOnInit becomes like

ngOnInit(){
    this.svg = d3
      .select('#d3noob')
      .append('svg')
      .attr('viewBox','0 0 900 500')
      .append('g')
      .attr(
        'transform',
        'translate('   (this.margin.left inc)   ','   this.margin.top   ')'
      );

    // declares a tree layout and assigns the size
    this.treemap = d3.tree().size([this.height, this.width]);

    // Assigns parent, children, height, depth
    this.root = d3.hierarchy(this.treeData, (d: any) => {
      return d.children;
    });

    this.root.x0 = this.height / 2;
    this.root.y0 = 0;
    // Collapse after the second level
    this.root.children.forEach((d:any) => {
      this.collapse(d);
    });

    this.update(this.root);
}

And the function update

  update(source: any) {
    // Assigns the x and y position for the nodes
    const treeData = this.treemap(this.root);

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

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

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

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

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

    // Add Circle for the nodes
    nodeEnter
      .append('circle')
      .attr('class', (d:any)=> d._children?'node fill':'node')
      .attr('r', 1e-6)
    // Add labels for the nodes
    nodeEnter
      .append('text')
      .attr('dy', '.35em')
      
      .attr('x', (d) => {
        return d.children || d._children ? -13 : 13;
      })
      .attr('text-anchor', (d: any) => {
        return d.children || d._children ? 'end' : 'start';
      })
      .text((d) => {
        return d.data.name;
      });
    // UPDATE
    const nodeUpdate = nodeEnter.merge(node);

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

    // Update the node attributes and style
    nodeUpdate
      .select('circle.node')
      .attr('r', 10)
      .attr('class', (d:any)=> d._children?'node fill':'node')
      .attr('cursor', 'pointer');

    // Remove any exiting nodes
    const nodeExit = node
      .exit()
      .transition()
      .duration(this.duration)
      .attr('transform', (d: any) => {
        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: any) => {
      return d.id;
    });

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

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

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

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

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

See that there're a minor changes because I choose use viewPort to make the svg fill the width of the screen if it's less than 960px and control the class of the "dots" using .css (In the code it was "hardcode" the "fill of the dots")

So, before, when we create the .svg we give value to width and height and now I give value to viewBox"

this.svg = d3
  .select('#d3noob')
  .append('svg')
  .attr('viewBox','0 0 960 500')
  .append('g')
  .attr(
    'transform',
    'translate('   (this.margin.left inc)   ','   this.margin.top   ')'
  );

Finally We create a component instead write the code in the app.component. For this we need some variables was inputs

  @Input()treeData:any={}

  @Input()margin = { top: 0, right: 30, bottom: 0, left: 30 };
  @Input()duration = 750;

The last is give credit to the author using a comment

As I choose the svg was adaptative we need calculate the "margin" to allow the text of the first node was visible. To make this, I create a "visibility:hidden" span with the text of the this node to calculate the "margin". Futhermore, I want that the text was visible, so force the font-size was around 14px creating an observable in the way

  fontSize=fromEvent(window,'resize').pipe(
    startWith(null),
    map(_=>{
      return window.innerWidth>960?'14px':14*960/window.innerWidth 'px'
    }),

The final stackblitz is here (you can compare the code)

  •  Tags:  
  • Related