Angular D3 树不会折叠回其父级

Anguler D3 tree not collapse back into its parent

我们能够在此处将此示例用于 Anguler(https://bl.ocks.org/d3noob/1a96af738c89b88723eb63456beb6510) 并实现可折叠树图。但是它并没有折叠回它的父级或者我们的点击动作没有正常工作。

这是我的代码 https://stackblitz.com/edit/angular-ivy-acd2yd?file=src/app/app.component.ts

将代码从 JS 转换为 typeScript 这不仅仅是复制+粘贴。我们需要慢一点。

首先,在打字稿中我们使用 letconst 来获得 block-scope 而不是 var。 “var”为所有应用程序创建一个全局变量

之后,我们就不用把所有的代码都放在ngOnInit中了。我们应该在函数中分离 ngOnInit 下的所有代码。我们可以关闭变量并在 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;

我们还需要下车功能,所以我们有功能

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

并且所有函数的转换都使用平面箭头语法,所以

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

并使用this.引用组件的变量。

所有这一切之后,Out ngOnInit 变得像

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

以及函数更新

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

看到有一个小的变化,因为我选择使用 viewPort 使 svg 填充屏幕宽度(如果它小于 960px)并使用 .[= 控制“点”的 class 74=](在代码中它是“硬编码”“点的填充”)

因此,之前,当我们创建 .svg 时,我们为宽度和高度赋值,现在我为 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 + ')'
  );

最后我们创建一个组件而不是在app.component中编写代码。为此我们需要一些变量是 inputs

  @Input()treeData:any={}

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

最后是使用评论向作者致谢

因为我选择 svg 是自适应的,所以我们需要计算“边距”以允许第一个节点的文本可见。为此,我使用此节点的文本创建了一个“visibility:hidden”跨度来计算“边距”。此外,我希望文本是可见的,因此强制 font-size 大约为 14px,从而以

的方式创建一个可观察对象
  fontSize=fromEvent(window,'resize').pipe(
    startWith(null),
    map(_=>{
      return window.innerWidth>960?'14px':14*960/window.innerWidth+'px'
    }),

最后的stackblitzis here(可以对比代码)

更新 真的很不喜欢这个结果

this stackblitz中我改进了一些代码。不同之处在于我使用函数

更改了宽度、高度和视口
  updateSize() {
    this.width = this.wrapper.nativeElement.getBoundingClientRect().width
    this.svg
      .attr('preserveAspectRatio', 'xMidYMid meet')
      .attr('width', '100%')
      .attr('height', this.height + 'px')
      .attr('viewBox', ''+(-this.margin.left)+' 0 ' + this.width  + ' ' + this.height);
  }

为了避免“裁剪”,我更改了节点之间的“harcode”space

// Normalize for fixed-depth.
nodes.forEach((d: any) => {
  d.y = (d.depth * (this.width-this.margin.left-this.margin.right))
          / this.maxDepth;
});

其中this.maxDepth是使用关于treeData

的递归函数计算的
  this.maxDepth = this.depthOfTree(this.treeData);
  depthOfTree(ptr: any, maxdepth: number = 0) {
    if (ptr == null || !ptr.children) return maxdepth;

    for (let it of ptr.children)
      maxdepth = Math.max(maxdepth, this.depthOfTree(it));

    return maxdepth + 1;
  }

我还需要使用我硬编码的“保证金”变量

  margin = { top: 0, right: 130, bottom: 0, left: 80 };

允许 SVG 不裁剪文本

这个答案是另一个答案的延续。我改进了 stackblitz,不要对“边距”进行硬编码。我知道我可以编辑答案,但有很多变化。所以首先我想解释一下树的工作。

当我们写作时

this.treemap = d3.tree().size([100,100]);

这会计算节点(x 和 y)的位置,就像“点”包含在 100x100 像素的矩形中一样。所以我们可以这样“缩放”

nodes.forEach((d: any) => {
  d.y = d.depth * step+innerMargin;
  d.x=this.height/2+(d.x-50)*this.height/100
});

其中“this.height”是 svg 的“高度”,step 是两个节点之间的距离。

所以,首先定义了几个我们需要的输入: 我们需要的变量

  @Input() set treeData(value) {
    this._treeData = value;
    this.maxDepth = this.depthOfTree(this._treeData);
  }

  get treeData() {
    return this._treeData;
  }

  @Input() duration = 750;

  @Input('max-height') set __(value: number) {
    this.maxHeight = value;
  }
  @Input('aspect-ratio') set _(value: number | string) {
    const split = ('' + value).split(':');
    this.factor = +split[1] / +split[0];
  }

看到我们在变量 this.factor 中存储了“aspect-ratio”,我们使用带有 threeData 的“getter”来获得“maxDepth”

我想知道文本的大小,所以我想用文本创建一个字符串数组,并用“visiblility:hidden”的样式绘制。我还想得到第一个文本和较大的文本,所以我们使用

  labels: string[] = [];
  margin = { right: 100, left: 100 };
  firstLabel: any;
  lastLabel: any;

我写了一个类似的模板

<span #label *ngFor="let label of labels" class='fade'>
   {{label}}
</span>
<div #wrapper id="tree" [attr.data-size]="size$|async" class="wrapper">
   <svg></svg>
</div>

我想把font-size改成media-queries,所以我去用ViewEncapsultion.None。这使得 .css 适用于所有应用程序,因此,为避免冲突,我们将所有 .css 与组件的选择器放在一起。此外。我选择使用 css 变量。这允许我们可以使用此变量更改节点的颜色。

  d3noob-collapsible-tree .wrapper{
    position:relative;
    max-width:960px;
    margin-left:auto;
    margin-right:auto;
    text-align:center;
  }
  d3noob-collapsible-tree .fade{
    display:inline-block;
    border:1px solid black;
    position:absolute;
    visibility:hidden;
  }
  d3noob-collapsible-tree .node circle {
    stroke-width: var(--circle-stroke-width,1px);
    stroke: var(--circle-stroke,steelblue);;
  }
  d3noob-collapsible-tree .node.fill {
    fill: var(--circle-fill,lightsteelblue);;
  }
  
  d3noob-collapsible-tree .link {
    stroke:var(--stroke-link,#ccc);
    stroke-width: var(--stroke-width-link,1px);
  }
  d3noob-collapsible-tree .node text,d3noob-collapsible-tree .fade {
    font-family: sans-serif;
    font-size: .675em;
  }
  d3noob-collapsible-tree .node circle {
    fill: var(--circle-empty,white);
  }
  
  d3noob-collapsible-tree .link {
    fill: none;
  }
  
  @media (min-width: 400px) {
    d3noob-collapsible-tree .node text,d3noob-collapsible-tree .fade {
      font-size: .75em;
    }
  }
  @media (min-width: 600px) {
    d3noob-collapsible-tree .node text,d3noob-collapsible-tree .fade {
      font-size: .875em;
    }
  }

我们可以在风格上。 css 使用一些类似的东西

d3noob-collapsible-tree
{
  --stroke-link:#FFC0CB;
  --stroke-width-link:1px;
  --circle-empty:#FFC0CB;
  --circle-fill:#FF69B4;
  --circle-stroke:#C71585;
  --circle-stroke-width:0;
}
d3noob-collapsible-tree .node circle {
  filter: drop-shadow(1px 1px 2px rgba(0,0,0,.15));
}

现在,我们正在使用 ngAfterViewInit 创建树并获取“firstLabel”(“主节点”的#label)和“lastLabel”(宽度较大的标签)

  @ViewChildren('label') labelsDiv: QueryList<ElementRef>;
  firstLabel: any;
  lastLabel: any;


  ngAfterViewInit(): void {
    this.firstLabel = this.labelsDiv.first.nativeElement;
    this.labelsDiv.forEach((x) => {
      this.lastLabel = !this.lastLabel
        ? x.nativeElement
        : this.lastLabel.getBoundingClientRect().width <
          x.nativeElement.getBoundingClientRect()
        ? x.nativeElement
        : this.lastLabel;
    });
    this.svg = d3.select('#tree').select('svg');
    this.svg.attr('preserveAspectRatio', 'xMidYMid meet').append('g');

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

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

    this.updateSize();
    setTimeout(() => {
      this.updateSize();
      this.root.children.forEach((d: any) => {
        this.collapse(d);
      });
      this.update(this.root);
    });
  }

updateSize 会根据“边距”更改 svg 的大小

  updateSize() {
    this.margin.left = this.firstLabel.getBoundingClientRect().width + 25;
    this.margin.right = this.lastLabel.getBoundingClientRect().width + 50;
    this.width = this.wrapper.nativeElement.getBoundingClientRect().width;
    if (this.factor)
      this.height =
        this.width * this.factor < this.maxHeight
          ? this.width * this.factor
          : this.maxHeight;
    else this.height = this.maxHeight;

    this.svg
      .attr('preserveAspectRatio', 'xMidYMid meet')
      .attr('width', this.width + 'px')
      .attr('height', this.height + 'px')
      .attr(
        'viewBox',
        '-' + this.margin.left + ' 0 ' + this.width + ' ' + this.height
      );
  }

看到我们使用宽度和高度来创建 viewBox 和宽度和高度,我们在 viewPost 中使用 -magin.left 来“水平转换”节点 -

更新只是将JS中的函数翻译成typescript

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

    let step =
      (this.width - this.margin.left - this.margin.right) / this.maxDepth;
    let innerMargin = 0;
    if (step > this.lastLabel.getBoundingClientRect().width + 100) {
      step = this.lastLabel.getBoundingClientRect().width + 100;
      innerMargin =
        (this.width -
          step * this.maxDepth -
          this.margin.left -
          this.margin.right -
          10) /
        2;
    }
    this.root.x0 = this.height / 2;
    this.root.y0 = 0;
    // Normalize for fixed-depth.
    nodes.forEach((d: any) => {
      d.y = d.depth * step + innerMargin;
      d.x = this.height / 2 + ((d.x - 50) * this.height) / 100;
    });
    // ****************** 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('text-rendering', 'optimizeLegibility')
      .attr('dy', '.35em')

      .attr('cursor', (d) => (d.children || d._children ? 'pointer' : 'auto'))
      .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', (d) => (d.children || d._children ? 'pointer' : 'auto'));

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

感谢您的阅读,final stackblitz