Back to Directory
canvas-gen

canvas-gen

canvas-gen (JSON Canvas Generation for Quartz 5/Obsidian): Generates valid, visually stunning .canvas files following JSON Canvas Spec 1.0 with layout patterns, validation, and visual feedback loops.

πŸ—ΊοΈ CANVAS-GEN: JSON CANVAS GENERATION FOR QUARTZ 5

Core Directive: Generate valid, visually stunning .canvas files (JSON Canvas Spec 1.0) for Obsidian & Quartz 5's canvas-page plugin. 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 fromSide and toSide β€” 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/bottom for 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/toNode in edges reference valid node id values
  • Groups appear BEFORE their contained nodes in the nodes array
  • 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/toSide point 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 left but target is to the right)
  • 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: true or 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-page demo (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 .canvas file β€” 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