reusableTidyTree = function () {
let updateData;
let datum;
let dataIndexingMap = {}
let dataKeyStack = [];
function getChildrenUpToDepth(depth, node) {
if (depth === 0 || node === null) {
return null;
}
if (depth === 1) {
return {
name: node.data.name,
data: node.data,
depth: node.depth,
children: null, childCount: node.children ? node.children.length : 0
}
}
return {
name: node.data.name,
data: node.data,
depth: node.depth,
children: node.children ? node.children.map(c => {
return getChildrenUpToDepth(depth - 1, c);
}) : null
}
}
function getDatumFromData(d) {
const datum = d3.hierarchy({
name: d.data.name,
data: d.data,
depth: d.depth,
children: d.children ? d.children.map(c => {
return getChildrenUpToDepth(depth, c);
}) : null
});
return tree(datum)
}
function getKey(e) {
return e.data.name + '-' + e.data.index + '-' + e.depth
}
function indexData(d) {
const allData = d.descendants();
allData.forEach(e => {
dataIndexingMap[getKey(e)] = e
});
}
function tidyTree(selection) {
const linksG = selection.append("g")
.attr("fill", "none")
.attr("stroke", "#555")
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 0.2);
const circleG = selection.append("g");
const textG = selection.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("stroke-linejoin", "round")
.attr("stroke-width", 3);
indexData(selection.datum());
const key = getKey(selection.datum());
datum = getDatumFromData(dataIndexingMap[key]);
dataKeyStack.push(key);
let updateLinks = function () {
const links = datum.links().filter(d => d.source.depth <= datum.depth + depth && d.target.depth <= datum.depth + depth);;
linksG.selectAll("path")
.data(links)
.join("path")
.attr("d", d3.linkRadial()
.angle(d => d.x)
.radius(d => d.y));
}
let nodeClick = function (d) {
d3.event.stopPropagation();
if (d.parent === null || !d.data.data.children) {
return
}
const key = getKey(d.data);
datum = getDatumFromData(dataIndexingMap[key]);
dataKeyStack.push(key);
updateData();
}
let updateCircles = function () {
const descendants = datum.descendants().filter(d => d.depth <= datum.depth + depth);
circleG.selectAll("circle")
.data(descendants)
.join("circle")
.on('click', nodeClick)
.style('cursor', d => (d.parent === null || !d.data.data.children) ? "" : 'pointer')
.attr("transform", d => `
rotate(${d.x * 180 / Math.PI - 90})
translate(${d.y},0)
`)
.attr("fill", d => d.children ? "#555" : "#999")
.attr("r", 1);
}
let updateTexts = function () {
const descendants = datum.descendants().filter(d => d.depth <= datum.depth + depth);
textG.selectAll("text").remove();
textG.selectAll("text")
.data(descendants)
.join("text")
.on('click', nodeClick)
.attr("transform", d => `
rotate(${d.x * 180 / Math.PI - 90})
translate(${d.y},0)
rotate(${d.x >= Math.PI ? 180 : 0})
`)
.attr("dy", "0.31em")
.attr("x", d => d.x < Math.PI === !d.children ? 6 : -6)
.attr("text-anchor", d => d.x < Math.PI === !d.children ? "start" : "end")
.style('font-size', 2)
.style('fill', d=> d.data.data.children ? 'black' : 'tomato')
.style('cursor', d => (d.parent === null || !d.data.data.children) ? "" : 'pointer')
.html(d => {
return d.data.name + (d.data.childCount > 0 ? `<tspan fill='green' font-weight='bold'> (${d.data.childCount})</tpsan>` : '');
})
.clone(true).lower()
.attr("stroke", "white");
}
selection.on('click', () => {
if (dataKeyStack.length > 1) {
dataKeyStack.pop();
const key = dataKeyStack[dataKeyStack.length - 1];
datum = getDatumFromData(dataIndexingMap[key]);
updateData();
}
})
updateData = function () {
updateCircles();
updateLinks();
updateTexts();
};
updateData();
}
return tidyTree;
}