# Graph Family

Canonical: https://socra.design/charts/graph-family

The graph.* namespace family — Scene renders a DAG on a full-canvas dendritic river with mount gate, canvas-unavailable fallback, motion-preference honoring, and theme-role color resolution; the rendering engine is a module secret.

Use graph.* for every DAG canvas surface: graph.Scene draws a directed-dependency graph as a flowing river of branches (the source on the left, tributaries fanning right), thickness carrying transitive flow, names always on and decluttered, hover lighting the neighborhood in the node's own colour. Consumers pass DagData (nodes with id + directed dependent → dependency edges); graph.buildDag derives the full structure — depth, flow, source — from any input shape. Every color resolves a palette role against the live theme; ambient pulse motion respects the caller device motion preference; SSR-safe through the internal mount gate.

Status: ready. Scene, buildDag, dagFor, and layoutDag ship from @socra/ui-web-graph with react-force-graph-2d as a module secret: SSR-safe mount gate, canvas-unavailable fallback, motion-preference honoring, theme role color resolution (primary/mint/pink/gold/indigo/coral/purple), deterministic dendrite layout (byte-identical positions, no two cell bodies touching), and executable derivation + layout tests.

Library: @socra/ui-web-graph

## Anatomy

- graph.Scene — the full-canvas renderer. Takes DagData + width/height; owns the mount gate (SSR-safe), the canvas-unavailable fallback, motion-preference honoring, theme role resolution, hover/selection flicker discipline (pointer state in refs so hover never re-renders React), and the imperative sceneRef.focusNode/reset handles for camera control.
- graph.buildDag — pure derivation: from a plain { nodes: [{ id, dependsOn }] } input, computes each node's dependents, depth (longest-path from roots), transitive flow, val (circle area weight), the graph's source (deepest × highest-flow), and drops edges to unknown ids / self-edges. Cycle-tolerant with hasCycle flagged.
- graph.dagFor — memoized graph.buildDag keyed by dagSignature: structurally-equal re-inputs preserve node identity so a mounted scene never re-heats its layout; the one non-structural field (summary) is refreshed in place so nothing goes stale.
- graph.layoutDag — the deterministic dendrite layout: source pinned at x=0, each node flows into ONE main channel (its highest-flow dependency), a cone tree fans children in 2D by leaf weight, symmetric push-apart guarantees no two cell bodies touch (DAG_MIN_SEP). Byte-identical positions across runs.

## States

- **Rest:** Shows the stable, enabled component without implied activity.
- **Hover:** Adds pointer affordance without moving content or changing meaning.
- **Focus:** Shows the shared visible focus treatment without depending on hover.
- **Pressed:** Acknowledges active input immediately and returns cleanly on release.
- **Selected:** Uses a neutral surface plus native selected semantics when selection applies.
- **Disabled:** Remains legible, unavailable, and absent from misleading interaction feedback.
- **Loading:** Preserves context while honestly identifying work that has not completed.
- **Error:** Places the failure and recovery path beside the action or content that failed.

## Motion

Stable state changes respond immediately. Appearance, expansion, and morphing begin at the triggering origin and use an interruptible spring that can retarget from its live position and velocity. When spatial motion is not meaningful, the component changes without decorative travel.

## Usage

- Pass DagData; let the family lay it out. The dendrite layout, source detection, and flow derivation are module secrets — consumers never compute positions, choose colors, or bind the engine directly.
- Feed a stable input shape to keep layout identity across re-polls. graph.dagFor memoizes on structural signature — re-inputs that change nothing preserve node identity so a mounted scene never re-heats.
- Provide a fallback for the canvas-unavailable case. The scene's mount gate detects when 2D canvas is unavailable (headless environments, locked-down clients) and renders the fallback in place — an honest degradation instead of a blank pane.

## Avoid

- Do not import react-force-graph-2d in product code. The engine is a module secret behind the family boundary — a product importing it forfeits fleet-wide improvement and reintroduces the SSR, theme, and mount-gate concerns the family already owns.
- Do not thread engine props (cooldownTicks, warmupTicks, particle machinery). The scene owns the render loop, pointer-frequency flicker discipline, and per-frame easing; a product prop for any of those would leak the engine and let visual defects diverge per-consumer.
- Do not hardcode positions or override node colors. Every color resolves a palette role at render (primary.main / mint.main / pink.main …) so the scene re-themes with the fleet; the dendrite layout is the family's hand-tuned visual, not a caller concern.

## Tokens

- `designTokens.color[mode].color.brand.primary`: Primary action role.
- `designTokens.color[mode].color.surface.secondary`: Working surface role.
- `designTokens.color[mode].color.content.primary`: Primary content role.
- `designTokens.spacing.role.contentGap`: Spacing relationship.
- `designTokens.shape.role.control`: Shape relationship.

## Example

```tsx
const data = graph.buildDag({
  nodes: [
    { id: 'id' },
    { id: 'account', dependsOn: ['id'] },
    { id: 'workspace', dependsOn: ['account'] },
    { id: 'module', dependsOn: ['workspace'] },
    { id: 'issue', dependsOn: ['module'] },
  ],
});

const [selected, setSelected] = React.useState<string | null>(null);
const { ref, width, height } = useElementSize();

<Box ref={ref} height={480}>
  <graph.Scene
    data={data}
    width={width}
    height={height}
    selected={selected}
    onNodeClick={setSelected}
    onBackgroundClick={() => setSelected(null)}
    fallback={<Typography variant="body2">canvas unavailable on this device</Typography>}
  />
</Box>
```
