From excalibrain
Start or resume a canvas session — a living Excalidraw workspace that builds incrementally. Modes: explore (iterative), architect (comprehensive), storyboard (sequential), wireframe (screens). Trigger on: start a canvas, architect this, storyboard, wireframe, explore this visually, let's explore, continue the diagram, resume the canvas, canvas session.
How this skill is triggered — by the user, by Claude, or both
Slash command
/excalibrain:canvasThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
A canvas session is a **living Excalidraw workspace** that builds incrementally across multiple conversation turns. Each turn adds a section, annotation, or connection — the diagram grows as understanding deepens.
A canvas session is a living Excalidraw workspace that builds incrementally across multiple conversation turns. Each turn adds a section, annotation, or connection — the diagram grows as understanding deepens.
CRITICAL: Always merge into the active canvas. When the user asks for alternative views, additional diagrams, or new sections, use --merge to add them to the existing canvas file. NEVER create separate files during a canvas session. The whole point is one living canvas. The only exception is if the user explicitly asks for a separate file.
Map user intent to mode:
| Trigger phrases | Mode |
|---|---|
| "let's explore...", "explore this visually", "work through...", "start a canvas" | Explore |
| "show me the full architecture...", "architect this" | Architect |
| "storyboard...", "phases...", "timeline of..." | Storyboard |
| "wireframe...", "screen flow...", "mock up..." | Wireframe |
If ambiguous, default to Explore.
On session start:
docs/ exists in the project: docs/diagrams/<topic-slug>.excalidraw./<topic-slug>.excalidraw in the current directory<path>. Sound good, or want it somewhere else?"<same-directory>/<same-name>.excalibrain.jsonRead these as needed — they are the ground truth:
| File | When to read |
|---|---|
references/color-palette.md | Before every diagram section — all hex values live here |
references/diagram-type-rubric.md | Type selection — which diagram type for each section |
references/patterns.md | Visual pattern library with examples |
references/layout-rules.md | Layout rules + coordinate templates |
references/graph-json-format.md | Dagre graph JSON input format |
references/diagram-recipes/<type>.md | Before generating — complete examples per type |
Philosophy: The agent thinks BY drawing, not stops TO draw. Each turn adds one section — the canvas grows as the conversation deepens. Use library-resolve.js components for all composition elements. Use --frame-id on every section build. Use 300px gaps. The infinite canvas is your friend — spread out.
Repeat for each turn:
Tell the user what you will draw and which diagram type, before touching any tool.
"Next I'll add the auth flow as a sequence diagram — it involves Client, Gateway, Auth Service, and Token Store exchanging messages over time, so a sequence diagram argues the interaction best."
Before choosing a tool, classify the content. Check the top rows first — sequence and ER are the types most likely to be incorrectly defaulted to dagre.
| Ask yourself... | If yes → | Tool |
|---|---|---|
| Are multiple participants exchanging messages over time? | Sequence diagram | mermaid-convert.js |
| Are entities related by foreign keys / cardinality? | ER diagram | mermaid-convert.js |
| Is there branching logic WITHIN a single service/process? | Flowchart | dagre-layout.js |
| Is it components and their static connections? | Architecture | dagre-layout.js |
| Is it states and transitions? | State diagram | dagre-layout.js |
| Is it a concept hierarchy or exploration? | Mindmap | dagre-layout.js |
| Are there time-bound tasks with durations? | Gantt chart | gantt-layout.js |
| Does position encode meaning, or no auto-layout fits? | Freeform | primitives.js |
Read references/diagram-type-rubric.md if uncertain.
When multiple rows match — resolve the tie before building.
Content often has multiple facets (e.g., auth has both participants exchanging messages AND branching logic). Different diagram types emphasize different aspects:
How to resolve ties depends on the mode:
Explore mode (interactive): Ask the user. Present the trade-off in one sentence and let them choose:
"The auth flow has both multiple participants (Gateway, Auth Service, Redis) and branching logic (valid? expired? revoked?). A sequence diagram would show who talks to whom and when. A flowchart would show all the failure paths. Which matters more to you?"
This takes 5 seconds and avoids building the wrong diagram type.
Architect / Storyboard mode (parallel): Resolve ties during the planning phase (step 2), NOT during sub-agent execution. The master agent lists each section with its chosen diagram type and justification in the plan. The user confirms the plan before any sub-agents are dispatched. Sub-agents receive an already-decided type — they never hit a tie.
Example plan entry: "Section 3: JWT Auth — sequence diagram (4 participants exchanging messages; branching shown via alt blocks)"
If the user disagrees with a type choice in the plan, they redirect before parallel build begins.
When to use freeform (the last row):
Common misclassifications to watch for:
Always build standalone first, then measure. Never guess sizes. Use the tool selected in step 1.5.
For dagre diagrams (architecture, flowchart, state, mindmap):
node ${CLAUDE_PLUGIN_ROOT}/tools/dagre-layout.js <input.json> \
--prefix <section_prefix> \
--output /tmp/<prefix>-sizing.excalidraw
For mermaid diagrams (sequence, ER):
node ${CLAUDE_PLUGIN_ROOT}/tools/mermaid-convert.js <input.mmd> \
--prefix <section_prefix> \
--output /tmp/<prefix>-sizing.excalidraw
For gantt charts:
node ${CLAUDE_PLUGIN_ROOT}/tools/gantt-layout.js <input.json> \
--prefix <section_prefix> \
--output /tmp/<prefix>-sizing.excalidraw
For freeform layouts (manual positioning with generic shapes):
node ${CLAUDE_PLUGIN_ROOT}/tools/primitives.js <input.json> \
--output /tmp/<prefix>-sizing.excalidraw
Freeform input uses primitives.js with generic shape types. You control all coordinates:
{
"primitives": [
{"type": "rectangle", "x": 0, "y": 0, "width": 200, "height": 80, "label": "Box A", "fill": "#bfdbfe", "stroke": "#1e40af", "rounded": true},
{"type": "ellipse", "x": 250, "y": 10, "width": 120, "height": 60, "label": "Node", "fill": "#bbf7d0", "stroke": "#15803d"},
{"type": "arrow", "fromX": 200, "fromY": 40, "toX": 250, "toY": 40, "label": "calls", "stroke": "#1e1e1e"},
{"type": "text-block", "x": 0, "y": 100, "text": "Annotation text", "fontSize": 14, "color": "#6b7280"},
{"type": "line", "x": 0, "y": 90, "points": [[0, 0], [400, 0]], "stroke": "#e5e7eb"}
]
}
Available generic types: rectangle (with optional label, fill, stroke, rounded), ellipse (same opts), text-block (freestanding text), arrow (fromX/Y, toX/Y, optional label, style), line (points array). All wireframe types (screen, button, input, card, etc.) also work in freeform sections.
Then measure (same for all tools):
node -e "
const { measureVisibleBbox } = require('${CLAUDE_PLUGIN_ROOT}/tools/library-resolve.js');
const canvas = JSON.parse(require('fs').readFileSync('/tmp/<prefix>-sizing.excalidraw','utf8'));
const bbox = measureVisibleBbox(canvas.elements);
console.log(JSON.stringify(bbox));
"
Record w and h — you need these for layout and frame sizing.
Every time a section is added, recompute the canvas layout using organicLayout. The layout is driven by relationships between sections — dagre positions sections based on their connections, so spatial proximity = conceptual proximity.
Process:
Collect all section sizes — the new section (just measured in step 2) plus all existing sections from the sidecar.
Define connections — what is the relationship between this new section and existing ones? Each connection becomes an edge that drives the layout:
Compute positions with organicLayout():
const { organicLayout } = require('${CLAUDE_PLUGIN_ROOT}/tools/library-resolve.js');
const result = organicLayout({
sections: [
{ id: 'edge', w: 800, h: 400 },
{ id: 'gateway', w: 600, h: 700 },
// ... all sections with measured sizes
],
connections: [
{ from: 'edge', to: 'gateway', label: 'enters infrastructure' },
// ... relationships between sections
],
direction: 'TB', // top-to-bottom reading flow
gap: 300, // minimum gap between sections
});
// result.positions = { edge: {x, y}, gateway: {x, y}, ... }
The connections array is also stored in the sidecar and used to generate spine arrows — the same data drives both layout AND visual connections.
# Strip composition elements from previous layout
node ${CLAUDE_PLUGIN_ROOT}/tools/canvas-edit.js <canvas> strip-prefix comp_
# Section 1 — creates canvas (no --merge). Use the tool matching its type:
# dagre (architecture, flowchart, state, mindmap):
node ${CLAUDE_PLUGIN_ROOT}/tools/dagre-layout.js <section1.json> \
--prefix <prefix1> --position <x1>,<y1> --frame-id frame_<prefix1> \
--output <canvas>
# mermaid (sequence, ER):
node ${CLAUDE_PLUGIN_ROOT}/tools/mermaid-convert.js <section1.mmd> \
--prefix <prefix1> --position <x1>,<y1> --frame-id frame_<prefix1> \
--output <canvas>
# freeform (manual positioning):
node ${CLAUDE_PLUGIN_ROOT}/tools/primitives.js <section1.json> \
--position <x1>,<y1> \
--output <canvas>
# Sections 2-N — merge (same pattern, add --merge flag)
node ${CLAUDE_PLUGIN_ROOT}/tools/<dagre-layout.js|mermaid-convert.js|primitives.js> <input> \
--merge <canvas> --prefix <prefixN> --position <xN>,<yN> --frame-id frame_<prefixN> \
--output <canvas>
This means keeping all section input files (graph JSON, .mmd files) in /tmp/ for the duration of the session so sections can be rebuilt at new positions. Track input file paths in the sidecar.
Why rebuild instead of moving? Moving individual elements is error-prone (arrows, zones, annotations have complex coordinate relationships). Rebuilding from source at new positions is atomic and correct — dagre/mermaid handles all internal layout.
The assembly step is now part of step 3's rebuild process. Every section is built with the tool matching its type:
# dagre sections:
node ${CLAUDE_PLUGIN_ROOT}/tools/dagre-layout.js <input.json> \
--prefix <section_prefix> \
--position <x>,<y> \
--frame-id <frame_id> \
--output <canvas.excalidraw>
# mermaid sections:
node ${CLAUDE_PLUGIN_ROOT}/tools/mermaid-convert.js <input.mmd> \
--prefix <section_prefix> \
--position <x>,<y> \
--frame-id <frame_id> \
--output <canvas.excalidraw>
# freeform sections:
node ${CLAUDE_PLUGIN_ROOT}/tools/primitives.js <input.json> \
--position <x>,<y> \
--output <canvas.excalidraw>
# Subsequent sections — merge into existing canvas (add --merge flag)
node ${CLAUDE_PLUGIN_ROOT}/tools/<tool> <input> \
--merge <canvas.excalidraw> \
--prefix <section_prefix> \
--position <x>,<y> \
--frame-id <frame_id> \
--output <canvas.excalidraw>
CRITICAL flags:
--frame-id <frame_id> — ALL elements get frameId baked in at creation. Format: frame_<prefix> (e.g., frame_auth). This is what makes content move with the frame.--prefix <str> — ID namespace. Max 8 chars.--position <x>,<y> — from step 3After assembling the section, add composition elements. Create a JSON file with components and merge:
node ${CLAUDE_PLUGIN_ROOT}/tools/library-resolve.js <composition.json> \
--merge <canvas.excalidraw> \
--output <canvas.excalidraw>
The composition JSON includes whichever of these are needed for this turn:
a) Canvas title (first section only):
{"type": "canvas-title", "title": "CANVAS NAME", "subtitle": "description", "x": 30, "y": -80}
b) Section frame (every section): Size from measured bbox + 40px padding each side.
{"type": "section-frame", "name": "① Section Name", "x": bbox.x-40, "y": bbox.y-40, "width": bbox.w+80, "height": bbox.h+80, "id": "frame_<prefix>"}
c) Spine arrow (from second section onwards): Connect the previous frame to this frame. Use frame center coordinates — Excalidraw's binding system recalculates to frame borders in the editor. Always 2-point straight lines — no curves, no L-shapes.
{"type": "spine-arrow", "fromX": prev_cx, "fromY": prev_bottom, "toX": this_cx, "toY": this_top, "label": "transition text", "fromId": "frame_prev", "toId": "frame_this"}
d) Post-it notes (when there's an insight to capture): Place in the 300px gap between frames, outside all frames.
{"type": "postit", "text": "key insight or decision", "x": gap_x, "y": gap_y}
e) Margin notes (optional, for quiet meta-observations):
{"type": "margin-note", "text": "quiet observation", "x": right_margin, "y": some_y}
The library-resolve.js post-merge pass automatically handles:
frameId to elements inside frame bounds (for primitives.js which doesn't support --frame-id)boundElements on frames for all arrows with startBinding/endBindingWrite or update the .excalibrain.json file with the new section info (see Sidecar Management below).
Brief chat message about what was drawn and why. Keep it to 2-3 sentences. Reference specific nodes or connections if helpful.
Ask the user what to do next:
"What next? You can:
What TO add:
What NOT to add:
Visual hierarchy (font stack):
| Role | Font | Size | Color |
|---|---|---|---|
| Spine arrow labels | Virgil (1) | 20px | #4f46e5 (deep indigo) |
| Diagram content | Virgil (1) | 14-16px | varies |
| Post-it notes | Cascadia (3) | 13px | #1e293b on #fef9c3 |
| Canvas title | Helvetica (2) | 32px | #0f172a |
| Canvas subtitle | Virgil (1) | 15px | #94a3b8 |
Layout rules:
flexboxLayout() for multi-section positioning (space-evenly distribution)measureVisibleBbox() for frame sizing — excludes arrows that inflate mermaid outputThe .excalibrain.json file tracks session state. It lives alongside the .excalidraw file.
{
"canvas": "<filename>.excalidraw",
"mode": "explore",
"theme": "default",
"created": "<ISO timestamp>",
"lastUpdated": "<ISO timestamp>",
"diagramLocation": "<directory path>",
"sections": [
{
"id": "<section-name>",
"label": "<Human readable label>",
"mode": "explore",
"boundingBox": { "x": 0, "y": 0, "w": 600, "h": 400 },
"elementPrefix": "<prefix>_",
"elementCount": 12,
"annotations": [
{ "id": "anno_1", "text": "Chose Redis because writes are bursty" }
],
"decisions": [
"Event-driven invalidation over TTL for cache consistency"
]
}
],
"researchZone": {
"boundingBox": { "x": 0, "y": -200, "w": 1200, "h": 180 },
"findings": []
},
"connections": []
}
sections arraysections after each diagram addition — append the new section with its bounding box, prefix, element count, annotations, and decisionslastUpdated on every writeconnections when adding cross-section arrows or linksWhen the user says "continue the diagram", "resume the canvas", or references an existing .excalidraw file:
.excalidraw file.excalibrain.json sidecarcanvas-inspect.js on the canvaselementCount per sectionIf no sidecar exists but the .excalidraw file does, run inspect and reconstruct a sidecar from what's on the canvas. Ask the user to confirm before proceeding.
A dedicated area on the canvas for high-level findings:
y < 0){ x: 0, y: -200, w: 1200, h: 180 }#374151 (gray-700)| Property | Value |
|---|---|
| Font size | 14px |
| Color | #6b7280 (gray-500) |
| Position | 10-20px offset from section bounding box edge (below or beside) |
| Format | Short phrases, not paragraphs |
| Purpose | Reasoning notes — "Chose X because Y", "This connects to Z via..." |
Architect mode builds a comprehensive multi-section canvas in one pass. Use it when the user wants a full system overview rather than iterative exploration.
Read the codebase or context to understand the system:
Announce the plan to the user before building:
For each planned section, dispatch the section-builder agent:
section-builder(
topic: "<section topic>",
context: "<relevant code snippets, file summaries>",
canvas: "<canvas.excalidraw path>",
position: "<x>,<y>",
prefix: "<section_prefix>",
theme: "<session theme>"
)
CRITICAL: Use the two-phase build to avoid overlapping sections. Section sizes vary wildly (a simple dagre graph might be 400×300, a mermaid sequence diagram might be 4000×1900). Never guess positions — measure first.
Phase 1 — Measure (parallel):
Build each section into a temporary standalone file to discover its actual size:
# All sections build in parallel — no --merge, standalone output
node ${CLAUDE_PLUGIN_ROOT}/tools/dagre-layout.js <input.json> \
--prefix <section_prefix> \
--output /tmp/<prefix>-sizing.excalidraw
Then inspect each temp file to get its actual bounding box:
node ${CLAUDE_PLUGIN_ROOT}/tools/canvas-inspect.js /tmp/<prefix>-sizing.excalidraw --summary
Parse the bounding box w and h from the inspect output for each section.
Phase 2 — Compute positions:
With actual sizes known, compute a non-overlapping grid:
gap = 150 # generous gap between sections
columns = 2 or 3 # based on section count and total width budget
For each row:
row_x = 0
row_h = 0
For each section assigned to this row:
section.position = (row_x, current_y)
row_x += section.actual_width + gap
row_h = max(row_h, section.actual_height)
current_y += row_h + gap
Group sections thematically when possible (e.g., structure views in row 1, behavior views in row 2, resilience views in row 3).
Phase 3 — Assemble (parallel):
Rebuild each section into the final canvas with correct positions:
# Section 1 creates the canvas (no --merge)
node ${CLAUDE_PLUGIN_ROOT}/tools/dagre-layout.js <input_1.json> \
--position <computed_x>,<computed_y> \
--prefix <prefix> \
--output <canvas.excalidraw>
# Sections 2-N merge into the canvas (parallel — safe with --merge)
node ${CLAUDE_PLUGIN_ROOT}/tools/dagre-layout.js <input_N.json> \
--merge <canvas.excalidraw> \
--position <computed_x>,<computed_y> \
--prefix <prefix> \
--output <canvas.excalidraw>
Layout parameters:
| Parameter | Value |
|---|---|
| Gap between sections | 150px (generous — prevents visual crowding) |
| Research zone | Above main area, y < 0 |
| Max columns | 3 (keeps canvas readable without excessive horizontal scrolling) |
| Row assignment | Thematic grouping preferred; otherwise largest sections get their own row |
After all sections are merged, check for overlapping bounding boxes before proceeding:
canvas-inspect.js --summary on the canvasbbox from the outputoverlap = not (A.x + A.w + 50 < B.x or B.x + B.w + 50 < A.x or
A.y + A.h + 50 < B.y or B.y + B.h + 50 < A.y)
(50px minimum clearance between any two sections)This validation is mandatory. Overlapping sections produce unreadable diagrams — it is always better to spend 30 seconds rebuilding than to show the user a mess.
After all section-builders complete and overlap validation passes:
canvas-inspect.js to see all elements on the canvas.excalidraw JSON edits to add arrow elements between known node positionsy < 0) with key findings from step 1Write the .excalibrain.json sidecar with:
"architect"export.jsFor visual narratives that show how a system evolves — migrations, build plans, phased rollouts, before/after transformations.
Core principle: A storyboard shows state snapshots, not process diagrams. Each frame is "what the system looks like at this phase" — NOT a flowchart of what you do during that phase. The story emerges from seeing what changed between frames.
From user intent, determine:
Tell the user the frames and what changes in each:
"I'll build a 3-panel storyboard showing the migration:
Before drawing any panel, list ALL components that appear across ALL phases. This is critical — the same components must appear in the same position across panels so the viewer can spot what changed.
Example inventory:
Components: API, Auth, DB, Cache, Gateway, Users Service
Phase 1: API ✓, Auth ✓ (coupled), DB ✓, Cache ✗, Gateway ✗, Users ✗
Phase 2: API ✓, Auth ✓ (extracted), DB ✓, Cache ✓ (new), Gateway ✗, Users ✗
Phase 3: API ✓, Auth ✓, DB ✓ (split), Cache ✓, Gateway ✓ (new), Users ✓ (new)
For each panel, generate a dagre graph JSON with the SAME nodes but different colors:
Color coding for change:
| Status | Fill | Stroke | strokeStyle |
|---|---|---|---|
| Unchanged (exists, no change) | #f1f5f9 (gray-100) | #94a3b8 (gray-400) | solid |
| New in this phase | #dcfce7 (green-100) | #16a34a (green-600) | solid |
| Modified in this phase | #fef3c7 (amber-100) | #d97706 (amber-600) | solid |
| Being removed | #fecaca (red-100) | #dc2626 (red-600) | dashed |
| Already removed (gone) | omit from this panel |
Place panels left-to-right:
node ${CLAUDE_PLUGIN_ROOT}/tools/dagre-layout.js <panel_N.json> \
--merge <canvas.excalidraw> \
--position <x>,80 \
--prefix ph<N>_ \
--output <canvas.excalidraw>
Position: x = panel_index * (panel_width + 120), y = 80 (leave room for title bar)
A horizontal timeline arrow across the top of all panels:
Panel titles above each panel:
#111827, boldBelow each panel, add a narrative layer — not just a label, but the story:
What changed (required):
#374151Why (required):
#6b7280Risks / Metrics (optional):
#9ca3afBetween panels:
#6366f1 (indigo — distinct from the diagram arrows within panels)mode: "storyboard"Layout specifics:
| Parameter | Value |
|---|---|
| Panel width | 500px (or content-dependent) |
| Panel min height | 400px |
| Gap between panels | 120px |
| Timeline y-position | 30px (above panels) |
| Title y-position | 50px |
| Panels y-position | 80px |
| Narrative caption gap | 20px below panel bottom |
| Progression arrows | y-center of panels, dashed, indigo (#6366f1) |
What NOT to do:
Compare is a visual operation, not a mode. It can be used in any session or as a standalone Quick Draw.
Triggered by: "compare X vs Y", "show options side by side", "what are the trade-offs"
Extract 2-4 approaches from conversation context. Each option becomes a column.
Each option gets a column:
x = col_index * (400 + 80), y = 0Use dagre with --merge --position --prefix:
node ${CLAUDE_PLUGIN_ROOT}/tools/dagre-layout.js <option_input.json> \
--merge <canvas.excalidraw> \
--position <x>,0 \
--prefix opt<N>_ \
--output <canvas.excalidraw>
opt1_, opt2_, etc.Below each column, add pros and cons:
#16a34a), cons in red (#dc2626)At the bottom, spanning all columns:
canvas-inspect.js to find free space).excalidraw file and sidecarFor screen flows — login forms, onboarding, settings pages. Uses primitives.js to generate hand-drawn UI components.
From user intent, identify the screens in the flow (typically 2-5). Each screen represents a distinct view.
Tell the user the screens you'll build:
"I'll wireframe a 3-screen flow: Login, Dashboard, Settings. Sound good?"
For each screen:
screen frame + UI elements (buttons, inputs, cards, etc.)x = screen_index * (screen_width + 120)primitives.js with merge flagsnode ${CLAUDE_PLUGIN_ROOT}/tools/primitives.js <screen_input.json> \
--merge <canvas.excalidraw> \
--position <x>,0 \
--output <canvas.excalidraw>
Primitives JSON format:
{
"primitives": [
{ "type": "screen", "x": 0, "y": 0, "size": "mobile", "title": "Login" },
{ "type": "input", "x": 30, "y": 100, "placeholder": "Email", "label": "Email" },
{ "type": "button", "x": 30, "y": 200, "label": "Sign In", "variant": "primary" }
]
}
Available primitive types: screen, button, input, textarea, card, nav-bar, modal, divider, image-placeholder, avatar, list-item.
Read references/primitives/wireframe.md for sizing, variants, and styling details.
Between screens, add horizontal arrows showing the user flow:
Each screen is a section in the sidecar. Set mode to "wireframe".
export.jsLayout specifics:
| Parameter | Value |
|---|---|
| Mobile screen | 390×780 |
| Desktop screen | 1280×800 |
| Tablet screen | 768×1024 |
| Gap between screens | 120px |
| Navigation arrows | y-center, dashed, labeled with user action |
All tools live at ${CLAUDE_PLUGIN_ROOT}/tools/. CLI signatures:
| Tool | Command |
|---|---|
| Canvas inspect | canvas-inspect.js <file> |
| Canvas edit | canvas-edit.js <file> strip-prefix <prefix> (also: update, delete, move) |
| Dagre layout | dagre-layout.js <input.json> [--merge file] [--position x,y] [--prefix str] [--theme name] [--output file] |
| Mermaid convert | mermaid-convert.js <input.mmd> [--merge file] [--position x,y] [--prefix str] [--theme name] [--output file] |
| Gantt layout | gantt-layout.js <input.json> [--merge file] [--position x,y] [--prefix str] [--theme name] [--output file] |
| Wireframe primitives | primitives.js <input.json> [--merge file] [--position x,y] [--output file] |
| Library resolve | library-resolve.js <input.json> [--merge file] [--output file] |
| Export | export.js <file.excalidraw> --format png|svg --output <file> |
Every section must have visual hierarchy. Without it, all elements compete for attention equally and the diagram becomes a flat, undifferentiated wall of shapes. The reader's eye has no entry point and no path to follow.
Three levers for hierarchy:
| Lever | Primary elements | Secondary elements |
|---|---|---|
| Stroke width | 2.5–3px for hubs, gateways, critical path | 1–1.5px for async workers, background services |
| Node size | Larger (220×120) for central/important nodes | Smaller (160×70) for peripheral/failure nodes |
| Fill style | solid with vivid colors for active components | dots or hachure for async, storage, or background |
Arrow weight differentiates flow importance:
| Flow type | Width | Style |
|---|---|---|
| Critical path / happy path | 2.5px | solid |
| Standard data flow | 2px | solid |
| Async / event / background | 1px | dashed |
| Error / failure path | 1px | dashed, red stroke |
| Internal / check | 1px | dotted |
Zone grouping creates visual structure:
Font size hierarchy:
| Element role | Font size |
|---|---|
| Hub/central node label | 16px |
| Primary service label | 14px |
| Secondary/async label | 12–13px |
| Edge labels | 12–13px (from layout rules) |
Rule of thumb: If you squint at the exported PNG and can't immediately tell which 2-3 elements are the most important, the section lacks hierarchy. The gateway, the hub, the start/end states — these should jump out visually.
Use "direction": "TB" for all dagre diagrams unless there is a specific reason not to. Top-to-bottom is the natural reading direction — the eye scans down, cause leads to effect, general leads to specific.
| Direction | When to use |
|---|---|
| TB (default) | Architecture, flowcharts, state machines, security layers — almost everything |
| LR | Mind maps (radial tree), timelines, left-to-right process chains where horizontal flow is the point |
Cyclic graphs (state machines with recovery edges):
Dagre cannot lay out cyclic graphs well. Back-edges (e.g., suspended→unverified) cause rank inversions that scramble node order and produce overlapping orthogonal arrows.
Fix:
annotations field.When redoing 1-2 sections on an existing canvas, do NOT rebuild the entire canvas. Use the strip-and-merge pattern:
# 1. Strip the old section by prefix
node ${CLAUDE_PLUGIN_ROOT}/tools/canvas-edit.js <canvas.excalidraw> strip-prefix <prefix>
# 2. Build new version to temp file, measure actual size
node ${CLAUDE_PLUGIN_ROOT}/tools/dagre-layout.js <new_input.json> \
--prefix <prefix> --output /tmp/<prefix>-sizing.excalidraw
node ${CLAUDE_PLUGIN_ROOT}/tools/canvas-inspect.js /tmp/<prefix>-sizing.excalidraw --summary
# 3. Check if new size fits in the old position
# If yes: merge at the same position
# If larger: inspect canvas for free space, pick new position
# 4. Merge new section into the stripped canvas
node ${CLAUDE_PLUGIN_ROOT}/tools/dagre-layout.js <new_input.json> \
--merge <canvas.excalidraw> \
--position <x>,<y> \
--prefix <prefix> \
--output <canvas.excalidraw>
This preserves all other sections untouched and only rebuilds the changed section.
Prefix collision warning: Mermaid generates random element IDs prefixed with the section prefix. If two sections have similar prefixes (e.g., er and err), strip-prefix err may accidentally remove elements from the er section. Use distinctive prefixes that don't share a common start (e.g., erdiag and errfl).
This is the most important cross-cutting rule. Do NOT default everything to flowcharts. Before drawing any section, ask: what is the best visual argument for THIS content?
| Content is about... | Use this type | Tool |
|---|---|---|
| Components and their relationships | Architecture (zones, layers, connections) | dagre |
| A process with decisions/branching | Flowchart (diamonds, conditionals) | dagre |
| How requests flow between services over time | Sequence diagram (participants, messages) | mermaid |
| Lifecycle states and transitions | State diagram (states, events, transitions) | dagre |
| Concept hierarchy or exploration | Mindmap (radial, parent-child) | dagre |
| Data model relationships | ER diagram (entities, foreign keys) | mermaid |
| Time-bound tasks | Gantt chart (timeline, bars) | gantt |
| System evolution over phases | Storyboard (state panels with visual diff) | dagre |
A single canvas should mix diagram types freely. An architecture document might have a system overview (architecture), a request flow (sequence), a session lifecycle (state diagram), and a build process (flowchart) — all on the same canvas, each chosen because it's the right visual argument for that piece.
The table above covers common cases, not all cases. The predefined types handle 80% of situations with proven layout strategies. For the other 20%, don't default to manual coordinate placement — compose a layout strategy from the tools you have:
Think in building blocks, not coordinates:
x = col * width + gap, y = row * height + gap). Works for floor plans, seating charts, comparison tables, kanban boards.When no recipe exists: Don't try to manually calculate 50 x,y coordinates. Instead:
.excalidraw JSON for truly custom layouts where no tool helpsThe visual intelligence is in choosing the right strategy, not in having a pre-built recipe for every possible diagram.
Read references/diagram-type-rubric.md for the full decision table.
Sections on the same canvas are NOT islands. After building multiple sections, add inter-section connection arrows showing:
Use distinct arrow styles for inter-section arrows (dashed, different color like #6366f1 indigo) to distinguish them from intra-section arrows.
You decide this — the user shouldn't have to ask. When planning what to draw, assess whether the subject has multiple facets that need different visual arguments:
| Subject facet | Best diagram type |
|---|---|
| What components exist and how they connect | Architecture |
| How a request flows between components over time | Sequence |
| What states something can be in and how it transitions | State diagram |
| What data is stored and how entities relate | ER diagram |
| What steps a process follows with decisions | Flowchart |
| What the concept hierarchy looks like | Mindmap |
If the subject has 2+ facets → plan multiple sections with different types. Announce this:
"This system has three aspects worth visualizing: the service architecture (how components connect), the auth flow (how a request moves through them over time), and the session lifecycle (what states a session can be in). I'll build all three on one canvas with zoom arrows connecting them."
If the subject has only one facet → single diagram is fine. Don't force multi-type when one diagram tells the whole story.
This applies in ALL modes — Explore, Architect, Storyboard. The mode determines the interaction pattern (iterative vs. comprehensive vs. sequential), but multi-type selection happens within any mode.
When a canvas needs multiple sections (3+), use the two-phase build pattern to avoid overlapping sections.
NEVER guess section sizes. Diagram tools produce wildly different output sizes depending on node count, label length, edge complexity, and diagram type. A simple 4-node dagre graph might be 400×300px. A mermaid sequence diagram with 11 participants and 30 messages might be 4000×1900px. Hardcoded grids (e.g., "x-step 700, y-step 500") will produce overlapping sections.
Orchestration pattern:
--merge, just --output /tmp/<prefix>-sizing.excalidraw). Each agent returns the section's actual bounding box dimensions.--merge --position <x>,<y>). Section 1 creates the file (no --merge), sections 2-N merge in parallel.canvas-inspect.js --summary, check all prefix group bounding boxes for overlaps. If any overlap, recompute positions and rebuild.#6366f1) arrows with labels.Multi-zoom pattern (overview + detail):
For complex systems, build at multiple zoom levels on the same canvas:
This creates a visual document you can read at two levels: the overview for the big picture, the details for depth. Each detail section uses the diagram type that best argues its content.
When to parallelize:
default, clean, dark, blueprint), use it for all sections throughout the session.json or .mmd input files created for tools after the tool finishesCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub dev-eloper1/excalibrain