我们能够将这个例子用于 Angular 这里(https://bl.ocks.org/d3noob/1a96af738c89b88723eb63456beb6510),并实现可折叠的树形图。但它不会折叠回其父级,或者我们的点击操作无法正常工作。
这是我的代码: https://stackblitz.com/edit/angular-ivy-acd2yd?file=src/app/app.component.ts
将代码从JS转换为typeScript,它不仅是复制+粘贴。我们需要放慢速度。
首先,在打字稿中,我们使用let
或const
来拥有块作用域而不是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;
}
并且所有函数的转换都使用平面箭头 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时,我们给宽度和高度赋值,现在我给 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 节点的文本创建了一个"可见性:隐藏"跨度来计算"边距"。此外,我希望文本是可见的,所以强制字体大小约为 14px,从而以可观察的方式创建可观察的内容
fontSize=fromEvent(window,'resize').pipe(
startWith(null),
map(_=>{
return window.innerWidth>960?'14px':14*960/window.innerWidth+'px'
}),
最终的堆栈闪电战在这里(您可以比较代码)
更新真的我不太喜欢结果
在这个堆栈闪电战中,我改进了一点代码。不同之处在于我使用函数更改宽度,高度和视口
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"空间
// 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 不裁剪文本
这个答案是另一个答案的延续。我改进了堆栈闪电战,以不对"边距"进行硬编码。我知道我可以编辑答案,但有很多变化。所以首先我想解释一个树的工作。
当我们写
this.treemap = d3.tree().size([100,100]);
这将计算节点(x 和 y)的位置,就像"点"包含在 100x100px 的矩形中一样。因此,我们可以以"扩展"的方式
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
"纵横比"中,我们使用带有 threeData 的 "getter" 来获得 "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>
我想使用媒体查询更改字体大小,所以我使用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
);
}
看到我们使用宽度和高度来创建视图框以及宽度和高度,我们使用 -magin.left in 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;
});
}
感谢您的阅读,最后的堆叠闪电战