@diagrammo/dgmo 0.36.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.cursorrules +3 -0
  2. package/.github/copilot-instructions.md +3 -0
  3. package/.windsurfrules +3 -0
  4. package/SKILL.md +3 -0
  5. package/dist/advanced.cjs +1058 -135
  6. package/dist/advanced.d.cts +122 -8
  7. package/dist/advanced.d.ts +122 -8
  8. package/dist/advanced.js +1055 -133
  9. package/dist/auto.cjs +1047 -132
  10. package/dist/auto.js +96 -95
  11. package/dist/auto.mjs +1048 -130
  12. package/dist/cli.cjs +117 -116
  13. package/dist/editor.cjs +4 -1
  14. package/dist/editor.js +4 -1
  15. package/dist/highlight.cjs +3 -0
  16. package/dist/highlight.js +3 -0
  17. package/dist/index.cjs +1043 -131
  18. package/dist/index.js +1044 -129
  19. package/docs/ai-integration.md +3 -0
  20. package/docs/language-reference.md +106 -7
  21. package/gallery/fixtures/map-coastline.dgmo +2 -2
  22. package/gallery/fixtures/map-route.dgmo +5 -5
  23. package/gallery/fixtures/treemap-branch.dgmo +15 -0
  24. package/gallery/fixtures/treemap-heat.dgmo +12 -0
  25. package/gallery/fixtures/treemap-tag-heat.dgmo +21 -0
  26. package/gallery/fixtures/treemap-tags.dgmo +18 -0
  27. package/package.json +1 -1
  28. package/src/advanced.ts +11 -0
  29. package/src/boxes-and-lines/layout-search.ts +19 -1
  30. package/src/chart-type-registry.ts +4 -0
  31. package/src/chart-types.ts +5 -0
  32. package/src/completion.ts +64 -0
  33. package/src/d3.ts +30 -18
  34. package/src/editor/highlight-api.ts +2 -0
  35. package/src/editor/keywords.ts +3 -0
  36. package/src/infra/parser.ts +95 -38
  37. package/src/map/dimensions.ts +19 -2
  38. package/src/map/layout.ts +91 -7
  39. package/src/map/parser.ts +51 -29
  40. package/src/map/renderer.ts +10 -0
  41. package/src/map/types.ts +9 -8
  42. package/src/treemap/layout.ts +189 -0
  43. package/src/treemap/parser.ts +475 -0
  44. package/src/treemap/renderer.ts +698 -0
  45. package/src/treemap/types.ts +57 -0
  46. package/src/utils/parsing.ts +1 -0
  47. package/src/utils/reserved-key-registry.ts +5 -0
@@ -143,6 +143,7 @@ Valid markup is the floor, not the goal. A good diagram reads at a glance. Apply
143
143
  | `quadrant` | 2×2 positioning matrix |
144
144
  | `pyramid` | stacked hierarchy of layers (Maslow, DIKW) |
145
145
  | `ring` | concentric rings of nested categories |
146
+ | `treemap` | nested rectangles sized by value (budgets, disk usage, portfolios) |
146
147
  | `map` | geographic concept map: regions, points, routes |
147
148
  | `wireframe` | low-fidelity UI layout with panels and controls |
148
149
  | `bar` | categorical comparisons |
@@ -167,6 +168,8 @@ Valid markup is the floor, not the goal. A good diagram reads at a glance. Apply
167
168
 
168
169
  **Need more than the index gives you?** Fetch the per-type section: MCP `get_language_reference(type)` / `get_examples(type)`, or read that type's section below. The `suggest_chart_type` tool returns the chosen type's section automatically.
169
170
 
171
+ **When the type isn't obvious, ask — don't guess.** Call `suggest_chart_type` first. If it returns an `⚠️ ASK THE USER` directive (the request is ambiguous between candidates, or nothing matched), present those candidate options to the user and wait for their pick before generating — never silently choose. A confident result (high/medium) you can proceed with.
172
+
170
173
  ### Common examples (curated, parse-clean)
171
174
 
172
175
  _The most common types, inline so you can generate them without a fetch. For the other 37, get the per-type section (see below)._
@@ -129,6 +129,7 @@ Valid markup is the floor, not the goal. A good diagram reads at a glance. Apply
129
129
  | `quadrant` | 2×2 positioning matrix |
130
130
  | `pyramid` | stacked hierarchy of layers (Maslow, DIKW) |
131
131
  | `ring` | concentric rings of nested categories |
132
+ | `treemap` | nested rectangles sized by value (budgets, disk usage, portfolios) |
132
133
  | `map` | geographic concept map: regions, points, routes |
133
134
  | `wireframe` | low-fidelity UI layout with panels and controls |
134
135
  | `bar` | categorical comparisons |
@@ -152,6 +153,8 @@ Valid markup is the floor, not the goal. A good diagram reads at a glance. Apply
152
153
  | `function` | mathematical expressions (colon required: `f(x): x^2`) |
153
154
 
154
155
  **Need more than the index gives you?** Fetch the per-type section: MCP `get_language_reference(type)` / `get_examples(type)`, or read that type's section below. The `suggest_chart_type` tool returns the chosen type's section automatically.
156
+
157
+ **When the type isn't obvious, ask — don't guess.** Call `suggest_chart_type` first. If it returns an `⚠️ ASK THE USER` directive (the request is ambiguous between candidates, or nothing matched), present those candidate options to the user and wait for their pick before generating — never silently choose. A confident result (high/medium) you can proceed with.
155
158
  <!-- AI-CORE:TYPE-INDEX end -->
156
159
 
157
160
  <!-- The grouped data-chart / matrix ids share one documented section. This map is the single source of truth for which TYPE block each id resolves to (read by gen-ai-core.mjs and the MCP slicer). -->
@@ -3087,6 +3090,100 @@ When ring band thickness would force the in-band label below the readable floor
3087
3090
 
3088
3091
  ---
3089
3092
 
3093
+ ## 24C. Treemaps
3094
+
3095
+ <!-- TYPE:treemap -->
3096
+
3097
+ <!-- TIPS start -->
3098
+ **Styling tips:** Put the unit in the title (`Cloud Spend ($)`) — there is no currency/format directive. Reach for `depth N` once a tree goes past ~3 levels (deeper subtrees collapse to a drillable block) and `other-below N` to fold a long tail of tiny categories into one **Other** box. Pick the color mode to match intent: tags for categories, `heat` for a gain/loss heatmap, branch for zero-config structure.
3099
+ <!-- TIPS end -->
3100
+
3101
+ Nested rectangles sized by value — the canonical way to show a hierarchy's proportions at a glance (budgets, disk usage, portfolios, taxonomies). Indentation is the hierarchy; a bare trailing number on a leaf is its size; parents auto-sum their children. Built on a squarified layout so cells keep good aspect ratios.
3102
+
3103
+ ### Declaration
3104
+
3105
+ ```
3106
+ treemap [Title with units, e.g. "Cloud Spend ($)"]
3107
+
3108
+ Branch
3109
+ Leaf 320
3110
+ Leaf 180
3111
+ ```
3112
+
3113
+ ### Example
3114
+
3115
+ ```
3116
+ treemap Q3 Budget
3117
+
3118
+ tag Team as t
3119
+ Eng blue
3120
+ Sales green
3121
+ Ops orange
3122
+
3123
+ Engineering t: Eng
3124
+ Platform 320
3125
+ Mobile 180
3126
+ Data 140
3127
+ Go-to-Market t: Sales
3128
+ Ads 90
3129
+ Field Sales 130
3130
+ Operations t: Ops
3131
+ Cloud 110
3132
+ Support 70
3133
+ ```
3134
+
3135
+ ### Value & hierarchy
3136
+
3137
+ - **Indentation** sets the hierarchy (same model as mindmap / org).
3138
+ - A **bare trailing number** on a leaf is its size (`Platform 320`) — the funnel/sankey idiom (§1.5). No thousands commas (`1_000` is fine).
3139
+ - **Parents auto-sum** their descendants; a trailing number on a branch line is ignored with a warning (auto-sum always wins).
3140
+ - To keep a label that genuinely ends in a digit, **quote it** (`"Region 5"`); unquoted `Region 5` parses as label `Region`, value `5`.
3141
+
3142
+ ### Color modes
3143
+
3144
+ Three modes, with the source-declared default selected as **tag → heat → branch** (the desktop app adds a runtime switcher that previews the others without editing source):
3145
+
3146
+ | Mode | When it's the default | Coloring |
3147
+ | ---------- | --------------------------- | ------------------------------------------------------------ |
3148
+ | **tag** | a `tag` group is declared | categorical color per tag value (legend hover-dims) |
3149
+ | **heat** | any `heat:` value / `heat` directive | value ramp over a second per-node `heat:` metric |
3150
+ | **branch** | neither tags nor heat | each top-level branch a distinct hue, tinted with depth |
3151
+
3152
+ ### Tags
3153
+
3154
+ Declare a `tag` group before content and apply it inline (`Engineering t: Eng`); children inherit the tag unless they override it. See §1.3.
3155
+
3156
+ ### Heat metric & ramp
3157
+
3158
+ Add a per-node `heat:` number (a second metric, distinct from size; negatives/floats ok) and name the ramp with a `heat <Label> [low] [high]` directive. Colors are optional and **data-aware**: data that crosses zero defaults to a diverging `red·neutral·green` ramp with the midpoint pinned at 0; one-sign data defaults to a sequential `neutral·accent` ramp. One explicit color = `neutral→hue`; two = `low → high` (with a neutral midpoint). **Named palette colors only — no hex.** `heat:` is used instead of `score:` (which journey-map reserves for 1–5 emotion, §22).
3159
+
3160
+ ### Node Metadata
3161
+
3162
+ | Key | Type | Default | Description |
3163
+ | ------- | --------------- | ------- | -------------------------------------------- |
3164
+ | `heat` | number | — | Color-by-value metric (signed / float ok) |
3165
+ | `<tag>` | declared value | — | Tag value via the group's alias or name |
3166
+
3167
+ ### Directives
3168
+
3169
+ | Directive | Effect |
3170
+ | --------------- | --------------------------------------------------------------------------------------- |
3171
+ | `heat <Label>` | Name (and optionally color) the value ramp; pairs with the `heat:` key. |
3172
+ | `depth N` | Render N levels; deeper subtrees collapse to a drillable solid block (a render budget). |
3173
+ | `other-below N` | Roll children under N% of their parent into a single hatched **Other** bucket (opt-in). |
3174
+ | `no-values` | Hide value labels. |
3175
+ | `no-percent` | Hide percentage labels. |
3176
+ | `no-headers` | Hide parent header bars. |
3177
+ | `no-legend` | Hide the legend. |
3178
+
3179
+ Numbers auto-compact (1.2M, 940k). Units live in the title — there is no format/currency directive.
3180
+
3181
+ ### Interactivity vs export
3182
+
3183
+ In the desktop app a treemap is interactive: click a parent to drill in (with a breadcrumb), hover for a tooltip (path / value / % / heat), and flip the color mode. Static SVG / PNG export is the clean, full tree with none of that chrome — the same interactive-vs-export split the map chart uses.
3184
+
3185
+ ---
3186
+
3090
3187
  ## 24A. RACI Matrices (RACI / RASCI / DACI)
3091
3188
 
3092
3189
  <!-- TYPE:raci -->
@@ -3264,15 +3361,17 @@ poi Austin red # direct marker color (trailing token, §1.5
3264
3361
 
3265
3362
  ### Routes & connectors
3266
3363
 
3267
- `route <origin> [style: arc]` — an ordered, auto-numbered voyage; the origin gets a distinct marker. Each indented line is a `[-label->] destination` leg that continues from the previous stop — a leg is an edge (in-arrow label, `value:` thickness, `->`/`~>` or header `style: arc` shape). A **tag on a leg colours the LINE** (categorical leg-type colouring — flights vs cruises vs trains); `label:`/`as` still name the destination stop. Repeat the origin as a leg's destination to close a loop (no second marker):
3364
+ `route <origin>` — an ordered, auto-numbered voyage; the origin gets a distinct marker, and the header takes **no shape option**. Each indented line is a `<arrow> destination` leg that continues from the previous stop, using the same indented arrow idiom as a sitemap — a leg is an edge (in-arrow label, `value:` thickness, and the **arrow glyph alone sets shape**: `-…->` straight, `~…~>` arc, mixable per leg). The **arrow is required and must be directional** — a bare destination errors, and so does an undirected `--`/`~~` glyph (a voyage always flows forward; use `->`/`~>`). A **tag on a leg colours the LINE** (categorical leg-type colouring — flights vs cruises vs trains); `label:`/`as` still name the destination stop. Repeat the origin as a leg's destination to close a loop (no second marker):
3268
3365
 
3269
3366
  ```
3270
- route Miami style: arc
3271
- -weigh anchor-> Havana value: 40
3272
- -> Kingston
3273
- -> Miami # destination == origin → closed loop
3367
+ route Miami
3368
+ ~weigh anchor~> Havana value: 40 # arc leg (its own ~> glyph)
3369
+ ~> Kingston
3370
+ ~> Miami # destination == origin → closed loop
3274
3371
  ```
3275
3372
 
3373
+ There is no header `style:` — bow a whole voyage by using `~>` on every leg.
3374
+
3276
3375
  Native edges handle any other connection (no `link`/`leg` keyword). A token draws an arrowhead iff it ends in `>`, an arc iff it starts with `~`; drop the `>` for a plain line when an arrow would mislead. The label always sits between the delimiters:
3277
3376
 
3278
3377
  | | no label | labeled |
@@ -3292,9 +3391,9 @@ JFK ~daily~> LHR # (NOT indented — indented legs are only valid
3292
3391
  JFK ~2x daily~> SFO # inside a `route` block, which is an ordered voyage)
3293
3392
  ```
3294
3393
 
3295
- Each native edge is ONE full line — `<origin> <connector> <destination>`. There is no indented-edge / shared-source hub form for native edges (it errors as "Malformed edge"); to fan out from a hub, repeat the origin on each line as above. Endpoints auto-create their POIs, so a connected map needs no separate `poi` lines.
3394
+ Each native edge is ONE full line — `<origin> <connector> <destination>`. A top-level indented `-> dest` with no parent errors as "Malformed edge"; to fan out, either repeat the origin on each line as above OR indent the spokes under a `poi` (a hub — `poi JFK` then indented `~> LAX`, `~> LHR`). An indented child under a `poi` that names a place **must carry an arrow** — a bare name there errors as a malformed hub edge. Endpoints auto-create their POIs, so a connected map needs no separate `poi` lines.
3296
3395
 
3297
- There is no geographic path-finding and no `surface:` — legs are plain straight or arced geometry (`style: arc` to bow one) and may cross land.
3396
+ There is no geographic path-finding and no `surface:` — legs are plain straight or arced geometry (use a `~>` glyph to bow one) and may cross land.
3298
3397
 
3299
3398
  **Tagging the line.** A tag value on any connector or route leg colours the **line itself** (the universal tag model applied to edges) — the "trip-leg type" idiom. Edge-only tag groups show in the legend as a line-colour key; hovering a legend entry dims the lines that don't match. To categorise a **stop** instead, tag its own `poi` line. An edge-only tag group never tints regions or suppresses the colorize dress.
3300
3399
 
@@ -3,5 +3,5 @@ map Smuggler's Run
3
3
  poi Havana value: 90
4
4
  poi Kingston value: 120
5
5
 
6
- route Havana style: arc
7
- -run the blockade-> Kingston
6
+ route Havana
7
+ ~run the blockade~> Kingston
@@ -1,7 +1,7 @@
1
1
  map Caribbean Cruise
2
2
 
3
- route Miami style: arc
4
- -weigh anchor-> Havana
5
- -> Kingston
6
- -> Santo Domingo
7
- -> Miami
3
+ route Miami
4
+ ~weigh anchor~> Havana
5
+ ~> Kingston
6
+ ~> Santo Domingo
7
+ ~> Miami
@@ -0,0 +1,15 @@
1
+ treemap Cloud Spend ($)
2
+
3
+ Compute
4
+ EC2 1240000
5
+ Lambda 180000
6
+ GPU 640000
7
+ Storage
8
+ S3 420000
9
+ EBS 210000
10
+ Network
11
+ Egress 380000
12
+ CDN 95000
13
+ Data
14
+ Warehouse 540000
15
+ Streaming 160000
@@ -0,0 +1,12 @@
1
+ treemap Portfolio — today
2
+ heat Day change %
3
+
4
+ AAPL 180 heat: -2.4
5
+ NVDA 220 heat: 4.1
6
+ MSFT 160 heat: 0.8
7
+ Energy
8
+ XOM 60 heat: -1.2
9
+ CVX 45 heat: -0.6
10
+ Financials
11
+ JPM 70 heat: 1.9
12
+ BAC 40 heat: -3.1
@@ -0,0 +1,21 @@
1
+ treemap Engineering Portfolio (effort, pts)
2
+
3
+ tag Squad as sq
4
+ Platform blue
5
+ Product green
6
+ Infra orange
7
+
8
+ heat Δ vs plan %
9
+
10
+ Platform sq: Platform
11
+ API Gateway 120 heat: 4.0
12
+ Auth 80 heat: -6.5
13
+ Service Mesh 60 heat: 1.2
14
+ Product sq: Product
15
+ Onboarding 140 heat: 8.1
16
+ Checkout 110 heat: -3.4
17
+ Search 70 heat: 0.5
18
+ Infra sq: Infra
19
+ CI Pipeline 90 heat: -1.8
20
+ Observability 75 heat: 5.6
21
+ Cost Controls 50 heat: 2.3
@@ -0,0 +1,18 @@
1
+ treemap Q3 Budget
2
+
3
+ tag Team as t
4
+ Eng blue
5
+ Sales green
6
+ Ops orange
7
+
8
+ Engineering t: Eng
9
+ Platform 320
10
+ Mobile 180
11
+ Data 140
12
+ Go-to-Market t: Sales
13
+ Ads 90
14
+ Field Sales 130
15
+ Events 40
16
+ Operations t: Ops
17
+ Cloud 110
18
+ Support 70
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/advanced.ts CHANGED
@@ -507,6 +507,17 @@ export { parseRing } from './ring/parser';
507
507
  export { renderRing, renderRingForExport } from './ring/renderer';
508
508
  export type { ParsedRing, RingLayer } from './ring/types';
509
509
 
510
+ export { parseTreemap } from './treemap/parser';
511
+ export { renderTreemap, renderTreemapForExport } from './treemap/renderer';
512
+ export { layoutTreemap } from './treemap/layout';
513
+ export type { TreemapCell, TreemapLayoutResult } from './treemap/layout';
514
+ export type {
515
+ ParsedTreemap,
516
+ TreemapNode,
517
+ TreemapColorMode,
518
+ TreemapOptions,
519
+ } from './treemap/types';
520
+
510
521
  // Map (§24B) — the interactive render surface for app/editor consumers. The
511
522
  // Node fs `loadMapData` is exported for completeness, but the browser supplies
512
523
  // `MapData` by DI (resolveMap/renderMap take it as an argument).
@@ -933,6 +933,17 @@ export async function layoutBoxesAndLinesSearch(
933
933
  const tick = onProgress
934
934
  ? (): Promise<void> => new Promise<void>((r) => setTimeout(r))
935
935
  : (): undefined => undefined;
936
+ // Yielding is only worth its cost once a search is genuinely slow. A typical
937
+ // diagram finishes its whole candidate pool in tens of ms; yielding (a
938
+ // setTimeout, clamped to ~4ms/turn in browsers) after every one of ~90
939
+ // candidates would turn a 30ms layout into ~1s — all to flash a throbber that
940
+ // never shows (the app only reveals it after 150ms). So we suppress yields
941
+ // until the search has burned through a budget, then throttle them: fast
942
+ // diagrams run flat-out, only truly heavy ones cede the main thread to paint.
943
+ const YIELD_AFTER_MS = 150;
944
+ const YIELD_EVERY_MS = 30;
945
+ const searchStart = performance.now();
946
+ let lastYield = searchStart;
936
947
 
937
948
  // collapsed group labels (shown as plain boxes) — mirrors the ELK path
938
949
  const collapsedGroupLabels = new Set<string>();
@@ -1514,7 +1525,14 @@ export async function layoutBoxesAndLinesSearch(
1514
1525
  const step = async (phase: string): Promise<void> => {
1515
1526
  if (!onProgress) return;
1516
1527
  onProgress(++progressDone, progressTotal, phase);
1517
- await tick();
1528
+ const now = performance.now();
1529
+ if (
1530
+ now - searchStart > YIELD_AFTER_MS &&
1531
+ now - lastYield > YIELD_EVERY_MS
1532
+ ) {
1533
+ lastYield = now;
1534
+ await tick();
1535
+ }
1518
1536
  };
1519
1537
 
1520
1538
  // Build the candidate pool.
@@ -66,6 +66,7 @@ import { parseCycle } from './cycle/parser';
66
66
  import { parseJourneyMap } from './journey-map/parser';
67
67
  import { parsePyramid } from './pyramid/parser';
68
68
  import { parseRing } from './ring/parser';
69
+ import { parseTreemap } from './treemap/parser';
69
70
  import { parseRaci, allTasks } from './raci/parser';
70
71
  import type { DgmoError } from './diagnostics';
71
72
 
@@ -451,6 +452,9 @@ export const CHART_TYPE_REGISTRY: readonly ChartTypeDescriptor[] = [
451
452
  { id: 'cycle', category: 'visualization', parse: parseCycle },
452
453
  { id: 'pyramid', category: 'visualization', parse: parsePyramid },
453
454
  { id: 'ring', category: 'visualization', parse: parseRing },
455
+ // Treemap: squarified hierarchy. No measure/minDims — a treemap fills whatever
456
+ // rectangle it's given and has no intrinsic aspect/size (F11).
457
+ { id: 'treemap', category: 'visualization', parse: parseTreemap },
454
458
 
455
459
  // ── Geographic map (own parser → resolver → layout → renderer) ──
456
460
  { id: 'map', category: 'visualization', parse: parseMap },
@@ -122,6 +122,11 @@ export const chartTypes: readonly ChartTypeMeta[] = [
122
122
  description:
123
123
  'Concentric rings showing nested or hierarchical categories (read core-out)',
124
124
  },
125
+ {
126
+ id: 'treemap',
127
+ description:
128
+ 'Nested rectangles sized by value — show a hierarchy’s proportions (budgets, disk usage, portfolios) with color by category, value heatmap, or branch',
129
+ },
125
130
  {
126
131
  id: 'map',
127
132
  description:
package/src/completion.ts CHANGED
@@ -521,6 +521,24 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
521
521
  // layer pipe-metadata, not directives — they live in PIPE_METADATA.
522
522
  withGlobals({}),
523
523
  ],
524
+ [
525
+ 'treemap',
526
+ // Hierarchy sized by a bare trailing number; color modes + opt-outs.
527
+ withGlobals({
528
+ heat: {
529
+ description:
530
+ 'Name the color-by-value ramp (`heat <Label> [low] [high]`); pairs with the per-node `heat:` key',
531
+ },
532
+ depth: {
533
+ description:
534
+ 'Render N levels; deeper subtrees collapse to a drillable block',
535
+ },
536
+ 'no-values': { description: 'Hide value labels' },
537
+ 'no-percent': { description: 'Hide percentage labels' },
538
+ 'no-headers': { description: 'Hide parent header bars' },
539
+ 'no-legend': { description: 'Hide the legend' },
540
+ }),
541
+ ],
524
542
  [
525
543
  'map',
526
544
  // Geographic map directives (§24B.2/.7). Cosmetics are ON by default — the
@@ -695,6 +713,7 @@ export const STRUCTURAL_KEYWORDS = new Map<string, string[]>([
695
713
  ['infra', ['tag']],
696
714
  ['pert', ['tag']],
697
715
  ['mindmap', ['tag']],
716
+ ['treemap', ['tag']],
698
717
  ['boxes-and-lines', ['tag']],
699
718
  ['er', ['tag']],
700
719
  ['cycle', ['direction-counterclockwise', 'circle-nodes']],
@@ -1828,6 +1847,50 @@ function extractMindmapSymbols(docText: string): DiagramSymbols {
1828
1847
  return { kind: 'mindmap', entities };
1829
1848
  }
1830
1849
 
1850
+ // ============================================================
1851
+ // Treemap extractor
1852
+ // ============================================================
1853
+
1854
+ function extractTreemapSymbols(docText: string): DiagramSymbols {
1855
+ const lines = docText.split('\n');
1856
+ const entities: string[] = [];
1857
+ let pastFirstLine = false;
1858
+ let inTagBlock = false;
1859
+
1860
+ for (const line of lines) {
1861
+ const trimmed = line.trim();
1862
+ if (!trimmed || trimmed.startsWith('//')) continue;
1863
+
1864
+ if (!pastFirstLine) {
1865
+ pastFirstLine = true;
1866
+ continue;
1867
+ }
1868
+
1869
+ // Directives and tag blocks are not node entities.
1870
+ if (
1871
+ /^(depth|heat|no-[a-z]+)\s/i.test(trimmed) ||
1872
+ /^no-[a-z]+$/i.test(trimmed)
1873
+ )
1874
+ continue;
1875
+ if (/^tag\s+/i.test(trimmed)) {
1876
+ inTagBlock = true;
1877
+ continue;
1878
+ }
1879
+ const indent = line.search(/\S/);
1880
+ if (inTagBlock) {
1881
+ if (indent > 0) continue;
1882
+ inTagBlock = false;
1883
+ }
1884
+
1885
+ // Node name: strip same-line metadata + the bare trailing value number.
1886
+ let label = trimmed.split(/\s+\w+:/)[0]!.trim();
1887
+ label = label.replace(/\s+-?\d[\d_,.]*$/, '').trim();
1888
+ if (label && !entities.includes(label)) entities.push(label);
1889
+ }
1890
+
1891
+ return { kind: 'treemap', entities };
1892
+ }
1893
+
1831
1894
  // ============================================================
1832
1895
  // Pyramid extractor
1833
1896
  // ============================================================
@@ -2311,6 +2374,7 @@ registerExtractor('daci', extractRaciSymbols);
2311
2374
  registerExtractor('org', extractOrgSymbols);
2312
2375
  registerExtractor('kanban', extractKanbanSymbols);
2313
2376
  registerExtractor('mindmap', extractMindmapSymbols);
2377
+ registerExtractor('treemap', extractTreemapSymbols);
2314
2378
  registerExtractor('pyramid', extractPyramidSymbols);
2315
2379
  registerExtractor('ring', extractRingSymbols);
2316
2380
  registerExtractor('arc', extractArcSymbols);
package/src/d3.ts CHANGED
@@ -117,6 +117,7 @@ export const DIAGRAM_EXPORT_HANDLERS: Record<string, DiagramExportHandler> = {
117
117
  map: exportMap,
118
118
  pyramid: exportPyramid,
119
119
  ring: exportRing,
120
+ treemap: exportTreemap,
120
121
  raci: exportRaci,
121
122
  rasci: exportRaci,
122
123
  daci: exportRaci,
@@ -962,12 +963,33 @@ async function exportRing(ctx: ExportContext): Promise<string> {
962
963
  if (ringParsed.error || ringParsed.layers.length === 0) return '';
963
964
 
964
965
  const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
965
- renderRingForExport(
966
+ renderRingForExport(container, ringParsed, effectivePalette, ctx.isDark, {
967
+ width: EXPORT_WIDTH,
968
+ height: EXPORT_HEIGHT,
969
+ });
970
+ return finalizeSvgExport(container, theme, effectivePalette);
971
+ }
972
+
973
+ async function exportTreemap(ctx: ExportContext): Promise<string> {
974
+ const { content, theme, palette } = ctx;
975
+ const { parseTreemap } = await import('./treemap/parser');
976
+ const { renderTreemapForExport } = await import('./treemap/renderer');
977
+
978
+ const effectivePalette = await resolveExportPalette(theme, palette);
979
+ const treemapParsed = parseTreemap(content, effectivePalette);
980
+ if (treemapParsed.error || treemapParsed.roots.length === 0) return '';
981
+
982
+ // Headless export: full tree (no depth window), source-declared color mode.
983
+ const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
984
+ renderTreemapForExport(
966
985
  container,
967
- ringParsed,
986
+ treemapParsed,
968
987
  effectivePalette,
969
988
  ctx.isDark,
970
- { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
989
+ {
990
+ width: EXPORT_WIDTH,
991
+ height: EXPORT_HEIGHT,
992
+ }
971
993
  );
972
994
  return finalizeSvgExport(container, theme, effectivePalette);
973
995
  }
@@ -982,13 +1004,10 @@ async function exportRaci(ctx: ExportContext): Promise<string> {
982
1004
  if (raciParsed.error) return '';
983
1005
 
984
1006
  const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
985
- renderRaciForExport(
986
- container,
987
- raciParsed,
988
- effectivePalette,
989
- ctx.isDark,
990
- { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
991
- );
1007
+ renderRaciForExport(container, raciParsed, effectivePalette, ctx.isDark, {
1008
+ width: EXPORT_WIDTH,
1009
+ height: EXPORT_HEIGHT,
1010
+ });
992
1011
  return finalizeSvgExport(container, theme, effectivePalette);
993
1012
  }
994
1013
 
@@ -1091,14 +1110,7 @@ async function exportVenn(ctx: ExportContext): Promise<string> {
1091
1110
  if (parsed.error || parsed.vennSets.length < 2) return '';
1092
1111
  const effectivePalette = await resolveExportPalette(theme, palette);
1093
1112
  const { container, dims } = beginVizExport();
1094
- renderVenn(
1095
- container,
1096
- parsed,
1097
- effectivePalette,
1098
- ctx.isDark,
1099
- undefined,
1100
- dims
1101
- );
1113
+ renderVenn(container, parsed, effectivePalette, ctx.isDark, undefined, dims);
1102
1114
  return finalizeSvgExport(container, theme, effectivePalette);
1103
1115
  }
1104
1116
 
@@ -213,6 +213,8 @@ export const ATTRIBUTE_KEYS = new Set([
213
213
  'value',
214
214
  'label',
215
215
  'style',
216
+ // Treemap color-by-value metric key
217
+ 'heat',
216
218
  // Infra node behavior + edge colon-keys (§4) come from the single-source
217
219
  // directives registry — the parser accepts them only as `key: value` node
218
220
  // properties, so they render as `propertyName`. The dual-use top-level SLO
@@ -27,6 +27,7 @@ export const CHART_TYPES = new Set([
27
27
  'journey-map',
28
28
  'pyramid',
29
29
  'ring',
30
+ 'treemap',
30
31
  'raci',
31
32
  'rasci',
32
33
  'daci',
@@ -91,6 +92,8 @@ export const METADATA_KEYS = new Set([
91
92
  'value',
92
93
  'label',
93
94
  'style',
95
+ // Treemap color-by-value metric key
96
+ 'heat',
94
97
  ]);
95
98
 
96
99
  /** Tag declaration keyword. */