@diagrammo/dgmo 0.37.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.
- package/.cursorrules +1 -0
- package/.github/copilot-instructions.md +1 -0
- package/.windsurfrules +1 -0
- package/SKILL.md +1 -0
- package/dist/advanced.cjs +919 -81
- package/dist/advanced.d.cts +110 -1
- package/dist/advanced.d.ts +110 -1
- package/dist/advanced.js +916 -79
- package/dist/auto.cjs +908 -78
- package/dist/auto.js +96 -95
- package/dist/auto.mjs +909 -76
- package/dist/cli.cjs +117 -116
- package/dist/editor.cjs +4 -1
- package/dist/editor.js +4 -1
- package/dist/highlight.cjs +3 -0
- package/dist/highlight.js +3 -0
- package/dist/index.cjs +904 -77
- package/dist/index.js +905 -75
- package/docs/ai-integration.md +1 -0
- package/docs/language-reference.md +95 -0
- package/gallery/fixtures/treemap-branch.dgmo +15 -0
- package/gallery/fixtures/treemap-heat.dgmo +12 -0
- package/gallery/fixtures/treemap-tag-heat.dgmo +21 -0
- package/gallery/fixtures/treemap-tags.dgmo +18 -0
- package/package.json +1 -1
- package/src/advanced.ts +11 -0
- package/src/boxes-and-lines/layout-search.ts +19 -1
- package/src/chart-type-registry.ts +4 -0
- package/src/chart-types.ts +5 -0
- package/src/completion.ts +64 -0
- package/src/d3.ts +30 -18
- package/src/editor/highlight-api.ts +2 -0
- package/src/editor/keywords.ts +3 -0
- package/src/map/dimensions.ts +19 -2
- package/src/treemap/layout.ts +189 -0
- package/src/treemap/parser.ts +475 -0
- package/src/treemap/renderer.ts +698 -0
- package/src/treemap/types.ts +57 -0
- package/src/utils/parsing.ts +1 -0
- package/src/utils/reserved-key-registry.ts +5 -0
package/docs/ai-integration.md
CHANGED
|
@@ -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 |
|
|
@@ -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 |
|
|
@@ -3089,6 +3090,100 @@ When ring band thickness would force the in-band label below the readable floor
|
|
|
3089
3090
|
|
|
3090
3091
|
---
|
|
3091
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
|
+
|
|
3092
3187
|
## 24A. RACI Matrices (RACI / RASCI / DACI)
|
|
3093
3188
|
|
|
3094
3189
|
<!-- TYPE:raci -->
|
|
@@ -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
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
|
-
|
|
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 },
|
package/src/chart-types.ts
CHANGED
|
@@ -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
|
-
|
|
986
|
+
treemapParsed,
|
|
968
987
|
effectivePalette,
|
|
969
988
|
ctx.isDark,
|
|
970
|
-
{
|
|
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
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
package/src/editor/keywords.ts
CHANGED
|
@@ -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. */
|
package/src/map/dimensions.ts
CHANGED
|
@@ -32,6 +32,13 @@ const TITLE_GAP = 16;
|
|
|
32
32
|
// width:100% in a narrow note column stays sane.
|
|
33
33
|
const ASPECT_MAX = 3.0;
|
|
34
34
|
const ASPECT_MIN = 0.9;
|
|
35
|
+
// Deliberate vertical boost for GLOBAL (whole-world) extents only. An honest
|
|
36
|
+
// whole-world projection is ~2.4:1, which reads as a flat letterbox band. We emit
|
|
37
|
+
// a canvas this fraction taller and let the global stretch-fill (layout.ts) fill
|
|
38
|
+
// the extra height — the projected land stretches ~12% taller while POI radii and
|
|
39
|
+
// label fonts (applied in the renderer, not here) stay un-squashed. US, regional,
|
|
40
|
+
// and non-global extents are untouched (they keep their true projected aspect).
|
|
41
|
+
const GLOBAL_HEIGHT_BOOST = 1.12;
|
|
35
42
|
// Minimum px of actual map area (below the chrome band) — keeps a short canvas
|
|
36
43
|
// (very wide extent) from being crowded out by the title/caption.
|
|
37
44
|
const MIN_MAP_BAND = 200;
|
|
@@ -96,11 +103,21 @@ export function mapExportDimensions(
|
|
|
96
103
|
Number.isFinite(aspectOverride) &&
|
|
97
104
|
aspectOverride > 0;
|
|
98
105
|
const raw = useOverride ? aspectOverride : mapContentAspect(resolved, data);
|
|
106
|
+
// Whole-world extents get the vertical boost (a taller canvas the global
|
|
107
|
+
// stretch-fill then fills). `fitIsGlobal` is the SAME classification the layout
|
|
108
|
+
// uses to decide stretch-fill vs contain, so the two always agree. The override
|
|
109
|
+
// path (app WYSIWYG) and non-global extents pass through at their true aspect.
|
|
110
|
+
const isGlobal =
|
|
111
|
+
!useOverride && buildMapProjection(resolved, data).fitIsGlobal;
|
|
112
|
+
// The target the renderer should hit BEFORE the pathological clamp: the boosted
|
|
113
|
+
// aspect for global views, the raw aspect otherwise. preferContain compares the
|
|
114
|
+
// clamp against THIS (not raw) so the deliberate boost never trips the letterbox.
|
|
115
|
+
const target = isGlobal ? raw / GLOBAL_HEIGHT_BOOST : raw;
|
|
99
116
|
// The override is the user's on-screen aspect — honour it as-is (no clamp);
|
|
100
117
|
// only the intrinsic path guards against pathological extents.
|
|
101
118
|
const clamped = useOverride
|
|
102
119
|
? raw
|
|
103
|
-
: Math.max(ASPECT_MIN, Math.min(ASPECT_MAX,
|
|
120
|
+
: Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, target));
|
|
104
121
|
const width = baseWidth;
|
|
105
122
|
let height = Math.round(width / clamped);
|
|
106
123
|
|
|
@@ -128,6 +145,6 @@ export function mapExportDimensions(
|
|
|
128
145
|
// contain-fit (letterbox) rather than stretch-distort. The WYSIWYG override is
|
|
129
146
|
// exempt: it stretch-fills (mirroring the preview pane) unless the MIN_MAP_BAND
|
|
130
147
|
// floor had to grow the canvas off-aspect.
|
|
131
|
-
const preferContain = useOverride ? floored : clamped !==
|
|
148
|
+
const preferContain = useOverride ? floored : clamped !== target || floored;
|
|
132
149
|
return { width, height, preferContain };
|
|
133
150
|
}
|