We were able to use this example for Angular here(https://bl.ocks.org/d3noob/1a96af738c89b88723eb63456beb6510) and achieve the collapsible tree diagram. But it's not collapse back into its parent or our click action is not working properly.
Here is my code: https://stackblitz.com/edit/angular-ivy-acd2yd?file=src/app/app.component.ts
Transform a code from JS to typeScript it's not only Copy+Paste. We need go slower.
First, in typescript we use let
or const
to have a block-scope instead of var
. "var" create a variable global to all the application
After, we needn't put all the code in ngOnInit. We should separate in functions all the code under ngOnInit. We can get off variables and declare outside the 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;
Also we need get off the functions, so we has the functions
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;
}
And transfor all the functions use the flat arrow sintax, so
//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 + ')';
})
And use this.
to make reference to the variables of the component.
After all of this, Out ngOnInit becomes like
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);
}
And the function update
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;
}
I need also use the "margin" variable that I hardcode like
margin = { top: 0, right: 130, bottom: 0, left: 80 };
That allow the SVG don't crop the text