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 rectangle
  • nodePadding: Vertical space between nodes
  • extent: 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.

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!