@diagrammo/dgmo 0.47.0 → 0.48.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 (319) hide show
  1. package/dist/auto.js +1 -1
  2. package/dist/auto.mjs +1 -1
  3. package/dist/element.js +1 -1
  4. package/dist/element.mjs +1 -1
  5. package/package.json +19 -70
  6. package/dist/advanced.cjs +0 -71648
  7. package/dist/advanced.d.cts +0 -5719
  8. package/dist/auto.cjs +0 -70648
  9. package/dist/auto.d.cts +0 -54
  10. package/dist/block.cjs +0 -70208
  11. package/dist/block.d.cts +0 -115
  12. package/dist/editor.cjs +0 -526
  13. package/dist/editor.d.cts +0 -26
  14. package/dist/element.cjs +0 -70530
  15. package/dist/element.d.cts +0 -62
  16. package/dist/highlight.cjs +0 -852
  17. package/dist/highlight.d.cts +0 -49
  18. package/dist/index.cjs +0 -70491
  19. package/dist/index.d.cts +0 -160
  20. package/dist/pert.cjs +0 -325
  21. package/dist/pert.d.cts +0 -559
  22. package/dist/themes-1-CuKpeH.d.cts +0 -908
  23. package/src/advanced.ts +0 -873
  24. package/src/arc/parser.ts +0 -15
  25. package/src/arc/renderer.ts +0 -633
  26. package/src/auto/index.ts +0 -621
  27. package/src/auto/shared.ts +0 -370
  28. package/src/auto/styles.ts +0 -73
  29. package/src/block/layout.ts +0 -198
  30. package/src/block/parser.ts +0 -427
  31. package/src/block/renderer.ts +0 -484
  32. package/src/block/types.ts +0 -67
  33. package/src/boxes-and-lines/collapse.ts +0 -96
  34. package/src/boxes-and-lines/focus.ts +0 -257
  35. package/src/boxes-and-lines/label-placement.ts +0 -339
  36. package/src/boxes-and-lines/layout-grouped.ts +0 -912
  37. package/src/boxes-and-lines/layout-layered.ts +0 -722
  38. package/src/boxes-and-lines/layout-search.ts +0 -1678
  39. package/src/boxes-and-lines/layout.ts +0 -398
  40. package/src/boxes-and-lines/parser.ts +0 -1181
  41. package/src/boxes-and-lines/renderer.ts +0 -1465
  42. package/src/boxes-and-lines/types.ts +0 -66
  43. package/src/c4/layout.ts +0 -2441
  44. package/src/c4/parser.ts +0 -1158
  45. package/src/c4/renderer.ts +0 -2008
  46. package/src/c4/types.ts +0 -89
  47. package/src/chart-type-registry.ts +0 -529
  48. package/src/chart-types.ts +0 -225
  49. package/src/chart.ts +0 -685
  50. package/src/charts-d3/bar.ts +0 -338
  51. package/src/charts-d3/chord.ts +0 -140
  52. package/src/charts-d3/function.ts +0 -147
  53. package/src/charts-d3/funnel.ts +0 -104
  54. package/src/charts-d3/heatmap.ts +0 -145
  55. package/src/charts-d3/index.ts +0 -337
  56. package/src/charts-d3/interactions.ts +0 -477
  57. package/src/charts-d3/line.ts +0 -344
  58. package/src/charts-d3/mount.ts +0 -151
  59. package/src/charts-d3/pie.ts +0 -117
  60. package/src/charts-d3/polar.ts +0 -89
  61. package/src/charts-d3/radar.ts +0 -0
  62. package/src/charts-d3/sankey.ts +0 -0
  63. package/src/charts-d3/scatter.ts +0 -185
  64. package/src/charts-d3/shared.ts +0 -208
  65. package/src/class/layout.ts +0 -326
  66. package/src/class/parser.ts +0 -635
  67. package/src/class/renderer.ts +0 -793
  68. package/src/class/types.ts +0 -58
  69. package/src/cli-banner.ts +0 -106
  70. package/src/cli.ts +0 -1080
  71. package/src/colors-diagnostics.ts +0 -35
  72. package/src/colors.ts +0 -379
  73. package/src/completion-types.ts +0 -27
  74. package/src/completion.ts +0 -2668
  75. package/src/cycle/layout.ts +0 -1026
  76. package/src/cycle/parser.ts +0 -387
  77. package/src/cycle/renderer.ts +0 -653
  78. package/src/cycle/types.ts +0 -101
  79. package/src/d3.ts +0 -1434
  80. package/src/data-chart-parser.ts +0 -1007
  81. package/src/dgmo-router.ts +0 -296
  82. package/src/diagnostics-registry.ts +0 -108
  83. package/src/diagnostics.ts +0 -0
  84. package/src/dimensions.ts +0 -20
  85. package/src/directives-registry.ts +0 -396
  86. package/src/editor/dgmo.grammar +0 -76
  87. package/src/editor/dgmo.grammar.d.ts +0 -2
  88. package/src/editor/dgmo.grammar.js +0 -18
  89. package/src/editor/dgmo.grammar.terms.d.ts +0 -5
  90. package/src/editor/dgmo.grammar.terms.js +0 -35
  91. package/src/editor/highlight-api.ts +0 -596
  92. package/src/editor/highlight.ts +0 -36
  93. package/src/editor/index.ts +0 -27
  94. package/src/editor/keywords.ts +0 -128
  95. package/src/editor/tokens.ts +0 -30
  96. package/src/element/index.ts +0 -432
  97. package/src/embed/css.ts +0 -312
  98. package/src/embed/escape.ts +0 -15
  99. package/src/embed/index.ts +0 -280
  100. package/src/er/classify.ts +0 -228
  101. package/src/er/layout.ts +0 -530
  102. package/src/er/parser.ts +0 -750
  103. package/src/er/renderer.ts +0 -725
  104. package/src/er/types.ts +0 -52
  105. package/src/error-card.ts +0 -269
  106. package/src/event-line/diagnostics.ts +0 -117
  107. package/src/event-line/parser.ts +0 -643
  108. package/src/event-line/renderer.ts +0 -1742
  109. package/src/event-line/types.ts +0 -74
  110. package/src/fonts.ts +0 -3
  111. package/src/gantt/calculator.ts +0 -979
  112. package/src/gantt/parser.ts +0 -1641
  113. package/src/gantt/renderer.ts +0 -3787
  114. package/src/gantt/resolver.ts +0 -172
  115. package/src/gantt/types.ts +0 -206
  116. package/src/graph/diagnostics.ts +0 -64
  117. package/src/graph/edge-spline.ts +0 -29
  118. package/src/graph/flowchart-parser.ts +0 -759
  119. package/src/graph/flowchart-renderer.ts +0 -817
  120. package/src/graph/layout.ts +0 -472
  121. package/src/graph/notes.ts +0 -21
  122. package/src/graph/state-collapse.ts +0 -78
  123. package/src/graph/state-parser.ts +0 -574
  124. package/src/graph/state-renderer.ts +0 -653
  125. package/src/graph/types.ts +0 -65
  126. package/src/index.ts +0 -261
  127. package/src/infra/compute.ts +0 -1385
  128. package/src/infra/layout.ts +0 -888
  129. package/src/infra/parser.ts +0 -1234
  130. package/src/infra/renderer.ts +0 -2534
  131. package/src/infra/roles.ts +0 -75
  132. package/src/infra/types.ts +0 -216
  133. package/src/infra/validation.ts +0 -201
  134. package/src/journey-map/layout.ts +0 -490
  135. package/src/journey-map/parser.ts +0 -557
  136. package/src/journey-map/renderer.ts +0 -1580
  137. package/src/journey-map/types.ts +0 -47
  138. package/src/kanban/mutations.ts +0 -175
  139. package/src/kanban/parser.ts +0 -509
  140. package/src/kanban/renderer.ts +0 -1449
  141. package/src/kanban/types.ts +0 -39
  142. package/src/label-layout.ts +0 -368
  143. package/src/map/colorize.ts +0 -54
  144. package/src/map/completion.ts +0 -316
  145. package/src/map/context-labels.ts +0 -609
  146. package/src/map/data/PROVENANCE.json +0 -1
  147. package/src/map/data/README.md +0 -40
  148. package/src/map/data/airport-collisions.json +0 -1
  149. package/src/map/data/airports.json +0 -1
  150. package/src/map/data/gazetteer.json +0 -1
  151. package/src/map/data/lakes.json +0 -1
  152. package/src/map/data/mountain-ranges.json +0 -1
  153. package/src/map/data/na-lakes.json +0 -1
  154. package/src/map/data/na-land.json +0 -1
  155. package/src/map/data/region-names.json +0 -1
  156. package/src/map/data/rivers.json +0 -1
  157. package/src/map/data/types.ts +0 -121
  158. package/src/map/data/us-states.json +0 -1
  159. package/src/map/data/water-bodies.json +0 -1
  160. package/src/map/data/world-coarse.json +0 -1
  161. package/src/map/data/world-detail.json +0 -1
  162. package/src/map/diagnostics.ts +0 -134
  163. package/src/map/dimensions.ts +0 -150
  164. package/src/map/geo-query.ts +0 -295
  165. package/src/map/geo.ts +0 -438
  166. package/src/map/invert.ts +0 -111
  167. package/src/map/layout.ts +0 -5069
  168. package/src/map/legend-band.ts +0 -127
  169. package/src/map/load-data.ts +0 -186
  170. package/src/map/parser.ts +0 -783
  171. package/src/map/renderer.ts +0 -1445
  172. package/src/map/resolved-types.ts +0 -160
  173. package/src/map/resolver.ts +0 -1146
  174. package/src/map/types.ts +0 -194
  175. package/src/mindmap/collapse.ts +0 -67
  176. package/src/mindmap/layout.ts +0 -649
  177. package/src/mindmap/parser.ts +0 -430
  178. package/src/mindmap/renderer.ts +0 -637
  179. package/src/mindmap/text-wrap.ts +0 -218
  180. package/src/mindmap/types.ts +0 -56
  181. package/src/org/collapse.ts +0 -151
  182. package/src/org/layout.ts +0 -1262
  183. package/src/org/parser.ts +0 -519
  184. package/src/org/renderer.ts +0 -866
  185. package/src/org/resolver.ts +0 -636
  186. package/src/palettes/atlas.ts +0 -77
  187. package/src/palettes/blueprint.ts +0 -73
  188. package/src/palettes/catppuccin.ts +0 -84
  189. package/src/palettes/color-utils.ts +0 -443
  190. package/src/palettes/index.ts +0 -67
  191. package/src/palettes/nord.ts +0 -67
  192. package/src/palettes/registry.ts +0 -114
  193. package/src/palettes/slate.ts +0 -73
  194. package/src/palettes/tidewater.ts +0 -73
  195. package/src/palettes/tokyo-night.ts +0 -86
  196. package/src/palettes/types.ts +0 -88
  197. package/src/pert/analyzer.ts +0 -1183
  198. package/src/pert/diagnostics.ts +0 -110
  199. package/src/pert/index.ts +0 -43
  200. package/src/pert/internal.ts +0 -252
  201. package/src/pert/layout.ts +0 -971
  202. package/src/pert/monte-carlo.ts +0 -446
  203. package/src/pert/parser.ts +0 -1606
  204. package/src/pert/renderer.ts +0 -4009
  205. package/src/pert/share-normalize.ts +0 -62
  206. package/src/pert/types.ts +0 -604
  207. package/src/pyramid/parser.ts +0 -185
  208. package/src/pyramid/renderer.ts +0 -768
  209. package/src/pyramid/types.ts +0 -28
  210. package/src/quadrant/parser.ts +0 -18
  211. package/src/quadrant/renderer.ts +0 -638
  212. package/src/raci/diagnostics.ts +0 -213
  213. package/src/raci/index.ts +0 -23
  214. package/src/raci/mutations.ts +0 -321
  215. package/src/raci/parser.ts +0 -699
  216. package/src/raci/renderer.ts +0 -1627
  217. package/src/raci/types.ts +0 -82
  218. package/src/raci/variants.ts +0 -320
  219. package/src/render.ts +0 -268
  220. package/src/ring/parser.ts +0 -212
  221. package/src/ring/renderer.ts +0 -490
  222. package/src/ring/types.ts +0 -27
  223. package/src/sequence/collapse.ts +0 -174
  224. package/src/sequence/parser.ts +0 -1596
  225. package/src/sequence/participant-inference.ts +0 -152
  226. package/src/sequence/renderer.ts +0 -3168
  227. package/src/sequence/tag-resolution.ts +0 -164
  228. package/src/sharing.ts +0 -211
  229. package/src/sitemap/collapse.ts +0 -100
  230. package/src/sitemap/layout.ts +0 -925
  231. package/src/sitemap/parser.ts +0 -755
  232. package/src/sitemap/renderer.ts +0 -828
  233. package/src/sitemap/types.ts +0 -42
  234. package/src/slope/parser.ts +0 -18
  235. package/src/slope/renderer.ts +0 -378
  236. package/src/swimlane/diagnostics.ts +0 -94
  237. package/src/swimlane/layout.ts +0 -597
  238. package/src/swimlane/parser.ts +0 -613
  239. package/src/swimlane/renderer.ts +0 -681
  240. package/src/swimlane/types.ts +0 -151
  241. package/src/tech-radar/interactive.ts +0 -1129
  242. package/src/tech-radar/layout.ts +0 -197
  243. package/src/tech-radar/parser.ts +0 -436
  244. package/src/tech-radar/renderer.ts +0 -1239
  245. package/src/tech-radar/shared.ts +0 -151
  246. package/src/tech-radar/types.ts +0 -86
  247. package/src/themes.ts +0 -22
  248. package/src/timeline/parser.ts +0 -472
  249. package/src/timeline/renderer.ts +0 -3024
  250. package/src/timeline/types.ts +0 -33
  251. package/src/timeline/viz-parser.ts +0 -21
  252. package/src/treemap/diagnostics.ts +0 -62
  253. package/src/treemap/layout-radial.ts +0 -233
  254. package/src/treemap/layout.ts +0 -189
  255. package/src/treemap/parser.ts +0 -463
  256. package/src/treemap/renderer-radial.ts +0 -438
  257. package/src/treemap/renderer.ts +0 -572
  258. package/src/treemap/treemap-shared.ts +0 -309
  259. package/src/treemap/types.ts +0 -60
  260. package/src/utils/arrow-markers.ts +0 -51
  261. package/src/utils/arrows-diagnostics.ts +0 -62
  262. package/src/utils/arrows.ts +0 -227
  263. package/src/utils/brand.ts +0 -61
  264. package/src/utils/card.ts +0 -183
  265. package/src/utils/collapse-engine/tree-edges.ts +0 -115
  266. package/src/utils/collapse-engine/tree.ts +0 -117
  267. package/src/utils/d3-helpers.ts +0 -219
  268. package/src/utils/d3-types.ts +0 -9
  269. package/src/utils/description-helpers.ts +0 -38
  270. package/src/utils/duration.ts +0 -312
  271. package/src/utils/export-container.ts +0 -41
  272. package/src/utils/expr-eval.ts +0 -27
  273. package/src/utils/fit-canvas.ts +0 -64
  274. package/src/utils/hover-styles.ts +0 -673
  275. package/src/utils/inline-markdown.ts +0 -96
  276. package/src/utils/legend-constants.ts +0 -48
  277. package/src/utils/legend-d3.ts +0 -618
  278. package/src/utils/legend-integration.ts +0 -107
  279. package/src/utils/legend-layout.ts +0 -776
  280. package/src/utils/legend-svg.ts +0 -214
  281. package/src/utils/legend-types.ts +0 -272
  282. package/src/utils/name-normalize.ts +0 -163
  283. package/src/utils/note-box/constants.ts +0 -25
  284. package/src/utils/note-box/index.ts +0 -11
  285. package/src/utils/note-box/metrics.ts +0 -90
  286. package/src/utils/note-box/svg.ts +0 -331
  287. package/src/utils/notes/bounds.ts +0 -30
  288. package/src/utils/notes/build.ts +0 -131
  289. package/src/utils/notes/index.ts +0 -18
  290. package/src/utils/notes/model.ts +0 -19
  291. package/src/utils/notes/parse.ts +0 -131
  292. package/src/utils/notes/place.ts +0 -177
  293. package/src/utils/notes/resolve.ts +0 -88
  294. package/src/utils/number-format.ts +0 -36
  295. package/src/utils/parsing.ts +0 -953
  296. package/src/utils/reserved-key-registry.ts +0 -239
  297. package/src/utils/safe-href.ts +0 -38
  298. package/src/utils/scaling.ts +0 -97
  299. package/src/utils/svg-embed.ts +0 -251
  300. package/src/utils/tag-groups.ts +0 -607
  301. package/src/utils/text-measure.ts +0 -122
  302. package/src/utils/time-ticks.ts +0 -253
  303. package/src/utils/title-constants.ts +0 -24
  304. package/src/utils/visual-conventions.ts +0 -61
  305. package/src/utils/wrapped-desc.ts +0 -87
  306. package/src/venn/parser.ts +0 -18
  307. package/src/venn/renderer.ts +0 -1047
  308. package/src/version-control/diagnostics.ts +0 -67
  309. package/src/version-control/parser.ts +0 -544
  310. package/src/version-control/renderer.ts +0 -724
  311. package/src/version-control/types.ts +0 -100
  312. package/src/visualizations/parse.ts +0 -1560
  313. package/src/visualizations/types.ts +0 -262
  314. package/src/wireframe/layout.ts +0 -479
  315. package/src/wireframe/parser.ts +0 -1114
  316. package/src/wireframe/renderer.ts +0 -1318
  317. package/src/wireframe/types.ts +0 -110
  318. package/src/wordcloud/parser.ts +0 -18
  319. package/src/wordcloud/renderer.ts +0 -357
@@ -1,572 +0,0 @@
1
- // ============================================================
2
- // Treemap — D3 SVG Renderer
3
- // ============================================================
4
- //
5
- // Emits a static, full-tree treemap. Interactive chrome (drill breadcrumb,
6
- // tooltips, the runtime color-mode switcher) lives app-side; the only
7
- // interactive affordance drawn here is the scope/target focus icon, marked
8
- // `data-export-ignore` so `finalizeSvgExport` strips it on export.
9
-
10
- import * as d3Selection from 'd3-selection';
11
- import { FONT_FAMILY } from '../fonts';
12
- import { contrastText, getSeriesColors, mix } from '../palettes/color-utils';
13
- import { tagAttrKey } from '../utils/tag-groups';
14
- import { measureText } from '../utils/text-measure';
15
- import { renderIntegratedLegend } from '../utils/legend-integration';
16
- import { getMaxLegendReservedHeight } from '../utils/legend-layout';
17
- import { LEGEND_GROUP_GAP } from '../utils/legend-constants';
18
- import type { LegendPosition } from '../utils/legend-types';
19
- import {
20
- TITLE_FONT_SIZE,
21
- TITLE_FONT_WEIGHT,
22
- TITLE_Y,
23
- } from '../utils/title-constants';
24
- import type { PaletteColors } from '../palettes';
25
- import type { D3ExportDimensions } from '../utils/d3-types';
26
- import type { ParsedTreemap, TreemapColorMode } from './types';
27
- import { layoutTreemap } from './layout';
28
- import {
29
- buildHeatScale,
30
- buildLegend,
31
- compactNumber,
32
- formatPct,
33
- resolveCellColor,
34
- resolveColorMode,
35
- type CellColorContext,
36
- } from './treemap-shared';
37
-
38
- const PADDING = 12;
39
- const HEADER_H = 18;
40
- const TITLE_BAND = 36;
41
- /** Percent of a leaf's own color kept when muting it toward the background. */
42
- const LEAF_MUTE_PCT = 50;
43
-
44
- /** Standard placement every dgmo legend uses: centered, under the title. */
45
- const LEGEND_POSITION: LegendPosition = {
46
- placement: 'top-center',
47
- titleRelation: 'below-title',
48
- };
49
-
50
- export interface TreemapRenderOptions {
51
- /** Color mode override (app's runtime switcher). Defaults to source. */
52
- colorMode?: TreemapColorMode;
53
- /** Render budget (interactive only). Export omits it → full tree. */
54
- maxDepth?: number;
55
- /** Shift the branch-hue index so a drilled-into view keeps the color it had
56
- * when expanded (the re-rooted node would otherwise become index 0 = the
57
- * first hue). Set to the drilled branch's original top-level index. */
58
- colorOffset?: number;
59
- /** Click handler for drillable cells (app interactivity). */
60
- onClickItem?: (lineNumber: number) => void;
61
- /** Color-mode switch fired when a legend pill is clicked (app interactivity).
62
- * The mode switcher is baked into the legend (clickable group pills), so the
63
- * app no longer renders a separate overlay control. */
64
- onSelectMode?: (mode: TreemapColorMode) => void;
65
- exportMode?: boolean;
66
- }
67
-
68
- /** Render for CLI/export (full tree, no drill chrome). */
69
- export function renderTreemapForExport(
70
- container: HTMLDivElement,
71
- parsed: ParsedTreemap,
72
- palette: PaletteColors,
73
- isDark: boolean,
74
- exportDims?: D3ExportDimensions,
75
- options?: TreemapRenderOptions
76
- ): void {
77
- renderTreemap(container, parsed, palette, isDark, exportDims, {
78
- exportMode: true,
79
- ...options,
80
- });
81
- }
82
-
83
- export function renderTreemap(
84
- container: HTMLDivElement,
85
- parsed: ParsedTreemap,
86
- palette: PaletteColors,
87
- isDark: boolean,
88
- exportDims?: D3ExportDimensions,
89
- options: TreemapRenderOptions = {}
90
- ): void {
91
- if (parsed.error || parsed.roots.length === 0) return;
92
- void isDark; // colors derive from the palette; kept for handler-signature parity.
93
-
94
- d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
95
- const width = exportDims?.width ?? container.clientWidth;
96
- const height = exportDims?.height ?? container.clientHeight;
97
- if (width <= 0 || height <= 0) return;
98
-
99
- const mode = resolveColorMode(parsed, options.colorMode);
100
- const opts = parsed.options;
101
- const solid = opts.solidFill;
102
- const exportMode = options.exportMode ?? false;
103
- const seriesColorsTop = getSeriesColors(palette);
104
- const showTitle = !!parsed.title;
105
- const titleH = showTitle ? TITLE_BAND : 0;
106
-
107
- const heat = buildHeatScale(parsed, palette);
108
- const legend = !opts.noLegend
109
- ? buildLegend(mode, parsed, heat, seriesColorsTop, options.colorOffset ?? 0)
110
- : null;
111
-
112
- // Reserve a band below the title for the standardized legend, exactly like
113
- // mindmap/org/map (top-center, below-title). The treemap fills the rest.
114
- const legendReserve = legend
115
- ? getMaxLegendReservedHeight(
116
- { groups: legend.groups, position: LEGEND_POSITION, mode: 'preview' },
117
- width
118
- ) + LEGEND_GROUP_GAP
119
- : 0;
120
-
121
- const headerH = opts.noHeaders ? 0 : HEADER_H;
122
- const areaX = PADDING;
123
- const areaY = titleH + legendReserve + PADDING;
124
- const areaW = Math.max(1, width - PADDING * 2);
125
- const areaH = Math.max(1, height - areaY - PADDING);
126
-
127
- const layout = layoutTreemap(parsed.roots, {
128
- width: areaW,
129
- height: areaH,
130
- headerH,
131
- ...(options.maxDepth !== undefined && { maxDepth: options.maxDepth }),
132
- });
133
-
134
- // ── SVG root ───────────────────────────────────────────────
135
- const svg = d3Selection
136
- .select(container)
137
- .append('svg')
138
- .attr('width', width)
139
- .attr('height', height)
140
- .attr('viewBox', `0 0 ${width} ${height}`)
141
- .attr('preserveAspectRatio', 'xMidYMin meet')
142
- .attr('xmlns', 'http://www.w3.org/2000/svg')
143
- .attr('class', 'dgmo-treemap')
144
- .style('font-family', FONT_FAMILY);
145
-
146
- // Drill icon: faint by default, a touch clearer when its cell is hovered.
147
- svg
148
- .append('style')
149
- .text(
150
- '.dgmo-treemap-focus{opacity:.3;transition:opacity .12s}' +
151
- '.dgmo-treemap-cell:hover .dgmo-treemap-focus{opacity:.7}'
152
- );
153
-
154
- svg
155
- .append('rect')
156
- .attr('width', width)
157
- .attr('height', height)
158
- .attr('fill', palette.bg);
159
-
160
- if (showTitle) {
161
- const title = svg
162
- .append('text')
163
- .attr('class', 'chart-title')
164
- .attr('x', width / 2)
165
- .attr('y', TITLE_Y)
166
- .attr('text-anchor', 'middle')
167
- .attr('fill', palette.text)
168
- .attr('font-family', FONT_FAMILY)
169
- .attr('font-size', TITLE_FONT_SIZE)
170
- .attr('font-weight', TITLE_FONT_WEIGHT)
171
- .text(parsed.title);
172
- if (parsed.titleLineNumber !== null) {
173
- title.attr('data-line-number', parsed.titleLineNumber);
174
- }
175
- }
176
-
177
- const root = svg
178
- .append('g')
179
- .attr('class', 'dgmo-treemap-cells')
180
- .attr('transform', `translate(${areaX},${areaY})`);
181
-
182
- const seriesColors = seriesColorsTop;
183
- const colorOffset = options.colorOffset ?? 0;
184
- const activeGroup =
185
- parsed.tagGroups.length > 0 ? parsed.tagGroups[0]!.name : null;
186
-
187
- // Branch hue is keyed off the top-level branch's SOURCE order (not d3's
188
- // value-sorted order) so the cell colors match the legend, which lists roots
189
- // in source order. `colorOffset` shifts the index so a drilled-into branch
190
- // keeps the hue it had at the top level.
191
- const rootIndexByLabel = new Map(parsed.roots.map((r, i) => [r.label, i]));
192
-
193
- const colorCtx: CellColorContext = {
194
- mode,
195
- heat,
196
- tagGroups: parsed.tagGroups,
197
- activeGroup,
198
- rootIndexByLabel,
199
- seriesColors,
200
- colorOffset,
201
- bg: palette.bg,
202
- };
203
-
204
- // ── Cells ──────────────────────────────────────────────────
205
- for (const cell of layout.cells) {
206
- const w = Math.max(0, cell.x1 - cell.x0);
207
- const h = Math.max(0, cell.y1 - cell.y0);
208
- if (w <= 0 || h <= 0) continue;
209
-
210
- // Emphasis: the containing box (header band) is pure color; the internal
211
- // leaf cells are muted (mixed toward the background). Both are opaque so the
212
- // pure container behind a leaf doesn't bleed through.
213
- const baseColor = resolveCellColor(cell, colorCtx);
214
- const fill = cell.isContainer
215
- ? baseColor
216
- : solid
217
- ? baseColor
218
- : mix(baseColor, palette.bg, LEAF_MUTE_PCT);
219
- const drillable = cell.isContainer || cell.isCollapsed;
220
-
221
- const g = root
222
- .append('g')
223
- .attr('class', 'dgmo-treemap-cell')
224
- .attr('transform', `translate(${cell.x0},${cell.y0})`)
225
- .attr('data-node-path', cell.path.join(' / '));
226
- if (cell.lineNumber !== undefined) {
227
- g.attr('data-line-number', cell.lineNumber);
228
- }
229
- if (cell.heat !== undefined) g.attr('data-heat', cell.heat);
230
- if (cell.node && activeGroup) {
231
- const key = tagAttrKey(activeGroup);
232
- const tagVal = cell.node.metadata[key];
233
- if (tagVal) g.attr(`data-tag-${key}`, tagVal);
234
- }
235
-
236
- const rect = g
237
- .append('rect')
238
- .attr(
239
- 'class',
240
- cell.isContainer ? 'dgmo-treemap-rect branch' : 'dgmo-treemap-rect'
241
- )
242
- .attr('width', w)
243
- .attr('height', h)
244
- .attr('rx', 2)
245
- .attr('fill', fill)
246
- .attr('fill-opacity', 1)
247
- .attr('stroke', palette.bg)
248
- // No stroke on internal leaf shapes — the paddingInner gap already
249
- // separates them; only the containing box keeps a frame.
250
- .attr('stroke-width', cell.isContainer ? 1 : 0);
251
-
252
- if (drillable && options.onClickItem && cell.lineNumber !== undefined) {
253
- const ln = cell.lineNumber;
254
- rect
255
- .style('cursor', 'pointer')
256
- .on('click', () => options.onClickItem!(ln));
257
- }
258
-
259
- const ink = contrastText(
260
- fill,
261
- palette.textOnFillLight,
262
- palette.textOnFillDark
263
- );
264
-
265
- // Header bar: group name (left) + its aggregate value/% (right) so a
266
- // container's share is readable without drilling.
267
- if (cell.isContainer && headerH > 0 && w > 34 && h > headerH) {
268
- const valParts: string[] = [];
269
- if (!opts.noValues) valParts.push(compactNumber(cell.value));
270
- if (!opts.noPercent) valParts.push(formatPct(cell.pctOfRoot));
271
- const valStr = valParts.join(' · ');
272
- // Only reserve space for the focus icon when one is actually drawn (a
273
- // non-drillable container's header value sits flush right).
274
- const ICON_RESERVE = drillable ? 16 : 4;
275
- const showVal = valStr.length > 0 && w > 110;
276
- const valW = showVal ? measureText(valStr, 10.5) : 0;
277
- const labelMax = w - 8 - ICON_RESERVE - (showVal ? valW + 10 : 0);
278
-
279
- g.append('text')
280
- .attr('class', 'dgmo-treemap-header')
281
- .attr('x', 6)
282
- .attr('y', 13)
283
- .attr('font-size', 11)
284
- .attr('font-weight', 700)
285
- .attr('fill', ink)
286
- .text(clipLabel(cell.label, Math.max(0, labelMax), 11));
287
-
288
- if (showVal) {
289
- g.append('text')
290
- .attr('class', 'dgmo-treemap-header-value')
291
- .attr('x', w - ICON_RESERVE - 4)
292
- .attr('y', 13)
293
- .attr('text-anchor', 'end')
294
- .attr('font-size', 10.5)
295
- .attr('fill', ink)
296
- .attr('opacity', 0.75)
297
- .text(valStr);
298
- }
299
- }
300
-
301
- // Leaf / collapsed-block labels. The name scales with the cell so a big
302
- // shape fills its space; the bigger the label, the more it's muted — a soft
303
- // watermark on large cells, crisp on small ones. Fit-to-width so the name
304
- // never has to truncate just because it scaled up.
305
- // Pad scales down on small cells so a tight label (e.g. a rolled-up
306
- // "Trivia" leaf) still fits instead of truncating; big cells keep the roomy
307
- // 14px gutter. MIN_FS lets the name shrink well below the comfortable 12px
308
- // floor on cramped cells — readability yields to showing the whole word.
309
- const PAD = Math.max(4, Math.min(14, Math.floor(Math.min(w, h) / 5)));
310
- const MIN_FS = 7;
311
- if (!cell.isContainer && w >= 2 * PAD + 8 && h >= 2 * PAD + 8) {
312
- const maxW = w - 2 * PAD;
313
-
314
- // Vertical fallback: when the name can't fit horizontally even at the
315
- // floor font, the cell is clearly taller than wide, and rotating buys
316
- // real length (height beats the horizontal budget), run the name down the
317
- // cell instead of truncating it. Value/% are dropped in this mode — a
318
- // narrow tile has no room for a second column, and showing the whole name
319
- // is the point. Skips the horizontal block entirely when it fires.
320
- const horizMinW = measureText(cell.label, MIN_FS) * 1.06;
321
- if (horizMinW > maxW && h > w * 1.25 && h - 2 * PAD > maxW) {
322
- drawVerticalLabel(g, cell.label, w, h, PAD, ink);
323
- if (drillable && w > 30 && h > 22) drawFocusIcon(g, w, ink, 9);
324
- continue;
325
- }
326
-
327
- let fs = clamp(Math.round(Math.min(w / 3.6, h / 3.4)), MIN_FS, 72);
328
- // Safety factor: text-measure under-estimates bold glyph widths, so shrink
329
- // a touch more to guarantee the PAD gap from the right edge.
330
- const nameW = measureText(cell.label, fs) * 1.06;
331
- if (nameW > maxW) fs = Math.max(MIN_FS, Math.floor((fs * maxW) / nameW));
332
- const tg = clamp((fs - MIN_FS) / (72 - MIN_FS), 0, 1);
333
- const nameOpacity = 0.95 - tg * 0.5; // 0.95 small → 0.45 large
334
- const vfs = clamp(Math.round(fs * 0.42), 11, 28);
335
- const valOpacity = Math.max(0.45, nameOpacity - 0.1);
336
-
337
- let y = PAD + Math.round(fs * 0.78); // top PAD above the cap height
338
- g.append('text')
339
- .attr('class', 'dgmo-treemap-label')
340
- .attr('x', PAD)
341
- .attr('y', y)
342
- .attr('font-size', fs)
343
- .attr('font-weight', 600)
344
- .attr('fill', ink)
345
- .attr('opacity', nameOpacity)
346
- .text(clipLabel(cell.label, maxW, fs));
347
-
348
- const valParts = [
349
- opts.noValues ? '' : compactNumber(cell.value),
350
- opts.noPercent ? '' : formatPct(cell.pctOfRoot),
351
- ].filter(Boolean);
352
- const combined = valParts.join(' · ');
353
- const nameBaseY = y;
354
- const addLineAt = (
355
- text: string,
356
- yy: number,
357
- size: number,
358
- opacity: number
359
- ): void => {
360
- g.append('text')
361
- .attr('class', 'dgmo-treemap-value')
362
- .attr('x', PAD)
363
- .attr('y', yy)
364
- .attr('font-size', size)
365
- .attr('fill', ink)
366
- .attr('opacity', opacity)
367
- .text(text);
368
- };
369
-
370
- if (combined) {
371
- const fitsW = measureText(combined, vfs) <= maxW;
372
- const valY = y + lineDrop(fs, vfs);
373
- const roomBelow = valY + descent(vfs) <= h - PAD;
374
- if (fitsW && roomBelow) {
375
- // Preferred: value · % on its own line under the name.
376
- addLineAt(combined, valY, vfs, valOpacity);
377
- } else if (fitsW) {
378
- // Short cell: no room below → trail the value after the name,
379
- // smaller, on the same line (if it fits the width).
380
- const ivfs = clamp(Math.round(fs * 0.5), 11, Math.max(11, vfs));
381
- const rendered = clipLabel(cell.label, maxW, fs);
382
- // Safety factor: text-measure under-estimates bold glyph width, so pad
383
- // it to the worst case (and add a real gap) or the value collides with
384
- // the name's end (e.g. "Hardtack70").
385
- const nW = measureText(rendered, fs) * 1.12;
386
- const gap = Math.max(12, Math.round(fs * 0.25));
387
- if (PAD + nW + gap + measureText(combined, ivfs) <= w - PAD) {
388
- g.append('text')
389
- .attr('class', 'dgmo-treemap-value')
390
- .attr('x', PAD + nW + gap)
391
- .attr('y', nameBaseY)
392
- .attr('font-size', ivfs)
393
- .attr('fill', ink)
394
- .attr('opacity', valOpacity)
395
- .text(combined);
396
- }
397
- } else if (valParts.length === 2) {
398
- // Tall-narrow cell: combined too wide → stack value then % below.
399
- const d1 = lineDrop(fs, vfs);
400
- if (y + d1 + descent(vfs) <= h - PAD) {
401
- y += d1;
402
- addLineAt(valParts[0]!, y, vfs, valOpacity);
403
- }
404
- const pfs = Math.max(10, Math.round(vfs * 0.9));
405
- const d2 = lineDrop(vfs, pfs);
406
- if (y + d2 + descent(pfs) <= h - PAD) {
407
- y += d2;
408
- addLineAt(valParts[1]!, y, pfs, valOpacity);
409
- }
410
- } else {
411
- const onlyY = y + lineDrop(fs, vfs);
412
- if (onlyY + descent(vfs) <= h - PAD) {
413
- addLineAt(combined, onlyY, vfs, valOpacity);
414
- }
415
- }
416
- }
417
- }
418
-
419
- // Scope/target focus icon on drillable cells (interactive-only).
420
- if (drillable && w > 30 && h > 22) {
421
- drawFocusIcon(g, w, ink, 9);
422
- }
423
- }
424
-
425
- if (legend) {
426
- const legendG = svg
427
- .append('g')
428
- .attr('class', 'dgmo-treemap-legend')
429
- .attr('transform', `translate(0, ${titleH})`);
430
- renderIntegratedLegend(legendG, {
431
- groups: legend.groups,
432
- palette: {
433
- bg: palette.bg,
434
- surface: palette.surface,
435
- text: palette.text,
436
- textMuted: palette.textMuted,
437
- primary: palette.primary,
438
- },
439
- isDark,
440
- width,
441
- mode: exportMode ? 'export' : 'preview',
442
- position: LEGEND_POSITION,
443
- activeGroup: legend.activeGroup,
444
- // The color-mode switcher is the legend itself: each applicable mode is a
445
- // group pill; the active one is the open capsule, the rest are clickable
446
- // pills that switch mode. Export shows only the active group.
447
- showInactivePills: !exportMode,
448
- showEmptyGroups: !exportMode,
449
- ...(options.onSelectMode !== undefined && {
450
- callbacks: {
451
- onGroupToggle: (name: string) => {
452
- const m = legend.modeByName.get(name);
453
- if (m) options.onSelectMode!(m);
454
- },
455
- },
456
- }),
457
- });
458
- }
459
- }
460
-
461
- // ============================================================
462
- // Helpers
463
- // ============================================================
464
-
465
- function drawFocusIcon(
466
- g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
467
- w: number,
468
- ink: string,
469
- cy: number
470
- ): void {
471
- // Small, subtle scope/target ring centered vertically at `cy` (the header
472
- // band's middle). Opacity is driven by the SVG <style> (faint by default,
473
- // clearer on cell hover). A generous transparent rect keeps it easy to click.
474
- const r = 4.5;
475
- const cx = w - r - 5;
476
- const fi = g
477
- .append('g')
478
- .attr('class', 'dgmo-treemap-focus')
479
- .attr('data-export-ignore', 'true');
480
- fi.append('rect')
481
- .attr('x', cx - 9)
482
- .attr('y', cy - 9)
483
- .attr('width', 18)
484
- .attr('height', 18)
485
- .attr('fill', 'transparent');
486
- fi.append('circle')
487
- .attr('cx', cx)
488
- .attr('cy', cy)
489
- .attr('r', r)
490
- .attr('fill', 'none')
491
- .attr('stroke', ink)
492
- .attr('stroke-width', 1.25);
493
- fi.append('circle')
494
- .attr('cx', cx)
495
- .attr('cy', cy)
496
- .attr('r', 1.4)
497
- .attr('fill', ink);
498
- }
499
-
500
- /**
501
- * Run a leaf's name vertically (reads bottom-to-top) down a tall-narrow cell.
502
- * Font is bound by the cell WIDTH (the rotated cap height) and the name length
503
- * by the cell HEIGHT; the name still clips if even rotated it can't fit. No
504
- * value/% — the caller only reaches here when the cell is too narrow for them.
505
- */
506
- function drawVerticalLabel(
507
- g: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
508
- label: string,
509
- w: number,
510
- h: number,
511
- pad: number,
512
- ink: string
513
- ): void {
514
- const MIN_FS = 6;
515
- const availLen = h - 2 * pad; // length budget, along the cell height
516
- const availThick = w - 2 * pad; // thickness budget, across the cell width
517
- // Thickness-bound first: cap the glyph height to the cell width (modest cap so
518
- // vertical names stay subtle), then shrink to fit the name into the length.
519
- let fs = clamp(Math.round(Math.min(availThick / 0.8, 14)), MIN_FS, 22);
520
- const nameW = measureText(label, fs) * 1.06;
521
- if (nameW > availLen)
522
- fs = Math.max(MIN_FS, Math.floor((fs * availLen) / nameW));
523
- const text = clipLabel(label, availLen, fs);
524
- if (!text) return;
525
-
526
- g.append('text')
527
- .attr('class', 'dgmo-treemap-label')
528
- // Center on the cell, rotate -90 so the string runs upward; the small y
529
- // offset recenters the glyph's cap height across the cell width (resvg has
530
- // no reliable dominant-baseline, so it's done by hand like the H labels).
531
- .attr('transform', `translate(${w / 2},${h / 2}) rotate(-90)`)
532
- .attr('y', Math.round(fs * 0.28))
533
- .attr('text-anchor', 'middle')
534
- .attr('font-size', fs)
535
- .attr('font-weight', 600)
536
- .attr('fill', ink)
537
- .attr('opacity', 0.95)
538
- .text(text);
539
- }
540
-
541
- /** Depth a glyph's descender (p, q, g, y, j) drops below the baseline. Inter's
542
- * deepest descenders sit at ~0.24em; round up so a big `g`/`y` tail never grazes
543
- * the line beneath it. */
544
- function descent(fs: number): number {
545
- return Math.ceil(fs * 0.26);
546
- }
547
-
548
- /**
549
- * Baseline-to-baseline drop from a line at `prevFs` to the next at `nextFs`:
550
- * clear the previous line's descender, leave a small gap, then drop by the next
551
- * line's cap height. Without the descender term, a large name's `p`/`g`/`y`
552
- * tails collide with the value line beneath it.
553
- */
554
- function lineDrop(prevFs: number, nextFs: number, gap = 4): number {
555
- return descent(prevFs) + gap + Math.round(nextFs * 0.72);
556
- }
557
-
558
- function clamp(x: number, lo: number, hi: number): number {
559
- return Math.max(lo, Math.min(hi, x));
560
- }
561
-
562
- function clipLabel(s: string, maxWidth: number, fs: number): string {
563
- if (measureText(s, fs) <= maxWidth) return s;
564
- let lo = 0;
565
- let hi = s.length;
566
- while (lo < hi) {
567
- const mid = Math.ceil((lo + hi) / 2);
568
- if (measureText(s.slice(0, mid) + '…', fs) <= maxWidth) lo = mid;
569
- else hi = mid - 1;
570
- }
571
- return lo <= 0 ? '' : s.slice(0, lo) + '…';
572
- }