Creating an Interactive Sankey Diagram
In this tutorial, we’ll build an interactive Sankey diagram using Svelte 5 and D3.js. Sankey diagrams are perfect for visualizing flow data, showing how quantities are transferred between different nodes in a system.
You can download the data here
Energy Flow Diagram
What You’ll Learn
- Setting up D3’s Sankey layout
- Creating flow paths between nodes
- Implementing interactive hover effects
- Handling hierarchical data structures
- Making the diagram responsive
Full Code Preview
<script>
import { sankey, sankeyLinkHorizontal } from 'd3-sankey';
import { scaleOrdinal } from 'd3-scale';
import { json } from 'd3-fetch';
import { onMount } from 'svelte';
// Data and dimensions
let data = $state({ nodes: [], links: [] });
let width = $state(800);
const height = 600;
const margin = { top: 20, right: 20, bottom: 20, left: 20 };
// Color scale for nodes
const colorScale = scaleOrdinal().range([
'#66c2a5',
'#fc8d62',
'#8da0cb',
'#e78ac3',
'#a6d854',
'#ffd92f',
'#e5c494',
'#b3b3b3'
]);
// Sankey generator
let sankeyGenerator = $derived(
sankey()
.nodeWidth(15)
.nodePadding(10)
.extent([
[margin.left, margin.top],
[width - margin.right, height - margin.bottom]
])
);
// Process data through sankey generator
let sankeyData = $derived(
data.nodes.length === 0
? null
: sankeyGenerator({
nodes: data.nodes.map((d) => ({ ...d })),
links: data.links.map((d) => ({ ...d }))
})
);
// Link generator
const linkGenerator = sankeyLinkHorizontal();
// Load data on mount
onMount(async () => {
try {
data = await json(
'https://gist.githubusercontent.com/kunalb/4647670/raw/a4b180a30763b555c43a0979527e93dc4076d6ce/energy.json'
);
} catch (error) {
console.error('Error loading JSON:', error);
}
});
</script>
<div
class="relative min-w-full max-w-4xl rounded-xl p-6"
bind:clientWidth={width}>
<h2 class="text-xl font-bold text-gray-200 mb-6 text-center">
Energy Flow Diagram
</h2>
{#if sankeyData}
<svg {width} {height}>
<!-- Links -->
{#each sankeyData.links as link}
<path
d={linkGenerator(link)}
fill="none"
stroke={colorScale(link.source.name)}
stroke-opacity="0.4"
stroke-width={Math.max(1, link.width)}
class="link" />
{/each}
<!-- Nodes -->
{#each sankeyData.nodes as node}
<g class="node" transform="translate({node.x0},{node.y0})">
<rect
height={node.y1 - node.y0}
width={node.x1 - node.x0}
fill={colorScale(node.name)}
class="node-rect">
<title>{node.name}: {node.value}</title>
</rect>
<text
x={node.x0 < width / 2 ? node.x1 - node.x0 + 6 : -6}
y={(node.y1 - node.y0) / 2}
dy="0.35em"
text-anchor={node.x0 < width / 2 ? 'start' : 'end'}
class="fill-gray-200 text-xs">
{node.name}
</text>
</g>
{/each}
</svg>
{/if}
</div>
<style>
.link {
transition: stroke-opacity 200ms ease-in-out;
}
.link:hover {
stroke-opacity: 0.8;
}
.node-rect {
transition: opacity 200ms ease-in-out;
opacity: 0.8;
}
.node-rect:hover {
opacity: 1;
}
</style>
Step-by-Step Guide
Setting Up D3 Modules
We start by importing the necessary D3 modules:
<script>
import { sankey, sankeyLinkHorizontal } from 'd3-sankey';
import { scaleOrdinal } from 'd3-scale';
import { json } from 'd3-fetch';
//
</script>
The d3-sankey
module provides the layout algorithm for calculating node positions and link paths. We also use scaleOrdinal
for color mapping and json
for data loading.
Creating the Sankey Generator
The Sankey generator is responsible for calculating the positions and dimensions of nodes and links:
<script>
//
let sankeyGenerator = $derived(
sankey()
.nodeWidth(15)
.nodePadding(10)
.extent([
[margin.left, margin.top],
[width - margin.right, height - margin.bottom]
])
);
</script>
Key parameters:
nodeWidth
: Width of each node rectanglenodePadding
: Vertical space between nodesextent
: The overall layout boundaries
Processing the Data
The data needs to be processed through the Sankey generator to calculate positions:
<script>
let sankeyData = $derived(
data.nodes.length === 0
? null
: sankeyGenerator({
nodes: data.nodes.map((d) => ({ ...d })),
links: data.links.map((d) => ({ ...d }))
})
);
</script>
We create copies of the nodes and links arrays to avoid mutating the original data.
Drawing Links
The links are drawn using the sankeyLinkHorizontal
generator:
{#each sankeyData.links as link}
<path
d={linkGenerator(link)}
fill="none"
stroke={colorScale(link.source.name)}
stroke-opacity="0.4"
stroke-width={Math.max(1, link.width)}
class="link" />
{/each}
The link width represents the flow quantity, and the color is based on the source node.
Drawing Nodes
Nodes are rendered as rectangles with labels:
{#each sankeyData.nodes as node}
<g class="node" transform="translate({node.x0},{node.y0})">
<rect
height={node.y1 - node.y0}
width={node.x1 - node.x0}
fill={colorScale(node.name)}
class="node-rect">
<title>{node.name}: {node.value}</title>
</rect>
<text
x={node.x0 < width / 2 ? node.x1 - node.x0 + 6 : -6}
y={(node.y1 - node.y0) / 2}
dy="0.35em"
text-anchor={node.x0 < width / 2 ? 'start' : 'end'}
class="fill-gray-200 text-xs">
{node.name}
</text>
</g>
{/each}
The node height represents its value in the system.
Data Format
The Sankey diagram expects JSON data in the following format:
{
"nodes": [
{ "name": "Agricultural waste" },
{ "name": "Bio-conversion" },
{ "name": "Liquid" },
{ "name": "Losses" },
{ "name": "Solid" },
{ "name": "Gas" }
],
"links": [
{ "source": 0, "target": 1, "value": 124.729 },
{ "source": 1, "target": 2, "value": 0.597 },
{ "source": 1, "target": 3, "value": 26.862 }
]
}
The links
array defines connections between nodes using their indices in the nodes
array, and the value
property determines the width of the flow.
Conclusion
In this tutorial, we created an interactive Sankey diagram that effectively visualizes flow relationships in a dataset. The diagram features smooth transitions, interactive hover effects, and automatic layout calculations.
Join our Discord to share your charts, ask questions, or collaborate with fellow developers!