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 这不仅仅是复制+粘贴。我们需要慢一点。
首先,在打字稿中我们使用 let
或 const
来获得 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
我们能够在此处将此示例用于 Anguler(https://bl.ocks.org/d3noob/1a96af738c89b88723eb63456beb6510) 并实现可折叠树图。但是它并没有折叠回它的父级或者我们的点击动作没有正常工作。
这是我的代码 https://stackblitz.com/edit/angular-ivy-acd2yd?file=src/app/app.component.ts
将代码从 JS 转换为 typeScript 这不仅仅是复制+粘贴。我们需要慢一点。
首先,在打字稿中我们使用 let
或 const
来获得 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