Edge bundling
module imports
Hierarchical edge bundling. For import graphs big enough that straight edges become a hairball.
- Format.d3
- Length87 lines
- Includesnone
The source
87 lines of D3 JavaScript, and uses only the bundled D3 build. Copy it, or open the template inside Gnomon and render it as it is.
// Hierarchical edge bundling — dependencies routed along the package tree.
// Bundling collapses parallel edges into visible cables, so "this package
// depends on half the codebase" reads as one thick strand instead of noise.
const data = {
name: 'app',
children: [
{ name: 'ui', children: [
{ name: 'ui.shell', imports: ['core.router', 'core.auth', 'data.store'] },
{ name: 'ui.editor', imports: ['core.router', 'data.store', 'data.files'] },
{ name: 'ui.preview', imports: ['data.files', 'render.svg'] },
]},
{ name: 'core', children: [
{ name: 'core.router', imports: ['core.log'] },
{ name: 'core.auth', imports: ['core.log', 'data.store'] },
{ name: 'core.log', imports: [] },
]},
{ name: 'data', children: [
{ name: 'data.store', imports: ['core.log'] },
{ name: 'data.files', imports: ['core.log'] },
]},
{ name: 'render', children: [
{ name: 'render.svg', imports: ['core.log', 'render.text'] },
{ name: 'render.text', imports: [] },
]},
],
};
const radius = Math.min(width, height) / 2 - 110;
const tree = d3.cluster().size([2 * Math.PI, radius]);
const root = tree(d3.hierarchy(data)
.sort((a, b) => d3.ascending(a.data.name, b.data.name)));
// Resolve each leaf's `imports` into node pairs, both directions, so an
// edge can be drawn from either end.
const byName = new Map(root.leaves().map(d => [d.data.name, d]));
for (const leaf of root.leaves()) {
leaf.incoming = [];
leaf.outgoing = (leaf.data.imports || [])
.map(name => [leaf, byName.get(name)])
.filter(pair => pair[1]);
}
for (const leaf of root.leaves()) {
for (const edge of leaf.outgoing) edge[1].incoming.push(edge);
}
const line = d3.lineRadial()
.curve(d3.curveBundle.beta(0.85))
.radius(d => d.y)
.angle(d => d.x);
const color = d3.scaleOrdinal(
data.children.map(c => c.name),
theme.palette,
);
const groupOf = d => d.ancestors().find(a => a.depth === 1)?.data.name ?? '';
const svg = d3.select(container).append('svg')
.attr('viewBox', [-width / 2, -height / 2, width, height])
.attr('width', width)
.attr('height', height)
.attr('font-family', 'system-ui, sans-serif')
.attr('font-size', 11);
svg.append('g')
.attr('fill', 'none')
.attr('stroke-opacity', 0.45)
.attr('stroke-width', 1.5)
.selectAll('path')
.data(root.leaves().flatMap(leaf => leaf.outgoing))
.join('path')
.attr('stroke', ([from]) => color(groupOf(from)))
.attr('d', ([from, to]) => line(from.path(to)));
svg.append('g')
.selectAll('g')
.data(root.leaves())
.join('g')
.attr('transform', d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`)
.append('text')
.attr('dy', '0.31em')
.attr('x', d => d.x < Math.PI ? 6 : -6)
.attr('text-anchor', d => d.x < Math.PI ? 'start' : 'end')
.attr('transform', d => d.x < Math.PI ? null : 'rotate(180)')
.attr('fill', d => color(groupOf(d)))
.text(d => d.data.name);Render this offline
This template ships in Gnomon and renders on your machine, with no account and nothing sent to a server. The browser editor is free and needs no install.
Others in D3 visualisations
- Sankey — request flowFlow with volume. The best chart here for "where does it all go".
- Chord — service interactionWho talks to whom, when the traffic is bidirectional.
- Arc diagram — dependenciesNodes on one axis, arcs above. Readable where a force graph is not, provided the ordering means something.
- Adjacency matrix — couplingAdjacency matrix. Unfashionable, and better than a force graph for dense dependencies.
- Sunburst — nested spendNested hierarchy, radially. Prettier than an icicle, harder to compare.
- Icicle — nested spend (linear)A sunburst unrolled flat. Harder to love, much easier to compare siblings.
- Circle packing — nested sizeCircle packing. Nested size when the nesting matters more than reading exact areas.
- Treemap — portfolio costNested size. Good for cost, storage, lines of code.
- Tidy tree — structureA tidy tree. The default for anything with one parent per node.
- Radial tree — structure (radial)The same tree bent into a circle. Fits more depth on a slide, costs you easy comparison.
- Dendrogram — clusteringClustering, with join height carrying the distance. Not merely a tree with curves.
- Indented tree — file/spec outlineA file or spec outline. The chart that looks like the thing it describes.
- Force graph — service mapService map. Use for clusters, not for reading individual edges.
- Force graph — disjoint clustersForce graph that keeps unconnected clusters apart rather than flinging them off screen.
- Force graph — radial tiersForce layout pinned to rings, so tier is a position instead of a colour legend.
- Directed graph — call directionDirected edges with arrowheads. For when direction is the question, not just adjacency.
- Force tree — blast radiusBlast radius from one node: what breaks if this goes.
- Calendar heatmap — daily activityDaily activity over a year: deploys, incidents, commits.
- Streamgraph — shifting mixShifting composition over time. Good for the mix, poor for reading any single value.
- Gantt — delivery roadmapDelivery roadmap, rendered from data.
- Radar — capability scoringCapability scoring across axes. Fine for one subject, misleading with four overlaid.
- Bullet chart — target vs actualTarget against actual, in one line. The chart a gauge wishes it were.
- Beeswarm — distribution by groupEvery point, grouped, without the overplotting a strip plot suffers.
- Horizon chart — many series, little spaceMany series in little space. Takes a moment to learn, then very dense.
- Slope chart — before and afterBefore and after, two points, one line each. Devastatingly clear.
- Parallel coordinates — multi-criteriaMulti-criteria comparison: for option analysis.
- Box plot — latency distributionLatency distribution. The chart that shows the tail a mean hides.
- Grouped bar — category comparisonCategory comparison. Unglamorous, and usually the right answer.
- Multi-line — metrics over timeMetrics over time. Keep it under about five series, or switch to horizon.