Skip to content

Instantly share code, notes, and snippets.

@estasney
Created November 25, 2019 17:03
Show Gist options
  • Save estasney/8401e88c8bce973ece9ce03ebec7783a to your computer and use it in GitHub Desktop.
Save estasney/8401e88c8bce973ece9ce03ebec7783a to your computer and use it in GitHub Desktop.
D3 Collapsible Tidy Tree (v5)
<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>
/*
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