我們可以在此處( https://bl.ocks.org/d3noob/1a96af738c89b88723eb63456beb6510 )將這個示例用于Anguler,并實作可折疊樹圖。但它不會折疊回其父級,或者我們的點擊操作無法正常作業。
這是我的代碼 https://stackblitz.com/edit/angular-ivy-acd2yd?file=src/app/app.component.ts
uj5u.com熱心網友回復:
將代碼從 JS 轉換為 typeScript 不僅僅是復制 粘貼。我們需要慢一點。
首先,在打字稿中,我們使用letorconst來代替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;
});
}
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)
Update Really I don't like so much the result
In this stackblitz I improve a bit the code. The diferences are that I change the width,height and viewPort using a function
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);
}
To avoid "crop" I change the "harcode" space between the nodes
// Normalize for fixed-depth.
nodes.forEach((d: any) => {
d.y = (d.depth * (this.width-this.margin.left-this.margin.right))
/ this.maxDepth;
});
Where this.maxDepth is calculate using a recursive function about 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 不裁剪文本
uj5u.com熱心網友回復:
這個答案是另一個答案的延續。我改進了 stackblitz 以不硬編碼“邊距”。我知道我可以編輯答案,但有很多變化。所以首先我想解釋一下樹的作業。
當我們寫
this.treemap = d3.tree().size([100,100]);
這以“點”包含在 100x100 像素的矩形中的方式計算節點(x 和 y)的位置。所以我們可以“縮放”的方式
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>
I want to change the font-size using media-queries, so I go to use ViewEncapsultion.None. This makes that the .css was to all the application, so, for avoid confict, we prexis all the .css with the selector of the component. futhermore. I choose use css variables. This allow we can change the colors of the nodes using this variables.
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;
}
}
We can in styles. css use some like
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));
}
Well now, we are using ngAfterViewInit to create the tree and get the "firstLabel" (the #label of the "main node") and the "lastLabel" (the label with the larger width)
@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);
});
}
The updateSize change the size of the svg taking account of the "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
);
}
See that we use the width and height to create the viewBox and the width and height and we use -magin.left in viewPost to "horizontal translate" the node-
The update is only the translate to typescript the functions in 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;
});
}
Thanks you for reading, the final stackblitz
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/427197.html
