| name | excalidraw |
| description | Create diagrams using Excalidraw JSON or hand-crafted SVG. Use when the user asks to create diagrams, flowcharts, architecture diagrams, RTL micro-architecture, wireframes, mind maps, ERDs, sequence diagrams, or any visual drawings. Generates `.excalidraw` JSON or `.svg` files. |
Excalidraw Diagram Creation
Generate .excalidraw files as JSON using the Write tool. No scripts or executables needed.
File Wrapper
Every .excalidraw file uses this structure:
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": null
},
"files": {}
}
elements: Array of all shapes, text, arrows, and lines
appState: Canvas settings (background color, grid)
files: Image data (only needed for embedded images)
Element Types
| Type | Shape | Use For |
|---|
rectangle | Box | Processes, services, containers |
ellipse | Oval | Start/end points, databases |
diamond | Diamond | Decisions, conditions |
text | Text | Labels, descriptions |
arrow | Arrow | Connections, data flow |
line | Line | Lifelines, boundaries |
freedraw | Freehand | Annotations |
frame | Frame | Grouping container |
Minimal Element Templates
Rectangle
{
"type": "rectangle",
"id": "rect-1",
"x": 100, "y": 100,
"width": 160, "height": 80,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"seed": 1001,
"version": 1,
"versionNonce": 1001,
"isDeleted": false,
"groupIds": [],
"boundElements": [
{ "id": "text-1", "type": "text" }
],
"link": null,
"locked": false,
"roundness": { "type": 3 }
}
Set "roundness": null for sharp corners. Use { "type": 3 } for rounded.
Text (inside a shape)
{
"type": "text",
"id": "text-1",
"x": 130, "y": 127,
"width": 100, "height": 25,
"text": "My Label",
"fontSize": 20,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "middle",
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"seed": 1002,
"version": 1,
"versionNonce": 1002,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"containerId": "rect-1",
"originalText": "My Label",
"autoResize": true,
"lineHeight": 1.25
}
When text is inside a shape: set containerId to the shape's ID, and add { "id": "text-1", "type": "text" } to the shape's boundElements.
Text (standalone)
For standalone text (labels, titles), set containerId: null and do not add it to any shape's boundElements.
Arrow (connecting two shapes)
{
"type": "arrow",
"id": "arrow-1",
"x": 265, "y": 140,
"width": 100, "height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"seed": 1003,
"version": 1,
"versionNonce": 1003,
"isDeleted": false,
"groupIds": [],
"boundElements": null,
"link": null,
"locked": false,
"points": [[0, 0], [100, 0]],
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "rect-1", "focus": 0, "gap": 5 },
"endBinding": { "elementId": "rect-2", "focus": 0, "gap": 5 }
}
Binding Rules (Critical)
Bindings are bidirectional. When an arrow connects to a shape:
- The arrow sets
startBinding/endBinding with { "elementId": "shape-id", "focus": 0, "gap": 5 }
- The shape must list the arrow in
boundElements: [{ "id": "arrow-id", "type": "arrow" }]
Same for text inside shapes:
- The text sets
containerId: "shape-id"
- The shape must list the text in
boundElements: [{ "id": "text-id", "type": "text" }]
Missing either side will cause rendering issues.
Legend Box Pattern (Critical for excalirender)
Legend boxes MUST use proper containerId binding. Never use standalone text overlapping a box.
Why: excalirender renders text at its literal x,y coordinates regardless of any visual overlap with a rectangle. If text is not bound via containerId, font metric differences between the web app and CLI renderer cause text to extend outside the box.
Sizing rule: Legend box WIDTH must accommodate the longest text line. At fontSize=12, each character is approximately 7px wide. Formula: width >= max_line_chars * 7 + 20. Minimum width: 240px for HLS diagrams (typical longest line: "Green ellipse: Data I/O port" = 28 chars). Height: num_lines * fontSize * lineHeight + 20 padding.
Pattern: Combine title and content into a single bound text element:
{
"type": "rectangle",
"id": "legend-box",
"x": 720, "y": 150,
"width": 260, "height": 185,
"boundElements": [{"id": "legend-text", "type": "text"}]
}
{
"type": "text",
"id": "legend-text",
"x": 730, "y": 160,
"width": 240, "height": 165,
"text": "Legend\n--- data stream\n-.- control signal\n[box] storage",
"textAlign": "left",
"verticalAlign": "top",
"containerId": "legend-box"
}
Anti-pattern (causes text overflow in CLI rendering):
{"id": "legend-title", "containerId": null}
{"id": "legend-content", "containerId": null}
{"id": "legend-box", "boundElements": null}
A container can hold ONE bound text element. For legends with title + content, merge them into one text element with newline separation.
Color Palette
Background Colors (light fills)
| Color | Hex | Use |
|---|
| Blue | #a5d8ff | Info, input, frontend |
| Green | #b2f2bb | Success, output, start |
| Yellow | #ffec99 | Warning, decisions |
| Red | #ffc9c9 | Error, danger, stop |
| Purple | #d0bfff | External, storage, database |
| Gray | #e9ecef | Neutral, disabled |
| Cyan | #99e9f2 | Highlight, accent |
Stroke Colors (borders and text)
| Color | Hex |
|---|
| Black | #1e1e1e |
| Blue | #1971c2 |
| Green | #2f9e44 |
| Yellow | #f08c00 |
| Red | #e03131 |
| Purple | #9c36b5 |
| Gray | #868e96 |
| Cyan | #0c8599 |
Styling Properties
| Property | Values |
|---|
fillStyle | "solid", "hachure", "cross-hatch" |
strokeWidth | 1 (thin), 2 (medium), 4 (bold) |
strokeStyle | "solid", "dashed", "dotted" |
roughness | 0 (clean/formal), 1 (hand-drawn), 2 (sketchy) |
opacity | 0-100 |
Font Families
| Value | Font | Use |
|---|
1 | Virgil | Hand-drawn, casual |
2 | Helvetica | Professional, formal |
3 | Cascadia | Code, technical labels |
Font Sizes
| Context | Size |
|---|
| Titles | 28-36 |
| Headers | 24 |
| Labels | 20 |
| Descriptions | 16 |
| Notes | 14 |
Arrow Details
Arrowhead Types
null, "arrow", "bar", "dot", "triangle"
Points Array
Points are relative to the arrow's x, y position. First point is always [0, 0]:
Horizontal: [[0, 0], [200, 0]]
Vertical: [[0, 0], [0, 150]]
L-shaped: [[0, 0], [100, 0], [100, 100]]
Diagonal: [[0, 0], [150, 80]]
Set width/height to match the bounding box of all points.
Binding Focus
The focus property (-1 to 1) controls where the arrow attaches along the shape edge. 0 = center, negative = left/top, positive = right/bottom.
Grouping
Give elements matching groupIds arrays:
{ "id": "rect-1", "groupIds": ["my-group"] }
{ "id": "text-1", "groupIds": ["my-group"] }
Element Dependency Rules
When repositioning any element, ALL dependent elements must move together. Failing to do this creates misaligned diagrams that only become visible after PNG rendering.
Dependency Types
| If you move... | Also move... | Why |
|---|
| A shape with bound text | The text element (recalculate x,y centering) | excalirender uses literal coordinates |
| A shape with bound arrows | Arrow x,y + recalculate points array | Arrow origin/endpoint shifts |
| A self-loop arrow | Its label text | Self-loop arc + label are an atomic visual unit |
| A state box in FSM | Self-loop arrow, label, all connected arrows | All are positioned relative to the state |
| Content area (shifting down) | Legend box and its bound text | Legend stays in consistent position relative to content |
Pre-Move Checklist
Before moving any element:
- Find all elements that reference it via
boundElements (arrows and text bound to it)
- Find all elements whose
containerId points to it (text inside it)
- Find all arrows whose
startBinding.elementId or endBinding.elementId points to it
- Find any labels positioned by visual proximity (arrow labels, annotations)
- Move ALL identified elements together, recalculating positions as needed
Self-Loop Atomic Unit
Self-loop arrows on FSM states consist of three tightly coupled elements:
- The state box (rectangle)
- The self-loop arrow (arc above the state)
- The transition label (text between subtitle and arc)
These MUST move as a unit. The arc sits 40px above the state box top. The label sits between the subtitle bottom and the arc top.
ID and Seed Conventions
- Use descriptive IDs:
"rect-client", "arrow-auth-db", "text-header"
- Every element needs a unique
seed (any positive integer)
- Set
versionNonce to the same value as seed
- Start
version at 1
Diagram Construction Checklist
- Plan layout and calculate positions (use 50px spacing multiples)
- Create shapes with positions and dimensions
- Add text elements (set
containerId for text inside shapes)
- Add arrows with
points, bindings, and arrowheads
- Verify all bindings are bidirectional
- Verify all IDs and seeds are unique
- Wrap in the file wrapper template
- Generate architecture summary JSON (see below)
- Render a PNG preview (see below)
- Verify text centering: for every text with
containerId, confirm x,y is explicitly centered within the container using the centering formula
- Verify element dependencies: for every repositioned element, confirm all dependent elements (bound text, arrows, labels) moved too
- Render to PNG via
excalirender and visually inspect - do NOT trust web app preview alone
Architecture Summary JSON
Always generate a .arch.json file alongside the .excalidraw file. This concise graph representation lets Claude Code agents understand the diagram's architecture without parsing the full Excalidraw JSON (~500 tokens vs ~8,500 tokens).
Output files per diagram
diagram.excalidraw # Full visual diagram (for editing/rendering)
diagram.arch.json # Concise graph summary (for agent consumption)
diagram.png # Rendered image (for human review)
Schema
{
"title": "Diagram Title",
"notes": ["Subtitle or context lines"],
"nodes": {
"node-id": {
"name": "Display Name",
"desc": "Brief description"
}
},
"edges": [
{
"from": "source-node-id",
"to": "target-node-id",
"type": "data|ctrl|flow|scalar",
"label": "signal name"
}
]
}
Extraction rules
Build the .arch.json by extracting semantic content from the .excalidraw elements:
- nodes: Each shape (rectangle, ellipse, diamond) that is NOT a legend or decorative element. Use the shape's
id as key. Get name from the associated title text ({id}-title or {id}-text), desc from {id}-desc.
- edges: Each arrow element. Set
from/to from startBinding.elementId/endBinding.elementId. Determine type from the arrow's visual encoding:
"data" - solid stroke, blue (#1971c2)
"ctrl" - dashed stroke, red (#e03131)
"scalar" - dashed stroke, orange (#f08c00)
"flow" - solid stroke, black/default (#1e1e1e)
Set label from the associated label text element (lbl-{arrow-suffix}).
- title: From the
title text element.
- notes: From
subtitle, topology-label, or other annotation text elements.
Omit desc, label, and notes when empty. Replace newlines in text with commas.
Rendering to PNG
Always generate a .png alongside the .excalidraw file so humans can review the diagram without opening a viewer.
Primary: excalirender CLI
Single command, no browser needed. Renders using the same engine as Excalidraw:
excalirender diagram.excalidraw -o diagram.png -s 2
Options:
-s 2: 2x scale for crisp output (recommended)
-d: Dark mode
--transparent: Transparent background
Install (one-time, Linux x64):
curl -fsSL https://raw.githubusercontent.com/JonRC/excalirender/main/install.sh | sh
If excalirender is not installed and cannot be installed, fall back to the Playwright method below.
Fallback: Playwright MCP
Use when excalirender is not available but the Playwright MCP plugin is. This approach loads the diagram into excalidraw.com via the system clipboard and takes a screenshot.
-
Start a temporary HTTP server in the directory containing the .excalidraw file:
cd /path/to/dir && python3 -m http.server 8765 &
-
Navigate to http://localhost:8765 and copy the JSON to the system clipboard:
async () => {
const resp = await fetch('http://localhost:8765/diagram.excalidraw');
const text = await resp.text();
await navigator.clipboard.writeText(text);
return 'Loaded';
}
-
Navigate to https://excalidraw.com, click the canvas, then press Ctrl+V.
-
Wait 2 seconds, then take a screenshot with browser_take_screenshot.
-
Clean up: kill the HTTP server, close the browser.
Key detail: navigator.clipboard.writeText() writes to the OS system clipboard, which persists across page navigations. Content set on localhost is available after navigating to excalidraw.com.
CLI Rendering Caveats
Critical: The Excalidraw web app and excalirender CLI handle text positioning differently. Diagrams that look correct in excalidraw.com may render incorrectly as PNG.
Web App vs CLI Rendering
The web app auto-centers bound text inside containers using the containerId binding - it ignores the text element's literal x,y and positions it at the center of its container. The excalirender CLI does NOT do this. It renders text at exactly the x,y coordinates specified in the JSON, regardless of containerId.
Consequence: If you create a text element with containerId set but position it at x=0, y=0, it will appear centered in the web app but in the top-left corner when rendered to PNG.
Text Centering Formula
Always compute explicit centered positions for text inside containers:
text.x = shape.x + (shape.width - text.width) / 2
text.y = shape.y + (shape.height - text.height) / 2
For verticalAlign: "top", use: text.y = shape.y + 10 (10px top padding).
Character Width Reference
Use these approximations to calculate text.width from content:
| Font Family | fontSize=20 | fontSize=16 | fontSize=14 | fontSize=12 |
|---|
| Virgil (1) | ~10px/char | ~8px/char | ~7px/char | ~7px/char |
| Helvetica (2) | ~9px/char | ~7px/char | ~6.5px/char | ~6px/char |
| Cascadia (3) | ~10px/char | ~8px/char | ~7px/char | ~7px/char |
Formula: text.width = max_line_length * char_width
Multi-line Text Height
text.height = num_lines * fontSize * lineHeight
Where lineHeight defaults to 1.25 for most elements.
Verification Requirement
Always render to PNG via excalirender and visually inspect before considering a diagram done. Do NOT trust the web app preview as the sole verification - it masks centering issues that will appear in CLI-rendered output.
HLS Diagram Content Specifications
When creating diagrams for HLS hardware modules, follow these content templates to ensure consistent quality.
System Integration Diagram
Required elements:
- 2D branching layout matching the system block diagram topology (not linear)
- INPUT/OUTPUT ellipses (green
#b2f2bb)
- Module boxes with: name, function name, type tag (DATAFLOW/FSM/FIR), per-module description
- Helper function boxes (gray
#e9ecef): splitters, duplicators, passthrough wrappers
- ALL inter-module FIFOs: labeled arrows with "stream_name (depth)"
- Data streams: solid blue (
#1971c2) arrows
- Control signals: dashed red (
#e03131) arrows
- Resource summary box (gray): system totals (DSP, BRAM, LUT, FF), timing (WNS, Fmax), latency ratio
- Topology description text
- FIFO depth notes section
- Legend box
FSM State Diagram
Required elements:
- All states from the code (check labeled loop names AND switch cases, not just enum)
- Initial state highlighted (green
#b2f2bb, bold stroke)
- Processing states (cyan
#99e9f2)
- Self-loops where applicable (non-blocking poll states)
- Transition conditions from actual code (not generic "loop complete")
- Data streaming path section: input port ellipse -> storage box -> output port ellipses
- Control input path: dashed red arrows from ctrl_in to receiving state
- Storage annotation: circ_buf size, type (purple
#d0bfff)
- Notes section: phase descriptions, pattern name (FP-ASYNC-DATA, etc.)
- Legend box (min width 240px)
Layout spacing rules (Critical - prevents element overlap in excalirender):
- Title at y=20, subtitle at y=58
- First state box at y>=150 (NOT y=100) to leave room for self-loop arcs above the initial state
- Self-loop arrows and their labels are a UNIT: when repositioning one, reposition both. Self-loop arc height is 40px above the state box top. Label sits between subtitle bottom and arc top.
- Formula:
state_box.y >= subtitle.y + subtitle.height + selfloop_arc_height + label_height + 15px_margin = 58 + 17 + 40 + 15 + 15 = 145, round to 150
- When shifting state boxes, shift ALL dependent elements together: state text, arrows originating/terminating at that state, arrow labels, and the legend box
DATAFLOW Decomposition Diagram
Required elements:
- Input port ellipses (green
#b2f2bb) with stream name and sample count
- Output port ellipses (green=data, red=control) with names
- Stage boxes colored by role: EXTRACT=cyan
#99e9f2, COMPUTE=yellow #ffec99, APPLY=blue #a5d8ff
- FORK topology when EXTRACT outputs to multiple stages (not linear/sequential)
- FIFO depth labels on ALL inter-stage arrows (e.g., "depth=168")
- Control signal paths: dashed red for scalar signals (cfo_stream, etc.)
- Storage elements below relevant stage: NCO LUTs, buffers (purple
#d0bfff)
- FIFO depths notes section with type and sizing rationale
- Legend box
RTL Micro-Architecture Diagram Conventions
When creating micro-architecture diagrams for HLS hardware modules, use these conventions to accurately represent the actual RTL datapath and control structure.
Diagram Style
All RTL diagrams use roughness: 0 (clean/formal), fontFamily: 3 (Cascadia/monospace), and strokeWidth: 2 for shapes, strokeWidth: 1 for control signals and pipeline boundaries.
Component Shape Mapping
| RTL Component | Excalidraw Shape | Background | Stroke | Size (WxH) |
|---|
| Pipeline Register | rectangle | #a5d8ff (blue) | #1971c2 | 60x40 |
| Shift Register (chain) | rectangle (wide) | #a5d8ff (blue) | #1971c2 | 160x40 |
| Adder / Subtractor | rectangle (small) | #b2f2bb (green) | #2f9e44 | 40x40, text "+" or "-" |
| Multiplier / DSP48 | rectangle | #ffec99 (yellow) | #f08c00 | 80x40, text "x" or "DSP48" |
| MUX | diamond | #e9ecef (gray) | #868e96 | 50x50, text "MUX" |
| BRAM / Memory | rectangle | #d0bfff (purple) | #9c36b5 | 120x50 |
| ROM / LUT | rectangle | #d0bfff (purple) | #9c36b5 | 100x40 |
| Comparator | rectangle (small) | #ffc9c9 (red) | #e03131 | 40x40, text ">" or "==" |
| Accumulator | rectangle | #99e9f2 (cyan) | #0c8599 | 80x40, text "ACC" |
| Counter | rectangle | #e9ecef (gray) | #868e96 | 80x40 |
| Truncation | rectangle (narrow) | #e9ecef (gray) | #868e96 | 50x30, text "TRUNC" |
| Stream I/O port | ellipse | #b2f2bb (green) | #2f9e44 | 100x50 |
| Controller/FSM block | rectangle (large, dashed) | white | #1e1e1e, strokeWidth=2, strokeStyle="dashed" | varies |
| FIR IP block | rectangle | #ffec99 (yellow) | #f08c00 | 140x50, text "hls::FIR (N taps)" |
| CORDIC block | rectangle | #ffec99 (yellow) | #f08c00 | 100x40, text "CORDIC atan2" |
Signal Convention
| Signal Type | Arrow Style | Color | Width | Example |
|---|
| Data bus | solid, endArrowhead="arrow" | #1971c2 (blue) | strokeWidth=2 | data_t [15:0] |
| Control signal | dashed, endArrowhead="arrow" | #e03131 (red) | strokeWidth=1 | en, sel, clr |
| Pipeline boundary | dashed line (no arrowhead) | #868e96 (gray) | strokeWidth=1 | stage boundary |
Layout Rules
- Orientation: Data flows left-to-right (primary datapath), top-to-bottom for multi-stage
- Datapath region: Upper 2/3 of diagram area
- Controller region: Lower 1/3, separated by horizontal dashed line labeled "CONTROLLER"
- Pipeline stages: Vertical dashed lines between clock cycle boundaries
- Bus width labels: Standalone text next to data arrows, fontSize=12, fontFamily=3
- Component labels: Inside the component shape via containerId binding
- Spacing: Minimum 30px between components, 50px between pipeline stages
- DATAFLOW stages: When module uses HLS DATAFLOW, show stages as large dashed-border regions containing their internal components
Diagram Composition Template
+-------------------------------------------------------------------+
| Title: module_name - Micro-Architecture |
| Subtitle: key specs (taps, II, DSP, type annotations) |
+-------------------------------------------------------------------+
| |
| [I/O Port] --data--> [Component] --bus--> [Component] --> [Port] |
| | | |
| [Storage] [Storage] |
| |
| - - - - - - - - CONTROLLER - - - - - - - - - - - - - - - - - - - |
| |
| [FSM/Counter Block] --en/sel/clr--> datapath control points |
| |
+-------------------------------------------------------------------+
| Legend | Resource Summary |
+-------------------------------------------------------------------+
For DATAFLOW modules, replace the single datapath with stage regions:
+-------------------------------------------------------------------+
| +-EXTRACT---------+ +-COMPUTE--------+ +-APPLY-----------+ |
| | [circ_buf] | | [correlator] | | [NCO LUT] | |
| | [FSM ctrl] |->| [CORDIC] |->| [complex mul] | |
| +-----------------+ +----------------+ +------------------+ |
+-------------------------------------------------------------------+
Container Padding Rules (prevents text overflow)
Text inside containers MUST have breathing room. These padding values ensure text renders correctly in both the web app and excalirender CLI.
| Container Type | Min Horizontal Padding | Min Vertical Padding |
|---|
| Rectangle | 10px each side (20px total) | 8px each side (16px total) |
| Ellipse | 30% width reduction | 30% height reduction |
| Diamond | 40% width reduction | 40% height reduction |
Container sizing formula:
rect.width = computed_text_width + 20
rect.height = computed_text_height + 16
ellipse.width = computed_text_width * 1.43
ellipse.height = computed_text_height * 1.43
Always add 15% safety margin to character width calculations. For multi-line text, use the longest line for width calculation.
Validation
Run overflow validation after creating any Excalidraw diagram:
python3 .claude/tools/validate_excalidraw.py <file.excalidraw>
When to Use SVG vs Excalidraw
Choose the right format based on diagram requirements:
| Criterion | Excalidraw | SVG |
|---|
| Interactive editing | Yes (excalidraw.com) | No (text editor only) |
| Sketch/hand-drawn aesthetic | Yes (roughness > 0) | No (always clean) |
| Simple diagrams (< 10 blocks) | Preferred | Acceptable |
| Cross-stage bypass arrows | Unreliable (CLI rendering) | Reliable (bezier curves) |
| Pixel-perfect rendering | No (CLI vs web discrepancy) | Yes (rsvg-convert) |
| RTL micro-architecture | Possible but fragile | Preferred |
| Publication/article use | Possible | Preferred (crisp output) |
| Spatial layout semantics | Manual (coordinate math) | Manual (same effort) |
Rule of thumb: Use Excalidraw for interactive diagrams, brainstorming, and simple system-level views. Use SVG for RTL micro-architecture, publication-quality output, and any diagram where precise arrow routing matters.
SVG Diagram Creation
Generate .svg files directly using the Write tool. SVG gives pixel-perfect rendering with zero discrepancy between source and output.
SVG Root Template
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 520" font-family="monospace" font-size="12">
<defs>
<marker id="arr-data" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#1971c2"/>
</marker>
<marker id="arr-ctrl" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6 Z" fill="#e03131"/>
</marker>
<style>
.stage-border { fill: #f8fafc; stroke: #94a3b8; stroke-width: 1.5; stroke-dasharray: 6 4; }
.stage-label { font-size: 13px; font-weight: bold; fill: #475569; text-anchor: middle; }
.block-dsp { fill: #dbeafe; stroke: #1971c2; stroke-width: 1.5; rx: 4; }
.block-mem { fill: #e0e7ff; stroke: #4f46e5; stroke-width: 1.5; rx: 4; }
.block-ctrl { fill: #fef3c7; stroke: #d97706; stroke-width: 1.5; rx: 4; }
.block-fir { fill: #fef9c3; stroke: #ca8a04; stroke-width: 1.5; rx: 4; }
.block-io { fill: #d1fae5; stroke: #059669; stroke-width: 1.5; }
.block-io-ctrl { fill: #fce7f3; stroke: #db2777; stroke-width: 1.5; }
.data-bus { stroke: #1971c2; stroke-width: 2; fill: none; marker-end: url(#arr-data); }
.ctrl-signal { stroke: #e03131; stroke-width: 1.5; fill: none; stroke-dasharray: 6 3; marker-end: url(#arr-ctrl); }
.fifo-label { font-size: 10px; fill: #1971c2; font-style: italic; }
.ctrl-label { font-size: 10px; fill: #e03131; font-style: italic; }
.block-text { font-size: 11px; fill: #1e3a5f; text-anchor: middle; }
.block-text-mem { font-size: 11px; fill: #3730a3; text-anchor: middle; }
.block-text-ctrl { font-size: 11px; fill: #92400e; text-anchor: middle; }
.block-text-fir { font-size: 11px; fill: #854d0e; text-anchor: middle; }
.title-text { font-size: 16px; font-weight: bold; fill: #1e293b; }
.subtitle-text { font-size: 12px; fill: #64748b; font-style: italic; }
.chan-label { font-size: 10px; fill: #6b7280; font-weight: bold; }
</style>
</defs>
<rect width="960" height="520" fill="white"/>
<text x="480" y="28" text-anchor="middle" class="title-text">Module Name - RTL Micro-Architecture</text>
<text x="480" y="46" text-anchor="middle" class="subtitle-text">Key specs | II=1 | CSR=X.X</text>
</svg>
Block Type Mapping
| Block Type | CSS Class | SVG Element | Notes |
|---|
| DSP / Logic | block-dsp | <rect> | Blue fill, computations |
| Memory (BRAM/ROM) | block-mem | <rect> | Purple/indigo fill, storage |
| Controller / FSM | block-ctrl | <rect> | Amber fill, control logic |
| FIR IP | block-fir | <rect> | Yellow fill, hls::FIR instances |
| Data I/O port | block-io | <ellipse> | Green fill, stream ports |
| Control I/O port | block-io-ctrl | <ellipse> | Pink fill, async control ports |
| DATAFLOW stage | stage-border | <rect> | Dashed border, light gray fill |
Arrow Routing Patterns
Straight data bus (horizontal or vertical):
<path d="M 100,200 L 300,200" class="data-bus"/>
L-shaped routing (avoid crossing blocks):
<path d="M 100,200 L 100,350 L 300,350" class="data-bus"/>
Cross-stage bypass (cubic bezier, routes below all stages):
<path d="M 390,374 L 435,374 C 445,374 445,460 455,460 L 760,460 C 770,460 770,320 800,320" class="data-bus"/>
The cubic bezier (C) command takes three coordinate pairs: control point 1, control point 2, and endpoint. Use it to create smooth curves that route around obstacles without crossing through blocks.
Control signal (dashed red):
<path d="M 75,340 L 110,333" class="ctrl-signal"/>
Arrow anti-patterns: (1) Never let an arrow path segment run along a block border (same x as left/right edge or same y as top/bottom edge) - offset by 3-5px or restructure the route. (2) Never end a path with a bezier C curve - always use a straight L segment >= 15px as the final command for correct arrowhead orientation. (3) L-shaped detour arrows need >= 25px final approach segment (not just the 15px minimum) for visual clearance between the vertical run and arrowhead landing. (4) Text labels within 10px of arrow paths need <rect fill="white"/> background masking to prevent arrow bleed-through.
Arrow terminal rules: Every arrow start/end point must lie exactly ON a block boundary (rect edge or ellipse perimeter). No two terminals may share the same (x,y) coordinate. For fan-out from ellipse ports, compute separate (x,y) positions on the ellipse perimeter for each arrow.
Arrow path quality rules: (1) No arrow endpoint at exact rect corners - connect to edge midpoints. (2) No two arrows may share any path segment - offset entries into the same block face by 10-15px. (3) Final straight segment >= 15px for arrowhead visibility; never end a path with a bezier C curve. (4) Arrow <path> elements should appear before <text> elements in source order for correct z-layering. (5) Stage labels must use text-anchor: middle with x at the stage border center. (6) L-shaped detour arrows must have final approach >= 25px for visual clearance between the turn point and arrowhead. (7) Text labels within 10px of arrow paths need white background rects (<rect fill="white"/> before <text> in source order) using painter's algorithm masking. Formula: rect.x = text.x-3, rect.y = text.y-10, rect.width = chars*6.2+6, rect.height = 14.
See references/svg-reference.md sections "Arrow Terminal Placement Rules" and "Arrow Path Quality Rules" for the full 11-step audit procedure and fix patterns.
FIFO Indicator Pattern
For inter-stage FIFOs in DATAFLOW diagrams, use a rounded dashed rectangle:
<rect x="270" y="310" width="120" height="28" rx="10"
style="fill:#eff6ff;stroke:#1971c2;stroke-width:1;stroke-dasharray:3 2"/>
<text x="330" y="328" class="fifo-label" text-anchor="middle">lstf_stream d=160</text>
Title and Subtitle Positioning
- Title at
y=28, centered with text-anchor: middle
- Subtitle at
y=46, centered
- Both use the
title-text and subtitle-text CSS classes
- Canvas center x = viewBox width / 2
Canvas Sizing Guide
| Complexity | viewBox | Use When |
|---|
| Simple (< 10 blocks) | 0 0 960 520 | Single-pipeline modules, dense processing |
| Medium (10-20 blocks) | 0 0 1200 660 | DATAFLOW with 3 stages, moderate routing |
| Complex (20+ blocks) | 0 0 1400 800 | Multi-channel + DATAFLOW, many cross-stage arrows |
Match viewBox to actual content. Avoid oversized canvases with large empty margins.
Resource Summary and Legend
Always include at the bottom of the diagram:
<rect x="90" y="395" width="470" height="105" rx="4"
style="fill:#f9fafb;stroke:#d1d5db;stroke-width:1"/>
<text x="100" y="415" font-weight="bold" font-size="12" fill="#374151">Resources (target_fpga)</text>
<text x="100" y="433" font-size="11" fill="#4b5563">DSP48E1: N (X% of 220)</text>
<text x="100" y="449" font-size="11" fill="#4b5563">BRAM: N - description</text>
<rect x="600" y="395" width="260" height="105" rx="4"
style="fill:#f9fafb;stroke:#d1d5db;stroke-width:1"/>
<text x="610" y="415" font-weight="bold" font-size="12" fill="#374151">Legend</text>
<line x1="610" y1="434" x2="660" y2="434" stroke="#1971c2" stroke-width="2"
marker-end="url(#arr-data)"/>
<text x="670" y="438" font-size="11" fill="#4b5563">Data bus (stream)</text>
<rect x="610" y="448" width="50" height="16" class="block-dsp"/>
<text x="670" y="460" font-size="11" fill="#4b5563">DSP / Logic</text>
<rect x="610" y="468" width="50" height="16" class="block-mem"/>
<text x="670" y="480" font-size="11" fill="#4b5563">Memory (BRAM/ROM)</text>
SVG Rendering
SVG renders with zero discrepancy - what you write is exactly what you get.
Primary: rsvg-convert
rsvg-convert -z 2 diagram.svg -o diagram.png
Options:
-z 2: 2x scale for crisp output (recommended)
-w 1920: Set specific output width
-b white: Background color
Alternative: cairosvg (Python)
pip install cairosvg
cairosvg diagram.svg -o diagram.png -s 2
Why SVG Rendering is Reliable
Unlike Excalidraw's excalirender CLI which has text positioning discrepancies with the web app, SVG rendering through rsvg-convert or cairosvg produces output that exactly matches the SVG source. There is no "web app vs CLI" problem - the SVG specification is deterministic.
Output Files
For SVG diagrams, generate:
diagram.svg # Source SVG (for editing, version control)
diagram.png # Rendered image (for human review, embedding)
No .arch.json companion is needed for SVG diagrams since SVG source is already human-readable and compact (~150-250 lines vs ~8,500 tokens for Excalidraw JSON).
Reference Files
Load these for detailed guidance when needed:
references/element-reference.md
Read when: creating unusual element types (freedraw, frame, image), need exact property specs, working with complex bindings or nested groups.
references/examples.md
Read when: starting a new diagram type, need a complete working template to adapt, want to verify JSON structure is correct. Includes both Excalidraw JSON and SVG examples.
references/best-practices.md
Read when: designing a professional diagram, choosing colors/fonts/layout, creating specific diagram types (flowchart, architecture, sequence, mind map, ERD, RTL micro-architecture).
references/svg-reference.md
Read when: creating SVG diagrams, need the full CSS class reference, block snippets, arrow patterns, or the complete SVG skeleton template.