Public
Edited
Apr 6, 2023
3 forks
Importers
7 stars
Insert cell
Insert cell
Insert cell
// Here's the drawing. It's pretty standard d3.selection.join type stuff built on
// top of all the tools below.

pic = {
let svg = d3.create("svg").attr("width", size.w).attr("height", size.h);
let g = svg
.append("g")
.attr("transform", `translate(${size.margin}, ${size.margin})`);

let links = root.links();
g.append("g")
.attr("id", "links")
.selectAll("path")
.data(root.links())
.join("path")
.attr("d", diagonal)
.attr("fill", "none")
.attr("stroke", "#555")
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 1.5);

let nodes = root.descendants();
g.append("g")
.selectAll("circle")
.data(root.descendants())
.join("circle")
.attr("cx", (d) => (size.w > size.size_break ? d.y : d.x))
.attr("cy", (d) => (size.w > size.size_break ? d.x : d.y))
.attr("r", 4 * (size.w / 1000) ** 0.5)
.attr("fill", "black");

return svg.node();
}
Insert cell
// There's a family of d3.link* functions that work well with d3.hierarchy and d3.tree.
// They all accept output from root.links (which returns pairs of linked nodes) and
// draws some type of path from one node to the other.

diagonal = {
if (size.w > size.size_break) {
return d3
.linkHorizontal()
.x((d) => d.y)
.y((d) => d.x);
} else {
return d3
.linkVertical()
.x((d) => d.x)
.y((d) => d.y);
}
}
Insert cell
// Here's another look at root, which was defined two cells down from here.
// This one waits until layout resolves, though, so you can see its effect.
// In particular, note that the nodes of this nested structure have x and y
// properties that we can use for placement.

{
layout;
return root;
}
Insert cell
// d3.tree()(hierarchy) supplements the input hierarchy further
// with layout information. See the next cell above to see what
// I mean.

layout = {
if (size.w > size.size_break) {
return d3.tree().size([size.h - 2 * size.margin, size.w - 2 * size.margin])(
root
);
} else {
return d3.tree().size([size.w - 2 * size.margin, size.h - 2 * size.margin])(
root
);
}
}
Insert cell
// d3.hierarchy accepts a nested structure like we already have and supplements it
// with additional tools. For example,
// any_node.descendants() will list all nodes descended from any_node.
// root.descendants() therefore lists all nodes.
// any_node.links() will, similarly, list all pairs of nodes that are connected.

root = d3.hierarchy(tree)
Insert cell
// This is the initial data set up via a standard random tree construction.
// The root node is an object with two properties:
// depth: which tells you how many steps from the root node you are and
// children: which is an array initialized to be just []

// We put the root on a stack and then, while the stack is non-empty,
// we pop a node off, randomly generate up to three children, put
// those the stack, and continue. Note that the probability of generating
// children decreases exponentially with depth.

tree = {
new_tree;
let p = 0.7;
let root = { depth: 0, children: [] };
let stack = [root];

while (stack.length > 0) {
let node = stack.pop();
for (let i = 0; i < 3; i++) {
if (d3.randomUniform(0, 1.2)() < p ** node.depth) {
let child = { depth: node.depth + 1, children: [] };
node.children.push(child);
stack.push(child);
}
}
}
return root;
}
Insert cell
// Just a little global variable that affects both layout and pic.

size = {
let w = d3.min([width, 1000]);
let h = 0.625 * w;
let margin = 10;
let size_break = 0;
return { w, h, margin, size_break };
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more