excalidraw-studio
Generate Excalidraw diagrams from natural language descriptions. Outputs .excalidraw JSON files openable in Excalidraw. Use when asked to "create a diagram", "make a flowchart", "visualize a process", "draw a system architecture", "create a mind map", "generate an Excalidraw file", "draw an ER diagram", "create a sequence diagram", or "make a class diagram". Supports flowcharts, relationship diagrams, mind maps, architecture, DFD, swimlane, class, sequence, and ER diagrams. Can use icon libraries (AWS, GCP, etc.) when set up. Do NOT use for code architecture analysis (use the architecture skills), Mermaid diagram rendering (use mermaid-studio), or non-visual documentation (use docs-writer).
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install tech-leads-club-agent-skills-excalidraw-studio
Repository
Skill path: packages/skills-catalog/skills/(tooling)/excalidraw-studio
Generate Excalidraw diagrams from natural language descriptions. Outputs .excalidraw JSON files openable in Excalidraw. Use when asked to "create a diagram", "make a flowchart", "visualize a process", "draw a system architecture", "create a mind map", "generate an Excalidraw file", "draw an ER diagram", "create a sequence diagram", or "make a class diagram". Supports flowcharts, relationship diagrams, mind maps, architecture, DFD, swimlane, class, sequence, and ER diagrams. Can use icon libraries (AWS, GCP, etc.) when set up. Do NOT use for code architecture analysis (use the architecture skills), Mermaid diagram rendering (use mermaid-studio), or non-visual documentation (use docs-writer).
Open repositoryBest for
Primary workflow: Write Technical Docs.
Technical facets: Full Stack, Tech Writer, Designer.
Target audience: everyone.
License: CC-BY-4.0.
Original source
Catalog source: SkillHub Club.
Repository owner: tech-leads-club.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install excalidraw-studio into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/tech-leads-club/agent-skills before adding excalidraw-studio to shared team environments
- Use excalidraw-studio for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: excalidraw-studio
description: Generate Excalidraw diagrams from natural language descriptions. Outputs .excalidraw JSON files openable in Excalidraw. Use when asked to "create a diagram", "make a flowchart", "visualize a process", "draw a system architecture", "create a mind map", "generate an Excalidraw file", "draw an ER diagram", "create a sequence diagram", or "make a class diagram". Supports flowcharts, relationship diagrams, mind maps, architecture, DFD, swimlane, class, sequence, and ER diagrams. Can use icon libraries (AWS, GCP, etc.) when set up. Do NOT use for code architecture analysis (use the architecture skills), Mermaid diagram rendering (use mermaid-studio), or non-visual documentation (use docs-writer).
license: CC-BY-4.0
metadata:
author: Felipe Rodrigues - github.com/felipfr
version: 1.0.1
---
# Excalidraw Studio
Generate Excalidraw-format diagrams from natural language descriptions. Outputs `.excalidraw` JSON files that can be opened directly in Excalidraw (web, VS Code extension, or Obsidian plugin).
## Workflow
```
UNDERSTAND → CHOOSE TYPE → EXTRACT → GENERATE → SAVE
```
### Step 1: Understand the Request
Analyze the user's description to determine:
1. **Diagram type** — Use the decision matrix below
2. **Key elements** — Entities, steps, concepts, actors
3. **Relationships** — Flow direction, connections, hierarchy
4. **Complexity** — Number of elements (target: under 20 for clarity)
### Step 2: Choose the Diagram Type and Visual Mode
**Diagram type:**
| User Intent | Diagram Type | Keywords |
| -------------------------- | -------------------- | --------------------------------------------- |
| Process flow, steps | **Flowchart** | "workflow", "process", "steps" |
| Connections, dependencies | **Relationship** | "relationship", "connections", "dependencies" |
| Concept hierarchy | **Mind Map** | "mind map", "concepts", "breakdown" |
| System design | **Architecture** | "architecture", "system", "components" |
| Data movement | **Data Flow (DFD)** | "data flow", "data processing" |
| Cross-functional processes | **Swimlane** | "business process", "swimlane", "actors" |
| Object-oriented design | **Class Diagram** | "class", "inheritance", "OOP" |
| Interaction sequences | **Sequence Diagram** | "sequence", "interaction", "messages" |
| Database design | **ER Diagram** | "database", "entity", "data model" |
**Visual mode** — decide upfront and apply consistently to all elements:
| Mode | `roughness` | `fontFamily` | When to use |
| ---------- | ----------- | ------------ | ---------------------------------------------------- |
| **Sketch** | `1` | `5` | Default — informal, approachable, Excalidraw-native |
| **Clean** | `0` | `2` | Executive presentations, formal specs |
| **Mixed** | zones: `0`, shapes: `1` | `5` | Architecture diagrams (structural zones + sketchy shapes) |
### Step 3: Extract Structured Information
Extract the key components based on diagram type. For each type, identify:
- **Nodes/entities** — What are the boxes/shapes?
- **Connections** — What connects to what, and with what label?
- **Hierarchy** — What contains what, what comes before what?
- **Decision points** — Where does the flow branch?
For detailed extraction guidelines per diagram type, read `references/element-types.md`.
### Step 4: Generate the Excalidraw JSON
**CRITICAL: Read `references/excalidraw-schema.md` before generating your first diagram.** It contains the correct element format, text container model, and binding system.
Key rules for generation:
1. **Text inside shapes** — Use `boundElements` on the shape and a separate text element with `containerId`. Never use a `label` shorthand:
```json
[
{
"id": "step-1",
"type": "rectangle",
"x": 100, "y": 100, "width": 200, "height": 80,
"boundElements": [{ "type": "text", "id": "text-step-1" }]
},
{
"id": "text-step-1",
"type": "text",
"x": 130, "y": 128, "width": 140, "height": 24,
"text": "My Step", "originalText": "My Step",
"fontSize": 20, "fontFamily": 5,
"textAlign": "center", "verticalAlign": "middle",
"containerId": "step-1", "lineHeight": 1.25, "roundness": null
}
]
```
2. **Arrow labels** — Also use `boundElements` + separate text element with `containerId`. Never use a `label` shorthand on arrows:
```json
[
{
"id": "arrow-1",
"type": "arrow",
"x": 100, "y": 150,
"points": [[0, 0], [200, 0]],
"boundElements": [{ "type": "text", "id": "text-arrow-1" }]
},
{
"id": "text-arrow-1",
"type": "text",
"x": 160, "y": 132, "width": 80, "height": 18,
"text": "sends data", "originalText": "sends data",
"fontSize": 14, "fontFamily": 5,
"textAlign": "center", "verticalAlign": "middle",
"containerId": "arrow-1", "lineHeight": 1.25, "roundness": null
}
]
```
3. **Arrow bindings** — Use `startBinding`/`endBinding` (not `start`/`end`). Connected shapes must list the arrow in their `boundElements`:
```json
{
"id": "shape-1",
"boundElements": [
{ "type": "text", "id": "text-shape-1" },
{ "type": "arrow", "id": "arrow-1" }
]
}
```
```json
{
"id": "arrow-1",
"type": "arrow",
"startBinding": { "elementId": "shape-1", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "shape-2", "focus": 0, "gap": 1 }
}
```
4. **Element order for z-index** — Always declare shapes first, arrows second, text elements last. This guarantees text renders on top and is never obscured by arrows or other shapes.
5. **Positioning** — Use grid-aligned coordinates (multiples of 20px when `gridSize: 20`). Leave 200-300px horizontal gap, 100-150px vertical gap between elements.
6. **Unique IDs** — Every element must have a unique `id`. Use descriptive IDs like `"step-1"`, `"decision-valid"`, `"arrow-1-to-2"`, `"text-step-1"`.
7. **Colors** — Use a consistent palette:
| Role | Color | Hex |
|------|-------|-----|
| Primary entities | Light blue | `#a5d8ff` |
| Process steps | Light green | `#b2f2bb` |
| Important/Central | Yellow | `#ffd43b` |
| Warnings/Errors | Light red | `#ffc9c9` |
| Secondary | Cyan | `#96f2d7` |
| Default stroke | Dark | `#1e1e1e` |
### Step 5: Save and Present
1. Save as `<descriptive-name>.excalidraw`
2. Provide a summary:
```
Created: user-workflow.excalidraw
Type: Flowchart
Elements: 7 shapes, 6 arrows, 1 title
Total: 14 elements
To view:
1. Visit https://excalidraw.com → Open → drag and drop the file
2. Or use the Excalidraw VS Code extension
3. Or open in Obsidian with the Excalidraw plugin
```
## Templates
Pre-built templates are available in `assets/` for quick starting points. Use these when the diagram type matches — they provide correct structure and styling:
| Template | File |
| ---------------- | ------------------------------------------------------ |
| Flowchart | `assets/flowchart-template.json` |
| Relationship | `assets/relationship-template.json` |
| Mind Map | `assets/mindmap-template.json` |
| Data Flow (DFD) | `assets/data-flow-diagram-template.json` |
| Swimlane | `assets/business-flow-swimlane-template.json` |
| Class Diagram | `assets/class-diagram-template.json` |
| Sequence Diagram | `assets/sequence-diagram-template.json` |
| ER Diagram | `assets/er-diagram-template.json` |
Read a template when creating that diagram type for the first time. Use its structure as a base, then modify elements to match the user's request.
## Icon Libraries
For professional architecture diagrams with service icons (AWS, GCP, Azure, etc.), icon libraries can be set up. Read `references/icon-libraries.md` when:
- User requests an AWS/cloud architecture diagram
- User mentions wanting specific service icons
- You need to check if icon libraries are available
## Best Practices
### Element Count
| Diagram Type | Recommended | Maximum |
| --------------------- | ----------- | ------- |
| Flowchart steps | 3-10 | 15 |
| Relationship entities | 3-8 | 12 |
| Mind map branches | 4-6 | 8 |
| Sub-topics per branch | 2-4 | 6 |
If the user's request exceeds maximum, suggest breaking into multiple diagrams:
> "Your request includes 15 components. For clarity, I recommend: (1) High-level architecture diagram with 6 main components, (2) Detailed sub-diagrams for each subsystem. Want me to start with the high-level view?"
### Layout
- **Flow direction**: Left-to-right for processes, top-to-bottom for hierarchies
- **Spacing**: 200-300px horizontal, 100-150px vertical between elements
- **Grid alignment**: Position on multiples of 20px for clean alignment
- **Margins**: Minimum 50px from canvas edge
- **Text sizing**: 28-36px titles, 18-22px labels, 14-16px annotations
- **Font**: Use `fontFamily: 5` (Excalifont) for hand-drawn consistency. Fallback to `1` (Virgil) if 5 is not supported.
- **Background zones**: For architecture diagrams, add semi-transparent dashed zone rectangles (`opacity: 35`, `strokeStyle: "dashed"`, `roughness: 0`) as the first elements in the array to create visual grouping regions. See `references/excalidraw-schema.md` → Background Zones.
- **Element order**: zones first → shapes → arrows → text elements (ensures correct z-index and text always renders on top)
### Common Mistakes to Avoid
- ❌ Using `label: { text: "..." }` shorthand on shapes or arrows — not supported by the Excalidraw parser
- ❌ Putting `text` directly on shape elements without `containerId`
- ❌ Using `start`/`end` for arrow bindings — use `startBinding`/`endBinding` with `elementId`/`focus`/`gap`
- ❌ Forgetting to add arrows to their connected shapes' `boundElements` arrays
- ❌ Omitting `originalText`, `lineHeight`, `autoResize`, or `backgroundColor: "transparent"` from text elements inside containers
- ❌ Omitting required base properties (`angle`, `strokeStyle`, `opacity`, `groupIds`, `frameId`, `index`, `isDeleted`, `seed`, `version`, `versionNonce`, `updated`, `link`, `locked`) — elements will not render
- ❌ Missing `"files": {}` at the top level of the JSON
- ❌ Using `roundness: { "type": 3 }` on ellipses — ellipses must use `roundness: null`
- ❌ Missing `lastCommittedPoint`, `startArrowhead`, `endArrowhead` on arrows
- ❌ Declaring text elements before arrows — text renders underneath and gets obscured
- ❌ Floating arrows without bindings (won't move with shapes)
- ❌ Overlapping elements (increase spacing)
- ❌ Inconsistent color usage (define palette upfront)
- ❌ Too many elements on one diagram (break into sub-diagrams)
## Validation Checklist
Before delivering the diagram, verify:
- [ ] All elements have unique IDs
- [ ] Every element has ALL required base properties: `angle`, `strokeStyle`, `opacity`, `groupIds`, `frameId`, `index`, `isDeleted`, `link`, `locked`, `seed`, `version`, `versionNonce`, `updated`
- [ ] `index` values are assigned in order (`"a0"`, `"a1"`, …) with text elements getting higher values than shapes/arrows
- [ ] Top-level JSON includes `"files": {}`
- [ ] Shapes with text use `boundElements` + separate text element with `containerId`
- [ ] Text elements inside containers have `containerId`, `originalText`, `lineHeight: 1.25`, `autoResize: true`, `roundness: null`, `backgroundColor: "transparent"`
- [ ] Arrows use `startBinding`/`endBinding` (with `elementId`, `focus`, `gap`) when connecting shapes, plus `lastCommittedPoint: null`, `startArrowhead: null`, `endArrowhead: "arrow"`
- [ ] Connected shapes list the arrow in their `boundElements` arrays
- [ ] Element order: shapes → arrows → text elements (text always on top)
- [ ] Ellipses use `roundness: null` (not `{ "type": 3 }`)
- [ ] Coordinates prevent overlapping (check spacing)
- [ ] Text is readable (font size 16+)
- [ ] Colors follow consistent scheme
- [ ] File is valid JSON
- [ ] Element count is reasonable (<20 for clarity)
## Troubleshooting
| Issue | Solution |
| ----------------------------- | --------------------------------------------------------------------------------------------- |
| Text not showing in shapes | Use `boundElements` + separate text element with `containerId`, `originalText`, `lineHeight` |
| Text hidden behind arrows | Move text elements to end of `elements` array (after all arrows) |
| Arrows don't move with shapes | Use `startBinding`/`endBinding` with `elementId`, `focus: 0`, `gap: 1` |
| Shape not moving with arrows | Add the arrow to the shape's `boundElements` array |
| Elements overlap | Increase spacing between coordinates |
| Text doesn't fit | Increase shape width or reduce font size |
| Too many elements | Break into multiple diagrams |
| Colors look inconsistent | Define color palette upfront, apply consistently |
## Limitations
- Complex curves are simplified to straight/basic curved lines
- Hand-drawn roughness is set to default (1)
- No embedded images in auto-generation (use icon libraries for service icons)
- Maximum recommended: 20 elements per diagram for clarity
- No automatic collision detection — use spacing guidelines
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/element-types.md
```markdown
# Excalidraw Element Types Guide
Read this file when you need detailed guidance on which elements to use for specific diagram types, and how to construct them correctly.
For the JSON properties and format, see `excalidraw-schema.md`.
## Element Type Overview
| Type | Shape | Primary Use | Text binding | Arrow binding |
| ----------- | ----- | -------------------------------------- | ------------------- | --------------- |
| `rectangle` | □ | Boxes, containers, process steps | via `boundElements` | Arrows can bind |
| `ellipse` | ○ | Start/end, states, emphasis | via `boundElements` | Arrows can bind |
| `diamond` | ◇ | Decision points, conditions | via `boundElements` | Arrows can bind |
| `arrow` | → | Directional flow, relationships | via `boundElements` | Binds to shapes |
| `line` | — | Non-directional connections, dividers | ❌ | Binds to shapes |
| `text` | A | Standalone labels, titles, annotations | — | Not bindable |
## Shapes — Rectangle, Ellipse, Diamond
### When to use each
| Shape | Best for | Visual meaning |
| ------------- | ------------------------------------------------ | ----------------------------------------- |
| **Rectangle** | Process steps, entities, components, data stores | "This is a thing" or "This is an action" |
| **Ellipse** | Start/end terminals, states, emphasis | "This is a boundary" or "This is a state" |
| **Diamond** | Decision points, conditional branches | "This is a question" |
### Text in shapes
**NEVER use `label: { text: "..." }` shorthand** — it is not supported in the `.excalidraw` file format. Always create a separate `text` element and link it via `containerId` and `boundElements`.
```json
[
{
"id": "step-1",
"type": "rectangle",
"x": 100,
"y": 100,
"width": 200,
"height": 80,
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeColor": "#1971c2",
"strokeWidth": 2,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-step-1" }
]
},
{
"id": "text-step-1",
"type": "text",
"x": 130,
"y": 128,
"width": 140,
"height": 24,
"text": "Process Input",
"originalText": "Process Input",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "step-1",
"lineHeight": 1.25,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"autoResize": true,
"roundness": null
}
]
```
**Multi-line text:** Use `\n` in the `text` and `originalText` fields:
```json
{
"text": "User\nAuthentication\nService",
"originalText": "User\nAuthentication\nService"
}
```
**Text element positioning inside a container at (x, y, w, h):**
- `text.x = container.x + 20`
- `text.y = container.y + (container.height / 2) - (fontSize / 2)`
- `text.width = container.width - 40`
- `text.height = fontSize * 1.25`
### Size guidelines
| Content length | Rectangle | Ellipse | Diamond |
| -------------- | --------- | ------- | ------- |
| 1 word | 140×70 | 120×120 | 140×140 |
| 2-4 words | 200×80 | 160×120 | 180×180 |
| Short sentence | 280×100 | 200×140 | 220×220 |
### Styling for elegance
**Use stroke + fill combinations** — matching stroke to the fill's deeper shade:
| Role | Fill | Stroke | Effect |
| ------- | --------- | --------- | ----------------------------- |
| Primary | `#a5d8ff` | `#1971c2` | Blue card with defined border |
| Success | `#b2f2bb` | `#2f9e44` | Green step with emphasis |
| Warning | `#ffec99` | `#e67700` | Amber decision with warmth |
| Danger | `#ffc9c9` | `#e03131` | Red error with urgency |
| Neutral | `#e9ecef` | `#868e96` | Subtle, de-emphasized |
| Accent | `#d0bfff` | `#7048e8` | Purple highlight |
**fillStyle variations** for visual variety within the same diagram:
- `"solid"` — Clean, modern look (default for most shapes)
- `"hachure"` — Sketchy fill, good for secondary/background elements
- `"cross-hatch"` — Dense fill, good for emphasis or "completed" states
## Arrows
### Basic directional arrow
```json
{
"id": "flow-1",
"type": "arrow",
"x": 300,
"y": 140,
"width": 200,
"height": 0,
"points": [
[0, 0],
[200, 0]
],
"strokeWidth": 2,
"roundness": { "type": 2 },
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow"
}
```
### Arrow with label
Arrow labels also require `boundElements` on the arrow + a separate text element with `containerId`. **Never use `label: { text: "..." }` on arrows.**
```json
[
{
"id": "flow-1",
"type": "arrow",
"x": 300,
"y": 140,
"width": 200,
"height": 0,
"points": [[0, 0], [200, 0]],
"strokeWidth": 2,
"roundness": { "type": 2 },
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"boundElements": [
{ "type": "text", "id": "text-flow-1" }
]
},
{
"id": "text-flow-1",
"type": "text",
"x": 360,
"y": 122,
"width": 80,
"height": 18,
"text": "HTTP/JSON",
"originalText": "HTTP/JSON",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "flow-1",
"lineHeight": 1.25,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"autoResize": true,
"roundness": null
}
]
```
### Bound arrow (connects to shapes)
Use `startBinding`/`endBinding` — **never `start`/`end`**. Connected shapes must list the arrow in their `boundElements`.
```json
[
{
"id": "source-box",
"type": "rectangle",
"boundElements": [
{ "type": "text", "id": "text-source" },
{ "type": "arrow", "id": "flow-1" }
]
},
{
"id": "target-box",
"type": "rectangle",
"boundElements": [
{ "type": "text", "id": "text-target" },
{ "type": "arrow", "id": "flow-1" }
]
},
{
"id": "flow-1",
"type": "arrow",
"points": [[0, 0], [200, 0]],
"startBinding": { "elementId": "source-box", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "target-box", "focus": 0, "gap": 1 },
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow"
}
]
```
### Arrow styles for semantic meaning
| Style | strokeStyle | strokeWidth | Meaning |
| ---------------------- | ----------- | ----------- | ------------------------------------ |
| **Primary flow** | `"solid"` | 2 | Main path, normal flow |
| **Important flow** | `"solid"` | 3 | Critical path, emphasis |
| **Optional/alternate** | `"dashed"` | 2 | Optional path, fallback |
| **Indirect/async** | `"dotted"` | 2 | Event-driven, async, weak dependency |
### Arrow directions
| Direction | Points | Use case |
| ------------------ | -------------------------------- | ----------------------- |
| → Right | `[[0, 0], [200, 0]]` | Process flow |
| ↓ Down | `[[0, 0], [0, 150]]` | Hierarchy, sequence |
| ↘ Diagonal | `[[0, 0], [200, 150]]` | Cross-connections |
| → then ↓ (L-shape) | `[[0, 0], [200, 0], [200, 150]]` | Routing around elements |
## Lines
Non-directional connections with no arrowhead:
```json
{
"type": "line",
"x": 100,
"y": 300,
"points": [
[0, 0],
[400, 0]
],
"strokeStyle": "dashed",
"strokeWidth": 1,
"strokeColor": "#868e96"
}
```
**Use cases:** Section dividers, boundaries, non-directional relationships (UML association).
## Standalone Text
For titles, headers, annotations not inside a shape. Set `containerId: null`:
```json
{
"id": "title-1",
"type": "text",
"x": 100,
"y": 40,
"width": 300,
"height": 35,
"text": "System Architecture Overview",
"originalText": "System Architecture Overview",
"fontSize": 28,
"fontFamily": 5,
"textAlign": "center",
"strokeColor": "#1e1e1e",
"containerId": null,
"lineHeight": 1.25,
"roundness": null
}
```
**Width/height calculation:**
- Width ≈ `text.length × fontSize × 0.6`
- Height ≈ `fontSize × 1.2 × numberOfLines`
## Background Zones (Visual Grouping)
Background zones are large semi-transparent rectangles placed **behind** other elements to visually group them into regions. They are a key technique for professional architecture diagrams.
**Key properties for a zone:**
- `opacity: 35` — semi-transparent so elements behind/in front remain visible
- `strokeStyle: "dashed"` — clearly marks it as a boundary, not a shape
- `roughness: 0` — clean edges for background zones
- `fillStyle: "solid"` — needed for the opacity to show color
Zones must be declared **first** in the `elements` array so they render behind everything else.
```json
[
{
"id": "zone-backend",
"type": "rectangle",
"x": 300,
"y": 140,
"width": 480,
"height": 320,
"strokeColor": "#1971c2",
"backgroundColor": "#dbe4ff",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 0,
"opacity": 35,
"roundness": { "type": 3 },
"boundElements": []
}
]
```
Add a standalone text label near the top-left corner of the zone:
```json
{
"id": "label-zone-backend",
"type": "text",
"x": 320,
"y": 148,
"width": 120,
"height": 20,
"text": "Backend Services",
"originalText": "Backend Services",
"fontSize": 14,
"fontFamily": 5,
"strokeColor": "#1971c2",
"containerId": null,
"lineHeight": 1.25,
"roundness": null
}
```
**Element order with zones:** zones → shapes → arrows → text elements
**Zone color recommendations:**
| Zone purpose | backgroundColor | strokeColor |
| --------------- | --------------- | ----------- |
| Services/Logic | `#dbe4ff` | `#4c6ef5` |
| Data layer | `#d3f9d8` | `#2f9e44` |
| External/Users | `#fff9db` | `#f08c00` |
| Messaging/Events| `#f3d9fa` | `#ae3ec9` |
| Infrastructure | `#e3fafc` | `#0c8599` |
## Visual Modes
Choose the diagram's visual mode upfront and apply it consistently to all elements.
### Sketch Mode (default — recommended for most diagrams)
Hand-drawn aesthetic, matches Excalidraw's signature look:
- `roughness: 1` on all shapes and arrows
- `fontFamily: 5` (Excalifont) for all text
- `strokeWidth: 2` for shapes, `strokeWidth: 2` for arrows
Best for: informal diagrams, brainstorming, process docs, most use cases.
### Clean Mode (for formal/technical diagrams)
Precise, polished, presentation-ready:
- `roughness: 0` on all shapes and arrows
- `fontFamily: 2` (Helvetica) for body text, `fontFamily: 5` for titles
- `strokeWidth: 1.5-2` for shapes
Best for: executive presentations, client-facing docs, technical specifications.
**Mixed mode:** Use `roughness: 0` for background zones and `roughness: 1` for shapes — zones feel like structure, shapes feel like content.
## Diagram Type Recipes
### Flowchart
```
[Start ellipse] → [Step rect] → [Decision diamond] → Yes → [Step rect] → [End ellipse]
↓ No
[Step rect]
```
- Start/End: `ellipse` with light green/red (`#b2f2bb`/`#ffc9c9`), `roundness: null`
- Steps: `rectangle` with light blue (`#a5d8ff`)
- Decisions: `diamond` with amber (`#ffec99`)
- Flow: solid arrows, labeled at decision branches ("Yes"/"No")
### Architecture Diagram
Use **background zones** to show layers (see Background Zones section):
```
[zone: Infrastructure]
[zone: Backend Services]
[API Gateway rect] → [Order Service rect] → [Orders DB ellipse]
↓
[Event Bus rect] → [Worker rect]
[zone: Data/Consumers]
```
- Components: `rectangle` with varied colors by layer
- Connections: solid arrows with protocol labels ("REST", "gRPC", "SQL")
- Zone boundaries: semi-transparent dashed rectangles with `opacity: 35`
### ER Diagram
- Entities: `rectangle` with entity name (bold, larger font)
- Attributes: listed in multi-line text inside the entity box
- Relationships: `diamond` with relationship name
- Cardinality: standalone text labels near arrows ("1", "N", "0..1")
### Sequence Diagram
- Actors: `rectangle` at top with actor name
- Lifelines: `line` (vertical, dashed, `strokeStyle: "dashed"`)
- Messages: `arrow` (horizontal, solid = sync, dashed = async)
- Return: `arrow` (dashed, reverse direction)
- Activation: thin `rectangle` on lifeline, no fill
### Mind Map
- Center: large `ellipse` with main topic (bright color, `roughness: 1`)
- Branches: `rectangle` connected via diagonal arrows from center
- Sub-topics: smaller `rectangle` connected from branches
- Use different colors per branch for visual grouping (one color family per branch)
### Class Diagram
- Classes: `rectangle` with multi-line text:
```
ClassName
─────────
-field: Type
+field: Type
─────────
+method(): Return
-method(arg): Return
```
- Inheritance: solid arrow with label "extends"
- Implementation: dashed arrow with label "implements"
- Association: solid line (no arrowhead)
### Swimlane
- Lanes: tall `rectangle` with `fillStyle: "hachure"`, `opacity: 40`, `roughness: 0`
- Lane headers: standalone `text` at top of each lane
- Activities: `rectangle` inside lanes
- Handoffs: arrows crossing lane boundaries
### Data Flow Diagram (DFD)
- External entities: `rectangle` (bold stroke `strokeWidth: 3`)
- Processes: `ellipse` with process number and name
- Data stores: `rectangle` with open side (use two horizontal lines via `line` elements)
- Data flows: labeled arrows (always show data name on arrow)
- Direction: left-to-right or top-left to bottom-right
## Design Principles for Elegant Diagrams
1. **Visual hierarchy** — Use size and color intensity to signal importance. Primary elements get saturated fills; secondary elements use neutral or hachure fills.
2. **Consistent stroke weight** — Use `strokeWidth: 2` for all shapes and arrows. Only increase to 3-4 to emphasize critical paths.
3. **Color harmony** — Use at most 3-4 fill colors per diagram. Pick from the same palette row (see excalidraw-schema.md Design Tokens). Avoid mixing warm and cool haphazardly.
4. **Whitespace is structure** — More spacing between unrelated groups, less between related elements. This creates visual grouping without borders.
5. **Aligned, not scattered** — Align elements on a grid. Centers should be aligned vertically or horizontally whenever possible.
6. **Label everything that isn't obvious** — Every arrow should either have a label or its meaning should be clear from context. Every shape needs text.
7. **Flow direction convention** — Left-to-right for process flows. Top-to-bottom for hierarchies and sequences. Pick one and be consistent.
8. **Matching stroke to fill** — Use the deeper shade from the palette as stroke color for the corresponding fill. This creates depth and definition.
9. **Background zones over borders** — For grouping related elements, prefer semi-transparent background zones over explicit border rectangles. Zones feel spatial; borders feel like containers.
10. **Choose visual mode upfront** — Decide Sketch or Clean mode before generating elements. Never mix `roughness: 0` and `roughness: 1` on same-level shapes (zones and shapes can differ intentionally).
## Summary
| When you need... | Use this element |
| ------------------------------ | ----------------------------------------------------------------------------- |
| Process box, entity, component | `rectangle` with `boundElements` + separate text element |
| Decision point | `diamond` with `boundElements` + separate text element |
| Start/End terminal | `ellipse` with `boundElements` + separate text element (`roundness: null`) |
| Flow direction | `arrow` with `startBinding`/`endBinding`, label via `boundElements` if needed |
| Title/Header | `text` (large font, `containerId: null`) |
| Annotation | `text` (small font, `containerId: null`, positioned near target) |
| Non-directional connection | `line` |
| Section divider | `line` (horizontal, dashed) |
| Visual grouping region | `rectangle` (large, `opacity: 35`, `strokeStyle: "dashed"`, `roughness: 0`) |
```
### references/excalidraw-schema.md
```markdown
# Excalidraw JSON Schema Reference
Read this file before generating your first diagram. It contains the correct element format, text container model, and binding system.
## Top-Level Structure
```json
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": 20
},
"files": {}
}
```
## Element Properties
All elements share these base properties:
| Property | Type | Default | Description |
| ----------------- | ----------- | --------------- | --------------------------------------------------------------------------------------- |
| `id` | string | required | Unique identifier (e.g., `"step-1"`, `"arrow-a-b"`) |
| `type` | string | required | `"rectangle"`, `"ellipse"`, `"diamond"`, `"arrow"`, `"line"`, `"text"` |
| `x`, `y` | number | required | Position in pixels from top-left. Use multiples of 20 for grid alignment. |
| `width`, `height` | number | required | Dimensions in pixels |
| `strokeColor` | string | `"#1e1e1e"` | Hex color for outline |
| `backgroundColor` | string | `"transparent"` | Hex color for fill |
| `fillStyle` | string | `"solid"` | `"solid"`, `"hachure"`, `"cross-hatch"` |
| `strokeWidth` | number | `2` | Outline thickness (1-4) |
| `strokeStyle` | string | `"solid"` | `"solid"`, `"dashed"`, `"dotted"` |
| `roughness` | number | `1` | Hand-drawn effect (0 = clean, 1 = sketch, 2 = rough) |
| `opacity` | number | `100` | Transparency (0-100) |
| `roundness` | object/null | varies | `{ "type": 3 }` for rounded corners, `{ "type": 2 }` for curved arrows, `null` for text |
| `groupIds` | string[] | `[]` | Group membership for compound elements |
| `locked` | boolean | `false` | Lock element from editing |
**Required properties on ALL elements (Excalidraw will reject or misrender elements missing these):**
```json
{
"angle": 0,
"strokeStyle": "solid",
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1234567890,
"version": 1,
"versionNonce": 987654321,
"updated": 1706659200000
}
```
- `index` is a fractional z-index string. Use `"a0"`, `"a1"`, `"a2"`, etc. in element order. Text elements must have higher index values than shapes and arrows so they render on top.
- Generate unique `seed` and `versionNonce` per element (any distinct integers work).
- Omitting any of these properties will cause elements to not render correctly.
**Additional required properties for arrows:**
```json
{
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"backgroundColor": "transparent",
"fillStyle": "solid"
}
```
**Additional required properties for text elements inside containers:**
```json
{
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"autoResize": true
}
```
## Text Inside Shapes (CRITICAL)
**DO NOT use a `label` shorthand or put `text` directly on shape elements.** The `label` shorthand is not parsed by Excalidraw's file format.
**Correct approach:** Add `boundElements` to the shape, then create a **separate text element** that references the shape via `containerId`. Always declare shapes first, arrows second, and text elements last — this ensures text renders on top and is never obscured by arrows.
```json
[
{
"id": "step-1",
"type": "rectangle",
"x": 100,
"y": 100,
"width": 200,
"height": 80,
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeColor": "#1971c2",
"strokeWidth": 2,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-step-1" }
]
},
{
"id": "text-step-1",
"type": "text",
"x": 130,
"y": 128,
"width": 140,
"height": 24,
"text": "Process Input",
"originalText": "Process Input",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "step-1",
"lineHeight": 1.25,
"strokeColor": "#1e1e1e",
"roundness": null
}
]
```
This works for `rectangle`, `ellipse`, and `diamond` elements.
**Required properties for text elements inside containers:**
| Property | Value | Description |
| --------------- | ---------- | -------------------------------------------------------------------- |
| `containerId` | required | ID of the parent shape |
| `originalText` | required | Exact copy of `text` — used by the Excalidraw editor |
| `lineHeight` | `1.25` | Always set this for contained text |
| `text` | required | The text content. Use `\n` for line breaks. |
| `fontSize` | `20` | 14-36 depending on purpose |
| `fontFamily` | `5` | 5 = Excalifont (hand-drawn), 1 = Virgil, 2 = Helvetica, 3 = Cascadia |
| `textAlign` | `"center"` | `"left"`, `"center"`, `"right"` |
| `verticalAlign` | `"middle"` | `"top"`, `"middle"`, `"bottom"` |
| `strokeColor` | `"#1e1e1e"` | Text color |
| `roundness` | `null` | Always null for text elements |
**Text element positioning inside a container at (x, y, w, h):**
- `text.x = container.x + 20`
- `text.y = container.y + (container.height / 2) - (fontSize / 2)`
- `text.width = container.width - 40`
- `text.height = fontSize * lineHeight`
## Arrows and Bindings (CRITICAL)
### Basic Arrow
```json
{
"id": "arrow-1",
"type": "arrow",
"x": 300,
"y": 140,
"width": 100,
"height": 0,
"points": [
[0, 0],
[100, 0]
],
"roundness": { "type": 2 },
"strokeWidth": 2
}
```
### Arrow with Label
Arrow labels also require `boundElements` on the arrow and a separate text element with `containerId`. **Do NOT use a `label` shorthand** — it is not supported.
```json
[
{
"id": "arrow-1",
"type": "arrow",
"x": 300,
"y": 140,
"width": 200,
"height": 0,
"points": [
[0, 0],
[200, 0]
],
"roundness": { "type": 2 },
"strokeWidth": 2,
"boundElements": [
{ "type": "text", "id": "text-arrow-1" }
]
},
{
"id": "text-arrow-1",
"type": "text",
"x": 360,
"y": 122,
"width": 80,
"height": 18,
"text": "sends data",
"originalText": "sends data",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "arrow-1",
"lineHeight": 1.25,
"strokeColor": "#1e1e1e",
"roundness": null
}
]
```
### Bound Arrows (connect to shapes)
For arrows that move when shapes are repositioned, use `startBinding` and `endBinding`. **Do NOT use `start`/`end`** — they are not valid Excalidraw properties.
Every connected shape must also list the arrow in its `boundElements` array.
**Element ordering matters for z-index:** declare shapes first, arrows second, text elements last — so text always renders on top.
```json
[
{
"id": "box-a",
"type": "rectangle",
"x": 100,
"y": 100,
"width": 160,
"height": 80,
"boundElements": [
{ "type": "text", "id": "text-box-a" },
{ "type": "arrow", "id": "arrow-a-b" }
]
},
{
"id": "box-b",
"type": "rectangle",
"x": 460,
"y": 100,
"width": 160,
"height": 80,
"boundElements": [
{ "type": "text", "id": "text-box-b" },
{ "type": "arrow", "id": "arrow-a-b" }
]
},
{
"id": "arrow-a-b",
"type": "arrow",
"x": 260,
"y": 140,
"width": 200,
"height": 0,
"points": [
[0, 0],
[200, 0]
],
"startBinding": { "elementId": "box-a", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "box-b", "focus": 0, "gap": 1 },
"boundElements": [
{ "type": "text", "id": "text-arrow-a-b" }
]
},
{
"id": "text-box-a",
"type": "text",
"x": 120,
"y": 128,
"width": 120,
"height": 24,
"text": "Service A",
"originalText": "Service A",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "box-a",
"lineHeight": 1.25,
"strokeColor": "#1e1e1e",
"roundness": null
},
{
"id": "text-box-b",
"type": "text",
"x": 480,
"y": 128,
"width": 120,
"height": 24,
"text": "Service B",
"originalText": "Service B",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "box-b",
"lineHeight": 1.25,
"strokeColor": "#1e1e1e",
"roundness": null
},
{
"id": "text-arrow-a-b",
"type": "text",
"x": 320,
"y": 122,
"width": 80,
"height": 18,
"text": "REST API",
"originalText": "REST API",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "arrow-a-b",
"lineHeight": 1.25,
"strokeColor": "#1e1e1e",
"roundness": null
}
]
```
**Binding properties:**
| Property | Value | Description |
| ----------- | ------- | ------------------------------------------------------- |
| `elementId` | string | ID of the connected shape |
| `focus` | `0` | Connection point: 0 = center, -1/+1 = top-bottom edges |
| `gap` | `1` | Gap in pixels between arrow tip and shape boundary |
### Arrow Directions
| Direction | Points |
| -------------- | -------------------------------- |
| Horizontal (→) | `[[0, 0], [200, 0]]` |
| Vertical (↓) | `[[0, 0], [0, 150]]` |
| Diagonal (↘) | `[[0, 0], [200, 150]]` |
| L-shaped (→↓) | `[[0, 0], [200, 0], [200, 150]]` |
## Design Tokens — Elegant Palette
Use these curated colors for professional, modern diagrams. Avoid raw primary colors.
### Light Theme (default)
| Role | Fill | Stroke | Hex Fill | Hex Stroke |
| --------------------- | ----------- | ------------ | --------- | ---------- |
| **Primary** | Soft blue | Deeper blue | `#a5d8ff` | `#1971c2` |
| **Success/Process** | Mint green | Forest green | `#b2f2bb` | `#2f9e44` |
| **Warning/Decision** | Warm amber | Deep amber | `#ffec99` | `#e67700` |
| **Danger/Error** | Soft rose | Deep rose | `#ffc9c9` | `#e03131` |
| **Neutral/Secondary** | Light gray | Medium gray | `#e9ecef` | `#868e96` |
| **Accent** | Soft violet | Deep violet | `#d0bfff` | `#7048e8` |
| **Info/Highlight** | Soft cyan | Teal | `#96f2d7` | `#0c8599` |
| **Canvas** | White | — | `#ffffff` | — |
| **Default stroke** | — | Near-black | — | `#1e1e1e` |
### Open Colors — Full Palette
Excalidraw's native color picker is built around [Open Colors](https://yeun.github.io/open-colors/). Use shade-2 for fills and shade-8 for matching strokes to create depth.
| Family | Fill (shade-2) | Stroke (shade-8) | Fill hex | Stroke hex |
| ------- | -------------- | ---------------- | --------- | ---------- |
| Gray | `gray-2` | `gray-8` | `#e9ecef` | `#343a40` |
| Red | `red-2` | `red-8` | `#ffc9c9` | `#c92a2a` |
| Pink | `pink-2` | `pink-8` | `#fcc2d7` | `#a61e4d` |
| Grape | `grape-2` | `grape-8` | `#e5dbff` | `#6741d9` |
| Violet | `violet-2` | `violet-8` | `#d0bfff` | `#5f3dc4` |
| Indigo | `indigo-2` | `indigo-8` | `#bac8ff` | `#3b5bdb` |
| Blue | `blue-2` | `blue-8` | `#a5d8ff` | `#1864ab` |
| Cyan | `cyan-2` | `cyan-8` | `#99e9f2` | `#0b7285` |
| Teal | `teal-2` | `teal-8` | `#96f2d7` | `#087f5b` |
| Green | `green-2` | `green-8` | `#b2f2bb` | `#2b8a3e` |
| Lime | `lime-2` | `lime-8` | `#d8f5a2` | `#5c940d` |
| Yellow | `yellow-2` | `yellow-8` | `#ffec99` | `#e67700` |
| Orange | `orange-2` | `orange-8` | `#ffd8a8` | `#d9480f` |
### Curated Professional Palettes
Use one palette per diagram for visual coherence:
**Blue-Tech** (APIs, microservices, cloud):
- API/Gateway: fill `#a5d8ff`, stroke `#1971c2`
- Services: fill `#b2f2bb`, stroke `#2f9e44`
- Data: fill `#96f2d7`, stroke `#0c8599`
- Events: fill `#d0bfff`, stroke `#7048e8`
**Warm-Neutral** (business processes, workflows):
- Primary: fill `#ffd8a8`, stroke `#d9480f`
- Secondary: fill `#ffec99`, stroke `#e67700`
- Supporting: fill `#e9ecef`, stroke `#868e96`
- Action: fill `#b2f2bb`, stroke `#2f9e44`
**Monochrome** (clean/minimal):
- Main: fill `#e9ecef`, stroke `#343a40`
- Emphasis: fill `#ced4da`, stroke `#212529`
- Accent: fill `#a5d8ff`, stroke `#1971c2` (single color accent)
### Dark Theme
When user requests dark mode, set `"viewBackgroundColor": "#1e1e1e"` and use these:
| Role | Fill | Stroke | Hex Fill | Hex Stroke |
| ------------------ | ---------- | ----------- | --------- | ---------- |
| **Primary** | Deep blue | Light blue | `#1864ab` | `#74c0fc` |
| **Success** | Deep green | Light green | `#2b8a3e` | `#8ce99a` |
| **Warning** | Deep amber | Light amber | `#e67700` | `#ffd43b` |
| **Danger** | Deep red | Light red | `#c92a2a` | `#ff8787` |
| **Neutral** | Dark gray | Light gray | `#343a40` | `#adb5bd` |
| **Default stroke** | — | White | — | `#ffffff` |
### Typography Scale
| Purpose | Font Size | Font Family |
| --------------- | --------- | --------------- |
| Diagram title | 28-32 | `fontFamily: 5` |
| Section header | 22-24 | `fontFamily: 5` |
| Element label | 18-20 | `fontFamily: 5` |
| Arrow label | 14-16 | `fontFamily: 5` |
| Annotation/note | 12-14 | `fontFamily: 5` |
### Spacing System
All spacing based on `gridSize: 20`:
| Context | Value | Grid multiples |
| ------------------------------- | --------- | -------------- |
| Between elements (horizontal) | 200-300px | 10-15 units |
| Between elements (vertical) | 100-150px | 5-7.5 units |
| Element padding (inside shapes) | 20-40px | 1-2 units |
| Arrow-to-shape clearance | 20px | 1 unit |
| Canvas margin | 60px | 3 units |
| Between groups of elements | 400px | 20 units |
## Font Families
| ID | Name | Style | When to use |
| --- | ---------- | -------------------- | -------------------------------------------------- |
| 5 | Excalifont | Hand-drawn (newest) | Default — matches Excalidraw's signature aesthetic |
| 1 | Virgil | Hand-drawn (classic) | Fallback if fontFamily 5 is not supported |
| 2 | Helvetica | Clean sans-serif | Technical/formal diagrams when requested |
| 3 | Cascadia | Monospace | Code labels, technical identifiers |
**Default to fontFamily 5 for all text** unless the user explicitly requests a formal/clean style.
## Visual Modes
Choose a visual mode upfront and apply it consistently. Never mix modes on same-level elements.
### Sketch Mode (default)
Excalidraw's signature hand-drawn aesthetic:
```json
{
"roughness": 1,
"fontFamily": 5
}
```
Apply `roughness: 1` to all shapes and arrows. Use `fontFamily: 5` (Excalifont) for all text.
### Clean Mode
Precise, polished, presentation-ready:
```json
{
"roughness": 0,
"fontFamily": 2
}
```
Apply `roughness: 0` to all shapes and arrows. Use `fontFamily: 2` (Helvetica) for body, `fontFamily: 5` for titles.
### Mixed Mode (recommended for architecture)
Background zones clean (`roughness: 0`) + foreground shapes sketchy (`roughness: 1`):
- Zones: `roughness: 0` — structural, precise
- Shapes: `roughness: 1` — content, approachable
## Background Zones
Background zones are semi-transparent dashed rectangles placed **before all other elements** (lowest `index` values) to create visual grouping regions.
**Required zone properties:**
```json
{
"id": "zone-backend",
"type": "rectangle",
"x": 300,
"y": 140,
"width": 480,
"height": 320,
"angle": 0,
"strokeColor": "#4c6ef5",
"backgroundColor": "#dbe4ff",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 0,
"opacity": 35,
"roundness": { "type": 3 },
"groupIds": [],
"frameId": null,
"index": "a0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 111111111,
"version": 1,
"versionNonce": 222222222,
"updated": 1706659200000,
"boundElements": []
}
```
**Critical zone properties:**
- `opacity: 35` — semi-transparent (20-40 range works; 35 is sweet spot)
- `strokeStyle: "dashed"` — marks zone as boundary, not a regular shape
- `roughness: 0` — clean zone edges even in sketch-mode diagrams
- `fillStyle: "solid"` — required for opacity tinting to work
**Zone label** — add a standalone text near top-left of zone, after other text in the array:
```json
{
"id": "label-zone-backend",
"type": "text",
"x": 320,
"y": 148,
"width": 140,
"height": 18,
"text": "Backend Services",
"originalText": "Backend Services",
"fontSize": 14,
"fontFamily": 5,
"strokeColor": "#4c6ef5",
"textAlign": "left",
"containerId": null,
"lineHeight": 1.25,
"roundness": null
}
```
**Element ordering when zones are present:**
1. Background zones (indices `a0`, `a1`, ...)
2. Content shapes (indices continue after zones)
3. Arrows
4. Text elements (highest indices)
## Frames
Frames are named containers that group elements visually with a title bar. They appear in Excalidraw's left panel for navigation.
```json
{
"id": "frame-1",
"type": "frame",
"x": 80,
"y": 80,
"width": 680,
"height": 480,
"angle": 0,
"strokeColor": "#bbb",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 100000001,
"version": 1,
"versionNonce": 100000002,
"updated": 1706659200000,
"name": "Order Processing Flow",
"boundElements": []
}
```
Elements inside a frame reference it via `"frameId": "frame-1"`. The frame `name` appears as the frame's title.
Use frames for: multi-page diagrams, distinct diagram sections, export boundaries.
## Coordinate System
- Origin `(0, 0)` is top-left corner
- X increases to the right
- Y increases downward
- All units are in pixels
- Align to grid: position on multiples of 20 (when `gridSize: 20`)
## Element Sizing Guide
| Shape | Content | Width | Height |
| --------- | ------------------------ | --------- | --------- |
| Rectangle | Single word | 140-160px | 60-80px |
| Rectangle | Short phrase (2-4 words) | 180-220px | 80-100px |
| Rectangle | Sentence | 250-320px | 100-120px |
| Ellipse | Short text (circle) | 120×120px | — |
| Ellipse | Longer text | 160×120px | — |
| Diamond | Short question | 140×140px | — |
| Diamond | Longer question | 180×180px | — |
**Width formula for text:** `text.length × fontSize × 0.6`
**Height formula:** `fontSize × 1.2 × numberOfLines`
## Grouping Elements
Use `groupIds` to create compound elements that move together:
```json
[
{
"id": "server-box",
"type": "rectangle",
"x": 100,
"y": 100,
"width": 180,
"height": 80,
"groupIds": ["server-group"],
"boundElements": [
{ "type": "text", "id": "text-server-box" }
]
},
{
"id": "server-icon",
"type": "text",
"x": 105,
"y": 185,
"width": 30,
"height": 30,
"text": "🖥️",
"fontSize": 20,
"fontFamily": 5,
"groupIds": ["server-group"],
"containerId": null,
"roundness": null
},
{
"id": "text-server-box",
"type": "text",
"x": 120,
"y": 128,
"width": 140,
"height": 24,
"text": "Web Server",
"originalText": "Web Server",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "server-box",
"lineHeight": 1.25,
"strokeColor": "#1e1e1e",
"groupIds": ["server-group"],
"roundness": null
}
]
```
## `customData` for Metadata
Store extra information on elements that persists with the file but doesn't render:
```json
{
"id": "step-1",
"type": "rectangle",
"customData": {
"diagramType": "flowchart",
"stepNumber": 1,
"generatedBy": "excalidraw-studio"
}
}
```
## Complete Minimal Example
A flowchart with two connected shapes. Note the element order: shapes → arrows → text elements (ensures text is always rendered on top, never obscured by arrows).
```json
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "title",
"type": "text",
"x": 100,
"y": 40,
"width": 300,
"height": 35,
"text": "User Registration Flow",
"originalText": "User Registration Flow",
"fontSize": 28,
"fontFamily": 5,
"textAlign": "center",
"strokeColor": "#1e1e1e",
"opacity": 100,
"roundness": null,
"containerId": null,
"lineHeight": 1.25
},
{
"id": "step-1",
"type": "rectangle",
"x": 100,
"y": 120,
"width": 200,
"height": 80,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"roughness": 1,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-step-1" },
{ "type": "arrow", "id": "arrow-1-2" }
]
},
{
"id": "step-2",
"type": "rectangle",
"x": 400,
"y": 120,
"width": 200,
"height": 80,
"strokeColor": "#2f9e44",
"backgroundColor": "#b2f2bb",
"fillStyle": "solid",
"strokeWidth": 2,
"roughness": 1,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-step-2" },
{ "type": "arrow", "id": "arrow-1-2" }
]
},
{
"id": "arrow-1-2",
"type": "arrow",
"x": 300,
"y": 160,
"width": 100,
"height": 0,
"points": [
[0, 0],
[100, 0]
],
"strokeColor": "#1e1e1e",
"strokeWidth": 2,
"roundness": { "type": 2 },
"startBinding": { "elementId": "step-1", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "step-2", "focus": 0, "gap": 1 }
},
{
"id": "text-step-1",
"type": "text",
"x": 130,
"y": 148,
"width": 140,
"height": 24,
"text": "Enter Email",
"originalText": "Enter Email",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "step-1",
"lineHeight": 1.25,
"strokeColor": "#1e1e1e",
"roundness": null
},
{
"id": "text-step-2",
"type": "text",
"x": 430,
"y": 148,
"width": 140,
"height": 24,
"text": "Verify Email",
"originalText": "Verify Email",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "step-2",
"lineHeight": 1.25,
"strokeColor": "#1e1e1e",
"roundness": null
}
],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": 20
},
"files": {}
}
```
```
### assets/flowchart-template.json
```json
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "start",
"type": "ellipse",
"x": 280, "y": 80, "width": 200, "height": 60,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "#b2f2bb",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1001,
"version": 1,
"versionNonce": 1002,
"updated": 1706659200000,
"roundness": null,
"boundElements": [
{ "type": "text", "id": "text-start" },
{ "type": "arrow", "id": "arrow-start-step1" }
]
},
{
"id": "step1",
"type": "rectangle",
"x": 260, "y": 200, "width": 240, "height": 80,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1003,
"version": 1,
"versionNonce": 1004,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-step1" },
{ "type": "arrow", "id": "arrow-start-step1" },
{ "type": "arrow", "id": "arrow-step1-decision" }
]
},
{
"id": "decision",
"type": "diamond",
"x": 260, "y": 340, "width": 240, "height": 100,
"angle": 0,
"strokeColor": "#e67700",
"backgroundColor": "#ffec99",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a2",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1005,
"version": 1,
"versionNonce": 1006,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [
{ "type": "text", "id": "text-decision" },
{ "type": "arrow", "id": "arrow-step1-decision" },
{ "type": "arrow", "id": "arrow-yes" },
{ "type": "arrow", "id": "arrow-no" }
]
},
{
"id": "step2",
"type": "rectangle",
"x": 600, "y": 350, "width": 200, "height": 80,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a3",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1007,
"version": 1,
"versionNonce": 1008,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-step2" },
{ "type": "arrow", "id": "arrow-yes" }
]
},
{
"id": "step3",
"type": "rectangle",
"x": 260, "y": 500, "width": 240, "height": 80,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a4",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1009,
"version": 1,
"versionNonce": 1010,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-step3" },
{ "type": "arrow", "id": "arrow-no" },
{ "type": "arrow", "id": "arrow-step3-end" }
]
},
{
"id": "end",
"type": "ellipse",
"x": 280, "y": 640, "width": 200, "height": 60,
"angle": 0,
"strokeColor": "#e03131",
"backgroundColor": "#ffc9c9",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a5",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1011,
"version": 1,
"versionNonce": 1012,
"updated": 1706659200000,
"roundness": null,
"boundElements": [
{ "type": "text", "id": "text-end" },
{ "type": "arrow", "id": "arrow-step3-end" }
]
},
{
"id": "arrow-start-step1",
"type": "arrow",
"x": 380, "y": 140, "width": 0, "height": 60,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a6",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1013,
"version": 1,
"versionNonce": 1014,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [0, 60]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "start", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "step1", "focus": 0, "gap": 1 }
},
{
"id": "arrow-step1-decision",
"type": "arrow",
"x": 380, "y": 280, "width": 0, "height": 60,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a7",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1015,
"version": 1,
"versionNonce": 1016,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [0, 60]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "step1", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "decision", "focus": 0, "gap": 1 }
},
{
"id": "arrow-yes",
"type": "arrow",
"x": 500, "y": 390, "width": 100, "height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a8",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1017,
"version": 1,
"versionNonce": 1018,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [100, 0]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "decision", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "step2", "focus": 0, "gap": 1 }
},
{
"id": "arrow-no",
"type": "arrow",
"x": 380, "y": 440, "width": 0, "height": 60,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a9",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1019,
"version": 1,
"versionNonce": 1020,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [0, 60]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "decision", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "step3", "focus": 0, "gap": 1 }
},
{
"id": "arrow-step3-end",
"type": "arrow",
"x": 380, "y": 580, "width": 0, "height": 60,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1021,
"version": 1,
"versionNonce": 1022,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [0, 60]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "step3", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "end", "focus": 0, "gap": 1 }
},
{
"id": "title",
"type": "text",
"x": 220, "y": 30,
"width": 320, "height": 35,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1023,
"version": 1,
"versionNonce": 1024,
"updated": 1706659200000,
"text": "Flowchart Title",
"originalText": "Flowchart Title",
"fontSize": 28,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "label-yes",
"type": "text",
"x": 512, "y": 370,
"width": 40, "height": 18,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b2",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1025,
"version": 1,
"versionNonce": 1026,
"updated": 1706659200000,
"text": "Yes",
"originalText": "Yes",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "label-no",
"type": "text",
"x": 390, "y": 450,
"width": 30, "height": 18,
"angle": 0,
"strokeColor": "#e03131",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b3",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1027,
"version": 1,
"versionNonce": 1028,
"updated": 1706659200000,
"text": "No",
"originalText": "No",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-start",
"type": "text",
"x": 300, "y": 100,
"width": 160, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b4",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1029,
"version": 1,
"versionNonce": 1030,
"updated": 1706659200000,
"text": "Start",
"originalText": "Start",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "start",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-step1",
"type": "text",
"x": 280, "y": 230,
"width": 200, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b5",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1031,
"version": 1,
"versionNonce": 1032,
"updated": 1706659200000,
"text": "Step 1",
"originalText": "Step 1",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "step1",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-decision",
"type": "text",
"x": 280, "y": 378,
"width": 200, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b6",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1033,
"version": 1,
"versionNonce": 1034,
"updated": 1706659200000,
"text": "Condition?",
"originalText": "Condition?",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "decision",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-step2",
"type": "text",
"x": 620, "y": 380,
"width": 160, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b7",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1035,
"version": 1,
"versionNonce": 1036,
"updated": 1706659200000,
"text": "Step 2 (Yes)",
"originalText": "Step 2 (Yes)",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "step2",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-step3",
"type": "text",
"x": 280, "y": 530,
"width": 200, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b8",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1037,
"version": 1,
"versionNonce": 1038,
"updated": 1706659200000,
"text": "Step 3 (No)",
"originalText": "Step 3 (No)",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "step3",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-end",
"type": "text",
"x": 300, "y": 660,
"width": 160, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b9",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 1039,
"version": 1,
"versionNonce": 1040,
"updated": 1706659200000,
"text": "End",
"originalText": "End",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "end",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
}
],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": 20
},
"files": {}
}
```
### assets/relationship-template.json
```json
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "entity-a",
"type": "rectangle",
"x": 60, "y": 220, "width": 200, "height": 80,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 7001,
"version": 1,
"versionNonce": 7002,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-entity-a" },
{ "type": "arrow", "id": "arrow-a-b" }
]
},
{
"id": "entity-b",
"type": "rectangle",
"x": 380, "y": 100, "width": 200, "height": 80,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "#b2f2bb",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 7003,
"version": 1,
"versionNonce": 7004,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-entity-b" },
{ "type": "arrow", "id": "arrow-a-b" },
{ "type": "arrow", "id": "arrow-b-c" }
]
},
{
"id": "entity-c",
"type": "rectangle",
"x": 380, "y": 340, "width": 200, "height": 80,
"angle": 0,
"strokeColor": "#7048e8",
"backgroundColor": "#d0bfff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a2",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 7005,
"version": 1,
"versionNonce": 7006,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-entity-c" },
{ "type": "arrow", "id": "arrow-b-c" }
]
},
{
"id": "arrow-a-b",
"type": "arrow",
"x": 260, "y": 260, "width": 120, "height": -120,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a3",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 7007,
"version": 1,
"versionNonce": 7008,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [{ "type": "text", "id": "text-rel-a-b" }],
"points": [[0, 0], [120, -120]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "entity-a", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "entity-b", "focus": 0, "gap": 1 }
},
{
"id": "arrow-b-c",
"type": "arrow",
"x": 480, "y": 180, "width": 0, "height": 160,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a4",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 7009,
"version": 1,
"versionNonce": 7010,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [{ "type": "text", "id": "text-rel-b-c" }],
"points": [[0, 0], [0, 160]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "entity-b", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "entity-c", "focus": 0, "gap": 1 }
},
{
"id": "title",
"type": "text",
"x": 180, "y": 40,
"width": 440, "height": 35,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a5",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 7011,
"version": 1,
"versionNonce": 7012,
"updated": 1706659200000,
"text": "Relationship Diagram",
"originalText": "Relationship Diagram",
"fontSize": 28,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-entity-a",
"type": "text",
"x": 80, "y": 250,
"width": 160, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a6",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 7013,
"version": 1,
"versionNonce": 7014,
"updated": 1706659200000,
"text": "Entity A",
"originalText": "Entity A",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "entity-a",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-entity-b",
"type": "text",
"x": 400, "y": 130,
"width": 160, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a7",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 7015,
"version": 1,
"versionNonce": 7016,
"updated": 1706659200000,
"text": "Entity B",
"originalText": "Entity B",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "entity-b",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-entity-c",
"type": "text",
"x": 400, "y": 370,
"width": 160, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a8",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 7017,
"version": 1,
"versionNonce": 7018,
"updated": 1706659200000,
"text": "Entity C",
"originalText": "Entity C",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "entity-c",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-rel-a-b",
"type": "text",
"x": 278, "y": 182,
"width": 100, "height": 18,
"angle": 0,
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 7019,
"version": 1,
"versionNonce": 7020,
"updated": 1706659200000,
"text": "uses",
"originalText": "uses",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "arrow-a-b",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-rel-b-c",
"type": "text",
"x": 448, "y": 243,
"width": 80, "height": 18,
"angle": 0,
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 7021,
"version": 1,
"versionNonce": 7022,
"updated": 1706659200000,
"text": "contains",
"originalText": "contains",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "arrow-b-c",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
}
],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": 20
},
"files": {}
}
```
### assets/mindmap-template.json
```json
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "center",
"type": "ellipse",
"x": 380, "y": 280, "width": 200, "height": 80,
"angle": 0,
"strokeColor": "#e67700",
"backgroundColor": "#ffec99",
"fillStyle": "solid",
"strokeWidth": 3,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2001,
"version": 1,
"versionNonce": 2002,
"updated": 1706659200000,
"roundness": null,
"boundElements": [
{ "type": "text", "id": "text-center" },
{ "type": "arrow", "id": "arrow-center-b1" },
{ "type": "arrow", "id": "arrow-center-b2" },
{ "type": "arrow", "id": "arrow-center-b3" },
{ "type": "arrow", "id": "arrow-center-b4" }
]
},
{
"id": "branch1",
"type": "rectangle",
"x": 80, "y": 180, "width": 200, "height": 60,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2003,
"version": 1,
"versionNonce": 2004,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-branch1" },
{ "type": "arrow", "id": "arrow-center-b1" },
{ "type": "arrow", "id": "arrow-b1-s1" },
{ "type": "arrow", "id": "arrow-b1-s2" }
]
},
{
"id": "branch2",
"type": "rectangle",
"x": 80, "y": 400, "width": 200, "height": 60,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "#b2f2bb",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a2",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2005,
"version": 1,
"versionNonce": 2006,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-branch2" },
{ "type": "arrow", "id": "arrow-center-b2" },
{ "type": "arrow", "id": "arrow-b2-s1" },
{ "type": "arrow", "id": "arrow-b2-s2" }
]
},
{
"id": "branch3",
"type": "rectangle",
"x": 680, "y": 180, "width": 200, "height": 60,
"angle": 0,
"strokeColor": "#7048e8",
"backgroundColor": "#d0bfff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a3",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2007,
"version": 1,
"versionNonce": 2008,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-branch3" },
{ "type": "arrow", "id": "arrow-center-b3" },
{ "type": "arrow", "id": "arrow-b3-s1" },
{ "type": "arrow", "id": "arrow-b3-s2" }
]
},
{
"id": "branch4",
"type": "rectangle",
"x": 680, "y": 400, "width": 200, "height": 60,
"angle": 0,
"strokeColor": "#0c8599",
"backgroundColor": "#96f2d7",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a4",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2009,
"version": 1,
"versionNonce": 2010,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-branch4" },
{ "type": "arrow", "id": "arrow-center-b4" },
{ "type": "arrow", "id": "arrow-b4-s1" },
{ "type": "arrow", "id": "arrow-b4-s2" }
]
},
{
"id": "sub-b1-1",
"type": "rectangle",
"x": -160, "y": 140, "width": 180, "height": 50,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#dbe4ff",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a5",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2011,
"version": 1,
"versionNonce": 2012,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-sub-b1-1" },
{ "type": "arrow", "id": "arrow-b1-s1" }
]
},
{
"id": "sub-b1-2",
"type": "rectangle",
"x": -160, "y": 220, "width": 180, "height": 50,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#dbe4ff",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a6",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2013,
"version": 1,
"versionNonce": 2014,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-sub-b1-2" },
{ "type": "arrow", "id": "arrow-b1-s2" }
]
},
{
"id": "sub-b2-1",
"type": "rectangle",
"x": -160, "y": 360, "width": 180, "height": 50,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "#d3f9d8",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a7",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2015,
"version": 1,
"versionNonce": 2016,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-sub-b2-1" },
{ "type": "arrow", "id": "arrow-b2-s1" }
]
},
{
"id": "sub-b2-2",
"type": "rectangle",
"x": -160, "y": 440, "width": 180, "height": 50,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "#d3f9d8",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a8",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2017,
"version": 1,
"versionNonce": 2018,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-sub-b2-2" },
{ "type": "arrow", "id": "arrow-b2-s2" }
]
},
{
"id": "sub-b3-1",
"type": "rectangle",
"x": 940, "y": 140, "width": 180, "height": 50,
"angle": 0,
"strokeColor": "#7048e8",
"backgroundColor": "#e5dbff",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a9",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2019,
"version": 1,
"versionNonce": 2020,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-sub-b3-1" },
{ "type": "arrow", "id": "arrow-b3-s1" }
]
},
{
"id": "sub-b3-2",
"type": "rectangle",
"x": 940, "y": 220, "width": 180, "height": 50,
"angle": 0,
"strokeColor": "#7048e8",
"backgroundColor": "#e5dbff",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2021,
"version": 1,
"versionNonce": 2022,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-sub-b3-2" },
{ "type": "arrow", "id": "arrow-b3-s2" }
]
},
{
"id": "sub-b4-1",
"type": "rectangle",
"x": 940, "y": 360, "width": 180, "height": 50,
"angle": 0,
"strokeColor": "#0c8599",
"backgroundColor": "#c3fae8",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2023,
"version": 1,
"versionNonce": 2024,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-sub-b4-1" },
{ "type": "arrow", "id": "arrow-b4-s1" }
]
},
{
"id": "sub-b4-2",
"type": "rectangle",
"x": 940, "y": 440, "width": 180, "height": 50,
"angle": 0,
"strokeColor": "#0c8599",
"backgroundColor": "#c3fae8",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b2",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2025,
"version": 1,
"versionNonce": 2026,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-sub-b4-2" },
{ "type": "arrow", "id": "arrow-b4-s2" }
]
},
{
"id": "arrow-center-b1",
"type": "arrow",
"x": 380, "y": 310, "width": 100, "height": -100,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b3",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2027,
"version": 1,
"versionNonce": 2028,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [-100, -100]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "center", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "branch1", "focus": 0, "gap": 1 }
},
{
"id": "arrow-center-b2",
"type": "arrow",
"x": 380, "y": 340, "width": 100, "height": 90,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b4",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2029,
"version": 1,
"versionNonce": 2030,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [-100, 90]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "center", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "branch2", "focus": 0, "gap": 1 }
},
{
"id": "arrow-center-b3",
"type": "arrow",
"x": 580, "y": 310, "width": 100, "height": -100,
"angle": 0,
"strokeColor": "#7048e8",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b5",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2031,
"version": 1,
"versionNonce": 2032,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [100, -100]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "center", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "branch3", "focus": 0, "gap": 1 }
},
{
"id": "arrow-center-b4",
"type": "arrow",
"x": 580, "y": 340, "width": 100, "height": 90,
"angle": 0,
"strokeColor": "#0c8599",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b6",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2033,
"version": 1,
"versionNonce": 2034,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [100, 90]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "center", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "branch4", "focus": 0, "gap": 1 }
},
{
"id": "arrow-b1-s1",
"type": "arrow",
"x": 80, "y": 200, "width": 60, "height": -35,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b7",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2035,
"version": 1,
"versionNonce": 2036,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [-60, -35]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "branch1", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "sub-b1-1", "focus": 0, "gap": 1 }
},
{
"id": "arrow-b1-s2",
"type": "arrow",
"x": 80, "y": 220, "width": 60, "height": 25,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b8",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2037,
"version": 1,
"versionNonce": 2038,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [-60, 25]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "branch1", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "sub-b1-2", "focus": 0, "gap": 1 }
},
{
"id": "arrow-b2-s1",
"type": "arrow",
"x": 80, "y": 415, "width": 60, "height": -30,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b9",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2039,
"version": 1,
"versionNonce": 2040,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [-60, -30]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "branch2", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "sub-b2-1", "focus": 0, "gap": 1 }
},
{
"id": "arrow-b2-s2",
"type": "arrow",
"x": 80, "y": 440, "width": 60, "height": 45,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "c0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2041,
"version": 1,
"versionNonce": 2042,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [-60, 45]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "branch2", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "sub-b2-2", "focus": 0, "gap": 1 }
},
{
"id": "arrow-b3-s1",
"type": "arrow",
"x": 880, "y": 200, "width": 60, "height": -35,
"angle": 0,
"strokeColor": "#7048e8",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "c1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2043,
"version": 1,
"versionNonce": 2044,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [60, -35]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "branch3", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "sub-b3-1", "focus": 0, "gap": 1 }
},
{
"id": "arrow-b3-s2",
"type": "arrow",
"x": 880, "y": 220, "width": 60, "height": 25,
"angle": 0,
"strokeColor": "#7048e8",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "c2",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2045,
"version": 1,
"versionNonce": 2046,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [60, 25]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "branch3", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "sub-b3-2", "focus": 0, "gap": 1 }
},
{
"id": "arrow-b4-s1",
"type": "arrow",
"x": 880, "y": 415, "width": 60, "height": -30,
"angle": 0,
"strokeColor": "#0c8599",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "c3",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2047,
"version": 1,
"versionNonce": 2048,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [60, -30]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "branch4", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "sub-b4-1", "focus": 0, "gap": 1 }
},
{
"id": "arrow-b4-s2",
"type": "arrow",
"x": 880, "y": 440, "width": 60, "height": 45,
"angle": 0,
"strokeColor": "#0c8599",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "c4",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2049,
"version": 1,
"versionNonce": 2050,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [60, 45]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "branch4", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "sub-b4-2", "focus": 0, "gap": 1 }
},
{
"id": "title",
"type": "text",
"x": 300, "y": 220,
"width": 360, "height": 35,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "c5",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2051,
"version": 1,
"versionNonce": 2052,
"updated": 1706659200000,
"text": "Mind Map Title",
"originalText": "Mind Map Title",
"fontSize": 28,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-center",
"type": "text",
"x": 400, "y": 307,
"width": 160, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "c6",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2053,
"version": 1,
"versionNonce": 2054,
"updated": 1706659200000,
"text": "Central Topic",
"originalText": "Central Topic",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "center",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-branch1",
"type": "text",
"x": 100, "y": 197,
"width": 160, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "c7",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2055,
"version": 1,
"versionNonce": 2056,
"updated": 1706659200000,
"text": "Branch 1",
"originalText": "Branch 1",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "branch1",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-branch2",
"type": "text",
"x": 100, "y": 417,
"width": 160, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "c8",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2057,
"version": 1,
"versionNonce": 2058,
"updated": 1706659200000,
"text": "Branch 2",
"originalText": "Branch 2",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "branch2",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-branch3",
"type": "text",
"x": 700, "y": 197,
"width": 160, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "c9",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2059,
"version": 1,
"versionNonce": 2060,
"updated": 1706659200000,
"text": "Branch 3",
"originalText": "Branch 3",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "branch3",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-branch4",
"type": "text",
"x": 700, "y": 417,
"width": 160, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "d0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2061,
"version": 1,
"versionNonce": 2062,
"updated": 1706659200000,
"text": "Branch 4",
"originalText": "Branch 4",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "branch4",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-sub-b1-1",
"type": "text",
"x": -140, "y": 152,
"width": 140, "height": 20,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "d1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2063,
"version": 1,
"versionNonce": 2064,
"updated": 1706659200000,
"text": "Sub-topic 1.1",
"originalText": "Sub-topic 1.1",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "sub-b1-1",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-sub-b1-2",
"type": "text",
"x": -140, "y": 232,
"width": 140, "height": 20,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "d2",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2065,
"version": 1,
"versionNonce": 2066,
"updated": 1706659200000,
"text": "Sub-topic 1.2",
"originalText": "Sub-topic 1.2",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "sub-b1-2",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-sub-b2-1",
"type": "text",
"x": -140, "y": 372,
"width": 140, "height": 20,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "d3",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2067,
"version": 1,
"versionNonce": 2068,
"updated": 1706659200000,
"text": "Sub-topic 2.1",
"originalText": "Sub-topic 2.1",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "sub-b2-1",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-sub-b2-2",
"type": "text",
"x": -140, "y": 452,
"width": 140, "height": 20,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "d4",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2069,
"version": 1,
"versionNonce": 2070,
"updated": 1706659200000,
"text": "Sub-topic 2.2",
"originalText": "Sub-topic 2.2",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "sub-b2-2",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-sub-b3-1",
"type": "text",
"x": 960, "y": 152,
"width": 140, "height": 20,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "d5",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2071,
"version": 1,
"versionNonce": 2072,
"updated": 1706659200000,
"text": "Sub-topic 3.1",
"originalText": "Sub-topic 3.1",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "sub-b3-1",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-sub-b3-2",
"type": "text",
"x": 960, "y": 232,
"width": 140, "height": 20,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "d6",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2073,
"version": 1,
"versionNonce": 2074,
"updated": 1706659200000,
"text": "Sub-topic 3.2",
"originalText": "Sub-topic 3.2",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "sub-b3-2",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-sub-b4-1",
"type": "text",
"x": 960, "y": 372,
"width": 140, "height": 20,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "d7",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2075,
"version": 1,
"versionNonce": 2076,
"updated": 1706659200000,
"text": "Sub-topic 4.1",
"originalText": "Sub-topic 4.1",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "sub-b4-1",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-sub-b4-2",
"type": "text",
"x": 960, "y": 452,
"width": 140, "height": 20,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "d8",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 2077,
"version": 1,
"versionNonce": 2078,
"updated": 1706659200000,
"text": "Sub-topic 4.2",
"originalText": "Sub-topic 4.2",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "sub-b4-2",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
}
],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": 20
},
"files": {}
}
```
### assets/data-flow-diagram-template.json
```json
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "external-user",
"type": "rectangle",
"x": 60, "y": 240, "width": 160, "height": 80,
"angle": 0,
"strokeColor": "#e67700",
"backgroundColor": "#ffec99",
"fillStyle": "solid",
"strokeWidth": 3,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 6001,
"version": 1,
"versionNonce": 6002,
"updated": 1706659200000,
"roundness": null,
"boundElements": [
{ "type": "text", "id": "text-external-user" },
{ "type": "arrow", "id": "arrow-user-process" }
]
},
{
"id": "process",
"type": "ellipse",
"x": 320, "y": 220, "width": 200, "height": 120,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 6003,
"version": 1,
"versionNonce": 6004,
"updated": 1706659200000,
"roundness": null,
"boundElements": [
{ "type": "text", "id": "text-process" },
{ "type": "arrow", "id": "arrow-user-process" },
{ "type": "arrow", "id": "arrow-process-db" }
]
},
{
"id": "datastore",
"type": "rectangle",
"x": 620, "y": 240, "width": 200, "height": 80,
"angle": 0,
"strokeColor": "#0c8599",
"backgroundColor": "#96f2d7",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a2",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 6005,
"version": 1,
"versionNonce": 6006,
"updated": 1706659200000,
"roundness": null,
"boundElements": [
{ "type": "text", "id": "text-datastore" },
{ "type": "arrow", "id": "arrow-process-db" }
]
},
{
"id": "datastore-top-line",
"type": "line",
"x": 620, "y": 240, "width": 200, "height": 0,
"angle": 0,
"strokeColor": "#0c8599",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a3",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 6007,
"version": 1,
"versionNonce": 6008,
"updated": 1706659200000,
"roundness": null,
"boundElements": [],
"points": [[0, 0], [200, 0]]
},
{
"id": "arrow-user-process",
"type": "arrow",
"x": 220, "y": 280, "width": 100, "height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a4",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 6009,
"version": 1,
"versionNonce": 6010,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [{ "type": "text", "id": "text-arrow-user-process" }],
"points": [[0, 0], [100, 0]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "external-user", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "process", "focus": 0, "gap": 1 }
},
{
"id": "arrow-process-db",
"type": "arrow",
"x": 520, "y": 280, "width": 100, "height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a5",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 6011,
"version": 1,
"versionNonce": 6012,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [{ "type": "text", "id": "text-arrow-process-db" }],
"points": [[0, 0], [100, 0]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "process", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "datastore", "focus": 0, "gap": 1 }
},
{
"id": "title",
"type": "text",
"x": 200, "y": 160,
"width": 480, "height": 35,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a6",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 6013,
"version": 1,
"versionNonce": 6014,
"updated": 1706659200000,
"text": "Data Flow Diagram",
"originalText": "Data Flow Diagram",
"fontSize": 28,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-external-user",
"type": "text",
"x": 80, "y": 270,
"width": 120, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a7",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 6015,
"version": 1,
"versionNonce": 6016,
"updated": 1706659200000,
"text": "User",
"originalText": "User",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "external-user",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-process",
"type": "text",
"x": 340, "y": 263,
"width": 160, "height": 34,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a8",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 6017,
"version": 1,
"versionNonce": 6018,
"updated": 1706659200000,
"text": "1.0\nProcess Data",
"originalText": "1.0\nProcess Data",
"fontSize": 16,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "process",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-datastore",
"type": "text",
"x": 640, "y": 270,
"width": 160, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a9",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 6019,
"version": 1,
"versionNonce": 6020,
"updated": 1706659200000,
"text": "D1: Database",
"originalText": "D1: Database",
"fontSize": 16,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "datastore",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-arrow-user-process",
"type": "text",
"x": 242, "y": 262,
"width": 100, "height": 18,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 6021,
"version": 1,
"versionNonce": 6022,
"updated": 1706659200000,
"text": "input data",
"originalText": "input data",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "arrow-user-process",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-arrow-process-db",
"type": "text",
"x": 542, "y": 262,
"width": 100, "height": 18,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 6023,
"version": 1,
"versionNonce": 6024,
"updated": 1706659200000,
"text": "processed data",
"originalText": "processed data",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "arrow-process-db",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
}
],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": 20
},
"files": {}
}
```
### assets/business-flow-swimlane-template.json
```json
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "zone-lane1",
"type": "rectangle",
"x": 60, "y": 100, "width": 800, "height": 160,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#dbe4ff",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 0,
"opacity": 35,
"groupIds": [],
"frameId": null,
"index": "a0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 5001,
"version": 1,
"versionNonce": 5002,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": []
},
{
"id": "zone-lane2",
"type": "rectangle",
"x": 60, "y": 280, "width": 800, "height": 160,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "#d3f9d8",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 0,
"opacity": 35,
"groupIds": [],
"frameId": null,
"index": "a1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 5003,
"version": 1,
"versionNonce": 5004,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": []
},
{
"id": "step1",
"type": "rectangle",
"x": 120, "y": 140, "width": 180, "height": 80,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a2",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 5005,
"version": 1,
"versionNonce": 5006,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-step1" },
{ "type": "arrow", "id": "arrow1" }
]
},
{
"id": "step2",
"type": "rectangle",
"x": 380, "y": 320, "width": 180, "height": 80,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "#b2f2bb",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a3",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 5007,
"version": 1,
"versionNonce": 5008,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-step2" },
{ "type": "arrow", "id": "arrow1" },
{ "type": "arrow", "id": "arrow2" }
]
},
{
"id": "step3",
"type": "rectangle",
"x": 640, "y": 140, "width": 180, "height": 80,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a4",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 5009,
"version": 1,
"versionNonce": 5010,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "text", "id": "text-step3" },
{ "type": "arrow", "id": "arrow2" }
]
},
{
"id": "arrow1",
"type": "arrow",
"x": 300, "y": 180, "width": 80, "height": 180,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a5",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 5011,
"version": 1,
"versionNonce": 5012,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [80, 0], [80, 180]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "step1", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "step2", "focus": 0, "gap": 1 }
},
{
"id": "arrow2",
"type": "arrow",
"x": 560, "y": 360, "width": 80, "height": -180,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a6",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 5013,
"version": 1,
"versionNonce": 5014,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [],
"points": [[0, 0], [80, 0], [80, -180]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "step2", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "step3", "focus": 0, "gap": 1 }
},
{
"id": "title",
"type": "text",
"x": 200, "y": 50,
"width": 480, "height": 35,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a7",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 5015,
"version": 1,
"versionNonce": 5016,
"updated": 1706659200000,
"text": "Business Process Flow",
"originalText": "Business Process Flow",
"fontSize": 28,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "label-lane1",
"type": "text",
"x": 72, "y": 110,
"width": 120, "height": 20,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a8",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 5017,
"version": 1,
"versionNonce": 5018,
"updated": 1706659200000,
"text": "Customer",
"originalText": "Customer",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "label-lane2",
"type": "text",
"x": 72, "y": 290,
"width": 120, "height": 20,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a9",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 5019,
"version": 1,
"versionNonce": 5020,
"updated": 1706659200000,
"text": "Sales Team",
"originalText": "Sales Team",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-step1",
"type": "text",
"x": 140, "y": 170,
"width": 140, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 5021,
"version": 1,
"versionNonce": 5022,
"updated": 1706659200000,
"text": "Submit Request",
"originalText": "Submit Request",
"fontSize": 16,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "step1",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-step2",
"type": "text",
"x": 400, "y": 350,
"width": 140, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 5023,
"version": 1,
"versionNonce": 5024,
"updated": 1706659200000,
"text": "Review Request",
"originalText": "Review Request",
"fontSize": 16,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "step2",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-step3",
"type": "text",
"x": 660, "y": 170,
"width": 140, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b2",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 5025,
"version": 1,
"versionNonce": 5026,
"updated": 1706659200000,
"text": "Approve",
"originalText": "Approve",
"fontSize": 16,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "step3",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
}
],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": 20
},
"files": {}
}
```
### assets/class-diagram-template.json
```json
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "class-base",
"type": "rectangle",
"x": 100, "y": 120, "width": 280, "height": 200,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 4001,
"version": 1,
"versionNonce": 4002,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "arrow", "id": "arrow-inherit" }
]
},
{
"id": "class-child",
"type": "rectangle",
"x": 500, "y": 120, "width": 280, "height": 200,
"angle": 0,
"strokeColor": "#7048e8",
"backgroundColor": "#d0bfff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 4003,
"version": 1,
"versionNonce": 4004,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "arrow", "id": "arrow-inherit" }
]
},
{
"id": "sep-base-1",
"type": "line",
"x": 100, "y": 168, "width": 280, "height": 0,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a2",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 4005,
"version": 1,
"versionNonce": 4006,
"updated": 1706659200000,
"roundness": null,
"boundElements": [],
"points": [[0, 0], [280, 0]]
},
{
"id": "sep-base-2",
"type": "line",
"x": 100, "y": 258, "width": 280, "height": 0,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a3",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 4007,
"version": 1,
"versionNonce": 4008,
"updated": 1706659200000,
"roundness": null,
"boundElements": [],
"points": [[0, 0], [280, 0]]
},
{
"id": "sep-child-1",
"type": "line",
"x": 500, "y": 168, "width": 280, "height": 0,
"angle": 0,
"strokeColor": "#7048e8",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a4",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 4009,
"version": 1,
"versionNonce": 4010,
"updated": 1706659200000,
"roundness": null,
"boundElements": [],
"points": [[0, 0], [280, 0]]
},
{
"id": "sep-child-2",
"type": "line",
"x": 500, "y": 258, "width": 280, "height": 0,
"angle": 0,
"strokeColor": "#7048e8",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a5",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 4011,
"version": 1,
"versionNonce": 4012,
"updated": 1706659200000,
"roundness": null,
"boundElements": [],
"points": [[0, 0], [280, 0]]
},
{
"id": "arrow-inherit",
"type": "arrow",
"x": 380, "y": 220, "width": 120, "height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a6",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 4013,
"version": 1,
"versionNonce": 4014,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [{ "type": "text", "id": "text-inherit-label" }],
"points": [[0, 0], [120, 0]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "triangle",
"startBinding": { "elementId": "class-base", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "class-child", "focus": 0, "gap": 1 }
},
{
"id": "title",
"type": "text",
"x": 200, "y": 60,
"width": 480, "height": 35,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a7",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 4015,
"version": 1,
"versionNonce": 4016,
"updated": 1706659200000,
"text": "Class Diagram",
"originalText": "Class Diagram",
"fontSize": 28,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-base-name",
"type": "text",
"x": 120, "y": 126,
"width": 240, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a8",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 4017,
"version": 1,
"versionNonce": 4018,
"updated": 1706659200000,
"text": "BaseClass",
"originalText": "BaseClass",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-base-fields",
"type": "text",
"x": 112, "y": 176,
"width": 260, "height": 72,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a9",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 4019,
"version": 1,
"versionNonce": 4020,
"updated": 1706659200000,
"text": "+ id: string\n- name: string\n# createdAt: Date",
"originalText": "+ id: string\n- name: string\n# createdAt: Date",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-base-methods",
"type": "text",
"x": 112, "y": 266,
"width": 260, "height": 45,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 4021,
"version": 1,
"versionNonce": 4022,
"updated": 1706659200000,
"text": "+ save(): void\n+ delete(): boolean",
"originalText": "+ save(): void\n+ delete(): boolean",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-child-name",
"type": "text",
"x": 520, "y": 126,
"width": 240, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 4023,
"version": 1,
"versionNonce": 4024,
"updated": 1706659200000,
"text": "ChildClass",
"originalText": "ChildClass",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-child-fields",
"type": "text",
"x": 512, "y": 176,
"width": 260, "height": 72,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b2",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 4025,
"version": 1,
"versionNonce": 4026,
"updated": 1706659200000,
"text": "- role: string\n+ permissions: string[]",
"originalText": "- role: string\n+ permissions: string[]",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-child-methods",
"type": "text",
"x": 512, "y": 266,
"width": 260, "height": 45,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b3",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 4027,
"version": 1,
"versionNonce": 4028,
"updated": 1706659200000,
"text": "+ hasPermission(p): boolean\n+ elevate(): void",
"originalText": "+ hasPermission(p): boolean\n+ elevate(): void",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-inherit-label",
"type": "text",
"x": 400, "y": 202,
"width": 80, "height": 18,
"angle": 0,
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b4",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 4029,
"version": 1,
"versionNonce": 4030,
"updated": 1706659200000,
"text": "extends",
"originalText": "extends",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "arrow-inherit",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
}
],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": 20
},
"files": {}
}
```
### assets/sequence-diagram-template.json
```json
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "actor-client",
"type": "rectangle",
"x": 80, "y": 80, "width": 160, "height": 60,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3001,
"version": 1,
"versionNonce": 3002,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [{ "type": "text", "id": "text-actor-client" }]
},
{
"id": "actor-server",
"type": "rectangle",
"x": 380, "y": 80, "width": 160, "height": 60,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "#b2f2bb",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3003,
"version": 1,
"versionNonce": 3004,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [{ "type": "text", "id": "text-actor-server" }]
},
{
"id": "actor-db",
"type": "rectangle",
"x": 680, "y": 80, "width": 160, "height": 60,
"angle": 0,
"strokeColor": "#7048e8",
"backgroundColor": "#d0bfff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a2",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3005,
"version": 1,
"versionNonce": 3006,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [{ "type": "text", "id": "text-actor-db" }]
},
{
"id": "lifeline-client",
"type": "line",
"x": 160, "y": 140, "width": 0, "height": 440,
"angle": 0,
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a3",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3007,
"version": 1,
"versionNonce": 3008,
"updated": 1706659200000,
"roundness": null,
"boundElements": [],
"points": [[0, 0], [0, 440]]
},
{
"id": "lifeline-server",
"type": "line",
"x": 460, "y": 140, "width": 0, "height": 440,
"angle": 0,
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a4",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3009,
"version": 1,
"versionNonce": 3010,
"updated": 1706659200000,
"roundness": null,
"boundElements": [],
"points": [[0, 0], [0, 440]]
},
{
"id": "lifeline-db",
"type": "line",
"x": 760, "y": 140, "width": 0, "height": 440,
"angle": 0,
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a5",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3011,
"version": 1,
"versionNonce": 3012,
"updated": 1706659200000,
"roundness": null,
"boundElements": [],
"points": [[0, 0], [0, 440]]
},
{
"id": "activation-server",
"type": "rectangle",
"x": 452, "y": 200, "width": 16, "height": 200,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "#b2f2bb",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a6",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3013,
"version": 1,
"versionNonce": 3014,
"updated": 1706659200000,
"roundness": null,
"boundElements": []
},
{
"id": "msg1",
"type": "arrow",
"x": 160, "y": 200, "width": 300, "height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a7",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3015,
"version": 1,
"versionNonce": 3016,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [{ "type": "text", "id": "text-msg1" }],
"points": [[0, 0], [300, 0]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": null,
"endBinding": null
},
{
"id": "msg2",
"type": "arrow",
"x": 460, "y": 280, "width": 300, "height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a8",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3017,
"version": 1,
"versionNonce": 3018,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [{ "type": "text", "id": "text-msg2" }],
"points": [[0, 0], [300, 0]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": null,
"endBinding": null
},
{
"id": "msg3",
"type": "arrow",
"x": 760, "y": 340, "width": 300, "height": 0,
"angle": 0,
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a9",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3019,
"version": 1,
"versionNonce": 3020,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [{ "type": "text", "id": "text-msg3" }],
"points": [[0, 0], [-300, 0]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": null,
"endBinding": null
},
{
"id": "msg4",
"type": "arrow",
"x": 460, "y": 400, "width": 300, "height": 0,
"angle": 0,
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3021,
"version": 1,
"versionNonce": 3022,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [{ "type": "text", "id": "text-msg4" }],
"points": [[0, 0], [-300, 0]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": null,
"endBinding": null
},
{
"id": "title",
"type": "text",
"x": 200, "y": 30,
"width": 520, "height": 35,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3023,
"version": 1,
"versionNonce": 3024,
"updated": 1706659200000,
"text": "Sequence Diagram",
"originalText": "Sequence Diagram",
"fontSize": 28,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-actor-client",
"type": "text",
"x": 100, "y": 100,
"width": 120, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b2",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3025,
"version": 1,
"versionNonce": 3026,
"updated": 1706659200000,
"text": "Client",
"originalText": "Client",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "actor-client",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-actor-server",
"type": "text",
"x": 400, "y": 100,
"width": 120, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b3",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3027,
"version": 1,
"versionNonce": 3028,
"updated": 1706659200000,
"text": "Server",
"originalText": "Server",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "actor-server",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-actor-db",
"type": "text",
"x": 700, "y": 100,
"width": 120, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b4",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3029,
"version": 1,
"versionNonce": 3030,
"updated": 1706659200000,
"text": "Database",
"originalText": "Database",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "actor-db",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-msg1",
"type": "text",
"x": 220, "y": 182,
"width": 180, "height": 18,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b5",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3031,
"version": 1,
"versionNonce": 3032,
"updated": 1706659200000,
"text": "1: request()",
"originalText": "1: request()",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "msg1",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-msg2",
"type": "text",
"x": 520, "y": 262,
"width": 180, "height": 18,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b6",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3033,
"version": 1,
"versionNonce": 3034,
"updated": 1706659200000,
"text": "2: query()",
"originalText": "2: query()",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "msg2",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-msg3",
"type": "text",
"x": 520, "y": 322,
"width": 180, "height": 18,
"angle": 0,
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b7",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3035,
"version": 1,
"versionNonce": 3036,
"updated": 1706659200000,
"text": "3: result rows",
"originalText": "3: result rows",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "msg3",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-msg4",
"type": "text",
"x": 220, "y": 382,
"width": 180, "height": 18,
"angle": 0,
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b8",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 3037,
"version": 1,
"versionNonce": 3038,
"updated": 1706659200000,
"text": "4: response",
"originalText": "4: response",
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "msg4",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
}
],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": 20
},
"files": {}
}
```
### assets/er-diagram-template.json
```json
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "entity-user",
"type": "rectangle",
"x": 60, "y": 120, "width": 220, "height": 180,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8001,
"version": 1,
"versionNonce": 8002,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "arrow", "id": "arrow-user-order" }
]
},
{
"id": "entity-order",
"type": "rectangle",
"x": 400, "y": 120, "width": 220, "height": 180,
"angle": 0,
"strokeColor": "#e67700",
"backgroundColor": "#ffec99",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8003,
"version": 1,
"versionNonce": 8004,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "arrow", "id": "arrow-user-order" },
{ "type": "arrow", "id": "arrow-order-product" }
]
},
{
"id": "entity-product",
"type": "rectangle",
"x": 740, "y": 120, "width": 220, "height": 180,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "#b2f2bb",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a2",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8005,
"version": 1,
"versionNonce": 8006,
"updated": 1706659200000,
"roundness": { "type": 3 },
"boundElements": [
{ "type": "arrow", "id": "arrow-order-product" }
]
},
{
"id": "sep-user",
"type": "line",
"x": 60, "y": 168, "width": 220, "height": 0,
"angle": 0,
"strokeColor": "#1971c2",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a3",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8007,
"version": 1,
"versionNonce": 8008,
"updated": 1706659200000,
"roundness": null,
"boundElements": [],
"points": [[0, 0], [220, 0]]
},
{
"id": "sep-order",
"type": "line",
"x": 400, "y": 168, "width": 220, "height": 0,
"angle": 0,
"strokeColor": "#e67700",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a4",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8009,
"version": 1,
"versionNonce": 8010,
"updated": 1706659200000,
"roundness": null,
"boundElements": [],
"points": [[0, 0], [220, 0]]
},
{
"id": "sep-product",
"type": "line",
"x": 740, "y": 168, "width": 220, "height": 0,
"angle": 0,
"strokeColor": "#2f9e44",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a5",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8011,
"version": 1,
"versionNonce": 8012,
"updated": 1706659200000,
"roundness": null,
"boundElements": [],
"points": [[0, 0], [220, 0]]
},
{
"id": "arrow-user-order",
"type": "arrow",
"x": 280, "y": 210, "width": 120, "height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a6",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8013,
"version": 1,
"versionNonce": 8014,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [{ "type": "text", "id": "text-rel-user-order" }],
"points": [[0, 0], [120, 0]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "entity-user", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "entity-order", "focus": 0, "gap": 1 }
},
{
"id": "arrow-order-product",
"type": "arrow",
"x": 620, "y": 210, "width": 120, "height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a7",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8015,
"version": 1,
"versionNonce": 8016,
"updated": 1706659200000,
"roundness": { "type": 2 },
"boundElements": [{ "type": "text", "id": "text-rel-order-product" }],
"points": [[0, 0], [120, 0]],
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"startBinding": { "elementId": "entity-order", "focus": 0, "gap": 1 },
"endBinding": { "elementId": "entity-product", "focus": 0, "gap": 1 }
},
{
"id": "title",
"type": "text",
"x": 300, "y": 60,
"width": 440, "height": 35,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a8",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8017,
"version": 1,
"versionNonce": 8018,
"updated": 1706659200000,
"text": "ER Diagram",
"originalText": "ER Diagram",
"fontSize": 28,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-user-name",
"type": "text",
"x": 80, "y": 128,
"width": 180, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a9",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8019,
"version": 1,
"versionNonce": 8020,
"updated": 1706659200000,
"text": "User",
"originalText": "User",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-user-attrs",
"type": "text",
"x": 72, "y": 176,
"width": 200, "height": 80,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b0",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8021,
"version": 1,
"versionNonce": 8022,
"updated": 1706659200000,
"text": "PK user_id: int\n name: varchar\n email: varchar\n created_at: date",
"originalText": "PK user_id: int\n name: varchar\n email: varchar\n created_at: date",
"fontSize": 13,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-order-name",
"type": "text",
"x": 420, "y": 128,
"width": 180, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b1",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8023,
"version": 1,
"versionNonce": 8024,
"updated": 1706659200000,
"text": "Order",
"originalText": "Order",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-order-attrs",
"type": "text",
"x": 412, "y": 176,
"width": 200, "height": 80,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b2",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8025,
"version": 1,
"versionNonce": 8026,
"updated": 1706659200000,
"text": "PK order_id: int\nFK user_id: int\n total: decimal\n order_date: date",
"originalText": "PK order_id: int\nFK user_id: int\n total: decimal\n order_date: date",
"fontSize": 13,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-product-name",
"type": "text",
"x": 760, "y": 128,
"width": 180, "height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b3",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8027,
"version": 1,
"versionNonce": 8028,
"updated": 1706659200000,
"text": "Product",
"originalText": "Product",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-product-attrs",
"type": "text",
"x": 752, "y": 176,
"width": 200, "height": 64,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b4",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8029,
"version": 1,
"versionNonce": 8030,
"updated": 1706659200000,
"text": "PK product_id: int\n name: varchar\n price: decimal",
"originalText": "PK product_id: int\n name: varchar\n price: decimal",
"fontSize": 13,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-rel-user-order",
"type": "text",
"x": 298, "y": 192,
"width": 100, "height": 18,
"angle": 0,
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b5",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8031,
"version": 1,
"versionNonce": 8032,
"updated": 1706659200000,
"text": "places (1:N)",
"originalText": "places (1:N)",
"fontSize": 13,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "arrow-user-order",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
},
{
"id": "text-rel-order-product",
"type": "text",
"x": 638, "y": 192,
"width": 100, "height": 18,
"angle": 0,
"strokeColor": "#868e96",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b6",
"isDeleted": false,
"link": null,
"locked": false,
"seed": 8033,
"version": 1,
"versionNonce": 8034,
"updated": 1706659200000,
"text": "contains (N:M)",
"originalText": "contains (N:M)",
"fontSize": 13,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "arrow-order-product",
"lineHeight": 1.25,
"autoResize": true,
"roundness": null
}
],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": 20
},
"files": {}
}
```
### references/icon-libraries.md
```markdown
# Icon Libraries
Read this file when the user requests diagrams with service icons (AWS, GCP, Azure, Kubernetes, etc.) or asks for professional architecture diagrams.
## How It Works
Excalidraw supports icon libraries (`.excalidrawlib` files) that provide professional, standardized icons. This skill can use pre-split icon libraries to create polished architecture diagrams.
## Checking for Available Libraries
```
Look for: libraries/<library-name>/reference.md
```
If `reference.md` exists, the library is ready. It contains a lookup table of all available icon names.
If no libraries are set up, offer two options:
1. Create the diagram using basic shapes (rectangles, ellipses) with labels — still functional, just less visually polished
2. Guide the user through library setup (see below)
## Using Icons (Python Scripts — Recommended)
The repository includes Python scripts that handle icon integration without consuming AI context tokens:
### Adding icons to a diagram
```bash
python scripts/add-icon-to-diagram.py \
<diagram-path> <icon-name> <x> <y> \
[--label "Text"] [--library-path PATH]
```
The `--label` flag adds a text label below the icon. Edit via `.excalidraw.edit` is enabled by default; pass `--no-use-edit-suffix` to disable.
**Examples:**
```bash
# Add EC2 icon at position (400, 300) with label
python scripts/add-icon-to-diagram.py diagram.excalidraw EC2 400 300 --label "Web Server"
# Add icon from a different library
python scripts/add-icon-to-diagram.py diagram.excalidraw Compute-Engine 500 200 \
--library-path libraries/gcp-icons --label "API Server"
```
### Adding connecting arrows
```bash
python scripts/add-arrow.py \
<diagram-path> <from-x> <from-y> <to-x> <to-y> \
[--label "Text"] [--style solid|dashed|dotted] [--color HEX]
```
**Examples:**
```bash
# Simple arrow
python scripts/add-arrow.py diagram.excalidraw 300 250 500 300
# Arrow with label and custom style
python scripts/add-arrow.py diagram.excalidraw 400 350 600 400 \
--label "HTTPS" --style dashed --color "#7950f2"
```
### Complete workflow
```bash
# 1. Create base diagram with title and structure
# (Create .excalidraw file with basic elements: title text, region rectangles)
# 2. Check icon availability
# Read: libraries/aws-architecture-icons/reference.md
# 3. Add icons with labels
python scripts/add-icon-to-diagram.py my-diagram.excalidraw "Internet-gateway" 200 150 --label "Internet Gateway"
python scripts/add-icon-to-diagram.py my-diagram.excalidraw VPC 250 250
python scripts/add-icon-to-diagram.py my-diagram.excalidraw ELB 350 300 --label "Load Balancer"
python scripts/add-icon-to-diagram.py my-diagram.excalidraw EC2 450 350 --label "Web Server"
python scripts/add-icon-to-diagram.py my-diagram.excalidraw RDS 550 400 --label "Database"
# 4. Add connecting arrows
python scripts/add-arrow.py my-diagram.excalidraw 250 200 300 250 --label "traffic"
python scripts/add-arrow.py my-diagram.excalidraw 300 300 400 300
python scripts/add-arrow.py my-diagram.excalidraw 500 380 600 400 --label "SQL" --style dashed
```
**Why use scripts:**
- ✅ No token consumption — icon JSON data (200-1000 lines each) never enters AI context
- ✅ Accurate coordinates — calculations handled deterministically
- ✅ Automatic ID management — no risk of collision
- ✅ Fast and reusable — works with any Excalidraw library
## Setting Up a Library
Guide the user through these steps:
### Step 1: Create library directory
```bash
mkdir -p skills/excalidraw-studio/libraries/<library-name>
```
### Step 2: Download library
- Visit: <https://libraries.excalidraw.com/>
- Search for the desired icon set (e.g., "AWS Architecture Icons")
- Click download to get the `.excalidrawlib` file
- Place it in the directory from Step 1
### Step 3: Run splitter script
```bash
python skills/excalidraw-studio/scripts/split-excalidraw-library.py \
skills/excalidraw-studio/libraries/<library-name>/
```
### Step 4: Verify
After running the script, this structure should exist:
```
libraries/<library-name>/
<library-name>.excalidrawlib (original)
reference.md (generated — icon lookup table)
icons/ (generated — individual icon files)
API-Gateway.json
EC2.json
Lambda.json
S3.json
...
```
## Manual Icon Integration (Fallback)
Only use this if Python scripts are unavailable. This approach is token-expensive and error-prone.
1. Read `libraries/<library-name>/reference.md` to find icon names
2. Read individual icon JSON files from `icons/` (200-1000 lines each)
3. Extract elements array, calculate bounding box, apply coordinate offset
4. Generate new unique IDs, update groupIds references
5. Copy transformed elements into the diagram
**Challenges:**
- ⚠️ High token consumption (200-1000 lines per icon)
- ⚠️ Complex coordinate transformation
- ⚠️ Risk of ID collision
- ⚠️ Time-consuming for many icons
## Supported Libraries
This workflow works with any valid `.excalidrawlib` file from <https://libraries.excalidraw.com/>. Common categories:
- Cloud service icons (AWS, GCP, Azure)
- Kubernetes / infrastructure icons
- UI / Material icons
- Network diagram icons
Availability and naming vary; verify on the site before recommending to users.
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### scripts/README.md
```markdown
# Excalidraw Library Tools
This directory contains scripts for working with Excalidraw libraries.
## split-excalidraw-library.py
Splits an Excalidraw library file (`*.excalidrawlib`) into individual icon JSON files for efficient token usage by AI assistants.
### Prerequisites
- Python 3.6 or higher
- No additional dependencies required (uses only standard library)
### Usage
```bash
python split-excalidraw-library.py <path-to-library-directory>
```
### Step-by-Step Workflow
1. **Create library directory**:
```bash
mkdir -p skills/excalidraw-studio/libraries/aws-architecture-icons
```
2. **Download and place library file**:
- Visit: <https://libraries.excalidraw.com/>
- Search for "AWS Architecture Icons" and download the `.excalidrawlib` file
- Rename it to match the directory name: `aws-architecture-icons.excalidrawlib`
- Place it in the directory created in step 1
3. **Run the script**:
```bash
python skills/excalidraw-studio/scripts/split-excalidraw-library.py skills/excalidraw-studio/libraries/aws-architecture-icons/
```
### Output Structure
The script creates the following structure in the library directory:
```
skills/excalidraw-studio/libraries/aws-architecture-icons/
aws-architecture-icons.excalidrawlib # Original file (kept)
reference.md # Generated: Quick reference table
icons/ # Generated: Individual icon files
API-Gateway.json
CloudFront.json
EC2.json
S3.json
...
```
### What the Script Does
1. **Reads** the `.excalidrawlib` file
2. **Extracts** each icon from the `libraryItems` array
3. **Sanitizes** icon names to create valid filenames (spaces → hyphens, removes special characters)
4. **Saves** each icon as a separate JSON file in the `icons/` directory
5. **Generates** a `reference.md` file with a table mapping icon names to filenames
### Benefits
- **Token Efficiency**: AI can first read the lightweight `reference.md` to find relevant icons, then load only the specific icon files needed
- **Organization**: Icons are organized in a clear directory structure
- **Extensibility**: Users can add multiple library sets side-by-side
### Recommended Workflow
1. Download desired Excalidraw libraries from <https://libraries.excalidraw.com/>
2. Run this script on each library file
3. Move the generated folders to `../libraries/`
4. The AI assistant will use `reference.md` files to locate and use icons efficiently
### Library Sources (Examples — verify availability)
- Examples found on <https://libraries.excalidraw.com/> may include cloud/service icon sets.
- Availability changes over time; verify the exact library names on the site before use.
- This script works with any valid `.excalidrawlib` file you provide.
### Troubleshooting
**Error: File not found**
- Check that the file path is correct
- Make sure the file has a `.excalidrawlib` extension
**Error: Invalid library file format**
- Ensure the file is a valid Excalidraw library file
- Check that it contains a `libraryItems` array
### License Considerations
When using third-party icon libraries:
- **AWS Architecture Icons**: Subject to AWS Content License
- **GCP Icons**: Subject to Google's terms
- **Other libraries**: Check each library's license
This script is for personal/organizational use. Redistribution of split icon files should comply with the original library's license terms.
## add-icon-to-diagram.py
Adds a specific icon from a split Excalidraw library into an existing `.excalidraw` diagram. The script handles coordinate translation and ID collision avoidance, and can optionally add a label under the icon.
### Prerequisites
- Python 3.6 or higher
- A diagram file (`.excalidraw`)
- A split icon library directory (created by `split-excalidraw-library.py`)
### Usage
```bash
python add-icon-to-diagram.py <diagram-path> <icon-name> <x> <y> [OPTIONS]
```
**Options**
- `--library-path PATH` : Path to the icon library directory (default: `aws-architecture-icons`)
- `--label TEXT` : Add a text label below the icon
-- `--use-edit-suffix` : Edit via `.excalidraw.edit` to avoid editor overwrite issues (enabled by default; pass `--no-use-edit-suffix` to disable)
### Examples
```bash
# Add EC2 icon at position (400, 300)
python add-icon-to-diagram.py diagram.excalidraw EC2 400 300
# Add VPC icon with label
python add-icon-to-diagram.py diagram.excalidraw VPC 200 150 --label "VPC"
# Safe edit mode is enabled by default (avoids editor overwrite issues)
# Use `--no-use-edit-suffix` to disable
python add-icon-to-diagram.py diagram.excalidraw EC2 500 300
# Add icon from another library
python add-icon-to-diagram.py diagram.excalidraw Compute-Engine 500 200 \
--library-path libraries/gcp-icons --label "API Server"
```
### What the Script Does
1. **Loads** the icon JSON from the library’s `icons/` directory
2. **Calculates** the icon’s bounding box
3. **Offsets** all coordinates to the target position
4. **Generates** unique IDs for all elements and groups
5. **Appends** the transformed elements to the diagram
6. **(Optional)** Adds a label beneath the icon
---
## add-arrow.py
Adds a straight arrow between two points in an existing `.excalidraw` diagram. Supports optional labels and line styles.
### Prerequisites
- Python 3.6 or higher
- A diagram file (`.excalidraw`)
### Usage
```bash
python add-arrow.py <diagram-path> <from-x> <from-y> <to-x> <to-y> [OPTIONS]
```
**Options**
- `--style {solid|dashed|dotted}` : Line style (default: `solid`)
- `--color HEX` : Arrow color (default: `#1e1e1e`)
- `--label TEXT` : Add a text label on the arrow
-- `--use-edit-suffix` : Edit via `.excalidraw.edit` to avoid editor overwrite issues (enabled by default; pass `--no-use-edit-suffix` to disable)
### Examples
```bash
# Simple arrow
python add-arrow.py diagram.excalidraw 300 200 500 300
# Arrow with label
python add-arrow.py diagram.excalidraw 300 200 500 300 --label "HTTPS"
# Dashed arrow with custom color
python add-arrow.py diagram.excalidraw 400 350 600 400 --style dashed --color "#7950f2"
# Safe edit mode is enabled by default (avoids editor overwrite issues)
# Use `--no-use-edit-suffix` to disable
python add-arrow.py diagram.excalidraw 300 200 500 300
```
### What the Script Does
1. **Creates** an arrow element from the given coordinates
2. **(Optional)** Adds a label near the arrow midpoint
3. **Appends** elements to the diagram
4. **Saves** the updated file
```
### scripts/add-arrow.py
```python
#!/usr/bin/env python3
"""
Add arrows (connections) between elements in Excalidraw diagrams.
Usage:
python add-arrow.py <diagram_path> <from_x> <from_y> <to_x> <to_y> [OPTIONS]
Options:
--style {solid|dashed|dotted} Arrow line style (default: solid)
--color HEX Arrow color (default: #1e1e1e)
--label TEXT Add text label on the arrow
--use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)
Examples:
python add-arrow.py diagram.excalidraw 300 200 500 300
python add-arrow.py diagram.excalidraw 300 200 500 300 --label "HTTP"
python add-arrow.py diagram.excalidraw 300 200 500 300 --style dashed --color "#7950f2"
python add-arrow.py diagram.excalidraw 300 200 500 300 --use-edit-suffix
"""
import json
import sys
import uuid
from pathlib import Path
from typing import Dict, Any
def generate_unique_id() -> str:
"""Generate a unique ID for Excalidraw elements."""
return str(uuid.uuid4()).replace('-', '')[:16]
def prepare_edit_path(diagram_path: Path, use_edit_suffix: bool) -> tuple[Path, Path | None]:
"""
Prepare a safe edit path to avoid editor overwrite issues.
Returns:
(work_path, final_path)
- work_path: file path to read/write during edit
- final_path: file path to rename back to (or None if not used)
"""
if not use_edit_suffix:
return diagram_path, None
if diagram_path.suffix != ".excalidraw":
return diagram_path, None
edit_path = diagram_path.with_suffix(diagram_path.suffix + ".edit")
if diagram_path.exists():
if edit_path.exists():
raise FileExistsError(f"Edit file already exists: {edit_path}")
diagram_path.rename(edit_path)
return edit_path, diagram_path
def finalize_edit_path(work_path: Path, final_path: Path | None) -> None:
"""Finalize edit by renaming .edit back to .excalidraw if needed."""
if final_path is None:
return
if final_path.exists():
final_path.unlink()
work_path.rename(final_path)
def create_arrow(
from_x: float,
from_y: float,
to_x: float,
to_y: float,
style: str = "solid",
color: str = "#1e1e1e",
label: str = None
) -> list:
"""
Create an arrow element.
Args:
from_x: Starting X coordinate
from_y: Starting Y coordinate
to_x: Ending X coordinate
to_y: Ending Y coordinate
style: Line style (solid, dashed, dotted)
color: Arrow color
label: Optional text label on the arrow
Returns:
List of elements (arrow and optional label)
"""
elements = []
# Arrow element
arrow = {
"id": generate_unique_id(),
"type": "arrow",
"x": from_x,
"y": from_y,
"width": to_x - from_x,
"height": to_y - from_y,
"angle": 0,
"strokeColor": color,
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": style,
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": None,
"index": "a0",
"roundness": {
"type": 2
},
"seed": 1000000000 + hash(f"{from_x}{from_y}{to_x}{to_y}") % 1000000000,
"version": 1,
"versionNonce": 2000000000 + hash(f"{from_x}{from_y}{to_x}{to_y}") % 1000000000,
"isDeleted": False,
"boundElements": [],
"updated": 1738195200000,
"link": None,
"locked": False,
"points": [
[0, 0],
[to_x - from_x, to_y - from_y]
],
"startBinding": None,
"endBinding": None,
"startArrowhead": None,
"endArrowhead": "arrow",
"lastCommittedPoint": None
}
elements.append(arrow)
# Optional label
if label:
mid_x = (from_x + to_x) / 2 - (len(label) * 5)
mid_y = (from_y + to_y) / 2 - 10
label_element = {
"id": generate_unique_id(),
"type": "text",
"x": mid_x,
"y": mid_y,
"width": len(label) * 10,
"height": 20,
"angle": 0,
"strokeColor": color,
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": None,
"index": "a0",
"roundness": None,
"seed": 1000000000 + hash(label) % 1000000000,
"version": 1,
"versionNonce": 2000000000 + hash(label) % 1000000000,
"isDeleted": False,
"boundElements": [],
"updated": 1738195200000,
"link": None,
"locked": False,
"text": label,
"fontSize": 14,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "top",
"containerId": None,
"originalText": label,
"autoResize": True,
"lineHeight": 1.25
}
elements.append(label_element)
return elements
def add_arrow_to_diagram(
diagram_path: Path,
from_x: float,
from_y: float,
to_x: float,
to_y: float,
style: str = "solid",
color: str = "#1e1e1e",
label: str = None
) -> None:
"""
Add an arrow to an Excalidraw diagram.
Args:
diagram_path: Path to the Excalidraw diagram file
from_x: Starting X coordinate
from_y: Starting Y coordinate
to_x: Ending X coordinate
to_y: Ending Y coordinate
style: Line style (solid, dashed, dotted)
color: Arrow color
label: Optional text label
"""
print(f"Creating arrow from ({from_x}, {from_y}) to ({to_x}, {to_y})")
arrow_elements = create_arrow(from_x, from_y, to_x, to_y, style, color, label)
if label:
print(f" With label: '{label}'")
# Load diagram
print(f"Loading diagram: {diagram_path}")
with open(diagram_path, 'r', encoding='utf-8') as f:
diagram = json.load(f)
# Add arrow elements
if 'elements' not in diagram:
diagram['elements'] = []
original_count = len(diagram['elements'])
diagram['elements'].extend(arrow_elements)
print(f" Added {len(arrow_elements)} elements (total: {original_count} -> {len(diagram['elements'])})")
# Save diagram
print(f"Saving diagram")
with open(diagram_path, 'w', encoding='utf-8') as f:
json.dump(diagram, f, indent=2, ensure_ascii=False)
print(f"✓ Successfully added arrow to diagram")
def main():
"""Main entry point."""
if len(sys.argv) < 6:
print("Usage: python add-arrow.py <diagram_path> <from_x> <from_y> <to_x> <to_y> [OPTIONS]")
print("\nOptions:")
print(" --style {solid|dashed|dotted} Line style (default: solid)")
print(" --color HEX Color (default: #1e1e1e)")
print(" --label TEXT Text label on arrow")
print(" --use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)")
print("\nExamples:")
print(" python add-arrow.py diagram.excalidraw 300 200 500 300")
print(" python add-arrow.py diagram.excalidraw 300 200 500 300 --label 'HTTP'")
sys.exit(1)
diagram_path = Path(sys.argv[1])
from_x = float(sys.argv[2])
from_y = float(sys.argv[3])
to_x = float(sys.argv[4])
to_y = float(sys.argv[5])
# Parse optional arguments
style = "solid"
color = "#1e1e1e"
label = None
# Default: use edit suffix to avoid editor overwrite issues
use_edit_suffix = True
i = 6
while i < len(sys.argv):
if sys.argv[i] == '--style':
if i + 1 < len(sys.argv):
style = sys.argv[i + 1]
if style not in ['solid', 'dashed', 'dotted']:
print(f"Error: Invalid style '{style}'. Must be: solid, dashed, or dotted")
sys.exit(1)
i += 2
else:
print("Error: --style requires an argument")
sys.exit(1)
elif sys.argv[i] == '--color':
if i + 1 < len(sys.argv):
color = sys.argv[i + 1]
i += 2
else:
print("Error: --color requires an argument")
sys.exit(1)
elif sys.argv[i] == '--label':
if i + 1 < len(sys.argv):
label = sys.argv[i + 1]
i += 2
else:
print("Error: --label requires a text argument")
sys.exit(1)
elif sys.argv[i] == '--use-edit-suffix':
use_edit_suffix = True
i += 1
elif sys.argv[i] == '--no-use-edit-suffix':
use_edit_suffix = False
i += 1
else:
print(f"Error: Unknown option: {sys.argv[i]}")
sys.exit(1)
# Validate inputs
if not diagram_path.exists():
print(f"Error: Diagram file not found: {diagram_path}")
sys.exit(1)
try:
work_path, final_path = prepare_edit_path(diagram_path, use_edit_suffix)
add_arrow_to_diagram(work_path, from_x, from_y, to_x, to_y, style, color, label)
finalize_edit_path(work_path, final_path)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == '__main__':
main()
```
### scripts/add-icon-to-diagram.py
```python
#!/usr/bin/env python3
"""
Add icons from Excalidraw libraries to diagrams.
This script reads an icon JSON file from an Excalidraw library, transforms its coordinates
to a target position, generates unique IDs, and adds it to an existing Excalidraw diagram.
Works with any Excalidraw library (AWS, GCP, Azure, Kubernetes, etc.).
Usage:
python add-icon-to-diagram.py <diagram_path> <icon_name> <x> <y> [OPTIONS]
Options:
--library-path PATH Path to the icon library directory (default: aws-architecture-icons)
--label TEXT Add a text label below the icon
--use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)
Examples:
python add-icon-to-diagram.py diagram.excalidraw EC2 500 300
python add-icon-to-diagram.py diagram.excalidraw EC2 500 300 --label "Web Server"
python add-icon-to-diagram.py diagram.excalidraw VPC 200 150 --library-path libraries/gcp-icons
python add-icon-to-diagram.py diagram.excalidraw EC2 500 300 --use-edit-suffix
"""
import json
import sys
import uuid
from pathlib import Path
from typing import Dict, List, Any, Tuple
def generate_unique_id() -> str:
"""Generate a unique ID for Excalidraw elements."""
return str(uuid.uuid4()).replace('-', '')[:16]
def calculate_bounding_box(elements: List[Dict[str, Any]]) -> Tuple[float, float, float, float]:
"""Calculate the bounding box (min_x, min_y, max_x, max_y) of icon elements."""
if not elements:
return (0, 0, 0, 0)
min_x = float('inf')
min_y = float('inf')
max_x = float('-inf')
max_y = float('-inf')
for element in elements:
if 'x' in element and 'y' in element:
x = element['x']
y = element['y']
width = element.get('width', 0)
height = element.get('height', 0)
min_x = min(min_x, x)
min_y = min(min_y, y)
max_x = max(max_x, x + width)
max_y = max(max_y, y + height)
return (min_x, min_y, max_x, max_y)
def transform_icon_elements(
elements: List[Dict[str, Any]],
target_x: float,
target_y: float
) -> List[Dict[str, Any]]:
"""
Transform icon elements to target coordinates with unique IDs.
Args:
elements: Icon elements from JSON file
target_x: Target X coordinate (top-left position)
target_y: Target Y coordinate (top-left position)
Returns:
Transformed elements with new coordinates and IDs
"""
if not elements:
return []
# Calculate bounding box
min_x, min_y, max_x, max_y = calculate_bounding_box(elements)
# Calculate offset
offset_x = target_x - min_x
offset_y = target_y - min_y
# Create ID mapping: old_id -> new_id
id_mapping = {}
for element in elements:
if 'id' in element:
old_id = element['id']
id_mapping[old_id] = generate_unique_id()
# Create group ID mapping
group_id_mapping = {}
for element in elements:
if 'groupIds' in element:
for old_group_id in element['groupIds']:
if old_group_id not in group_id_mapping:
group_id_mapping[old_group_id] = generate_unique_id()
# Transform elements
transformed = []
for element in elements:
new_element = element.copy()
# Update coordinates
if 'x' in new_element:
new_element['x'] = new_element['x'] + offset_x
if 'y' in new_element:
new_element['y'] = new_element['y'] + offset_y
# Update ID
if 'id' in new_element:
new_element['id'] = id_mapping[new_element['id']]
# Update group IDs
if 'groupIds' in new_element:
new_element['groupIds'] = [
group_id_mapping[gid] for gid in new_element['groupIds']
]
# Update binding references if they exist
if 'startBinding' in new_element and new_element['startBinding']:
if 'elementId' in new_element['startBinding']:
old_id = new_element['startBinding']['elementId']
if old_id in id_mapping:
new_element['startBinding']['elementId'] = id_mapping[old_id]
if 'endBinding' in new_element and new_element['endBinding']:
if 'elementId' in new_element['endBinding']:
old_id = new_element['endBinding']['elementId']
if old_id in id_mapping:
new_element['endBinding']['elementId'] = id_mapping[old_id]
# Update containerId if it exists
if 'containerId' in new_element and new_element['containerId']:
old_id = new_element['containerId']
if old_id in id_mapping:
new_element['containerId'] = id_mapping[old_id]
# Update boundElements if they exist
if 'boundElements' in new_element and new_element['boundElements']:
new_bound_elements = []
for bound_elem in new_element['boundElements']:
if isinstance(bound_elem, dict) and 'id' in bound_elem:
old_id = bound_elem['id']
if old_id in id_mapping:
bound_elem['id'] = id_mapping[old_id]
new_bound_elements.append(bound_elem)
new_element['boundElements'] = new_bound_elements
transformed.append(new_element)
return transformed
def load_icon(icon_name: str, library_path: Path) -> List[Dict[str, Any]]:
"""
Load icon elements from library.
Args:
icon_name: Name of the icon (e.g., "EC2", "VPC")
library_path: Path to the icon library directory
Returns:
List of icon elements
"""
icon_file = library_path / "icons" / f"{icon_name}.json"
if not icon_file.exists():
raise FileNotFoundError(f"Icon file not found: {icon_file}")
with open(icon_file, 'r', encoding='utf-8') as f:
icon_data = json.load(f)
return icon_data.get('elements', [])
def prepare_edit_path(diagram_path: Path, use_edit_suffix: bool) -> tuple[Path, Path | None]:
"""
Prepare a safe edit path to avoid editor overwrite issues.
Returns:
(work_path, final_path)
- work_path: file path to read/write during edit
- final_path: file path to rename back to (or None if not used)
"""
if not use_edit_suffix:
return diagram_path, None
if diagram_path.suffix != ".excalidraw":
return diagram_path, None
edit_path = diagram_path.with_suffix(diagram_path.suffix + ".edit")
if diagram_path.exists():
if edit_path.exists():
raise FileExistsError(f"Edit file already exists: {edit_path}")
diagram_path.rename(edit_path)
return edit_path, diagram_path
def finalize_edit_path(work_path: Path, final_path: Path | None) -> None:
"""Finalize edit by renaming .edit back to .excalidraw if needed."""
if final_path is None:
return
if final_path.exists():
final_path.unlink()
work_path.rename(final_path)
def create_text_label(text: str, x: float, y: float) -> Dict[str, Any]:
"""
Create a text label element.
Args:
text: Label text
x: X coordinate
y: Y coordinate
Returns:
Text element dictionary
"""
return {
"id": generate_unique_id(),
"type": "text",
"x": x,
"y": y,
"width": len(text) * 10, # Approximate width
"height": 20,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": None,
"index": "a0",
"roundness": None,
"seed": 1000000000 + hash(text) % 1000000000,
"version": 1,
"versionNonce": 2000000000 + hash(text) % 1000000000,
"isDeleted": False,
"boundElements": [],
"updated": 1738195200000,
"link": None,
"locked": False,
"text": text,
"fontSize": 16,
"fontFamily": 5, # Excalifont
"textAlign": "center",
"verticalAlign": "top",
"containerId": None,
"originalText": text,
"autoResize": True,
"lineHeight": 1.25
}
def add_icon_to_diagram(
diagram_path: Path,
icon_name: str,
x: float,
y: float,
library_path: Path,
label: str = None
) -> None:
"""
Add an icon to an Excalidraw diagram.
Args:
diagram_path: Path to the Excalidraw diagram file
icon_name: Name of the icon to add
x: Target X coordinate
y: Target Y coordinate
library_path: Path to the icon library directory
label: Optional text label to add below the icon
"""
# Load icon elements
print(f"Loading icon: {icon_name}")
icon_elements = load_icon(icon_name, library_path)
print(f" Loaded {len(icon_elements)} elements")
# Transform icon elements
print(f"Transforming to position ({x}, {y})")
transformed_elements = transform_icon_elements(icon_elements, x, y)
# Calculate icon bounding box for label positioning
if label and transformed_elements:
min_x, min_y, max_x, max_y = calculate_bounding_box(transformed_elements)
icon_width = max_x - min_x
icon_height = max_y - min_y
# Position label below icon, centered
label_x = min_x + (icon_width / 2) - (len(label) * 5)
label_y = max_y + 10
label_element = create_text_label(label, label_x, label_y)
transformed_elements.append(label_element)
print(f" Added label: '{label}'")
# Load diagram
print(f"Loading diagram: {diagram_path}")
with open(diagram_path, 'r', encoding='utf-8') as f:
diagram = json.load(f)
# Add transformed elements
if 'elements' not in diagram:
diagram['elements'] = []
original_count = len(diagram['elements'])
diagram['elements'].extend(transformed_elements)
print(f" Added {len(transformed_elements)} elements (total: {original_count} -> {len(diagram['elements'])})")
# Save diagram
print(f"Saving diagram")
with open(diagram_path, 'w', encoding='utf-8') as f:
json.dump(diagram, f, indent=2, ensure_ascii=False)
print(f"✓ Successfully added '{icon_name}' icon to diagram")
def main():
"""Main entry point."""
if len(sys.argv) < 5:
print("Usage: python add-icon-to-diagram.py <diagram_path> <icon_name> <x> <y> [OPTIONS]")
print("\nOptions:")
print(" --library-path PATH Path to icon library directory")
print(" --label TEXT Add text label below icon")
print(" --use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)")
print("\nExamples:")
print(" python add-icon-to-diagram.py diagram.excalidraw EC2 500 300")
print(" python add-icon-to-diagram.py diagram.excalidraw EC2 500 300 --label 'Web Server'")
sys.exit(1)
diagram_path = Path(sys.argv[1])
icon_name = sys.argv[2]
x = float(sys.argv[3])
y = float(sys.argv[4])
# Default library path
script_dir = Path(__file__).parent
default_library_path = script_dir.parent / "libraries" / "aws-architecture-icons"
# Parse optional arguments
library_path = default_library_path
label = None
# Default: use edit suffix to avoid editor overwrite issues
use_edit_suffix = True
i = 5
while i < len(sys.argv):
if sys.argv[i] == '--library-path':
if i + 1 < len(sys.argv):
library_path = Path(sys.argv[i + 1])
i += 2
else:
print("Error: --library-path requires a path argument")
sys.exit(1)
elif sys.argv[i] == '--label':
if i + 1 < len(sys.argv):
label = sys.argv[i + 1]
i += 2
else:
print("Error: --label requires a text argument")
sys.exit(1)
elif sys.argv[i] == '--use-edit-suffix':
use_edit_suffix = True
i += 1
elif sys.argv[i] == '--no-use-edit-suffix':
use_edit_suffix = False
i += 1
else:
print(f"Error: Unknown option: {sys.argv[i]}")
sys.exit(1)
# Validate inputs
if not diagram_path.exists():
print(f"Error: Diagram file not found: {diagram_path}")
sys.exit(1)
if not library_path.exists():
print(f"Error: Library path not found: {library_path}")
sys.exit(1)
try:
work_path, final_path = prepare_edit_path(diagram_path, use_edit_suffix)
add_icon_to_diagram(work_path, icon_name, x, y, library_path, label)
finalize_edit_path(work_path, final_path)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == '__main__':
main()
```
### scripts/split-excalidraw-library.py
```python
#!/usr/bin/env python3
"""
Excalidraw Library Splitter
This script splits an Excalidraw library file (*.excalidrawlib) into individual
icon JSON files and generates a reference.md file for easy lookup.
The script expects the following structure:
skills/excalidraw-studio/libraries/{icon-set-name}/
{icon-set-name}.excalidrawlib (place this file first)
Usage:
python split-excalidraw-library.py <path-to-library-directory>
Example:
python split-excalidraw-library.py skills/excalidraw-studio/libraries/aws-architecture-icons/
"""
import json
import os
import re
import sys
from pathlib import Path
def sanitize_filename(name: str) -> str:
"""
Sanitize icon name to create a valid filename.
Args:
name: Original icon name
Returns:
Sanitized filename safe for all platforms
"""
# Replace spaces with hyphens
filename = name.replace(' ', '-')
# Remove or replace special characters
filename = re.sub(r'[^\w\-.]', '', filename)
# Remove multiple consecutive hyphens
filename = re.sub(r'-+', '-', filename)
# Remove leading/trailing hyphens
filename = filename.strip('-')
return filename
def find_library_file(directory: Path) -> Path:
"""
Find the .excalidrawlib file in the given directory.
Args:
directory: Directory to search
Returns:
Path to the library file
Raises:
SystemExit: If no library file or multiple library files found
"""
library_files = list(directory.glob('*.excalidrawlib'))
if len(library_files) == 0:
print(f"Error: No .excalidrawlib file found in {directory}")
print(f"Please place a .excalidrawlib file in {directory} first.")
sys.exit(1)
if len(library_files) > 1:
print(f"Error: Multiple .excalidrawlib files found in {directory}")
print(f"Please keep only one library file in {directory}.")
sys.exit(1)
return library_files[0]
def split_library(library_dir: str) -> None:
"""
Split an Excalidraw library file into individual icon files.
Args:
library_dir: Path to the directory containing the .excalidrawlib file
"""
library_dir = Path(library_dir)
if not library_dir.exists():
print(f"Error: Directory not found: {library_dir}")
sys.exit(1)
if not library_dir.is_dir():
print(f"Error: Path is not a directory: {library_dir}")
sys.exit(1)
# Find the library file
library_path = find_library_file(library_dir)
print(f"Found library: {library_path.name}")
# Load library file
print(f"Loading library data...")
with open(library_path, 'r', encoding='utf-8') as f:
library_data = json.load(f)
# Validate library structure
if 'libraryItems' not in library_data:
print("Error: Invalid library file format (missing 'libraryItems')")
sys.exit(1)
# Create icons directory
icons_dir = library_dir / 'icons'
icons_dir.mkdir(exist_ok=True)
print(f"Output directory: {library_dir}")
# Process each library item (icon)
library_items = library_data['libraryItems']
icon_list = []
print(f"Processing {len(library_items)} icons...")
for item in library_items:
# Get icon name
icon_name = item.get('name', 'Unnamed')
# Create sanitized filename
filename = sanitize_filename(icon_name) + '.json'
# Save icon data
icon_path = icons_dir / filename
with open(icon_path, 'w', encoding='utf-8') as f:
json.dump(item, f, ensure_ascii=False, indent=2)
# Add to reference list
icon_list.append({
'name': icon_name,
'filename': filename
})
print(f" ✓ {icon_name} → {filename}")
# Sort icon list by name
icon_list.sort(key=lambda x: x['name'])
# Generate reference.md
library_name = library_path.stem
reference_path = library_dir / 'reference.md'
with open(reference_path, 'w', encoding='utf-8') as f:
f.write(f"# {library_name} Reference\n\n")
f.write(f"This directory contains {len(icon_list)} icons extracted from `{library_path.name}`.\n\n")
f.write("## Available Icons\n\n")
f.write("| Icon Name | Filename |\n")
f.write("|-----------|----------|\n")
for icon in icon_list:
f.write(f"| {icon['name']} | `icons/{icon['filename']}` |\n")
f.write("\n## Usage\n\n")
f.write("Each icon JSON file contains the complete `elements` array needed to render that icon in Excalidraw.\n")
f.write("You can copy the elements from these files into your Excalidraw diagrams.\n")
print(f"\n✅ Successfully split library into {len(icon_list)} icons")
print(f"📄 Reference file created: {reference_path}")
print(f"📁 Icons directory: {icons_dir}")
def main():
"""Main entry point."""
if hasattr(sys.stdout, "reconfigure"):
# Ensure consistent UTF-8 output on Windows consoles.
sys.stdout.reconfigure(encoding="utf-8")
if len(sys.argv) != 2:
print("Usage: python split-excalidraw-library.py <path-to-library-directory>")
print("\nExample:")
print(" python split-excalidraw-library.py skills/excalidraw-studio/libraries/aws-architecture-icons/")
print("\nNote: The directory should contain a .excalidrawlib file.")
sys.exit(1)
library_dir = sys.argv[1]
split_library(library_dir)
if __name__ == '__main__':
main()
```