Currently working on building a visual representation of a tree/hierarchy data structure using d3.js v4 within an Angular environment. I've taken inspiration from this particular implementation https://bl.ocks.org/d3noob/43a860bc0024792f8803bba8ca0d5ecd and integrated it into a component, encountering an issue with the 'click to expand/contract' functionality. After refreshing, the visualization renders correctly, but upon clicking a node, the following error is thrown:
ERROR TypeError: this.update is not a function
Stack trace:
./src/app/components/data-displayer.component.ts/DataDisplayerComponent.prototype.click@http://localhost:4200/main.js:304:9
contextListener/<@http://localhost:4200/vendor.js:69524:7
./node_modules/zone.js/dist/zone.js/</ZoneDelegate.prototype.invokeTask@http://localhost:4200/polyfills.js:2743:17
onInvokeTask@http://localhost:4200/vendor.js:33761:24
./node_modules/zone.js/dist/zone.js/</ZoneDelegate.prototype.invokeTask@http://localhost:4200/polyfills.js:2742:17
./node_modules/zone.js/dist/zone.js/</Zone.prototype.runTask@http://localhost:4200/polyfills.js:2510:28
./node_modules/zone.js/dist/zone.js/</ZoneTask.invokeTask@http://localhost:4200/polyfills.js:2818:24
invokeTask@http://localhost:4200/polyfills.js:3862:9
globalZoneAwareCallback@http://localhost:4200/polyfills.js:3888:17
core.js:1598
Upon triggering the onclick event, it appears that the subroutine (this.click) loses visibility of the other parts of the class (I validated this by logging the class fields, all of which were logged as undefined). Although the correct data is still passed, the this.update
method becomes uncallable. Version details:
angular: 6.0.1, Node: 8.9.1, OS: win32 x64
import { Component, Input, OnInit, ViewEncapsulation } from "@angular/core";
import * as d3 from "d3";
import { HierarchyPointNode } from "d3";
export const margin = { top: 20, right: 120, bottom: 20, left: 120 };
export const width = 960 - margin.right - margin.left;
export const height = 800 - margin.top - margin.bottom;
@Component({
selector: "data-displayer",
template: "<svg></svg>",
styleUrls: ["data-displayer.component.css"],
providers: [],
encapsulation: ViewEncapsulation.None,
})
export class DataDisplayerComponent implements OnInit {
private svg;
private treeLayout;
private root;
ngOnInit() {
d3.json("../../assets/flare.json").then(data => {
this.root = d3.hierarchy(data, (d) => d.children);
this.root.x0 = height / 2;
this.root.y0 = 0;
let collapse = function (d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
this.root.children.forEach(collapse);
this.update(this.root);
});
this.svg = d3.select("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
this.treeLayout = d3.tree().size([height, width]);
}
update(source) {
let i = 0;
let duration = 750;
let treeData = this.treeLayout(this.root);
let nodes = treeData.descendants();
let links = treeData.descendants().slice(1);
nodes.forEach(d => d.y = d.depth * 180);
let node = this.svg.selectAll("g.node")
.data(nodes, d => d.id || (d.id = ++i) );
let nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", d => "translate(" + source.y0 + "," + source.x0 + ")")
.on("click", this.click);
nodeEnter.append("circle")
.attr("class", "node")
.attr("r", 1e-6)
.style("fill", d => d._children ? "lightsteelblue" : "#fff");
let nodeUpdate = nodeEnter.merge(node);
nodeUpdate.transition()
.duration(duration)
.attr("transform", d => "translate(" + d.y + "," + d.x + ")");
nodeUpdate.select("circle.node")
.attr("r", 10)
.style("fill", d => d._children ? "lightsteelblue" : "#fff")
.attr("cursor", "pointer");
let nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", d => "translate(" + source.y + "," + source.x + ")")
.remove();
nodeExit.select("circle")
.attr("r", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
let link = this.svg.selectAll("path.link")
.data(links, d => d.id);
let linkEnter = link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", d => {
let o = { x: source.x0, y: source.y0 };
return this.diagonal(o, o);
});
let linkUpdate = linkEnter.merge(link);
linkUpdate.transition()
.duration(duration)
.attr("d", d => {
return this.diagonal(d, d.parent)
});
let linkExit = link.exit().transition()
.duration(duration)
.attr("d", d => {
let o = { x: source.x, y: source.y };
return this.diagonal(o, o);
})
.remove();
nodes.forEach(d => {
d.x0 = d.x;
d.y0 = d.y;
});
}
click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
this.update(d);
}
diagonal(s, d) {
let 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;
}