Heartwood Health · How to Build

07 · Sankey Ribbon.

← components.html
all how-tos
07
Sankey Ribbon

Flow · sources to targets

Where things go. Patient flow, funnel splits, traffic-source-to-conversion. Reads as a story when the ribbon thickness pops.

When to use

A population that splits, recombines, or branches between two stages. Don't use for more than 2 stages here — multi-stage sankeys exist but they need a different layout (and a lot more pixels). Cap sources at ~5 and targets at ~6 for legibility.

How it animates

Each ribbon is a closed Bezier path. Initial fill-opacity: 0; transitioning to 0.55 over 0.7s with a 60ms stagger. The cumulative reveal — top-of-source to bottom-of-target — gives the eye time to follow each ribbon's path before the next appears.

Data shape
patientFlow: {
  sources: [
    { label: 'Emergency',
      color: 'var(--primary)' },
    // add freely
  ],
  targets: [{ label: 'Discharged', color: ... }, ...],
  flows: [
    { src: 'Emergency', tgt: 'Discharged', value: 240 },
    // every src/tgt must match a node label
  ],
}
Build pattern
// Allocate Y position on BOTH sides as ribbons stack.
// Source side: iterate targets in declared order.
// Target side: accept ribbons in source declared order.
const d = `M ${xL} ${yL1} `
        + `C ${xMid} ${yL1}, ${xMid} ${yR1}, ${xR} ${yR1} `
        + `L ${xR} ${yR2} `
        + `C ${xMid} ${yR2}, ${xMid} ${yL2}, ${xL} ${yL2} Z`;
path.setAttribute('d', d);
⚠ Watch

Track ribbon allocation independently on each side. If you draw ribbons in the wrong order on either the source or target stack, ribbons crisscross visually inside their own node — unreadable. The pattern: for each source, walk targets in declared order; for each target, accept ribbons in source declared order. Both stacks must agree.