How to pick a physics HOC, keep process chrome readable, measure capacity, and ship examples that stay honest when the balls stop moving.
Decision table
| Choose | When | Examples |
|---|
| ProcessFlowChart | Many independent work items move through ordered stages with capacity, rework, and optional feature groups that complete only when every member is absorbed. | Hospital triage, moderation queues, ETL backlog |
| GauntletChart | One compound plan (a project core) carries tethered positive/negative properties through timed gate effects that pop lift or add drag. | Merge Pressure, Not in MY Backyard, legislation, product roadmap risk |
| EventDropChart | Event time vs arrival time, watermarks, lateness gutters — streaming time semantics. | Watermarks, Made Physical |
| GaltonBoardChart | Uncertainty as sampling process; settled quantile / histogram. | Plinko Quantile Dotplot |
| PhysicsCustomChart + kit | Bowtie volumes, bespoke regions, or essay layouts that still need Semiotic a11y and push. | Stakeholder Journey (processStageLayout) |
Gauntlet vs ProcessFlow
GauntletChart
One core + tethered properties + timed gates.
JSX
import { GauntletChart } from "semiotic/physics" // One compound plan · tethered properties · timed gate effects <GauntletChart data={projects} positiveProperties={lift} negativeProperties={drag} gates={gates} events={events} showProjection // viability / outcome strip showChrome />
ProcessFlowChart
Many PRs · live capacity · feature sockets. Capacity readout:—
Authoring kit
Custom process essays should import the kit rather than inventing colliders:processStageLayout, processVolumePolygons, region factories,createCapacityQueueController, processChrome.layoutConfig is the interaction hot path — regenerate geometry or styling without re-enqueueing bodies.
JSX
import { PhysicsCustomChart, processStageLayout, capacitatedRegion, createCapacityQueueController, processChrome, } from "semiotic/physics" layout={(ctx) => { const volume = processStageLayout({ width: ctx.dimensions.width, height: ctx.dimensions.height, stages: STAGE_DEFS, shape: "lane", }) return { bodies: spawnsFromData(ctx.data, volume), regionEffects: [ capacitatedRegion({ id: "review", ...volume.stages[1], capacity: 4 }), ], controllers: [ createCapacityQueueController({ regionId: "review", unitsPerSecond: 4, }), ], // layoutConfig changes rerun layout without re-enqueueing bodies overlays: processChrome({ ...volume, stages: chromeStages }), } }} layoutConfig={{ highlightId }}
Metric-driven volume geometry
A process metric can change available physical space, but the sensor, collision barriers, and explanatory border must remain one geometry. This example adds two pixels to the bowtie pinch for each participant reaching Leadership. Move the slider: every surface below is regenerated from the same ProcessVolumeLayout.
24px metric offset59px resolved impact height3 synchronized polygons
12 participants reached Leadership. The First Impact passage is 59 pixels high.
The cyan border is not an independent illustration. It is rendered from processVolumePolygons(layout); the canvas barriers use layout.colliders, and the observation envelope uses processStageRegions(layout).
JSX
import { StreamPhysicsFrame, processStageLayout, processStageRegions, processVolumePolygons, } from "semiotic/physics" const layout = processStageLayout({ width, height: 300, shape: "bowtie", stages, centerStageIndex: 2, pinchRatio: 0.16, // One metric changes the complete process volume, not just its paint. pinchHeightOffset: leadershipReached * 2, }) const polygons = processVolumePolygons(layout) const regionEffects = processStageRegions(layout) <StreamPhysicsFrame config={{ colliders: layout.colliders, fixedDt: 1 / 60 }} regionEffects={regionEffects} foregroundGraphics={() => ( <svg viewBox={`0 0 ${layout.width} ${layout.height}`}> {polygons.map((polygon) => ( <polygon key={polygon.id} points={polygon.points.join(" ")} /> ))} </svg> )} />
Journey and settled evidence
Region events should reduce into durable analytical state instead of making readers infer outcomes from a moving body. The process kit separates three useful views of that evidence:
processJourneyRows reports monotonic reached, actual unique entered, visits, repeat visits, conversion, and drop-off.aggregateRegionCounts counts each body once per region;regionCountsToProjectionRows makes those counts chart-ready.groupCompletionRows projects all-member, any-member, or weighted threshold completion from absorbed body ids.
JSX
import { aggregateRegionCounts, createProcessJourneyLedger, groupCompletionRows, processJourneyRows, processStageRegions, regionCountsToProjectionRows, updateProcessJourney, } from "semiotic/physics" const regions = processStageRegions(layout) const initialJourney = createProcessJourneyLedger({ stages, bodyIds: cohort.map((row) => row.id), }) function onRegionEvent(event) { setJourney((current) => updateProcessJourney(current, event)) setCounts((current) => aggregateRegionCounts(current, event)) } const journeyRows = processJourneyRows(journey) const settledRegionRows = regionCountsToProjectionRows(counts) const groupRows = groupCompletionRows(groups, absorbedBodyIds) <StreamPhysicsFrame regionEffects={regions} onRegionEvent={onRegionEvent} accessibleTable />
The reducers are serializable and independent of the live frame, so the same event tape can be replayed, persisted, compared across scenarios, or rendered as an accessible settled table. See the controlled comparison inStakeholder Journey.
ProcessFlow props that matter
JSX
import { ProcessFlowChart } from "semiotic/physics" // Many work items · capacitated stages · feature all-members completion <ProcessFlowChart data={prs} stageAccessor="stage" groupBy="featureId" workAccessor="reviewWork" bodyMark="halo" // or datum.__physicsMark liveCapacity // FIFO queue at unitsPerSecond bodyLimit={400} // soft stream budget (evict oldest) onCapacityChange={setStats} stages={[ { id: "coding", label: "Coding", force: 14 }, { id: "review", label: "Review", capacity: { unitsPerSecond: 4, unitAccessor: "reviewWork" } }, { id: "revision", portal: { targetStageId: "coding" } }, { id: "merged", absorb: true }, ]} />
- liveCapacity — FIFO queue at
unitsPerSecond, not just drag - onCapacityChange — queue depth + processed counts for readouts
- bodyMark / __physicsMark — circle, halo, faceted, pill, diamond, square
- bodyLimit — soft stream budget (evict oldest) for long runs
- selection — restyle without relayout
Example quality checklist
- Use a named HOC or recipe — don't re-derive colliders in the example
- Chrome encodes the claim (stages, capacity, late gutter, sockets)
- Settled projection still tells the story when motion is paused
- Domain setup for the chart itself stays under ~150 lines
- Tooltips own chrome (inline background) or rely on FlippingTooltip auto-chrome
- Corridor integrity: bodies spawn and settle inside walls
- If capacity matters, show queue depth / processed counts (onCapacityChange or chrome badges)
Flagship demos:Watermarks,Plinko,Merge Pressure,NIMBY,Stakeholder Journey.
Contracts we test
Corridor integrity, tooltip chrome (never class-only transparent), settled projection overlays by default, capacity metrics, and seeded builder determinism live inPhysicsContracts.test.ts.