import { ProcessSankey } from "semiotic"
Sankey-style flow with a real time x-axis. Each edge has astartTime / endTime; nodes may declare an xExtent: [start, end] to bound their lane explicitly. Use it for flow events with timestamps (PR commits over time, campaign-finance contributions, supply-chain shipments) where a static-snapshot SankeyDiagramwould erase the temporal structure.
How it differs from SankeyDiagram
- Edges carry time. Each edge has
startTime (when it leaves the source) andendTime (when it arrives at the target). Multiple edges can connect the same source / target pair at different times. Standard sankey treats edges as instantaneous and collapses parallel edges into one. - Nodes have lifetimes, not ranks. A node’s vertical lane spans
min(xExtent[0], earliestEdge)to max(xExtent[1], latestEdge). There is no node-rank prop; the layout reads timing from the data. Nodes may carry an optional xExtent: [start, end] to extend the lane outward — useful when a candidate exists before the first contribution arrives or stays open after the last spend settles. - Static-graph cycles are valid. If A sends to B and B later sends back to A, the graph has a topological cycle but every edge still moves forward in time.
ProcessSankey accepts this; standard sankey rejects it as a DAG violation. - Lane reuse instead of dedicated rows. When two nodes have non-overlapping lifetimes, they share a lane (interval-graph greedy, depth-sorted). Pass
packing="off" for one row per node.
Quick Start
The minimum a ProcessSankey needs isnodes, time-stamped edges, and adomain. Toggle Streaming to see the same chart built up via the push API — omit the edgesprop, push them in via the ref.
Sandbox
Pick a fixture and tune layout knobs to see how each affects the chart. Configurations that are good defaults for most data: Pairing: temporal, Packing: reuse,Lane order: crossing-min, Lifetime: half edge.
Fixture
Packing
Lane order
Pairing
Ribbon lane
Lifetime
Lane rails
Labels
Particles
Color by
Legend
Examples
Each example below isolates one setting against the same fixture so you can see what the knob does on its own. The full playground above lets you compose them.
Categorical coloring + legend
colorBy="category" shares one color across every commit / PR / library node, andshowLegend renders the swatch column to the right. Set colorBy="id" for the per-node rainbow instead.
Particle flow
showParticles renders a continuous stream of dots flowing along every ribbon. Spawn rate is proportional toedge.value; tune visual style viaparticleStyle — the same config shapeSankeyDiagram uses.
xExtent — pre-edge and post-edge lifetime
Each commit lane in the Library fixture opens two days before its OUT edge (xExtent[0] = c.start - 2d) and the Library lane stays open through the end of the domain (xExtent[1] = Apr 1). The synthesized leading mass is visible as the small left-tab on each blue commit; the green tail past the last PR merge shows the post-edge extension.
Packing off — one row per node
Default packing reuses lanes whenever two nodes have non-overlapping lifetimes. Settingpacking="off" gives every node its own row — useful when readers need a stable y-position per node, even at the cost of vertical space.
Ribbon routing
ribbonLane shifts where the bezier curves bend."source" hugs the source band (the ribbon body lives mostly under the source lane);"target" mirrors that on the target side. Useful for “these came from X” vs “these go to Y” readings.
systemInTime / systemOutTime — arrival + departure stubs
An edge’s startTime and endTimedescribe when it leaves the source and arrives at the target. Real systems often have a third and fourth moment that matter for the band layout: when did the unit of mass first appear at the source? and when did it finally leave the target? The optional systemInTime andsystemOutTime fields carry those.
When an edge carries systemInTime <startTime, the source band stops painting its flat fill and instead traces just the perimeter outline. Inside, the renderer drops a 20-px gradient stub at that edge’s source slot — band-color saturated fromsystemInTime through startTime, fading transparent → band-color in the 20 px immediately to the left of systemInTime. The same mechanic runs on the target side when an edge carriessystemOutTime > endTime: solid band-color from endTime throughsystemOutTime, then a 20-px fade to transparent on the right. Wire the fields through with the matching accessors:
JSX
<ProcessSankey // ... usual accessors ... systemInTimeAccessor="ticketed" systemOutTimeAccessor="closed" />
The helpdesk-ticket fixture below has six tickets. Each one opens (systemInTime), waits in queue, gets triaged (the visible ribbon spansstartTime → endTime), is worked on by the closing team, then finally closes (systemOutTime). The Queue band shows the wait time as a saturated stub leading into the soft fade-in at each ticket’s open time; the Closed band shows the active work time as a saturated stub that fades out at each ticket’s close. The bands’ outer strokes trace the full lifetime envelope, so the chart still reads as node-shaped even where the interior is empty.
Props reference
| Prop | Type | Required | Default | Description |
|---|
nodes | array | Yes | — | Node records; may carry xExtent: [start, end] for an explicit lifetime. |
edges | array | Yes | — | Edge records with source, target, value, startTime, endTime. |
domain | [number, number] | Yes | — | [tStart, tEnd] of the x-axis. |
axisTicks | array | — | — | Optional [{ date, label }] tick overrides. |
xExtentAccessor | string | function | — | — | Per-node [start, end] lifetime accessor. |
pairing | "value" | "temporal" | — | "value" | How incoming/outgoing flows are paired into ribbons. |
packing | "off" | "reuse" | — | "reuse" | Pack lifetime-disjoint nodes into shared rows. |
laneOrder | "crossing-min" | "inside-out" | "crossing-min+inside-out" | "insertion" | — | "crossing-min" | Vertical lane ordering strategy. |
lifetimeMode | "full" | "half" | — | "full" | Whether a node lane spans its full lifetime or half. |
ribbonLane | "source" | "target" | "both" | — | — | Which side(s) ribbons attach to. |
colorBy | string | function | — | — | Field/accessor that drives categorical node + ribbon color. |
showLaneRails | boolean | — | — | Draw the rail guides behind lanes. |
showLabels | boolean | — | true | Render node labels. |
showQualityReadout | boolean | — | — | Overlay the layout-quality (crossing) readout. |
showParticles | boolean | — | — | Animate particles along ribbons (pair with particleStyle). |
timeFormat | string | function | — | — | Formatter for axis ticks and tooltip time fields. |
valueFormat | string | function | — | — | Formatter for flow values. |
The subsections below are the detailed reference for the data shape, accessors, layout controls, and visual options summarized above.
Data
nodes — array of node records. Nodes may carry an optional xExtent: [start, end] tuple. When present, the node’s lane spansmin(xExtent[0], earliestEdge) tomax(xExtent[1], latestEdge) — set both endpoints to the same value for a pure-source “opens at T” anchor; set the second endpoint past the last edge to keep the lane drawing after the final flow settles.edges — array of edge records with source, target, value, startTime, endTime.domain — [tStart, tEnd] of the chart’s x-axis.axisTicks — optional array of { date, label }.
Accessors
nodeIdAccessor (default "id")sourceAccessor / targetAccessor / valueAccessorstartTimeAccessor / endTimeAccessor / xExtentAccessoredgeIdAccessor (defaults to a synthesized id)systemInTimeAccessor — optional per-edge stamp on the source attachment. When supplied AND less than startTime, the source band drops its flat fill in favor of an outline and paints a 20-px gradient stub at the edge’s slot fading transparent → band-color in the 20 px before systemInTime; the slot stays saturated through startTime. See the arrival + departure stubs example.systemOutTimeAccessor — mirror on the target side. When supplied AND greater thanendTime, the target band shows a saturated stub from endTime throughsystemOutTime, then a 20-px fade to transparent past systemOutTime.- Time accessors return
number, Date, or a parseable date string. Internal computation uses ms since epoch.
Layout
pairing: "value"(default) pairs largest-incoming with largest-outgoing;"temporal" pairs by arrival/departure order. Try "temporal" when ribbon ordering matters more than per-pair magnitude.packing: "reuse" (default) packs lifetime-disjoint nodes into the same row, sorted by topological depth so hierarchical fixtures collapse to one row per level. "off" gives every node its own row.laneOrder: "crossing-min"(default) — brute force for ≤8 lanes / ≤40 edges, barycentric + adjacent-swap above. "inside-out"places largest-mass slot at the median."crossing-min+inside-out" runs both;"insertion" preserves packing order.ribbonLane: "both" (default) routes ribbons via the horizontal midpoint;"source" hugs the source lane;"target" hops to the target lane early.lifetimeMode: "half"(default) charges each edge to the source-half of its duration at the source and the target-half at the target, shrinking lifetimes and enabling more reuse;"full" gives both endpoints the full extent.showLaneRails (default false) — toggle dashed lifetime rails behind each band.showLabels (default true) — render the node-id label at each band’s left edge. Turn off for dense layouts where labels would overlap, or when the legend already names every band.showQualityReadout — render the small “crossings: A → B edge length: X → Y” readout above the chart. Useful while tuning laneOrder.
Coloring
colorBy — node accessor used to drive the color scale. Pass a categorical field (e.g. "category") so every commit, person, or PR shares one color.colorScheme — preset name or array of colors.showLegend (defaults to true whencolorBy is set) — render a swatch + label legend to the right of the chart.legendPosition — "right"(default) or "bottom".
Formatting
timeFormat(d: Date) — applied to axis tick labels (overrides tick.label when set) and to time fields in the default tooltip (startTime,endTime, and node mass-history timestamps). Same convention as xFormat on XY charts.valueFormat(v: number) — applied tovalue in the default edge tooltip and to total mass in the node mass-history table. MirrorsyFormat on XY charts.
Default tooltip layouts
- Edge tooltip shows the source → target pair, the edge value, and the time window. Time fields use
timeFormat when supplied, the value usesvalueFormat. - Node tooltip shows the node id and a mass-history table — one row per distinct mass state across the node’s lifetime, with
{ Time, Mass } columns formatted viatimeFormat / valueFormat. The timestamps come from the layout’s sample series so each row corresponds to an event that changed the band’s width. The default caps display at five rows: when the node has more than five distinct mass states, the table condenses to the min / q25 / median / q75 / max picks (re-sorted by time), with a small footer noting the original sample count. To render the history a different way — sparkline, deltas, full series — pass a customtooltip function that overrides this default body. - A custom
tooltip prop overrides both defaults.
Interaction
tooltip — true/omitted shows a default key/value list of the hovered datum’s public fields;false disables; pass a Tooltip(...)config or custom function for full control. Hover targets are node bands and edge ribbons; the hovered datum is the original record from nodes or edges.enableHover (default true) — set to false to suppress hover detection entirely.onClick(datum, {x, y}) — fired when a band or ribbon is clicked.onObservation — fired with{type, datum, x, y, chartType, chartId, timestamp}for hover/hover-end/click events. Standard semiotic observation contract.
Particles
showParticles (default false) — render a continuous stream of dots along every ribbon. The band geometry encodes when a flow happens, the particles encode how much.particleStyle — visual config object. Same shapeSankeyDiagram uses:{ radius, opacity, spawnRate, maxPerEdge, speedMultiplier, color, colorBy, proportionalSpeed }. Defaults from DEFAULT_PARTICLE_STYLE (radius3, opacity 0.7, spawnRate0.1, maxPerEdge 50).
Particles ride the shared canvas + ParticlePool path the rest of the network family uses — spawn rate scales proportional to edge.value, particles recycle out of a pre- allocated pool, and the rAF loop pauses cleanly whenshowParticles is toggled off.
Push API
Like the rest of the chart catalog, ProcessSankey supports ref-based live ingestion. Omit edges from props to enter push mode — the component then owns the edge list and the ref methods mutate it. nodes can be either controlled or pushed.
TSX
import { useRef } from "react" import { ProcessSankey } from "semiotic" const ref = useRef(null) // Live mode: omit edges, push them as they arrive <ProcessSankey ref={ref} nodes={nodes} domain={[t0, t1]} /> // Add an edge ref.current.push({ id: "e1", source: "Alice", target: "Eng", value: 8, startTime: Date.now(), endTime: Date.now() + 86400e3, }) // Batch ref.current.pushMany([edge1, edge2, edge3]) // Update by id (requires edgeIdAccessor or auto-id) ref.current.update("e1", e => ({ ...e, value: 12 })) // Remove by id ref.current.remove(["e1", "e2"]) // Snapshot the current edge list const all = ref.current.getData() // Wipe everything ref.current.clear()
push auto-detects edges (records withsource + target) vs nodes; non-edges go to the internal node list. pushMany partitions a mixed batch the same way. Edge writes are silently dropped with a console warning if edges is being passed as a prop (controlled mode); node writes always flow through.
remove(id) and update(id, fn) address edges by id first (resolved via edgeIdAccessor, falling back to a synthesizedsource-target-index id) and fall through to nodes by nodeIdAccessor when no edge matches.
Caveats and known limitations
- Backward-in-time edges fail validation. An edge with
endTime ≤ startTime blocks rendering. Normalize your data before passing it in. - Mid-stream nodes assume balanced flow. If a transit node receives more or less than it sends across its lifetime, the synthesis falls back to
createevents at xExtent[0] - 1. SetxExtent on net-source nodes for predictable behavior. - Same-slot edges render as “handoff” ribbons along the bottom of the shared lane (a consequence of lane reuse). The visual reads correctly for hierarchical accreters; it can look unusual for cyclic fixtures with heavy reuse.