# NetworkGraph

## Overview

`NetworkGraph` renders a three-dimensional, force-directed graph for ontology mapping and any other information with relationships and connections. Nodes and edges are typed, the layout is deterministic, and the whole thing draws to a canvas with **no runtime dependency** — no three.js, no d3, no graph library.

---

## Import

```tsx
import {
  NetworkGraph,
  GraphNodeList,
  type GraphNode,
  type GraphEdge,
  type NetworkGraphControl,
} from 'xertica-ui/ui';
```

---

## Why there is no 3D library here

A graph draws discs and lines. It needs no mesh, material, lighting or shader — which is most of what a 3D engine provides. The parts that actually matter are the force simulation and the projection, and **the simulation has to be written either way**: no layout library ships with the package. Adding WebGL would replace the easy half and leave the hard half, at the cost of a dependency in every consuming application.

What is here instead: Barnes-Hut repulsion (`octree.ts`), Hooke springs with alpha cooling (`force-simulation.ts`), and a perspective projection with depth cueing (`camera.ts`). Each is independently unit-tested.

**The limit this implies:** interaction stays smooth to a few thousand nodes. Past roughly five thousand, a settling tick costs more than a frame and the layout visibly crawls. At that scale the answer changes and genuine WebGL is warranted.

---

## Props

| Prop              | Type                                      | Default                | Description                                                                  |
| ----------------- | ----------------------------------------- | ---------------------- | ---------------------------------------------------------------------------- |
| `nodes`           | `GraphNode[]`                             | required               | The vertices.                                                                |
| `edges`           | `GraphEdge[]`                             | required               | The relationships.                                                           |
| `layout`          | `'3d' \| '2d'`                            | `'3d'`                 | In `2d`, dragging pans instead of orbiting.                                  |
| `height`          | `string`                                  | `'480px'`              | Container height.                                                            |
| `selectedId`      | `string \| null`                          | -                      | Controlled selection.                                                        |
| `onSelect`        | `(id, feature) => void`                   | -                      | Fires with `null` when the user clicks empty space.                          |
| `onHover`         | `(id, feature) => void`                   | -                      | Fires with `null` when the pointer leaves a node.                            |
| `focusMode`       | `'neighbors' \| 'none'`                   | `'neighbors'`          | Dims everything outside the selection's neighbourhood.                       |
| `labels`          | `'auto' \| 'always' \| 'hover' \| 'none'` | `'auto'`               | `auto` shows a label once the node is large enough on screen.                |
| `directed`        | `boolean`                                 | `false`                | Draws arrowheads. Per-edge `directed` overrides it.                          |
| `nodeRadius`      | `number`                                  | `6`                    | Base radius; scaled by `node.weight`.                                        |
| `maxNodeRadiusPx` | `number`                                  | `11`                   | Ceiling on the largest node's on-screen radius.                              |
| `autoFit`         | `boolean`                                 | `true`                 | Frames the graph once the layout settles, until the viewer moves the camera. |
| `physics`         | `ForceSettings`                           | tuned by size          | Overrides for repulsion, springs, damping, cooling.                          |
| `autoRotate`      | `boolean`                                 | `false`                | Suppressed under reduced motion.                                             |
| `typeTokens`      | `ColorToken[]`                            | chart palette          | Colors assigned to types in order of first appearance.                       |
| `onLayoutSettled` | `() => void`                              | -                      | Fires once when the layout comes to rest.                                    |
| `controlRef`      | `React.Ref<NetworkGraphControl>`          | -                      | Imperative handle. **Separate from `ref`**, which is the container.          |
| `emptyState`      | `ReactNode`                               | built-in               | Replaces the built-in empty state.                                           |
| `ariaLabel`       | `string`                                  | `"Relationship graph"` | Accessible name; node and edge counts are appended.                          |

### `GraphNode`

| Field        | Type         | Description                                                        |
| ------------ | ------------ | ------------------------------------------------------------------ |
| `id`         | `string`     | Required and stable — the layout seeds from a hash of it.          |
| `label`      | `string`     | Falls back to `id`.                                                |
| `type`       | `string`     | Class or category. Drives color and the list grouping.             |
| `colorToken` | `ColorToken` | Overrides the color derived from `type`.                           |
| `weight`     | `number`     | Repels harder and draws larger.                                    |
| `radiusPx`   | `number`     | Fixed radius, ignoring `weight`.                                   |
| `pinned`     | `boolean`    | Holds the node still and arranges the rest around it.              |
| `position`   | `{x,y,z}`    | Seed position — restore a saved arrangement with `getPositions()`. |

### `GraphEdge`

| Field      | Type      | Description                                                      |
| ---------- | --------- | ---------------------------------------------------------------- |
| `source`   | `string`  | Node id. Edges pointing at unknown nodes are skipped, not fatal. |
| `target`   | `string`  | Node id.                                                         |
| `label`    | `string`  | The predicate, e.g. `"é subclasse de"`.                          |
| `relation` | `string`  | Relation kind. Drives color, like `type` does for nodes.         |
| `directed` | `boolean` | Overrides the graph-level `directed`.                            |
| `strength` | `number`  | Pulls harder at rest.                                            |
| `length`   | `number`  | Preferred length at rest.                                        |
| `dashed`   | `boolean` | Dashed stroke.                                                   |

### `NetworkGraphControl`

`fitView()`, `focusNode(id)`, `resetCamera()`, `restartLayout()`, and `getPositions()` — the last returns every node's coordinates, so a hand-arranged diagram can be saved and passed back through `node.position`.

---

## Sizing

Two mechanisms keep a graph legible without hand-tuning, because the layout's extent depends on how many nodes and edges there are:

- **`autoFit`** frames the whole graph once the layout settles, so a sparse graph is not lost in the middle of the canvas and a dense one does not spill off it. Any deliberate camera gesture — drag or wheel — hands control to the viewer and stops the reframing. `controlRef.fitView()` takes it back.
- **`maxNodeRadiusPx`** caps the largest node's on-screen radius. Radii are in layout units, so a framed sparse graph would otherwise draw discs big enough to swallow the edges between them. **One factor is applied to every node**, so relative weight survives the cap rather than every node flattening to the same size.

In practice a fourteen-node ontology and a two-thousand-node link graph both fill about 80% of the canvas, with node radii of 11px and 3px respectively.

---

## Interaction

| Gesture                       | Action                                 |
| ----------------------------- | -------------------------------------- |
| Drag                          | Orbit in `3d`, pan in `2d`.            |
| Wheel                         | Zoom.                                  |
| Click a node                  | Select. Click empty space to clear.    |
| `Tab` / `Shift+Tab` on canvas | Cycle the selection through the nodes. |
| Arrow keys                    | Orbit. Hold `Shift` for larger steps.  |
| `+` / `-`                     | Zoom.                                  |
| `Escape`                      | Clear the selection.                   |

---

## Example

```tsx
const [selectedId, setSelectedId] = useState<string | null>(null);

<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_320px]">
  <NetworkGraph
    height="520px"
    directed
    ariaLabel="Ontologia de processo judicial"
    nodes={[
      { id: 'processo', label: 'Processo', type: 'Classe', weight: 3 },
      { id: 'documento', label: 'Documento', type: 'Classe' },
      { id: 'peticao', label: 'Petição inicial', type: 'Subclasse' },
    ]}
    edges={[
      { source: 'peticao', target: 'documento', label: 'é subclasse de', relation: 'is-a' },
      { source: 'documento', target: 'processo', label: 'compõe', relation: 'part-of' },
    ]}
    selectedId={selectedId}
    onSelect={setSelectedId}
  />

  <GraphNodeList
    title="Nós e relações"
    nodes={nodes}
    edges={edges}
    selectedId={selectedId}
    onSelect={setSelectedId}
    maxHeight="520px"
  />
</div>;
```

---

## AI Rules

- **Always** pair the graph with `<GraphNodeList>` and share one `selectedId` in both directions. A canvas is a single opaque element to a screen reader; the list is the accessible path to the same data.
- **Always** use `node.type` and `edge.relation` for color rather than per-item `colorToken`. The palette, the legend and the list all derive from them.
- **Always** give nodes stable ids. The layout is seeded from a hash of the id, which is what makes the arrangement reproducible.
- **Always** keep `focusMode="neighbors"` beyond a few dozen nodes.
- **Never** swap `ref` for the imperative handle — use `controlRef`; `ref` is the container element.
- **Never** re-run the layout on every render by rebuilding `nodes`/`edges` inline. Memoize them, or the graph reseeds and jumps.
- **Never** raise `nodeRadius` to make a graph more visible. It is in layout units, so it grows the discs without spreading the nodes and they simply overlap. Reach for `maxNodeRadiusPx`, or `physics.springLength` to spread the layout itself.
- **Never** reach past a few thousand nodes and expect smooth settling. Filter server-side, or aggregate, before handing the graph a knowledge base.
- Under `prefers-reduced-motion`, the layout solves before first paint and auto-rotation never starts. Do not override that.
