Sankey Diagram

Dataviz logo representing a Sankey chart.

A Sankey Diagram display flows. Several entities (nodes) are represented by rectangles or text. Directed links are represented with arrows or arcs that have a width proportional to the importance of the flow.

This tutorial explains how to use React, D3.js and the d3-sankey plugin to build a Sankey diagram. It comes with explanations and code sandboxes to play along with the suggested implementation.

Useful links

The Data

Two layers of information are required to build a Sankey diagram: a list of nodes to build the rectangles and a list of links to build the paths between them.

Many different data structures can be used to store such information. In this tutorial I suggest to start with the following:

const data = {
  nodes: [
      { node: 0, name: "node0" },
      { node: 1, name: "node1" },
      { node: 2, name: "node2" },
      { node: 3, name: "node3" },
  ],
  links: [
      { source: 0, target: 2, value: 2 },
      { source: 1, target: 2, value: 2 },
      { source: 1, target: 3, value: 2 },
  ]
}

data is an object with 2 properties: nodes and links.

  • nodes is an array where each node is an object defined by its index (node) and its name. Note that any other feature can be added to nodes here.
  • links is another array listing the connections. They are defined by a source and a target and optionnaly with a value.
ToDoExplain how to build this data structure from various initial formats

Component skeleton

The goal here is to create a Sankey component that will be stored in a Sankey.tsx file. This component requires 3 props to render: a width, a height, and some data.

The shape of the data is described above. The width and height will be used to render an svg element in the DOM, in which we will insert the Sankey.

To put it in a nutshell, that's the skeleton of our Sankey component:

import * as d3 from "d3"; // we will need d3.js

type SankeyProps = {
  width: number;
  height: number;
  data: number[];
};

export const Sankey = ({ width, height, data }: SankeyProps) => {

  // read the data
  // create a color scale for the nodes
  // compute node position thanks to the d3.sankey() function
  // build the rectangles and arcs

  return (
    <div>
      <svg width={width} height={height}>
        // render all the <rect> and <path>
      </svg>
    </div>
  );
};

It's fundamental to understand that with this code organization, d3.js will be used to layout (rect and path positions), but it's React that will render them in the return() statement. We won't use d3 methods like append that you can find in usual d3.js examples.

Draw the nodes

To draw the nodes we first need to compute their positions on the SVG area. This is where the d3-sankey plugin gets helpful with its sankey()function.

The sankey() function must be called with a set of options described below as inline comments:

const sankeyGenerator = sankey()  // Main function of the d3-sankey plugin that computes the layout
    .nodeWidth(26)                  // width of the node in pixels
    .nodePadding(29)                // space between nodes
    .extent([                       // chart area:
      [MARGIN_X, MARGIN_Y],                   // top-left coordinates
      [width - MARGIN_X, height - MARGIN_Y],  // botton-right coordinates
    ])
    .nodeId((node) => node.id)      // Accessor function: how to retrieve the id that defines each node. This id is then used for the source and target props of links
    .nodeAlign(sankeyCenter);       // Algorithm used to decide node position

We now have a function called sankeyGenerator that computes the sankey layout from our dataset. We can use it as follow:

const { nodes, links } = sankeyGenerator(data);

And that's it. We now have 2 objects called nodes and links that provide all the necessary information about nodes and links. nodes is an array. For each item we havex0, y0, x1 and y1 that are the coordinates of the top-left and bottom right corners of the rectangle. We are ready for the drawing! ✏️

First step of our ongoing sankey diagram: the nodes are displayed using rectangles.

Draw the connections

The other object we got from the sankey() function is links. It is an array where each item provides detail about a link.

Each item is an object with several properties. Among them:

  • width → provides the width of the arc we will build. We will pass it to the strokeWidth property of the SVG path.
  • source and target → provide a lot of details about the source and target nodes, including their positions. It makes it possible to pass a link to the sankeyLinkHorizontal() function to get the SVG path we will give as the d argument.

To put it in a nutshell, we can loop through the links array and draw apath as follow:

const allLinks = links.map((link, i) => {
  const linkGenerator = sankeyLinkHorizontal();
  const path = linkGenerator(link);

  return (
    <path
      key={i}
      d={path}
      stroke="#a53253"
      fill="none"
      strokeOpacity={0.1}
      strokeWidth={link.width}
    />
  );
});

Resulting in a first Sankey diagram 🎉

First step of our ongoing arc diagram: the nodes are displayed at the bottom of the figure.

Responsive Sankey with react

The component above is not responsive. It expects 2 props called width and height and will render a Sankey of those dimensions.

Making the Sankey responsive requires adding a wrapper component that gets the dimension of the parent div, and listening to a potential dimension change. This is possible thanks to a hook called useDimensions that will do the job for us.

useDimensions: a hook to make your viz responsive
export const useDimensions = (targetRef: React.RefObject<HTMLDivElement>) => {

  const getDimensions = () => {
    return {
      width: targetRef.current ? targetRef.current.offsetWidth : 0,
      height: targetRef.current ? targetRef.current.offsetHeight : 0
    };
  };

  const [dimensions, setDimensions] = useState(getDimensions);

  const handleResize = () => {
    setDimensions(getDimensions());
  };

  useEffect(() => {
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  useLayoutEffect(() => {
    handleResize();
  }, []);

  return dimensions;
}

I'm in the process of writing a complete blog post on the topic. Subscribe to the project to know when it's ready.




Sankey inspiration

If you're looking for inspiration to create your next Sankey, note that dataviz-inspiration.com showcases many examples. Definitely the best place to get ... inspiration!

dataviz-inspiration.com showcases hundreds of stunning dataviz projects. Have a look to get some ideas on how to make your Sankey looks good!

visit

Application to a real dataset

This Sankey diagram visualizes the flow of energy: supplies are on the left, and demands are on the right. It is a reproduction of this famous observable example. Links show how varying amounts of energy are converted or transmitted before being consumed or lost.

The code is very similar to the example above. On top of it, a color scale is used for the node and connection colors, and some text labels have been added.

A Sankey diagram showing the flow of energy. Supplies on the left, demands on the right.

ToDoAdd hover effect to highlight links
ToDoAdd gradient along links
ToDoFix types

Flow

Contact

👋 Hey, I'm Yan and I'm currently working on this project!

Feedback is welcome ❤️. You can fill an issue on Github, drop me a message on Twitter, or even send me an email pasting yan.holtz.data with gmail.com. You can also subscribe to the newsletter to know when I publish more content!