# DataflowFigure

A pipeline: where data comes from, what happens to it, and who ends up with it.

- Group: Diagrams
- Import: `import { DataflowFigure } from '@misoto22/folio/diagrams'`
- Page: https://ui.misoto22.com/components/dataflow-figure/
- Related: architecture-figure, lifecycle-figure

## When to reach for it

Structurally close to an architecture map, read for a different question. classification gets its own chip because a governance reviewer is looking for exactly that.

## Anatomy

- **Figure shell** (required) — The frame around the picture. What a pipeline hands it is a flow list with the classification folded in — "clickstream — PII touch" — so the fact the diagram was opened for reaches a reader who never sees the chip.
- **Stage heading** (required) — One mono caption per stage on a rule across the top. It is the axis: a node’s stage is how far along the pipeline it sits, so "has this been aggregated yet" is answered by looking up rather than by tracing arrows.
- **Node plate** (required) — One box per node, placed by stage across and row down. The row pitch leaves a clear band between two boxes in the same column, which is where a line and its wording go.
- **Flow** — A routed line with a single arrowhead, at its to end. It is the direction claim the whole figure is read for.
- **Classification chip** — classification, printed in mono under the wording rather than inside it. "clickstream" and "clickstream / PII touch" are two different facts, and only one of them is what a governance review came for.

## Best practices

### Do

- Put the governance fact in classification rather than in the label. It prints as its own chip on the line and is appended to the flow’s summary line, so it survives both a reader who is scanning the picture and one who only has the text.
- Keep stage inside the stages you declared. A node’s x is computed from its own stage index rather than looked up, so a node at stage 5 beside four stages is drawn a full column past the last heading, under no heading at all — development prints DIAGRAM_STAGE_OUT_OF_RANGE, and the summary files it under a band called outside the declared stages rather than beside the nodes the axis does label.
- Write a two-way exchange as two flows. Every line carries a head at its to end only, so one flow between a service and its cache says data moves one way, whatever the label claims about the round trip.

### Don’t

- via, channelX and channelY are never honoured here — a data-flow node is always placed by stage and row, so an author’s waypoints refer to a grid this renderer did not build. They are dropped and every line is routed from scratch: the specification still typechecks and the picture is not the one it drew.
- Watch the fan-in. Lines sharing a face are spread evenly across it, and an arrowhead is 11 user units wide against a plate about 55 units tall — so four lines into one face is where the heads meet, and the fifth and sixth arrive as one thick mark. A pipeline at that density wants splitting, not more edges.

## Accessibility

- A flow’s classification is folded into its summary line, so "clickstream — PII touch" reaches a reader who cannot see the chip.

## DataflowFigure

A pipeline: where data comes from, what happens to it, and who ends up with it. Structurally close to an architecture map and read for a completely different question, which is why it is a separate renderer rather than a preset. An architecture diagram is read for "what talks to what". A data-flow diagram is read for "what is IN this arrow" — and that question is why `classification` gets its own mono chip under the label rather than being folded into the wording. `clickstream / PII touch` and `clickstream` are two different facts, and a governance reviewer is looking for the second one. Stages are printed as column headings on a rule across the top. They are the axis of the figure: a node's `stage` is its position along the pipeline, and a reader should be able to answer "how far has this got" by looking up rather than by tracing arrows.

### Props

- `spec` (required) — `DataflowSpec`.

Also accepts: `FigureChrome`.

## Example — default

```tsx
import { DataflowFigure } from '@misoto22/folio/diagrams'

<DataflowFigure
  spec={{
    meta: { title: 'Event pipeline', subtitle: 'What is in the arrow matters more than the arrow.' },
    stages: [{ label: 'Sources' }, { label: 'Gate' }, { label: 'Store' }],
    nodes: [
      { id: 'web', type: 'frontend', label: 'Web SDK', sublabel: 'browser', stage: 0, row: 0, tag: 'events' },
      { id: 'app', type: 'frontend', label: 'Mobile', sublabel: 'iOS / Android', stage: 0, row: 1, tag: 'events' },
      { id: 'gate', type: 'security', label: 'Consent gate', sublabel: 'policy filter', stage: 1, row: 0, tag: 'PII guard' },
      { id: 'wh', type: 'database', label: 'Warehouse', sublabel: 'analytics tables', stage: 2, row: 0, tag: 'curated' },
    ],
    flows: [
      { id: 'a', from: 'web', to: 'gate', label: 'clickstream', classification: 'user events', variant: 'emphasis' },
      { id: 'b', from: 'app', to: 'gate', label: 'app events', classification: 'device events' },
      { id: 'c', from: 'gate', to: 'wh', label: 'accepted', classification: 'no PII', variant: 'emphasis' },
    ],
  }}
/>
```

## Example — one source three consumers

```tsx
import { DataflowFigure } from '@misoto22/folio/diagrams'

<DataflowFigure
  spec={{
    meta: { title: 'What leaves the events table', subtitle: 'One source, three consumers, three payloads.' },
    stages: [{ label: 'Source' }, { label: 'Consumers' }],
    nodes: [
      { id: 'events', type: 'database', label: 'Events table', sublabel: 'raw', stage: 0, row: 1, tag: 'PII' },
      { id: 'bi', type: 'cloud', label: 'BI dashboards', sublabel: 'internal', stage: 1, row: 0 },
      { id: 'ml', type: 'backend', label: 'Model training', sublabel: 'nightly', stage: 1, row: 1 },
      { id: 'ads', type: 'external', label: 'Ad partner', sublabel: 'third party', stage: 1, row: 2 },
    ],
    flows: [
      { id: 'a', from: 'events', to: 'bi', label: 'daily rollup', classification: 'aggregated' },
      { id: 'b', from: 'events', to: 'ml', label: 'training set', classification: 'hashed ids' },
      { id: 'c', from: 'events', to: 'ads', label: 'conversions', classification: 'no PII', variant: 'security' },
    ],
    cards: [
      { dot: 'rose', title: 'Leaving the estate', items: ['Only the ad partner sits outside the account.', 'That is the arrow a reviewer reads first.'] },
    ],
  }}
/>
```

## Example — a stage the data skips

```tsx
import { DataflowFigure } from '@misoto22/folio/diagrams'

<DataflowFigure
  spec={{
    meta: { title: 'Clickstream', subtitle: 'Three stages, and one copy that skips the middle one.' },
    stages: [{ label: 'Collect' }, { label: 'Scrub' }, { label: 'Serve' }],
    nodes: [
      { id: 'sdk', type: 'frontend', label: 'Web SDK', sublabel: 'browser', stage: 0, row: 0, tag: 'raw' },
      { id: 'scrub', type: 'security', label: 'Redactor', sublabel: 'drops identifiers', stage: 1, row: 0 },
      { id: 'lake', type: 'database', label: 'Lake', sublabel: 'partitioned', stage: 2, row: 0, tag: 'curated' },
      { id: 'debug', type: 'database', label: 'Debug bucket', sublabel: '7-day TTL', stage: 2, row: 1 },
    ],
    flows: [
      { id: 'a', from: 'sdk', to: 'scrub', label: 'events', classification: 'PII' },
      { id: 'b', from: 'scrub', to: 'lake', label: 'hourly', classification: 'no PII', variant: 'emphasis' },
      { id: 'c', from: 'sdk', to: 'debug', label: 'sampled 1%', classification: 'PII', variant: 'dashed', fromSide: 'bottom', toSide: 'left' },
    ],
  }}
/>
```

## Example — what is in the arrow

```tsx
import { DataflowFigure } from '@misoto22/folio/diagrams'

<DataflowFigure
  spec={{
    meta: { title: 'Card data, minimised', subtitle: 'Read for what crosses each stage, not for what talks to what.' },
    stages: [{ label: 'Collected' }, { label: 'Minimised' }, { label: 'Stored' }, { label: 'Served' }],
    nodes: [
      { id: 'checkout', type: 'frontend', label: 'Checkout form', sublabel: 'browser', stage: 0, row: 0, tag: 'raw' },
      { id: 'support', type: 'frontend', label: 'Support inbox', sublabel: 'ticket bodies', stage: 0, row: 1, tag: 'raw' },
      { id: 'vault', type: 'security', label: 'Tokeniser', sublabel: 'Vault transit', stage: 1, row: 0 },
      { id: 'wh', type: 'database', label: 'Warehouse', sublabel: 'order tables', stage: 2, row: 0 },
      { id: 'bi', type: 'backend', label: 'Dashboards', sublabel: 'finance team', stage: 3, row: 0 },
    ],
    flows: [
      { id: 'a', from: 'checkout', to: 'vault', label: 'card and email', classification: 'PII', variant: 'emphasis' },
      { id: 'b', from: 'support', to: 'vault', label: 'free text', classification: 'PII, unstructured' },
      { id: 'c', from: 'vault', to: 'wh', label: 'order rows', classification: 'tokenised, no PAN', variant: 'emphasis' },
      { id: 'd', from: 'wh', to: 'bi', label: 'daily rollup', classification: 'aggregated' },
    ],
  }}
/>
```
