Angular D3树不会折叠回其父树

x0fgdtte  于 2023-10-19  发布在  Angular
关注(0)|答案(2)|浏览(132)

我们可以在这里使用这个Angular示例(https://bl.ocks.org/d3noob/1a96af738c89b88723eb63456beb6510)并实现可折叠的树图。但它没有折叠回其父节点,或者我们的单击操作无法正常工作。
下面是我的代码:https://stackblitz.com/edit/angular-ivy-acd2yd?file=src/app/app.component.ts

jfgube3f

jfgube3f1#

将代码从JS转换为typeScript不仅仅是复制+粘贴。我们得慢点。
首先,在typescript中,我们使用letconst来拥有一个块作用域,而不是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;
  }

并且transfor所有函数都使用了平箭头sintax,所以

//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,并使用.css控制“点”的类(在代码中,它是“硬编码”的“点的填充”)
所以,之前,当我们创建.svg时,我们给予width和height值,现在我给予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中编写代码。为此,我们需要输入一些变量

@Input()treeData:any={}

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

最后是用评论的方式给予作者的肯定
由于我选择的svg是自适应的,我们需要计算“边距”,以允许第一个节点的文本可见。为此,我用this节点的文本创建了一个“visibility:hidden”span来计算“margin”。然而,我希望文本是可见的,所以强制字体大小约为14px,以创建一个可观察的方式

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

最后的stackblitz is here(你可以比较代码)

更新我真的不喜欢这么多的结果

this stackblitz中,我改进了一些代码。不同的是,我改变了宽度,高度和viewPort使用一个函数

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

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

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

其中,.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不裁剪文本

zrfyljdw

zrfyljdw2#

这个答案是另一个答案的继续。我改进了stackblitz不硬编码的“利润率”。我知道我可以修改答案,但有很多变化。首先,我想介绍一种树。
谱写好

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

这将计算节点(x和y)的位置,就像“点”包含在一个100 x100 px的矩形中一样。所以我们可以“规模化”

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

其中“this.height”是svg的“height”,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];
  }

我们将“aspect-ratio”存储在变量this.factor中,并使用“getter”和threeData来获取“maxDepth”
我想知道文本的大小,所以我想用文本创建一个字符串数组,并使用样式“隐藏:隐藏”进行绘制。我还想得到第一个文本和较大的文本,所以我们使用

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>

我想使用media-queries更改字体大小,所以我使用ViewEncapsultion.None。这使得.css适用于所有的应用程序,因此,为了避免冲突,我们用组件的选择器prexis所有的.css。future.我选择使用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”(“main node”的#标签)和“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的大小,同时考虑“margin”

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

我们使用width和height创建viewBox,使用-magin.left在viewPost中“水平平移”节点-
更新只是翻译到JS中的函数

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

相关问题