Writing

Making a Sankey diagram people actually read

In short: a Sankey diagram works when a reader can follow one flow from left to right without reading a legend. Most unreadable ones fail on the same three things: too many nodes, an order nobody…

published
read time
5 min
words
1,044
lang
en
filed under
Engineering

In short: a Sankey diagram works when a reader can follow one flow from left to right without reading a legend. Most unreadable ones fail on the same three things: too many nodes, an order nobody chose, and colour that means nothing.

I built a small interactive alluvial diagram for this site. The code is public as interactableSankey, it is a single HTML page, and you can paste it into jsfiddle and play with it. Building it taught me more about what readers ignore than about the charting library. I have also been playing with scroll-driven storytelling, where the chart changes as you read, and the same lessons hold there.

A Sankey answers one question well: where did it go? Users from a sign-up page to a plan. Patients from a referral to a clinic. Budget from a source to a project. The width of each band is the quantity. That is the whole idea, and it is why a bad one is so bad. When every band is the same thin grey string, the one encoding the chart has is wasted.

Mistake 1: every category gets a node

Real data has a long tail. If your source column has thirty values, the default chart draws thirty nodes, and twenty-five of them are slivers you cannot hover, cannot label and cannot see. The reader's eye goes to the noise.

The fix is boring. Keep the handful of categories that carry most of the flow and fold the rest into one node called Other. My rule of thumb is five to eight nodes per column. If someone needs the tail, give it its own chart or a table under the diagram. A Sankey is a summary. It should look like one.

TipPick the cut by flow, not by count. Keep the categories that make up most of the total, then check that Other is not the biggest node. If it is, your cut is too aggressive.

Mistake 2: an order nobody chose

Most libraries place nodes in the order they first appear in the data, or they run a layout pass that tries to reduce crossings and gets stuck. Either way, the order is an accident. Bands cross each other, and the biggest flow takes a long diagonal through the middle of the chart, cutting across everything else.

Choose the order yourself. Sort each column by total flow, biggest at the top. Put Other at the bottom, always, so readers learn to skip it. When the biggest source feeds the biggest target, that band now runs almost straight, and a straight band reads as "the main story" without a word of explanation.

before
A B C X Y other after
The same kind of data drawn twice: folded, sorted, and with one flow allowed to stand out.

Mistake 3: colour as decoration

The default palette gives every node its own colour. Ten colours on one chart means the reader has to remember ten mappings, and nobody does. Colour ends up meaning nothing, which is worse than no colour at all, because people still try to read it.

Give colour one job. Decide the question the chart answers, colour the flows that answer it, and make everything else a light grey. If the question is "where do people from source A end up?", source A's bands get the colour and the rest fade back. Links should take the colour of their source, at low opacity, so overlaps stay readable.

Interactive does not mean people will hover

An interactive chart is tempting because you can push detail into tooltips. In practice most readers never hover. They look at the chart for a few seconds and move on. So:

  • Label the two or three main flows directly on the chart, with the number.
  • Use the tooltip for exact values, not for the point of the chart.
  • Make clicking a node highlight its paths and fade everything else. That one interaction is worth more than any other.
  • Write the title as the finding. "Most of the flow from A ends at X" beats "Flow by source and outcome".

The three fixes in code

The version on my site is plain HTML. To show the fixes I am using Python and Plotly, because it is the quickest place to try them on your own data. The input is a CSV with source, target and value columns.

import pandas as pd
import plotly.graph_objects as go

flows = pd.read_csv("flows.csv")  # source, target, value

# 1. Keep the biggest categories, fold the rest into "Other"
def fold(col, keep=6):
    top = flows.groupby(col)["value"].sum().nlargest(keep).index
    return flows[col].where(flows[col].isin(top), "Other")

flows["source"] = fold("source")
flows["target"] = fold("target")
flows = flows.groupby(["source", "target"], as_index=False)["value"].sum()

# 2. Sort each column by size, "Other" always last
def ordered(col):
    size = flows.groupby(col)["value"].sum().sort_values(ascending=False)
    names = [n for n in size.index if n != "Other"]
    return names + (["Other"] if "Other" in size.index else [])

left, right = ordered("source"), ordered("target")
idx = {("L", n): i for i, n in enumerate(left)}
idx.update({("R", n): len(left) + i for i, n in enumerate(right)})
x = [0.01] * len(left) + [0.99] * len(right)
y = [(i + 0.5) / len(left) for i in range(len(left))] + \
    [(i + 0.5) / len(right) for i in range(len(right))]

# 3. Colour answers one question; everything else is grey
focus = left[0]
colors = ["rgba(40, 90, 160, 0.55)" if s == focus else "rgba(150, 150, 150, 0.25)"
          for s in flows["source"]]

fig = go.Figure(go.Sankey(
    arrangement="fixed",
    node=dict(label=left + right, x=x, y=y, pad=12),
    link=dict(
        source=[idx[("L", s)] for s in flows["source"]],
        target=[idx[("R", t)] for t in flows["target"]],
        value=flows["value"],
        color=colors,
    ),
))
fig.update_layout(title=f"Where {focus} goes")
fig.show()

Setting arrangement="fixed" with explicit x and y is what stops the library from reordering your nodes. Without it, step 2 does nothing visible.

Try it on your own chart

Open the last Sankey you made, or one from a report you read this week, and run three checks:

  1. Count the nodes in each column. More than eight means fold.
  2. Find the biggest band. If it crosses the middle diagonally, re-sort.
  3. Cover the legend and say out loud what the colours mean. If you cannot, give colour one job.

Then change the title to the sentence you want the reader to remember. If the chart does not support that sentence at a glance, it is not done yet.

related

Keep reading