Where things go. Patient flow, funnel splits, traffic-source-to-conversion. Reads as a story when the ribbon thickness pops.
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.
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.
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
],
}
// 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);
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.