Created
November 25, 2019 17:03
-
-
Save estasney/8401e88c8bce973ece9ce03ebec7783a to your computer and use it in GitHub Desktop.
D3 Collapsible Tidy Tree (v5)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
</<!DOCTYPE html> | |
<html lang="en" dir="ltr"> | |
<head> | |
<meta charset="utf-8"> | |
<title></title> | |
<script src="https://d3js.org/d3.v5.js"></script> | |
<script type="text/javascript" src="d3-hierarchy.js"></script> | |
<style> | |
.link { | |
stroke: #049FD9; | |
opacity: 0.3; | |
stroke-width: 1.5px; | |
} | |
text { | |
opacity: 0.9; | |
pointer-events: none; | |
} | |
circle.depth_0 { | |
fill:#3778bf; | |
} | |
circle.depth_1 { | |
fill: #feb308; | |
} | |
circle.depth_2{ | |
fill: #7bb274; | |
} | |
circle.depth_3 { | |
fill: #d9544d; | |
} | |
circle.depth_4 { | |
fill: #d9544d; | |
} | |
circle.depth_5 { | |
fill: #d9544d; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="main"> | |
</div | |
</body> | |
<script type="text/javascript" src="tidytree.js"></script> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Debug variables that capture data at various points | |
*/ | |
let data, hData, links, nodes, rootnode; | |
/* | |
Globals | |
*/ | |
const dataURI = ""; | |
let hierarchy = {}; | |
let chart; | |
//Sizing | |
let margin = ({top: 30, right: 60, bottom: 30, left: 30}); | |
const viewportWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; | |
const viewportHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; | |
let dy = viewportWidth / 6; | |
let dx = 20; | |
let tree = d3.tree().nodeSize([dx, dy]); | |
let diagonal = d3.linkHorizontal() | |
.x(d => d.y) | |
.y(d => d.x); | |
/* | |
Chart Options | |
*/ | |
const initialDepth = 0; // Nodes with depth greater than this value are initially hidden | |
const animationDuration = 250; // Time in ms to animate update | |
/* | |
End Chart Options | |
*/ | |
/* | |
End Globals | |
*/ | |
/* | |
Initial run | |
*/ | |
d3.json(dataURI, { | |
crossOrigin: "anonymous" | |
}) | |
.then((json) => { | |
data = json; //data has json in initial state | |
return json; | |
}) | |
.then((json) => { | |
return levelNodes(json); | |
}) | |
.then((result) => { | |
hData = result; //captures that flat array levelNodes returns | |
return result | |
}) | |
.then((result) => { | |
hData = d3.stratify()(result); | |
chart = makeTree(hData); //keep chart reference around for updates | |
document.querySelector("body").appendChild(chart); | |
}); | |
// preprocess json data | |
function levelNodes(data) { | |
return new Promise((resolve) => { | |
hierarchy["Root"] = { | |
id: "0", | |
parentId: undefined, | |
weight: 1, | |
name: "", | |
}; | |
data.forEach(function (link) { | |
if (link.child_id !== 0) { | |
hierarchy[link.child_id] = { | |
id: link.child_id, | |
name: link.child, | |
parentId: link.parent, | |
weight: link.child_weight || 1, | |
parent_weight: link.parent_weight, | |
parent_level: link.parent_level | |
} | |
} | |
}); | |
resolve(Object.values(hierarchy)); | |
}) | |
} | |
function makeTree(data) { | |
const root = d3.hierarchy(data); | |
rootnode = root; //capture root in rootnode | |
root.x0 = dy / 2; | |
root.y0 = 0; | |
// A hidden node has its children stored in _children | |
root.descendants().forEach((d, i) => { | |
d._id = i; | |
d._children = d.children; | |
if (d.depth > initialDepth) d.children = null; | |
}); | |
const svg = d3.create("svg") | |
.attr("viewBox", [-margin.left, -margin.top, viewportWidth, dx]) // min-x, min-y, width, height | |
.style("font", "10px sans-serif") | |
.style("user-select", "none"); | |
const gLink = svg.append("g") | |
.attr("fill", "none") | |
.attr("stroke", "#555") | |
.attr("stroke-opacity", 0.4) | |
.attr("stroke-width", 1.5); | |
const gNode = svg.append("g") | |
.attr("cursor", "pointer") | |
.attr("pointer-events", "all"); | |
function update(source) { | |
const duration = d3.event && d3.event.altKey ? (animationDuration * 10) : animationDuration; // Slow animation by Alt + Clicking | |
const nodes = root.descendants().reverse(); | |
const links = root.links(); | |
// Compute the new tree layout. | |
tree(root); | |
let left = root; | |
let right = root; | |
let up = root; | |
// find the left-most, right-most, etc points | |
// this is probably more efficient than calling reduce multiple times | |
root.eachBefore(node => { | |
if (node.x < left.x) left = node; | |
if (node.x > right.x) right = node; | |
if (node.y > up.y) up = node; | |
}); | |
const chartHeight = right.x - left.x + margin.top + margin.bottom; | |
const _chartWidth = up.y + margin.left + margin.right; | |
const chartWidth = _chartWidth > viewportWidth ? _chartWidth : viewportWidth; | |
console.log(_chartWidth); | |
console.log(chartWidth); | |
const transition = svg.transition() | |
.duration(duration) | |
.attr("viewBox", [-margin.left, left.x - margin.top, chartWidth, chartHeight]) | |
.tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle")); | |
// Update the nodes… | |
const node = gNode.selectAll("g") | |
.data(nodes, d => d._id); | |
// Enter any new nodes at the parent's previous position. | |
const nodeEnter = node.enter().append("g") | |
.attr("transform", d => `translate(${source.y0},${source.x0})`) | |
.attr("fill-opacity", 0) | |
.attr("stroke-opacity", 0) | |
.on("click", d => { | |
d.children = d.children ? null : d._children; | |
update(d) | |
}); | |
nodeEnter.append("circle") | |
.attr("r", d => 10.5 / (d.depth + 1)) | |
.attr("fill", d => d._children ? "#555" : "#999") | |
.attr("class", d => "depth_" + d.depth); | |
nodeEnter.append("text") | |
.attr("dy", "0.31em") | |
.attr("x", d => d._children ? -6 : 6) | |
.attr("text-anchor", d => d._children ? "end" : "start") | |
.text(d => d.data.data.name) | |
.clone(true).lower() | |
.attr("stroke-linejoin", "round") | |
.attr("stroke-width", 3) | |
.attr("stroke", "white"); | |
// Transition nodes to their new position. | |
const nodeUpdate = node.merge(nodeEnter).transition(transition) | |
.attr("transform", d => `translate(${d.y},${d.x})`) | |
.attr("fill-opacity", 1) | |
.attr("stroke-opacity", 1); | |
// Transition exiting nodes to the parent's new position. | |
const nodeExit = node.exit().transition(transition).remove() | |
.attr("transform", d => `translate(${source.y},${source.x})`) | |
.attr("fill-opacity", 0) | |
.attr("stroke-opacity", 0); | |
// Update the links… | |
const link = gLink.selectAll("path") | |
.data(links, d => d.target._id); | |
// Enter any new links at the parent's previous position. | |
const linkEnter = link.enter().append("path") | |
.attr("d", d => { | |
const o = { | |
x: source.x0, | |
y: source.y0 | |
}; | |
return diagonal({ | |
source: o, | |
target: o | |
}); | |
}); | |
// Transition links to their new position. | |
link.merge(linkEnter).transition(transition) | |
.attr("d", diagonal); | |
// Transition exiting nodes to the parent's new position. | |
link.exit().transition(transition).remove() | |
.attr("d", d => { | |
const o = { | |
x: source.x, | |
y: source.y | |
}; | |
return diagonal({ | |
source: o, | |
target: o | |
}); | |
}); | |
// Stash the old positions for transition. | |
root.eachBefore(d => { | |
d.x0 = d.x; | |
d.y0 = d.y; | |
}); | |
} | |
update(root); | |
return svg.node(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment