Public
Edited
May 7
1 star
Insert cell
Insert cell
chart = {
// Specify the charts' dimensions with more space
const width = 1500;
const height = 900;
const marginTop = 60;
const marginRight = 260;
const marginBottom = 60;
const marginLeft = 260;

// Create a horizontal tree layout
const root = d3.hierarchy(data);
// Significantly increase spacing between nodes
const dx = 40; // increased vertical spacing between nodes
const dy = (width - marginRight - marginLeft) / (root.height + 0.5); // increased horizontal spacing between levels

// Define the tree layout with more separation between siblings
const tree = d3.tree()
.nodeSize([dx, dy])
.separation((a, b) => a.parent === b.parent ? 1.5 : 2.5); // Increase separation between nodes

const diagonal = d3.linkHorizontal().x(d => d.y).y(d => d.x);

// Create the SVG container
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-marginLeft, -marginTop, width, height])
.attr("style", "max-width: 100%; height: auto; font-family: Arial, sans-serif; user-select: none;");

// Add a background rectangle for better visibility
svg.append("rect")
.attr("x", -marginLeft)
.attr("y", -marginTop)
.attr("width", width)
.attr("height", height)
.attr("fill", "#f9f9f9");

// Create layers for links and nodes
const gLink = svg.append("g")
.attr("fill", "none")
.attr("stroke-opacity", 0.7)
.attr("stroke-width", 2.5);

const gNode = svg.append("g")
.attr("cursor", "pointer")
.attr("pointer-events", "all");

// Define color scheme for different levels (Professional, tech‑savvy theme)
const levelColors = [
"#0B1E3A", // Level 0 – Deep Navy (Root: trust & stability)
"#1F2A44", // Level 1 – Charcoal Blue (Emotional anchor)
"#3A5F8A", // Level 2 – Steel Blue (Analytical clarity)
"#2AA6B1", // Level 3 – Teal (Action & hope)
"#68C3A3", // Level 4 – Soft Mint (Concrete steps)
"#B8E1D3", // Level 5 – Pale Mint (Detail focus)
"#EAF2F8", // Level 6 – Light Sky (Background support)
"#FFFFFF", // Level 7 – Pure White (Clean space)
"#F0F4F8", // Level 8 – Off‑White (Subtle overlay)
"#DDE2E7" // Level 9 – Cool Gray (Low‑key accent)
];


// Function to get font size based on depth
function getFontSize(depth) {
const baseFontSize = 20;
return Math.max(baseFontSize - (depth * 1.2), 12) + "px";
}

function update(event, source) {
const nodes = root.descendants();
const links = root.links();

// Compute the new tree layout
tree(root);

let left = root;
let right = root;
root.eachBefore(node => {
if (node.x < left.x) left = node;
if (node.x > right.x) right = node;
});

// Ensure enough vertical space
const height = Math.max(900, right.x - left.x + marginTop + marginBottom + 100);

// Update the SVG height to fit the tree
svg.attr("height", height)
.attr("viewBox", [-marginLeft, left.x - marginTop, width, height]);

// Update the nodes
const node = gNode.selectAll("g")
.data(nodes, d => d.id);

// Remove old nodes
node.exit().remove();

// Enter any new nodes at their final position
const nodeEnter = node.enter().append("g")
.attr("transform", d => `translate(${d.y},${d.x})`)
.on("click", (event, d) => {
d.children = d.children ? null : d._children;
update(event, d);
});

// Add circles for nodes
nodeEnter.append("circle")
.attr("r", d => 8 - d.depth * 0.5)
.attr("fill", d => d._children ? levelColors[d.depth % levelColors.length] : "#fff")
.attr("stroke", d => levelColors[d.depth % levelColors.length])
.attr("stroke-width", 2);

// Add labels for nodes with increased spacing
nodeEnter.append("text")
.attr("dy", "0.31em")
.attr("x", d => d._children ? -12 : 12) // Increased spacing from circle
.attr("text-anchor", d => d._children ? "end" : "start")
.text(d => d.data.name)
.style("font-size", d => getFontSize(d.depth))
.style("font-weight", d => d.depth < 2 ? "bold" : "normal")
.style("fill", d => levelColors[d.depth % levelColors.length])
.clone(true).lower()
.attr("stroke", "white")
.attr("stroke-width", 4); // Increased for better text visibility

// Update existing nodes to their new position
node.attr("transform", d => `translate(${d.y},${d.x})`);

// Update circle fill based on whether node has hidden children
node.select("circle")
.attr("fill", d => d._children ? levelColors[d.depth % levelColors.length] : "#fff");

// Update the links
const link = gLink.selectAll("path")
.data(links, d => d.target.id);

// Remove old links
link.exit().remove();

// Enter any new links at their final position
link.enter().append("path")
.attr("d", diagonal)
.attr("stroke", d => levelColors[d.source.depth % levelColors.length])
.attr("stroke-width", d => Math.max(3.5 - d.source.depth * 0.3, 1.8));

// Update existing links
link.attr("d", diagonal);

// Stash the old positions for next update
root.eachBefore(d => {
d.x0 = d.x;
d.y0 = d.y;
});
}

// Initialize the display
root.x0 = dy / 2;
root.y0 = 0;
// Process all nodes
root.descendants().forEach((d, i) => {
d.id = i;
d._children = d.children;
// Only show the first two levels initially
if (d.depth > 1) d.children = null;
});

update(null, root);

return svg.node();
}

Insert cell
data = FileAttachment("tech-decisiontree.json").json()
Insert cell
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