Skip to main content

ProcessMap

Discoveryflow

Directly-follows process map: activities as nodes, transitions as edges, both painted from one metric choice, with an accessible table twin.

import { ProcessMap } from "@elabs-ai/components-process";
Install
npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-process

Skipping the @source line is the most common first failure: Tailwind does not scan node_modules, so every component renders unstyled. The one-time wiring

Prompt for your coding agent

Paste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.

Add the brand-ui ProcessMap component (from @elabs-ai/components-process) to this project.

1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`.
2. Look ProcessMap up before writing code: `npx -y @elabs-ai/components-cli docs ProcessMap` prints its real props, variants, what it pairs with and what to avoid.
3. If @elabs-ai/components-process is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-process` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-process`). If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`.
4. Use it here: [the screen or file, and what it should show]
5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop.
6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.

Avoid

  • Encoding the metric in colour alone — an edge carries its reading as stroke WIDTH and a printed label pill before it carries it as hue, and a node prints its value beside the meter bar. A greyscale render must still be readable (WCAG 1.4.1).
  • Reaching for raw `ReactFlow`/`Panel`/`Handle` to add canvas furniture — the map is a composition of flow's CanvasShell; an overlay is an absolutely-positioned sibling, and a missing primitive is a `@elabs-ai/components-flow` change.
  • Re-deriving the numbers for a table, an export or a tooltip — `tableView` and the canvas render from the SAME model, which is what stops the two from disagreeing.
  • Passing a raw event log on every render — pass `graph` from `discoverGraph` (or memoize the log) so a metric switch never re-runs discovery or the dagre layout.

Works with

Contains

Used inside

Examples

13 live examples

Performance

The same graph read as time: median activity duration on the nodes, median flow time on the edges. The value is always printed — a duration ramp is never asked to carry the reading on its own.

Rework

Rework tallies from `detectRework`, badged on the activities that repeat. The badge is a count and an `sr-only` sentence, not a colour.

With Activity Colors

With a shared `colorScale` (RM-054): each activity shows a small identity swatch beside its meter — the same colour `VariantExplorer` paints that activity's chips with when it gets the same scale. The swatch is a mark, never the card fill, so it does not compete with the metric reading.

Abstracted

Half the activities and a third of the paths kept — `/core`'s `abstractGraph`.

Selection

A controlled selection, plus the keyboard path, asserted by a REAL `Tab` walk rather than a synthetic `focus()` call: `Tab` reaches every activity in layout order, `Enter` selects the one it is standing on, `F` opens the filter-intent menu for it, and `Escape` puts focus back on the activity instead of on the Filter button. Wired through `useProcessExplorer` (RM-052 round 2, #227) rather than a bare `useState`, so `onFilterIntent` is the hook's real `applyIntent` — choosing an intent from the menu this story already opens does not shrink the canvas. The tail of the play function proves it: applying a real filter dims the activities it drops but leaves every one of them in the DOM (Invariant F).

Left To Right

Left-to-right layout. The same cached model; only the layout key changes. It repeats the whole framing check rather than trusting the top-to-bottom pass, because the two directions put the handles on different sides of the card and every one of the defects {@link expectWellFramedCanvas} locks behaved DIFFERENTLY here: the connector drift measured 69 px against top-to-bottom's 24 px, and it was the direction FLIP that triggered it — the node's motion transition was reaching React Flow's handle elements as well as the node body, so a dot animated to its new side while React Flow measured it.

Elk Layout

The same graph laid out by ELK (`layoutEngine="elk"`) instead of dagre. elkjs is loaded lazily on first layout; the map, its framing and its keyboard order behave exactly as the dagre default does.

Table View

The accessible twin required for a graph that cannot be read as a picture. It renders from the same model as the canvas, so the numbers are identical by construction.

Table View Selection

The accessible twin WITH an active selection AND an active filter exclusion (#373) — the cell of the story matrix that was missing before this fix. No shipped story combined `tableView` with a real `selection`/`selectionStates`, so the twin's state gap never reached the blocking interaction + axe job or autodocs. Locks that a selected row and an excluded row are each named for their real, perceivable state.

Loading

No graph yet — a whole-region loading panel, not an empty canvas.

Empty

An abstraction that hid everything, or a log with no events.

Large Graph

Sixty activities with skip paths, self-loops and back-edges — the layout budget case, and the one where the overview thumbnail earns its place: at this size the reader navigates by the minimap, so a blank one is not a cosmetic loss. Only the thumbnail is asserted here. Label crowding at sixty activities is a real property of the graph rather than a layout defect, so the pill/card check belongs on the two eleven-activity stories, where a collision means the spacing is wrong.

API

ProcessMap

PropTypeDescription
graphProcessGraphA discovered graph. Takes precedence over {@link log} when both are given.
logEventLogAn event log to discover from, for a surface with no explorer around it. Runs `/core`'s `discoverGraph` and `detectRework` internally, memoized on the log.
abstractionPick<AbstractionOptions, "activities" | "paths">Fractions of activities / paths to KEEP, as `/core`'s `abstractGraph` reads them.
metricrequiredProcessMetricSpecWhich readings the nodes and edges paint.
reworkReworkStatsRework tallies for the node badge. Derived from {@link log} when that is given.
selectionProcessSelection | nullControlled selection. Omit for an uncontrolled map that owns its own.
selectionStatesProcessSelectionStatesPer-element states an active filter contributes (RM-052 round 2, #227) — e.g. from `useProcessExplorer`'s own `selectionStates`. Applying or clearing a filter never adds or removes a node or an edge (Invariant F): it only re-inks the elements a filter dropped as `"excluded"` (dimmed, never `aria-disabled`). Omit for a map with no filtering, which reproduces today's selection-only behaviour exactly.
onSelect(target: ProcessSelection | null) => voidFires with the new selection, or `null` when the reader deselects.
onFilterIntent(intent: ProcessFilterIntent) => voidFires with an intent from the filter menu — hand it straight to `filterLog`.
directionFlowLayoutDirection@default "TB"
showMiniMapboolean@default true
showLegendboolean@default true
tableViewbooleanRender the accessible table twin instead of the canvas. @default false
loadingbooleanNo graph yet. Renders the loading panel rather than an empty canvas.
colorScaleActivityColorScaleA shared activity colour scale (RM-054, `/core`'s `activityColorScale`). When given, each activity node shows its identity swatch — the same colour `VariantExplorer` paints that activity with. Build it once from the FULL graph and hand the same instance to both views. Omit for today's uncoloured nodes.
conformanceConformanceResultA replay result (RM-061's `tokenReplay`) to overlay (RM-062). When given, every node and edge carries `data-conformance="both" | "logOnly" | "modelOnly"` — additive to `data-selection` — painted as a status tone PLUS a glyph and a line style, with the state's word in the accessible name and a Conformance column in the table twin. Omit for today's map, unchanged. Usually reached through `ConformanceOverlay`.
objectCentricObjectCentricGraphObject-centric — RM-066. An object-centric graph (`discoverObjectCentricGraph`, e.g. over `fromOcel(…).logs`) to draw instead of `graph`/`log` — mutually exclusive with both, and it wins when given together (with a dev-mode console warning). Shared activities merge into one node carrying a chip per object type; edges are drawn once per object type in that type's chart colour, side by side. `abstraction` applies to every type alike; for per-type abstraction pass `abstractObjectCentricGraph(…)`'s result here instead.
objectCentricLabelsObjectCentricMapLabelsObject-centric — RM-066. Strings the object-centric mode composes.
labelstringAccessible name for the canvas region. Defaults to the localized `process.map.label` message.
fitMinZoomnumberThe smallest zoom the OPENING fit may use. The default keeps activity names legible and, when the process does not fit at that size, opens on its start. A workspace that would rather open on the whole picture — a wide canvas, a reader who zooms in on what matters — lowers it. The reader can always zoom out further by hand. @default PROCESS_MAP_LEGIBLE_ZOOM
fitPaddingnumberThe fraction of the pane the opening fit keeps clear on every side. The default is generous because transition pills are drawn outside the fitted bounds; a pane that is short on the axis the process runs along can trade some of it for a larger picture. @default 0.15
refitKeystring | numberRe-frames the map whenever this value changes. The map already re-fits when its own structure or direction changes; it cannot know that the pane around it just changed shape — a dock opened, a side panel closed. Pass anything that changes when that happens. Unset: no extra re-fits, the reader's viewport is left alone.
layoutEngine"dagre" | "elk"The layout engine. `"elk"` lays the map out with `@elabs-ai/components-flow`'s `layoutFlowElk` — elkjs, an optional peer, loaded lazily on first use; when it is not installed the map falls back to dagre with a development-only warning. @default "dagre"

Also accepts: Omit<HTMLAttributes<HTMLDivElement>, "onSelect">

ZoomControls

PropTypeDescription
positionPanelPosition
classNamestring

FlowMiniMap

Also accepts: MiniMapProps<NodeType>