# Graph

> Whole-codebase structural map of the codanna index as one self-contained HTML disc. Wedges per top-level module, concentric rings with hubs at the centre, hover or click a symbol to light its edge web, search, hide or highlight modules, brush a date range on the ribbon timeline (symbols dated by git blame), heatmap of symbols added per day, per-module colour picker. Use when the question is about the shape of the whole codebase or a module family (what is central, which modules talk, where the hubs sit), not one symbol's neighbourhood. Needs codanna >= 0.14 (`codanna dump`).

- Skill: `bartolli/graph` (Agent Skill, multi-file: 35 files)
- Install (CLI): `npx skillmds@latest add bartolli/graph`
- Raw SKILL.md: https://api.skillmd.com/api/skills/bartolli/graph/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: bartolli (https://skillmd.com/u/bartolli)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/bartolli/graph

---


## What it draws

One HTML file, no server: the codanna index rendered as a pie chart of symbols. Each
top-level module owns a wedge sized by its share; inside a wedge symbols fill rings from
the centre outwards, best-connected first, so hubs sit near the middle and leaves on the
rim. Unlinked symbols form their own inner group. Above the disc a heatmap shows symbols
added per day (each symbol dated by the oldest surviving line of its span, via git
blame). Hover or click a symbol to see its edge
web and the connected symbols; the legend hides (eye) or highlights (label) modules and
submodules; the ribbon under the heatmap brushes a date range (drag its handles, use the
date fields, or click a year chip) and Refresh replays the codebase growing oldest-first.

## Run

```bash
node ${CLAUDE_SKILL_DIR}/graph.mjs                      # whole index, Calls edges, default kinds
node ${CLAUDE_SKILL_DIR}/graph.mjs --root crate::indexing   # one module family
node ${CLAUDE_SKILL_DIR}/graph.mjs --relation calls,uses,implements,extends
node ${CLAUDE_SKILL_DIR}/graph.mjs --kinds function,method,struct,trait --dates none --light
```

- Runs `codanna dump` in the project (`--from graph.jsonl` reuses a saved dump, `--binary PATH` picks the binary)
- Output: `.codanna/visualizations/graph-disc-<timestamp>.html`, opened in the browser (`--no-open` to skip, `--out FILE` to place it)
- `--group <wedge>[/<tint>]` -- wedge axis `module` (default), `language`, or `kind`; tint axis `module` (default), `kind`, or `visibility`. The wedge is the angle (share, neighbours adjacent); the tint is the legend tree inside it (three biggest get their own shade, the rest pool). `module/kind` keeps the structure and shows what each module is made of; `language/module` for polyglot indexes; `module/visibility` for API surface per module; `kind/module` is a census (edges cross everywhere)
- `--root PREFIX` scopes to a module prefix (`crate::` is dropped, `.`/`::`/`/` all separate segments); applies to the module segments whatever the wedge axis
- `--kinds` defaults to Function, Method, Struct, Class, Trait, Enum, Interface, TypeAlias, Constant, Macro
- `--relation` defaults to `calls`; any of calls, uses, implements, extends, defines, comma-separated
- `--dates blame|git|none` -- `blame` (default) dates every symbol by the oldest surviving line of its span (`git blame -M` over the working tree; per-file JSON cache at `.codanna/visualizations/dates-cache.json` keyed by content hash, so warm runs spawn no blame -- delete the file to reset); `git` uses the file's first-commit day (cheap, but a refactor reads as mass birth); `none` leaves the timeline and heatmap empty
- `--unlinked include|drop` -- `drop` removes symbols with no edge over the chosen relations at build time (smaller file, smaller disc); in the page, the legend eye on `(unlinked)` hides the same set with the cascade animation
- `--name NAME` sets the title (default: project directory name); `--light` builds the light theme

## Scoping and filters: what to apply when

Start whole-index once to see module shares and hubs, then scope. The disc is smooth to a
few thousand symbols; hover and filters fall to ~10 fps near 10,000 (the CLI says so
above 4,000). Measured on the codanna self index (14,003 symbols):

| Situation | Apply | Effect on self index |
|---|---|---|
| First look, whole codebase | defaults | 9,983 symbols, 8,248 calls edges, 51% unlinked |
| The hub hole dominates (`(unlinked)` is the biggest group) | `--unlinked drop` | 4,890 symbols, same 8,248 edges; or hide it in-page with its legend eye |
| Many constants/aliases, few calls | `--kinds function,method,struct,class,trait,enum,interface` | 8,453 symbols, unlinked 42% |
| Type-level structure matters (impls, inheritance, field types) | `--relation calls,uses,implements,extends` | 11,003 edges, unlinked 41%; both together: 10,305 edges, 32% |
| Work inside one module family | `--root crate::indexing` (or `src/...` for path-derived languages) | 701 symbols, 6 groups, smooth hover |
| "What is each module made of" (trait-heavy vs free functions) | `--group module/kind` | same wedges, tints = kinds |
| Polyglot repo / monorepo, languages first | `--group language/module` | 15 language wedges on self, modules as tints |
| Public surface vs internals per module | `--group module/visibility` | tints = public / private / crate... |
| Census by symbol kind | `--group kind/module` | wedges = kinds; every call crosses the disc, read the counts |
| More than twelve top-level groups | the twelve palette slots cycle; pin colours per module via the gear (persists in the browser) or right-click a legend row | repeats stay separated by wedge, rim label and legend row |
| Heatmap/timeline empty or misleading (shallow clone, vendored code) | `--dates none` | both panels blank, disc unchanged |
| Re-render without re-reading the index | `--from graph.jsonl` (from `codanna dump > graph.jsonl`) | same data, no dump run |

`defines` as a relation is containment; the legend tree already carries it, so add it
only when you want container->member lines on the disc. `--root` matches module
segments, so `crate::parsing::rust`, `parsing.rust` and `parsing/rust` are the same
prefix; a symbol with no `module_path` falls back to its file path segments.

## Reading it

- Wedge angle = module share of the drawn symbols; a wedge reaching the rim with few rings is sparse, many tight rings is dense
- Centre of a wedge = its hubs (highest degree over the chosen relations); the rim = leaves
- Hover: the blue web is the symbol's edges; the detail panel (click) shows the signature (syntax-highlighted via bundled highlight.js grammars for the index's languages), its `file:start-end`, and the connected symbols grouped by relation and direction (Calls / Called by, Uses / Used by, Implements..., `xN` = several call sites). Hopping through those lists builds a breadcrumb trail at the top of the card (back arrow steps back, first crumb recovers the starting symbol; Alt+Left or Backspace = back, Esc = close, `/` = search). Pin to hub drags or pins a symbol into the centre
- Camera: drag to pan (the corner cluster's pan button toggles it), wheel or pinch to zoom toward the pointer, zoom in / zoom out / fit in the bottom-right cluster (fit recentres the whole disc; double-click does the same)
- Legend eye hides a module and the rest regrow into a full circle; legend label pushes the module out and rings it -- highlight and visibility are separate axes
- Heatmap day hover haloes the symbols added that day; click pins the day and fills those symbols in the neutral `--today` colour. The ribbon below brushes the whole history; year chips jump to a year
- 51% of the default-kind symbols on a typical index have no Calls edge (constants, type aliases, trait items): they are the `(unlinked)` group. Narrow `--kinds`, widen `--relation`, or `--unlinked drop` -- see the table above

## Pick by question

- "what is the shape of this codebase / module family" -> this skill
- "what depends on this one symbol / what breaks" -> the sibling x-ray skill's
  `visualize-dump.js` (2D layered call DAG by default; `--3d` for the force view):
  `node ${CLAUDE_SKILL_DIR}/../x-ray/visualize-dump.js <name> [depth]`
- "how is one module organised" -> x-ray `visualize-tree.js` (collapsible tree;
  `--radial` for the poster); "which modules call which" -> x-ray
  `visualize-bundle.js --depth N`
- The x-ray pages share this skill's design tokens and the light/dark toggle

## Files

`graph.mjs` (CLI + assembly), `lib/dump.mjs` (dump reader), `lib/adapter.mjs` (dump -> disc data), `lib/dates.mjs` (blame line dates + cache, first-commit fallback, mtime), `lib/vendor.mjs` (vendored-bundle reader: strips unreachable network calls, count-gated),
`shell.html` + `page.html` + `page.css` + `page.js` + `vendor/` (the vault-graph page, MIT -- see `UPSTREAM.md` for the pin and the tagged hunks),
`vendor/hljs/` (highlight.js core + per-language grammars, BSD-3-Clause -- `BUILD.md` there has the recipe; only grammars for languages present in the dump are inlined),
`assets/logo-mask.svg` (the mark in the hub, painted with the wedge colours).

