Visualizing data with D3.js Trees

Visualizing data with D3.js Trees

Interactive diagrams don’t just look good — they tell stories.

For music fans, there's nothing more engaging than exploring musical lineage, and tools like D3.js bring that history to life with powerful, collapsible tree structures.

See my working demo here

Full code at the end but read the explainer first - don't be impatient!


🔹 Why Use D3.js for Trees?

D3.js offers fine-grained control over every element on the screen — from node positioning to transition timing. You can craft a visualization that:

  • Collapses and expands dynamically
  • Responds to user clicks
  • Scales beautifully on all screen sizes

Perfect for things like:

  • Musical lineages
  • Company org charts
  • Product line evolution
  • Tech stack inheritance

Getting Started: A Minimal Working Tree

Here’s a simplified code sample of what I used on the British Blues Boom demo:

<script src="https://d3js.org/d3.v7.min.js"></script>
<div id="tree"></div>
<script>
const data = {
  name: "Bands",
  children: [
    {
      name: "The Yardbirds",
      children: [
        { name: "Eric Clapton", children: [ { name: "Cream" } ] },
        { name: "Jimmy Page", children: [ { name: "Led Zeppelin" } ] },
        { name: "Jeff Beck", children: [ { name: "The Jeff Beck Group" } ] }
      ]
    }
  ]
};
// tree layout + render code goes here (see full example on site)
</script>

👉 Full code available here


💡 Why It Matters

This kind of interactivity does more than impress — it engages.

Your content becomes stickier, your bounce rates drop, and your storytelling becomes memorable.

Great UI isn’t decoration — it’s persuasion.

And it’s not just for music. You can adapt this technique for:

  • Mapping relationships
  • Explaining tech stacks
  • Visualizing customer journeys

Built-In to DevStack

If you're a developer looking to plug in a feature like this, I’ve already done the heavy lifting for you.

My DevStack includes a modular, reusable D3.js integration you can drop into any ASP.NET Core site.

No need to wrestle with JSON or browser quirks — I’ve been there.


🎯 Business Benefits

As someone with over 30 years of business experience, I don’t just build fancy visualizations — I build tools that make content more valuable, more engaging, and ultimately... more profitable.

Ask me how one client added £25K annual sales with a single UI enhancement.

Ready to Engage Your Audience?

This type of feature makes your content shareable, sticky, and smart — especially when paired with solid back-end architecture.

If you want to skip the D3.js learning curve and integrate it fast:

👉 Request access to DevStack or get in touch


Here's a fully working example of a collapsible tree diagram using D3.js from scratch.


Full Implementation

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Collapsible Tree</title>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <style>
        .node circle {
            fill: #999;
            stroke: steelblue;
            stroke-width: 2px;
        }

        .node text {
            font: 12px sans-serif;
        }

        .link {
            fill: none;
            stroke: #555;
            stroke-opacity: 0.4;
            stroke-width: 1.5px;
        }
    </style>
</head>
<body>
    <div id="tree"></div>

    <script>
        // JSON Data
        const data = {
            "name": "Bands",
            "children": [
                {
                    "name": "The Yardbirds",
                    "children": [
                        {
                            "name": "Eric Clapton",
                            "children": [
                                { "name": "Cream" },
                                { "name": "Derek and the Dominos" }
                            ]
                        },
                        {
                            "name": "Jimmy Page",
                            "children": [
                                { "name": "Led Zeppelin" }
                            ]
                        },
                        {
                            "name": "Jeff Beck",
                            "children": [
                                { "name": "The Jeff Beck Group" }
                            ]
                        }
                    ]
                },
                {
                    "name": "Fleetwood Mac",
                    "children": [
                        {
                            "name": "Peter Green",
                            "children": [
                                { "name": "Solo Career" }
                            ]
                        },
                        {
                            "name": "Mick Fleetwood",
                            "children": [
                                { "name": "Fleetwood Mac (1970s Evolution)" }
                            ]
                        }
                    ]
                }
            ]
        };

        // Create the tree layout
        const width = 960;
        const dx = 20;
        const dy = 180;

        const margin = { top: 20, right: 120, bottom: 20, left: 120 };
        const tree = d3.tree().nodeSize([dx, dy]);

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

        const svg = d3.select("#tree").append("svg")
            .attr("viewBox", [-margin.left, -margin.top, width, dx])
            .style("font", "10px sans-serif")
            .style("user-select", "none");

        const g = svg.append("g")
            .attr("transform", `translate(${margin.left},${margin.top})`);

        const root = d3.hierarchy(data);
        root.x0 = dx / 2;
        root.y0 = 0;

        // Collapse all nodes by default
        root.descendants().forEach((d, i) => {
            d.id = i;
            d._children = d.children;
            if (d.depth) d.children = null;
        });

        // Function to update the tree
        function update(source) {
            const nodes = root.descendants();
            const links = root.links();

            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;
            });

            const height = right.x - left.x + margin.top + margin.bottom;

            svg.transition()
                .duration(750)
                .attr("viewBox", [-margin.left, left.x - margin.top, width, height]);

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

            const nodeEnter = node.enter().append("g")
                .attr("class", "node")
                .attr("transform", d => `translate(${source.y0},${source.x0})`)
                .on("click", (event, d) => {
                    d.children = d.children ? null : d._children;
                    update(d);
                });

            nodeEnter.append("circle")
                .attr("r", 5)
                .attr("fill", d => d._children ? "#555" : "#999");

            nodeEnter.append("text")
                .attr("dy", "0.31em")
                .attr("x", d => d._children ? -10 : 10)
                .attr("text-anchor", d => d._children ? "end" : "start")
                .text(d => d.data.name);

            const nodeUpdate = node.merge(nodeEnter).transition()
                .duration(750)
                .attr("transform", d => `translate(${d.y},${d.x})`);

            nodeUpdate.select("circle")
                .attr("r", 5)
                .attr("fill", d => d._children ? "#555" : "#999");

            const nodeExit = node.exit().transition()
                .duration(750)
                .attr("transform", d => `translate(${source.y},${source.x})`)
                .remove();

            const link = g.selectAll("path.link")
                .data(links, d => d.target.id);

            const linkEnter = link.enter().append("path")
                .attr("class", "link")
                .attr("d", d => {
                    const o = { x: source.x0, y: source.y0 };
                    return diagonal({ source: o, target: o });
                });

            link.merge(linkEnter).transition()
                .duration(750)
                .attr("d", diagonal);

            link.exit().transition()
                .duration(750)
                .attr("d", d => {
                    const o = { x: source.x, y: source.y };
                    return diagonal({ source: o, target: o });
                })
                .remove();

            root.eachBefore(d => {
                d.x0 = d.x;
                d.y0 = d.y;
            });
        }

        update(root);
    </script>

<!-- Visual Studio Browser Link -->
<script type="text/javascript" src="/_vs/browserLink" async="async" id="__browserLink_initializationData" data-requestId="5c8b24c8ae3e4264a30fdf4f0f4fb2e7" data-requestMappingFromServer="false" data-connectUrl="http://localhost:56376/c11a15de0f1f44c49cfaad8279c090cf/browserLink"></script>
<!-- End Browser Link -->
<script src="/_framework/aspnetcore-browser-refresh.js"></script></body>
</html>