πΊοΈ CANVAS-GEN: JSON CANVAS GENERATION FOR QUARTZ 5
Core Directive: Generate valid, visually stunning
.canvasfiles (JSON Canvas Spec 1.0) for Obsidian & Quartz 5'scanvas-pageplugin. Every canvas must pass the Validation Checklist and at least one Visual Feedback Loop before delivery.
Activation
Trigger when: creating/editing .canvas files, mapping module relationships, generating visual knowledge maps, building mind maps or flowcharts for Quartz/Obsidian vaults.
JSON Canvas Spec 1.0 β Type Reference
Document Structure
{ "nodes": [ ...CanvasNode[] ], "edges": [ ...CanvasEdge[] ] }
Node Types
| Type | Required Fields | Description |
|---|---|---|
text |
id, x, y, width, height, text |
Markdown text rendered inline |
file |
id, x, y, width, height, file |
Vault file reference (clickable, embeddable). Optional subpath for heading/block links |
link |
id, x, y, width, height, url |
External URL (rendered as iframe) |
group |
id, x, y, width, height |
Visual container. Optional label, background, backgroundStyle |
Edges
| Field | Required | Values |
|---|---|---|
id |
β | Unique string |
fromNode / toNode |
β | Node id references |
fromSide / toSide |
β | top | right | bottom | left |
fromEnd / toEnd |
β | none | arrow |
color |
β | Preset 1β6 or #hex |
label |
β | Relationship text on edge |
Preset Colors (Semantic Palette)
| Preset | Color | Hex | Recommended Meaning |
|---|---|---|---|
1 |
π΄ Red | #fb464c |
Attention / Critical / Deprecated |
2 |
π Orange | #e9973f |
Reference / Standards / Outputs |
3 |
π‘ Yellow | #e0de71 |
In-Progress / Plans / Execution |
4 |
π’ Green | #44cf6e |
Stable / Infrastructure / Active |
5 |
π΅ Cyan | #53dfdd |
Core / Hub / Central Framework |
6 |
π£ Purple | #a882ff |
Knowledge / Skills / Wisdom |
Layout Patterns
1. Hub-and-Spoke (Mind Map) β RECOMMENDED
Central node radiating edges to categorized groups. Best for: module maps, architecture overviews, dependency graphs.
βββββββββββ βββββββββββ
β π£ Skillsββββ skill ββ€ βββ extends βββΊβββββββββββ
β cs2docs β β π΅ HUB β β π’ Stackβ
β docskurbyββββ skill ββ€ (OMP) βββ publishes βΊβ quartz5 β
βββββββββββ ββββββ¬βββββ βββββββββββ
β
governs ββββββΌβββββ generates
βΌ βΌ
βββββββββββββββ ββββββββββββ
β π‘ Plans β β π Refs β
β unidialog β β llms.txt β
βββββββββββββββ ββββββββββββ
2. Kanban Columns
Side-by-side vertical lanes for status-tracking. Best for: project boards, workflow pipelines.
ββββββββββββββ ββββββββββββββ ββββββββββββββ
β π΄ Backlog β β π‘ Active β β π’ Done β
β task-1 β β task-3 β β task-5 β
β task-2 β β task-4 β β task-6 β
ββββββββββββββ ββββββββββββββ ββββββββββββββ
3. Flowchart / Pipeline
Top-to-bottom or left-to-right sequential flow. Best for: data pipelines, decision trees.
[Input] βββΊ [Process A] βββΊ [Decision] βββΊ [Output]
β
βΌ
[Fallback]
4. Convergence (Multi-Source Funnel)
Multiple independent sources converging into a single target. Best for: migration plans, data aggregation, system consolidation.
βββββββββββ
β Source A ββββ
βββββββββββ β ββββββββββββ
βββββββββββ βββββΊβ Target β
β Source B ββββ€ ββββββββββββ
βββββββββββ β
βββββββββββ β
β Source C ββββ
βββββββββββ
Hard Rules
Grid Alignment
- ALL coordinates MUST snap to multiples of 20px (100px grid recommended for groups)
- Groups: round to nearest 100
- Nodes inside groups: use group_x + 40 for x, uniform y spacing
Z-Order (Array Position)
nodes[0..N] β groups (FIRST = background layer)
nodes[N+1..] β text nodes (middle layer)
nodes[N+M..] β file/link nodes (LAST = foreground, clickable)
Groups MUST appear before their contained nodes in the array. The first element renders at the bottom; the last renders on top.
Sizing Standards
| Node Type | Width | Height | Notes |
|---|---|---|---|
| File node | 400β460px | 60β80px | Wider prevents label truncation |
| Text node | 250β500px | 80β200px | Scale to markdown content |
| Link node | 400px | 300px | Needs iframe space |
| Group | content_w + 80px | content_h + 120px | 40px padding all sides + 60px label area |
| Hub/Central | 200β300px | 60β100px | Prominent, not oversized |
Group Containment (Implicit)
Nodes belong to a group when their (x, y, width, height) falls entirely within the group's bounds. There is NO explicit parent-child field.
group_pad = 40 # inner padding
label_area = 60 # top space for group label
first_node_y = group_y + group_pad + label_area
node_x = group_x + group_pad
β οΈ Common bug: placing a node at group_y + group_pad (without adding label_area) causes the node to overlap the group's label text. Always use first_node_y = group_y + group_pad + label_area.
Spacing
- Between nodes (vertical): 20px gap
- Between groups: 200px minimum
- Group inner padding: 40px all sides
Edge Routing
- ALWAYS specify
fromSideandtoSideβ never let the renderer guess - Use
toEnd: "arrow"for directional relationships - Edge labels should be 1β2 words β verb or relationship type
- Cross-group edges exit/enter the side FACING the target group
- Intra-group edges use
top/bottomfor vertical adjacency
File Paths
- Quartz canvas: paths relative to
content/directory - Obsidian canvas: paths relative to vault root
- Example:
"file": "skills/cs2docs.md"(not absolute paths) - File nodes render the filename minus extension as the clickable label
ID Conventions
- Groups:
grp-{name}(e.g.,grp-skills) - Files:
f-{shortname}(e.g.,f-cs2docs) - Edges:
e-{from}-{to}(e.g.,e-hub-cs2docs) - Text:
txt-{purpose}(e.g.,txt-hub-desc) - Links:
lnk-{name}(e.g.,lnk-jsoncanvas)
Generation Template
import json
# Constants
NODE_W, NODE_H = 440, 50
GROUP_PAD, NODE_GAP = 40, 20
LABEL_AREA = 60
def group_height(n_items):
"""Calculate group height for n file nodes."""
return GROUP_PAD + LABEL_AREA + n_items * (NODE_H + NODE_GAP) - NODE_GAP + GROUP_PAD
def make_group(gid, label, x, y, n_items, color):
w = NODE_W + GROUP_PAD * 2
h = group_height(n_items)
return {"id": gid, "type": "group", "x": x, "y": y,
"width": w, "height": h, "color": color, "label": label}
def make_file_nodes(items, gx, gy, color):
"""items: list of (id, file_path) tuples."""
y0 = gy + GROUP_PAD + LABEL_AREA
return [{"id": fid, "type": "file", "file": fp,
"x": gx + GROUP_PAD, "y": y0 + i * (NODE_H + NODE_GAP),
"width": NODE_W, "height": NODE_H, "color": color}
for i, (fid, fp) in enumerate(items)]
def make_text_node(tid, text, x, y, w=300, h=100, color="5"):
return {"id": tid, "type": "text", "text": text,
"x": x, "y": y, "width": w, "height": h, "color": color}
def make_link_node(lid, url, x, y, w=400, h=300, color="5"):
return {"id": lid, "type": "link", "url": url,
"x": x, "y": y, "width": w, "height": h, "color": color}
def make_edge(eid, fr, fs, to, ts, color, label=None):
e = {"id": eid, "fromNode": fr, "fromSide": fs,
"toNode": to, "toSide": ts, "toEnd": "arrow", "color": color}
if label: e["label"] = label
return e
# Build canvas dict, then: json.dumps({"nodes": [...], "edges": [...]}, indent=2)
Validation Checklist
Before writing a .canvas file, verify:
- All
fromNode/toNodein edges reference valid nodeidvalues - Groups appear BEFORE their contained nodes in the
nodesarray - All coordinates are multiples of 20
- No two nodes overlap (unless group intentionally contains them)
- File paths are relative and correct (verify files exist)
- Colors encode semantic meaning, not just aesthetics
- Edge
fromSide/toSidepoint toward the connected node's actual position - Directional edges have
toEnd: "arrow"set explicitly - JSON is valid and pretty-printed (2-space indent)
- Total file size < 50KB (Quartz parses at build time)
π Visual Feedback Loop (MANDATORY)
Every canvas MUST go through at least one deployβscreenshotβcorrect cycle before being considered done. Canvas JSON is spatial β overlaps, misaligned edges, truncated labels, and broken containment are invisible in JSON and only surface in the rendered viewport.
Loop Steps
βββββββββββββββ βββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β 1. Generate ββββββΊβ 2. Deploy ββββββΊβ 3. ScreenshotββββββΊβ 4. Evaluate β
β .canvas β β (build) β β (browser) β β (visual) β
βββββββββββββββ βββββββββββββββββ ββββββββββββββββ ββββββββ¬ββββββββ
β² β
β ββββββββββββββββ β
ββββββββββββββββββββββ€ 5. Patch JSONββββ issues found? ββββββββ
β & re-loop β YES β goto 2
ββββββββββββββββ NO β β
done
Step Details
| # | Action | Method | What to Check |
|---|---|---|---|
| 1 | Generate | Write .canvas JSON |
Pass Validation Checklist (above) |
| 2 | Deploy | npx quartz build or deploy script |
Build exits clean, no parse errors |
| 3 | Screenshot | tab.screenshot({ fullPage: true }) on deployed URL |
Capture the full rendered canvas viewport |
| 4 | Evaluate | Visual inspection of screenshot | See checklist below |
| 5 | Patch | Edit canvas JSON, fix coordinates/sizes/edges | Only if issues found β then re-loop from step 2 |
Visual Evaluation Checklist
Inspect the screenshot for these issues β any failure = patch & re-loop:
- Node overlap: two nodes visually stacking or clipping each other
- Label truncation: text cut off, ellipsized, or overflowing node bounds
- Broken containment: nodes visually outside their parent group border
- Edge misrouting: edge crossing through unrelated nodes instead of routing around
- Edge misalignment: edge entering wrong side of a node (e.g., enters
leftbut target is to theright) - Orphan nodes: visible nodes with no edges (intentional orphans must be inside a group)
- Color confusion: two semantically different groups sharing the same color
- Whitespace imbalance: one quadrant packed, another empty β redistribute for visual harmony
- Group label visibility: group emoji/label readable, not occluded by contained nodes
- Edge label readability: relationship text visible, not overlapping other edges
Hard Rules
- NEVER skip the screenshot step. JSON-only validation catches ~60% of spatial bugs; the other 40% are layout/visual and require rendered confirmation.
- At least 1 full loop (generate β deploy β screenshot β evaluate) is mandatory. Zero visual checks = incomplete canvas.
- Max 3 correction loops. If the canvas still has visual issues after 3 patches, step back and redesign the layout pattern (wrong pattern chosen, too many nodes for the space, etc.).
- Screenshot the FULL canvas (
fullPage: trueor zoom-to-fit) β a viewport crop misses off-screen issues. - Log each loop iteration in your reasoning: what was wrong, what coordinates/sizes changed, and what the fix addresses.
Quartz 5 Integration
Plugin Config
# quartz.config.yaml
plugins:
- source: github:quartz-community/canvas-page
enabled: true
layout:
byPageType:
canvas: {} # uses default CanvasFrame (fullscreen, pan/zoom)
Plugin Options (TypeScript override)
CanvasPage({
enableInteraction: true, // pan/zoom (default: true)
initialZoom: 1, // start zoom level
minZoom: 0.1, // zoom out limit
maxZoom: 5, // zoom in limit
})
Embedding Canvas in Markdown
![[my-canvas.canvas]] <!-- Obsidian-style transclusion -->
Canvas Features in Quartz
- File nodes: render as clickable links with popover previews
- Text nodes: full GFM markdown rendering (headings, bold, lists, code)
- Link nodes: iframe embed with fallback link
- Groups: dashed-border containers with colored labels
- Edges: SVG paths with optional arrow markers and text labels
- Controls: zoom in/out buttons, reset view, sidebar toggle
- Fullscreen: toggle for embedded canvas contexts
Anti-Patterns
| β Don't | β Do Instead |
|---|---|
| Use random colors for aesthetics | Assign semantic meaning to each color preset |
Omit fromSide/toSide on edges |
Always specify both sides explicitly |
| Place groups after their children | Groups FIRST in the nodes array |
| Use absolute file paths | Relative to content/vault root |
| Make nodes too narrow (< 300px) | 400β460px prevents label truncation |
| Skip edge labels | Label every cross-group edge with relationship verb |
| Nest groups inside groups | JSON Canvas 1.0 has no nested group support |
| Use IDs with spaces | Kebab-case: grp-my-group, f-my-file |
| Skip the visual feedback loop | Deploy β screenshot β evaluate at least once before marking done |
Leave // comments in .canvas JSON |
Strip all comments; JSON has no comment syntax |
π¦ Reference Canvas (Condensed from Quartz OG Canvas.canvas)
Canonical example exercising every node type, group containment, edge routing, preset + hex colors, labels, and z-order. Condensed from the official
quartz-community/canvas-pagedemo (24 nodes, 9 edges β 19 nodes, 8 edges below, all structural patterns preserved).
β οΈ The
//comments below are educational annotations (JSONC). Strip all comments before writing to a real.canvasfile β JSON has no comment syntax.
{
"nodes": [
// ββ Z-LAYER 0: Groups (background) ββββββββββββββββββββββ
// Groups MUST come first β they render behind everything.
{
"id": "grp-types", "type": "group",
"x": 0, "y": 260, "width": 1220, "height": 460,
"color": "6", "label": "π£ Node Types"
},
{
"id": "grp-colors", "type": "group",
"x": 0, "y": 800, "width": 1220, "height": 300,
"color": "4", "label": "π’ Preset Colors"
},
{
"id": "grp-edges", "type": "group",
"x": 0, "y": 1180, "width": 1220, "height": 300,
"color": "3", "label": "π‘ Edges & Connections"
},
// ββ Z-LAYER 1: Text nodes (middle) ββββββββββββββββββββββ
// Title node β standalone, above all groups
{
"id": "txt-title", "type": "text",
"x": 0, "y": 0, "width": 560, "height": 200, "color": "5",
"text": "# CanvasPage Plugin\n\nRenders [JSON Canvas](https://jsoncanvas.org) `.canvas` files as interactive, pannable canvas pages.\n\nInstall: `npx quartz plugin add github:quartz-community/canvas-page`"
},
// Text node INSIDE grp-types (containment = geometry falls within group bounds)
{
"id": "txt-text-demo", "type": "text",
"x": 40, "y": 320, "width": 360, "height": 280, "color": "1",
"text": "## Text Nodes\n\nRender **Markdown** with GFM:\n- **Bold**, *italic*, ~~strike~~\n- [Links](https://jsoncanvas.org)\n- `inline code`\n- Lists"
},
// Text node describing file nodes, INSIDE grp-types
{
"id": "txt-file-info", "type": "text",
"x": 440, "y": 320, "width": 360, "height": 160, "color": "2",
"text": "## File Nodes\n\nReference vault pages. Clickable with **popover preview** on hover."
},
// Color swatches INSIDE grp-colors (6 preset + 1 hex demo)
{
"id": "txt-c1", "type": "text", "text": "**1** π΄ Red",
"x": 40, "y": 860, "width": 160, "height": 60, "color": "1"
},
{
"id": "txt-c2", "type": "text", "text": "**2** π Orange",
"x": 220, "y": 860, "width": 160, "height": 60, "color": "2"
},
{
"id": "txt-c3", "type": "text", "text": "**3** π‘ Yellow",
"x": 400, "y": 860, "width": 160, "height": 60, "color": "3"
},
{
"id": "txt-c4", "type": "text", "text": "**4** π’ Green",
"x": 580, "y": 860, "width": 160, "height": 60, "color": "4"
},
{
"id": "txt-c5", "type": "text", "text": "**5** π΅ Cyan",
"x": 760, "y": 860, "width": 160, "height": 60, "color": "5"
},
{
"id": "txt-c6", "type": "text", "text": "**6** π£ Purple",
"x": 940, "y": 860, "width": 160, "height": 60, "color": "6"
},
// Custom hex color demo
{
"id": "txt-hex", "type": "text", "text": "**Custom** `#ff6600`",
"x": 400, "y": 960, "width": 360, "height": 60, "color": "#ff6600"
},
// Edge demo nodes INSIDE grp-edges
{
"id": "txt-edge-src", "type": "text",
"x": 40, "y": 1240, "width": 300, "height": 100, "color": "1",
"text": "## Edges\n\nSVG paths with **labels**, **arrows**, **colors**."
},
{
"id": "txt-edge-label", "type": "text",
"x": 460, "y": 1240, "width": 260, "height": 80, "color": "4",
"text": "Edge with **label** + arrow."
},
{
"id": "txt-edge-hex", "type": "text",
"x": 460, "y": 1360, "width": 260, "height": 80, "color": "2",
"text": "Edge with **custom color** `#ff6600`."
},
{
"id": "txt-edge-preset", "type": "text",
"x": 840, "y": 1240, "width": 300, "height": 100, "color": "6",
"text": "Edges use **preset 1β6** or hex."
},
// ββ Z-LAYER 2: File + Link nodes (foreground, clickable) β
{
"id": "f-canvas-doc", "type": "file",
"file": "plugins/CanvasPage.md",
"x": 440, "y": 520, "width": 360, "height": 80, "color": "4"
},
{
"id": "lnk-spec", "type": "link",
"url": "https://jsoncanvas.org/spec/1.0/",
"x": 840, "y": 320, "width": 360, "height": 200, "color": "5"
}
],
"edges": [
// Title β Node Types group (top-down flow)
{ "id": "e-title-types", "fromNode": "txt-title", "fromSide": "bottom", "toNode": "grp-types", "toSide": "top", "label": "supports", "toEnd": "arrow" },
// Info β File node (parent-child within group)
{ "id": "e-info-file", "fromNode": "txt-file-info", "fromSide": "bottom", "toNode": "f-canvas-doc", "toSide": "top", "color": "4", "toEnd": "arrow" },
// Group-to-group chain (vertical flow)
{ "id": "e-types-colors", "fromNode": "grp-types", "fromSide": "bottom", "toNode": "grp-colors", "toSide": "top", "toEnd": "arrow" },
{ "id": "e-colors-edges", "fromNode": "grp-colors", "fromSide": "bottom", "toNode": "grp-edges", "toSide": "top", "toEnd": "arrow" },
// Labeled edge demo (left β right within grp-edges)
{ "id": "e-demo-label", "fromNode": "txt-edge-src", "fromSide": "right", "toNode": "txt-edge-label", "toSide": "left", "label": "labeled edge", "toEnd": "arrow" },
// Hex-colored edge demo
{ "id": "e-demo-hex", "fromNode": "txt-edge-src", "fromSide": "right", "toNode": "txt-edge-hex", "toSide": "left", "color": "#ff6600", "toEnd": "arrow" },
// Preset-colored edge demo
{ "id": "e-demo-preset", "fromNode": "txt-edge-label","fromSide": "right", "toNode": "txt-edge-preset", "toSide": "left", "color": "6", "toEnd": "arrow" },
// File β Link cross-type edge (shows file-to-link wiring)
{ "id": "e-file-spec", "fromNode": "f-canvas-doc", "fromSide": "right", "toNode": "lnk-spec", "toSide": "left", "label": "spec", "color": "5", "toEnd": "arrow" }
]
}
What This Example Demonstrates
| Pattern | Where | Lesson |
|---|---|---|
| Z-order | Groups β Text β File/Link | Array position = render layer; groups paint first |
| Group containment | txt-text-demo at (40,320) inside grp-types at (0,260,1220,460) |
Geometry-only β no parent field |
| All 4 node types | text, file, link, group |
Complete spec coverage in one canvas |
| Preset colors 1β6 | Color swatch row | Consistent semantic palette |
| Custom hex | #ff6600 on node + edge |
Escape hatch beyond 6 presets |
| Labeled edges | "label": "supports", "labeled edge" |
Relationship verbs on connectors |
| Colored edges | Preset "4", "6" and hex "#ff6600" |
Edge color independent of node color |
| Edge side routing | fromSide: "bottom" β toSide: "top" (vertical), right β left (horizontal) |
Explicit side prevents renderer guessing |
| Group-to-group flow | grp-types β grp-colors β grp-edges |
Chain groups for vertical section flow |
| ID conventions | grp-*, txt-*, f-*, lnk-*, e-* |
Prefixed, kebab-case, unique |