@diagrammo/dgmo 0.47.0 → 0.48.1

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 (345) hide show
  1. package/dist/advanced.js +11 -11
  2. package/dist/auto.js +16 -16
  3. package/dist/auto.mjs +41 -12
  4. package/dist/block.js +4 -4
  5. package/dist/{chunk-FNB3PNGF.js → chunk-7DTWEFUE.js} +3 -3
  6. package/dist/{chunk-2DXMZRWJ.js → chunk-B65G5DNL.js} +8 -1
  7. package/dist/{chunk-RZCF2VVM.js → chunk-B7JUCQUA.js} +3 -1
  8. package/dist/{chunk-DU7W62N5.js → chunk-FANX6UJR.js} +3 -3
  9. package/dist/{chunk-6FZYCKXB.js → chunk-HSH73DP6.js} +2 -2
  10. package/dist/{chunk-P4RH4OSP.js → chunk-ILKORZ3H.js} +2 -2
  11. package/dist/{chunk-Y5QGVTVC.js → chunk-JTSMEYIZ.js} +7 -3
  12. package/dist/{chunk-RCERWOJR.js → chunk-ODZVHU3R.js} +1 -2
  13. package/dist/{chunk-DZAQBW35.js → chunk-SGZYXELT.js} +3 -7
  14. package/dist/{chunk-CUVHQHZ7.js → chunk-THXCFRTU.js} +3 -3
  15. package/dist/{chunk-IH6ZMVQ5.js → chunk-UC7TFHUM.js} +29 -11
  16. package/dist/{chunk-EMS2SK26.js → chunk-ZBWMSD5N.js} +3 -1
  17. package/dist/cli.cjs +15 -15
  18. package/dist/{dgmo-router-DZCIDUO5.js → dgmo-router-X35VLZLM.js} +3 -3
  19. package/dist/element.js +16 -16
  20. package/dist/element.mjs +41 -12
  21. package/dist/{flowchart-renderer-ZAEOL4DX.js → flowchart-renderer-SNDSII4D.js} +2 -2
  22. package/dist/index.d.ts +4 -2
  23. package/dist/index.js +4 -4
  24. package/dist/{parser-FUSKFGZN.js → parser-2EWECDSA.js} +2 -2
  25. package/dist/{renderer-UWA5XS74.js → renderer-4JHRUBE6.js} +3 -3
  26. package/dist/{renderer-WZRREQ2P.js → renderer-5MYBMBYO.js} +2 -2
  27. package/dist/{renderer-TOCOVLHS.js → renderer-B2OMQIQG.js} +3 -3
  28. package/dist/{renderer-2XQQQ6QR.js → renderer-N43WR5GV.js} +3 -3
  29. package/dist/{renderer-T7YKC5VP.js → renderer-ZRNG54CW.js} +3 -3
  30. package/dist/{state-renderer-RFYHSP4K.js → state-renderer-B4DQBM6M.js} +2 -2
  31. package/package.json +23 -71
  32. package/dist/advanced.cjs +0 -71648
  33. package/dist/advanced.d.cts +0 -5719
  34. package/dist/auto.cjs +0 -70648
  35. package/dist/auto.d.cts +0 -54
  36. package/dist/block.cjs +0 -70208
  37. package/dist/block.d.cts +0 -115
  38. package/dist/editor.cjs +0 -526
  39. package/dist/editor.d.cts +0 -26
  40. package/dist/element.cjs +0 -70530
  41. package/dist/element.d.cts +0 -62
  42. package/dist/highlight.cjs +0 -852
  43. package/dist/highlight.d.cts +0 -49
  44. package/dist/index.cjs +0 -70491
  45. package/dist/index.d.cts +0 -160
  46. package/dist/pert.cjs +0 -325
  47. package/dist/pert.d.cts +0 -559
  48. package/dist/themes-1-CuKpeH.d.cts +0 -908
  49. package/src/advanced.ts +0 -873
  50. package/src/arc/parser.ts +0 -15
  51. package/src/arc/renderer.ts +0 -633
  52. package/src/auto/index.ts +0 -621
  53. package/src/auto/shared.ts +0 -370
  54. package/src/auto/styles.ts +0 -73
  55. package/src/block/layout.ts +0 -198
  56. package/src/block/parser.ts +0 -427
  57. package/src/block/renderer.ts +0 -484
  58. package/src/block/types.ts +0 -67
  59. package/src/boxes-and-lines/collapse.ts +0 -96
  60. package/src/boxes-and-lines/focus.ts +0 -257
  61. package/src/boxes-and-lines/label-placement.ts +0 -339
  62. package/src/boxes-and-lines/layout-grouped.ts +0 -912
  63. package/src/boxes-and-lines/layout-layered.ts +0 -722
  64. package/src/boxes-and-lines/layout-search.ts +0 -1678
  65. package/src/boxes-and-lines/layout.ts +0 -398
  66. package/src/boxes-and-lines/parser.ts +0 -1181
  67. package/src/boxes-and-lines/renderer.ts +0 -1465
  68. package/src/boxes-and-lines/types.ts +0 -66
  69. package/src/c4/layout.ts +0 -2441
  70. package/src/c4/parser.ts +0 -1158
  71. package/src/c4/renderer.ts +0 -2008
  72. package/src/c4/types.ts +0 -89
  73. package/src/chart-type-registry.ts +0 -529
  74. package/src/chart-types.ts +0 -225
  75. package/src/chart.ts +0 -685
  76. package/src/charts-d3/bar.ts +0 -338
  77. package/src/charts-d3/chord.ts +0 -140
  78. package/src/charts-d3/function.ts +0 -147
  79. package/src/charts-d3/funnel.ts +0 -104
  80. package/src/charts-d3/heatmap.ts +0 -145
  81. package/src/charts-d3/index.ts +0 -337
  82. package/src/charts-d3/interactions.ts +0 -477
  83. package/src/charts-d3/line.ts +0 -344
  84. package/src/charts-d3/mount.ts +0 -151
  85. package/src/charts-d3/pie.ts +0 -117
  86. package/src/charts-d3/polar.ts +0 -89
  87. package/src/charts-d3/radar.ts +0 -0
  88. package/src/charts-d3/sankey.ts +0 -0
  89. package/src/charts-d3/scatter.ts +0 -185
  90. package/src/charts-d3/shared.ts +0 -208
  91. package/src/class/layout.ts +0 -326
  92. package/src/class/parser.ts +0 -635
  93. package/src/class/renderer.ts +0 -793
  94. package/src/class/types.ts +0 -58
  95. package/src/cli-banner.ts +0 -106
  96. package/src/cli.ts +0 -1080
  97. package/src/colors-diagnostics.ts +0 -35
  98. package/src/colors.ts +0 -379
  99. package/src/completion-types.ts +0 -27
  100. package/src/completion.ts +0 -2668
  101. package/src/cycle/layout.ts +0 -1026
  102. package/src/cycle/parser.ts +0 -387
  103. package/src/cycle/renderer.ts +0 -653
  104. package/src/cycle/types.ts +0 -101
  105. package/src/d3.ts +0 -1434
  106. package/src/data-chart-parser.ts +0 -1007
  107. package/src/dgmo-router.ts +0 -296
  108. package/src/diagnostics-registry.ts +0 -108
  109. package/src/diagnostics.ts +0 -0
  110. package/src/dimensions.ts +0 -20
  111. package/src/directives-registry.ts +0 -396
  112. package/src/editor/dgmo.grammar +0 -76
  113. package/src/editor/dgmo.grammar.d.ts +0 -2
  114. package/src/editor/dgmo.grammar.js +0 -18
  115. package/src/editor/dgmo.grammar.terms.d.ts +0 -5
  116. package/src/editor/dgmo.grammar.terms.js +0 -35
  117. package/src/editor/highlight-api.ts +0 -596
  118. package/src/editor/highlight.ts +0 -36
  119. package/src/editor/index.ts +0 -27
  120. package/src/editor/keywords.ts +0 -128
  121. package/src/editor/tokens.ts +0 -30
  122. package/src/element/index.ts +0 -432
  123. package/src/embed/css.ts +0 -312
  124. package/src/embed/escape.ts +0 -15
  125. package/src/embed/index.ts +0 -280
  126. package/src/er/classify.ts +0 -228
  127. package/src/er/layout.ts +0 -530
  128. package/src/er/parser.ts +0 -750
  129. package/src/er/renderer.ts +0 -725
  130. package/src/er/types.ts +0 -52
  131. package/src/error-card.ts +0 -269
  132. package/src/event-line/diagnostics.ts +0 -117
  133. package/src/event-line/parser.ts +0 -643
  134. package/src/event-line/renderer.ts +0 -1742
  135. package/src/event-line/types.ts +0 -74
  136. package/src/fonts.ts +0 -3
  137. package/src/gantt/calculator.ts +0 -979
  138. package/src/gantt/parser.ts +0 -1641
  139. package/src/gantt/renderer.ts +0 -3787
  140. package/src/gantt/resolver.ts +0 -172
  141. package/src/gantt/types.ts +0 -206
  142. package/src/graph/diagnostics.ts +0 -64
  143. package/src/graph/edge-spline.ts +0 -29
  144. package/src/graph/flowchart-parser.ts +0 -759
  145. package/src/graph/flowchart-renderer.ts +0 -817
  146. package/src/graph/layout.ts +0 -472
  147. package/src/graph/notes.ts +0 -21
  148. package/src/graph/state-collapse.ts +0 -78
  149. package/src/graph/state-parser.ts +0 -574
  150. package/src/graph/state-renderer.ts +0 -653
  151. package/src/graph/types.ts +0 -65
  152. package/src/index.ts +0 -261
  153. package/src/infra/compute.ts +0 -1385
  154. package/src/infra/layout.ts +0 -888
  155. package/src/infra/parser.ts +0 -1234
  156. package/src/infra/renderer.ts +0 -2534
  157. package/src/infra/roles.ts +0 -75
  158. package/src/infra/types.ts +0 -216
  159. package/src/infra/validation.ts +0 -201
  160. package/src/journey-map/layout.ts +0 -490
  161. package/src/journey-map/parser.ts +0 -557
  162. package/src/journey-map/renderer.ts +0 -1580
  163. package/src/journey-map/types.ts +0 -47
  164. package/src/kanban/mutations.ts +0 -175
  165. package/src/kanban/parser.ts +0 -509
  166. package/src/kanban/renderer.ts +0 -1449
  167. package/src/kanban/types.ts +0 -39
  168. package/src/label-layout.ts +0 -368
  169. package/src/map/colorize.ts +0 -54
  170. package/src/map/completion.ts +0 -316
  171. package/src/map/context-labels.ts +0 -609
  172. package/src/map/data/PROVENANCE.json +0 -1
  173. package/src/map/data/README.md +0 -40
  174. package/src/map/data/airport-collisions.json +0 -1
  175. package/src/map/data/airports.json +0 -1
  176. package/src/map/data/gazetteer.json +0 -1
  177. package/src/map/data/lakes.json +0 -1
  178. package/src/map/data/mountain-ranges.json +0 -1
  179. package/src/map/data/na-lakes.json +0 -1
  180. package/src/map/data/na-land.json +0 -1
  181. package/src/map/data/region-names.json +0 -1
  182. package/src/map/data/rivers.json +0 -1
  183. package/src/map/data/types.ts +0 -121
  184. package/src/map/data/us-states.json +0 -1
  185. package/src/map/data/water-bodies.json +0 -1
  186. package/src/map/data/world-coarse.json +0 -1
  187. package/src/map/data/world-detail.json +0 -1
  188. package/src/map/diagnostics.ts +0 -134
  189. package/src/map/dimensions.ts +0 -150
  190. package/src/map/geo-query.ts +0 -295
  191. package/src/map/geo.ts +0 -438
  192. package/src/map/invert.ts +0 -111
  193. package/src/map/layout.ts +0 -5069
  194. package/src/map/legend-band.ts +0 -127
  195. package/src/map/load-data.ts +0 -186
  196. package/src/map/parser.ts +0 -783
  197. package/src/map/renderer.ts +0 -1445
  198. package/src/map/resolved-types.ts +0 -160
  199. package/src/map/resolver.ts +0 -1146
  200. package/src/map/types.ts +0 -194
  201. package/src/mindmap/collapse.ts +0 -67
  202. package/src/mindmap/layout.ts +0 -649
  203. package/src/mindmap/parser.ts +0 -430
  204. package/src/mindmap/renderer.ts +0 -637
  205. package/src/mindmap/text-wrap.ts +0 -218
  206. package/src/mindmap/types.ts +0 -56
  207. package/src/org/collapse.ts +0 -151
  208. package/src/org/layout.ts +0 -1262
  209. package/src/org/parser.ts +0 -519
  210. package/src/org/renderer.ts +0 -866
  211. package/src/org/resolver.ts +0 -636
  212. package/src/palettes/atlas.ts +0 -77
  213. package/src/palettes/blueprint.ts +0 -73
  214. package/src/palettes/catppuccin.ts +0 -84
  215. package/src/palettes/color-utils.ts +0 -443
  216. package/src/palettes/index.ts +0 -67
  217. package/src/palettes/nord.ts +0 -67
  218. package/src/palettes/registry.ts +0 -114
  219. package/src/palettes/slate.ts +0 -73
  220. package/src/palettes/tidewater.ts +0 -73
  221. package/src/palettes/tokyo-night.ts +0 -86
  222. package/src/palettes/types.ts +0 -88
  223. package/src/pert/analyzer.ts +0 -1183
  224. package/src/pert/diagnostics.ts +0 -110
  225. package/src/pert/index.ts +0 -43
  226. package/src/pert/internal.ts +0 -252
  227. package/src/pert/layout.ts +0 -971
  228. package/src/pert/monte-carlo.ts +0 -446
  229. package/src/pert/parser.ts +0 -1606
  230. package/src/pert/renderer.ts +0 -4009
  231. package/src/pert/share-normalize.ts +0 -62
  232. package/src/pert/types.ts +0 -604
  233. package/src/pyramid/parser.ts +0 -185
  234. package/src/pyramid/renderer.ts +0 -768
  235. package/src/pyramid/types.ts +0 -28
  236. package/src/quadrant/parser.ts +0 -18
  237. package/src/quadrant/renderer.ts +0 -638
  238. package/src/raci/diagnostics.ts +0 -213
  239. package/src/raci/index.ts +0 -23
  240. package/src/raci/mutations.ts +0 -321
  241. package/src/raci/parser.ts +0 -699
  242. package/src/raci/renderer.ts +0 -1627
  243. package/src/raci/types.ts +0 -82
  244. package/src/raci/variants.ts +0 -320
  245. package/src/render.ts +0 -268
  246. package/src/ring/parser.ts +0 -212
  247. package/src/ring/renderer.ts +0 -490
  248. package/src/ring/types.ts +0 -27
  249. package/src/sequence/collapse.ts +0 -174
  250. package/src/sequence/parser.ts +0 -1596
  251. package/src/sequence/participant-inference.ts +0 -152
  252. package/src/sequence/renderer.ts +0 -3168
  253. package/src/sequence/tag-resolution.ts +0 -164
  254. package/src/sharing.ts +0 -211
  255. package/src/sitemap/collapse.ts +0 -100
  256. package/src/sitemap/layout.ts +0 -925
  257. package/src/sitemap/parser.ts +0 -755
  258. package/src/sitemap/renderer.ts +0 -828
  259. package/src/sitemap/types.ts +0 -42
  260. package/src/slope/parser.ts +0 -18
  261. package/src/slope/renderer.ts +0 -378
  262. package/src/swimlane/diagnostics.ts +0 -94
  263. package/src/swimlane/layout.ts +0 -597
  264. package/src/swimlane/parser.ts +0 -613
  265. package/src/swimlane/renderer.ts +0 -681
  266. package/src/swimlane/types.ts +0 -151
  267. package/src/tech-radar/interactive.ts +0 -1129
  268. package/src/tech-radar/layout.ts +0 -197
  269. package/src/tech-radar/parser.ts +0 -436
  270. package/src/tech-radar/renderer.ts +0 -1239
  271. package/src/tech-radar/shared.ts +0 -151
  272. package/src/tech-radar/types.ts +0 -86
  273. package/src/themes.ts +0 -22
  274. package/src/timeline/parser.ts +0 -472
  275. package/src/timeline/renderer.ts +0 -3024
  276. package/src/timeline/types.ts +0 -33
  277. package/src/timeline/viz-parser.ts +0 -21
  278. package/src/treemap/diagnostics.ts +0 -62
  279. package/src/treemap/layout-radial.ts +0 -233
  280. package/src/treemap/layout.ts +0 -189
  281. package/src/treemap/parser.ts +0 -463
  282. package/src/treemap/renderer-radial.ts +0 -438
  283. package/src/treemap/renderer.ts +0 -572
  284. package/src/treemap/treemap-shared.ts +0 -309
  285. package/src/treemap/types.ts +0 -60
  286. package/src/utils/arrow-markers.ts +0 -51
  287. package/src/utils/arrows-diagnostics.ts +0 -62
  288. package/src/utils/arrows.ts +0 -227
  289. package/src/utils/brand.ts +0 -61
  290. package/src/utils/card.ts +0 -183
  291. package/src/utils/collapse-engine/tree-edges.ts +0 -115
  292. package/src/utils/collapse-engine/tree.ts +0 -117
  293. package/src/utils/d3-helpers.ts +0 -219
  294. package/src/utils/d3-types.ts +0 -9
  295. package/src/utils/description-helpers.ts +0 -38
  296. package/src/utils/duration.ts +0 -312
  297. package/src/utils/export-container.ts +0 -41
  298. package/src/utils/expr-eval.ts +0 -27
  299. package/src/utils/fit-canvas.ts +0 -64
  300. package/src/utils/hover-styles.ts +0 -673
  301. package/src/utils/inline-markdown.ts +0 -96
  302. package/src/utils/legend-constants.ts +0 -48
  303. package/src/utils/legend-d3.ts +0 -618
  304. package/src/utils/legend-integration.ts +0 -107
  305. package/src/utils/legend-layout.ts +0 -776
  306. package/src/utils/legend-svg.ts +0 -214
  307. package/src/utils/legend-types.ts +0 -272
  308. package/src/utils/name-normalize.ts +0 -163
  309. package/src/utils/note-box/constants.ts +0 -25
  310. package/src/utils/note-box/index.ts +0 -11
  311. package/src/utils/note-box/metrics.ts +0 -90
  312. package/src/utils/note-box/svg.ts +0 -331
  313. package/src/utils/notes/bounds.ts +0 -30
  314. package/src/utils/notes/build.ts +0 -131
  315. package/src/utils/notes/index.ts +0 -18
  316. package/src/utils/notes/model.ts +0 -19
  317. package/src/utils/notes/parse.ts +0 -131
  318. package/src/utils/notes/place.ts +0 -177
  319. package/src/utils/notes/resolve.ts +0 -88
  320. package/src/utils/number-format.ts +0 -36
  321. package/src/utils/parsing.ts +0 -953
  322. package/src/utils/reserved-key-registry.ts +0 -239
  323. package/src/utils/safe-href.ts +0 -38
  324. package/src/utils/scaling.ts +0 -97
  325. package/src/utils/svg-embed.ts +0 -251
  326. package/src/utils/tag-groups.ts +0 -607
  327. package/src/utils/text-measure.ts +0 -122
  328. package/src/utils/time-ticks.ts +0 -253
  329. package/src/utils/title-constants.ts +0 -24
  330. package/src/utils/visual-conventions.ts +0 -61
  331. package/src/utils/wrapped-desc.ts +0 -87
  332. package/src/venn/parser.ts +0 -18
  333. package/src/venn/renderer.ts +0 -1047
  334. package/src/version-control/diagnostics.ts +0 -67
  335. package/src/version-control/parser.ts +0 -544
  336. package/src/version-control/renderer.ts +0 -724
  337. package/src/version-control/types.ts +0 -100
  338. package/src/visualizations/parse.ts +0 -1560
  339. package/src/visualizations/types.ts +0 -262
  340. package/src/wireframe/layout.ts +0 -479
  341. package/src/wireframe/parser.ts +0 -1114
  342. package/src/wireframe/renderer.ts +0 -1318
  343. package/src/wireframe/types.ts +0 -110
  344. package/src/wordcloud/parser.ts +0 -18
  345. package/src/wordcloud/renderer.ts +0 -357
@@ -1,653 +0,0 @@
1
- // ============================================================
2
- // Cycle Diagram — D3 SVG Renderer
3
- // ============================================================
4
-
5
- import * as d3Selection from 'd3-selection';
6
- import { FONT_FAMILY } from '../fonts';
7
- import {
8
- TITLE_FONT_SIZE,
9
- TITLE_FONT_WEIGHT,
10
- TITLE_Y,
11
- } from '../utils/title-constants';
12
- import { LEGEND_HEIGHT } from '../utils/legend-constants';
13
- import { renderIntegratedLegend } from '../utils/legend-integration';
14
- import type {
15
- LegendCallbacks,
16
- ControlsGroupToggle,
17
- } from '../utils/legend-types';
18
- import { contrastText, shapeFill } from '../palettes/color-utils';
19
- import { resolveColor } from '../colors';
20
- import { renderInlineText } from '../utils/inline-markdown';
21
- import type { PaletteColors } from '../palettes';
22
- import type { D3ExportDimensions } from '../utils/d3-types';
23
- import type { CompactViewState } from '../sharing';
24
- import {
25
- DEFAULT_EDGE_WIDTH,
26
- MIN_EDGE_WIDTH,
27
- arrowHeadLength,
28
- type ParsedCycle,
29
- } from './types';
30
- import { computeCycleLayout, wrapEdgeLabelText } from './layout';
31
- import { ScaleContext } from '../utils/scaling';
32
- import { measureText } from '../utils/text-measure';
33
-
34
- // ── Constants ────────────────────────────────────────────────
35
- const NODE_FONT_SIZE = 13;
36
- const DESC_FONT_SIZE = 11;
37
- const EDGE_LABEL_FONT_SIZE = 11;
38
- const DESC_LINE_HEIGHT = 15;
39
- const TITLE_AREA_HEIGHT = 50;
40
-
41
- export interface CycleRenderOptions {
42
- onClickItem?: (lineNumber: number) => void;
43
- exportDims?: D3ExportDimensions;
44
- viewState?: CompactViewState;
45
- hideDescriptions?: boolean;
46
- controlsExpanded?: boolean;
47
- onToggleDescriptions?: (active: boolean) => void;
48
- onToggleControlsExpand?: () => void;
49
- exportMode?: boolean;
50
- /** When 'app', the description toggle is hosted by the app overlay strip:
51
- * the inline gear is suppressed and a controls row + anchor are reserved.
52
- * Default (inline) renders the gear as before. */
53
- controlsHost?: 'app' | 'inline';
54
- }
55
-
56
- /**
57
- * Render a cycle diagram into the given container.
58
- */
59
- export function renderCycle(
60
- container: HTMLDivElement,
61
- parsed: ParsedCycle,
62
- palette: PaletteColors,
63
- isDark: boolean,
64
- onClickItem?: (lineNumber: number) => void,
65
- exportDims?: D3ExportDimensions,
66
- viewState?: CompactViewState,
67
- renderOptions?: CycleRenderOptions
68
- ): void {
69
- if (parsed.nodes.length === 0) return;
70
- const solid = parsed.options?.['solid-fill'] === 'on';
71
-
72
- // Clear previous render
73
- d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
74
- const width = exportDims?.width ?? container.clientWidth;
75
- const height = exportDims?.height ?? container.clientHeight;
76
- if (width <= 0 || height <= 0) return;
77
-
78
- const idealWidth = width;
79
- const sctx = exportDims
80
- ? ScaleContext.identity()
81
- : ScaleContext.from(width, idealWidth);
82
-
83
- const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
84
- const sTitleY = sctx.structural(TITLE_Y);
85
- const sTitleAreaHeight = sctx.structural(TITLE_AREA_HEIGHT);
86
- const sLegendHeight = sctx.structural(LEGEND_HEIGHT);
87
-
88
- const hideDescriptions =
89
- (renderOptions?.hideDescriptions ?? false) ||
90
- parsed.options['no-descriptions'] === 'true' ||
91
- viewState?.hd === true;
92
- const showDescriptions = !hideDescriptions;
93
-
94
- // Check if descriptions exist in the diagram
95
- const hasDescriptions =
96
- parsed.nodes.some((n) => n.description.length > 0) ||
97
- parsed.edges.some((e) => e.description.length > 0);
98
- // App-hosted: controls live in the app overlay strip. Cycle has no tag groups,
99
- // so there's no in-SVG legend left to render — don't reserve a legend band.
100
- const appHostedControls = renderOptions?.controlsHost === 'app';
101
- const hasLegend =
102
- !appHostedControls &&
103
- hasDescriptions &&
104
- !!renderOptions?.onToggleDescriptions;
105
-
106
- const showTitle = !!parsed.title && parsed.options['no-title'] !== 'on';
107
- const legendOffset = hasLegend ? sLegendHeight : 0;
108
- const layoutHeight =
109
- height - (showTitle ? sTitleAreaHeight : 0) - legendOffset;
110
- const layout = computeCycleLayout(parsed, {
111
- width,
112
- height: layoutHeight,
113
- hideDescriptions,
114
- });
115
-
116
- const svg = d3Selection
117
- .select(container)
118
- .append('svg')
119
- .attr('width', width)
120
- .attr('height', height)
121
- .attr('viewBox', `0 0 ${width} ${height}`)
122
- .attr('preserveAspectRatio', 'xMidYMid meet')
123
- .attr('xmlns', 'http://www.w3.org/2000/svg')
124
- .style('font-family', FONT_FAMILY);
125
-
126
- if (sctx.isBelowFloor) {
127
- svg.attr('width', '100%');
128
- }
129
-
130
- svg
131
- .append('rect')
132
- .attr('width', width)
133
- .attr('height', height)
134
- .attr('fill', palette.bg);
135
-
136
- if (showTitle) {
137
- const titleText = svg
138
- .append('text')
139
- .attr('x', width / 2)
140
- .attr('y', sTitleY)
141
- .attr('text-anchor', 'middle')
142
- .attr('fill', palette.text)
143
- .attr('font-family', FONT_FAMILY)
144
- .attr('font-size', sTitleFontSize)
145
- .attr('font-weight', TITLE_FONT_WEIGHT)
146
- .attr('data-line-number', parsed.titleLineNumber)
147
- .text(parsed.title)
148
- .style('cursor', onClickItem ? 'pointer' : 'default');
149
- if (onClickItem) {
150
- titleText.on('click', () => onClickItem(parsed.titleLineNumber));
151
- }
152
- }
153
-
154
- // Legend (controls toggle for descriptions)
155
- if (hasLegend) {
156
- const controlsGroup: { toggles: ControlsGroupToggle[] } = {
157
- toggles: [
158
- {
159
- id: 'descriptions',
160
- type: 'toggle',
161
- label: 'Descriptions',
162
- active: !hideDescriptions,
163
- onToggle: () => {},
164
- },
165
- ],
166
- };
167
- const legendCallbacks: LegendCallbacks = {
168
- ...(renderOptions?.onToggleControlsExpand !== undefined && {
169
- onControlsExpand: renderOptions.onToggleControlsExpand,
170
- }),
171
- onControlsToggle: (toggleId, active) => {
172
- if (
173
- toggleId === 'descriptions' &&
174
- renderOptions?.onToggleDescriptions
175
- ) {
176
- renderOptions.onToggleDescriptions(active);
177
- }
178
- },
179
- };
180
- const titleOffsetForLegend = showTitle ? sTitleAreaHeight : 0;
181
- const legendG = svg
182
- .append('g')
183
- .attr('transform', `translate(0, ${titleOffsetForLegend + 4})`);
184
- renderIntegratedLegend(legendG, {
185
- groups: [],
186
- activeGroup: null,
187
- mode: renderOptions?.exportMode ? 'export' : 'preview',
188
- controlsGroup,
189
- ...(renderOptions?.controlsHost !== undefined && {
190
- controlsHost: renderOptions.controlsHost,
191
- }),
192
- ...(renderOptions?.controlsExpanded !== undefined && {
193
- controlsExpanded: renderOptions.controlsExpanded,
194
- }),
195
- callbacks: legendCallbacks,
196
- palette,
197
- isDark,
198
- width,
199
- });
200
- }
201
-
202
- const diagramTop = (showTitle ? sTitleAreaHeight : 0) + legendOffset;
203
- const g = svg.append('g').attr('transform', `translate(0, ${diagramTop})`);
204
-
205
- // Defs for arrowheads
206
- const defs = svg.append('defs');
207
-
208
- // Resolve default node color: first palette color (uniform)
209
- const defaultNodeColor = palette.primary;
210
-
211
- // ── Arrowhead markers (per color+width, markerUnits=strokeWidth) ──
212
- const markerKeys = new Set<string>();
213
- for (const edge of parsed.edges) {
214
- const color = resolveEdgeColor(edge, parsed, palette, defaultNodeColor);
215
- const sw = Math.max(edge.width ?? DEFAULT_EDGE_WIDTH, MIN_EDGE_WIDTH);
216
- const key = `${color}|${sw}`;
217
- if (!markerKeys.has(key)) {
218
- markerKeys.add(key);
219
- ensureArrowMarker(defs, color, sw);
220
- }
221
- }
222
-
223
- // ── Render edges paths (below nodes); labels rendered later, on top ──
224
- const edgeLabelInfo: Array<{
225
- le: (typeof layout.edges)[number];
226
- edge: (typeof parsed.edges)[number];
227
- }> = [];
228
- for (let i = 0; i < layout.edges.length; i++) {
229
- // In-bounds by loop guard; layout.edges and parsed.edges are parallel arrays.
230
- const le = layout.edges[i]!;
231
- const edge = parsed.edges[i]!;
232
- const color = resolveEdgeColor(edge, parsed, palette, defaultNodeColor);
233
- const strokeWidth = Math.max(
234
- edge.width ?? DEFAULT_EDGE_WIDTH,
235
- MIN_EDGE_WIDTH
236
- );
237
- const markerId = arrowMarkerId(color, strokeWidth);
238
-
239
- const edgeG = g.append('g').attr('class', 'cycle-edge');
240
-
241
- if (edge.lineNumber) {
242
- edgeG.attr('data-line-number', edge.lineNumber);
243
- }
244
- // Endpoint node indices for baked-CSS connection-highlight.
245
- edgeG
246
- .attr('data-from', String(edge.sourceIndex))
247
- .attr('data-to', String(edge.targetIndex));
248
-
249
- const pathEl = edgeG
250
- .append('path')
251
- .attr('d', le.path)
252
- .attr('fill', 'none')
253
- .attr('stroke', color)
254
- .attr('stroke-width', strokeWidth)
255
- .attr('marker-end', `url(#${markerId})`);
256
-
257
- if (onClickItem && edge.lineNumber) {
258
- const ln = edge.lineNumber;
259
- pathEl.style('cursor', 'pointer').on('click', () => onClickItem(ln));
260
- }
261
-
262
- edgeLabelInfo.push({ le, edge });
263
- }
264
-
265
- // ── Render nodes ──
266
- const HEADER_H = 36 * layout.scale;
267
- // Font floors are intentionally low (≈ half the natural size) so text keeps
268
- // shrinking alongside the shrinking nodes when the canvas is small. Higher
269
- // floors decouple text from shape scale, causing labels to overflow node
270
- // boundaries at small canvas sizes.
271
- const scaledNodeFont = Math.max(6, Math.round(NODE_FONT_SIZE * layout.scale));
272
- const scaledEdgeLabelFont = Math.max(
273
- 6,
274
- Math.round(EDGE_LABEL_FONT_SIZE * layout.scale)
275
- );
276
- const CIRCLE_LABEL_FONT_SIZE = 16;
277
- const scaledCircleLabelFont = Math.max(
278
- 8,
279
- Math.round(CIRCLE_LABEL_FONT_SIZE * layout.scale)
280
- );
281
- const scaledDescFont = Math.max(5, Math.round(DESC_FONT_SIZE * layout.scale));
282
- const scaledDescLineH = Math.max(
283
- 7,
284
- Math.round(DESC_LINE_HEIGHT * layout.scale)
285
- );
286
- const scaledEdgeLineH = Math.max(
287
- 8,
288
- Math.round(DESC_LINE_HEIGHT * layout.scale)
289
- );
290
-
291
- for (let i = 0; i < layout.nodes.length; i++) {
292
- // In-bounds by loop guard; layout.nodes and parsed.nodes are parallel arrays.
293
- const ln = layout.nodes[i]!;
294
- const node = parsed.nodes[i]!;
295
- const solidColor = resolveNodeColor(node.color, palette, defaultNodeColor);
296
- // Canonical 25% tinted fill via shapeFill() (or full intent when solid-fill is on).
297
- const fillColor = shapeFill(palette, solidColor, isDark, { solid });
298
- const textColor = contrastText(
299
- fillColor,
300
- palette.textOnFillLight,
301
- palette.textOnFillDark
302
- );
303
- // Description text sits on top of the node fill — must follow the same
304
- // contrast rule as the title, NOT use a fixed `palette.textMuted` gray
305
- // (which is only legible against bg/surface, not against saturated fills).
306
- const descColor = textColor;
307
- const nodeW = ln.width;
308
- const nodeH = ln.height;
309
- const wrappedDesc = ln.wrappedDesc;
310
- const hasDesc = showDescriptions && wrappedDesc.length > 0;
311
-
312
- const nodeG = g
313
- .append('g')
314
- .attr('class', 'cycle-node')
315
- .attr('data-line-number', node.lineNumber)
316
- // Node index for baked-CSS connection-highlight (cycle nodes have no
317
- // string id; edges key on the same index).
318
- .attr('data-node-index', String(i))
319
- .style('cursor', onClickItem ? 'pointer' : 'default');
320
-
321
- if (onClickItem) {
322
- const lineNum = node.lineNumber;
323
- nodeG.on('click', () => onClickItem(lineNum));
324
- }
325
-
326
- if (ln.isCircle) {
327
- // ── Circle node shape ──
328
- const r = nodeW / 2;
329
- nodeG
330
- .append('circle')
331
- .attr('cx', ln.x)
332
- .attr('cy', ln.y)
333
- .attr('r', r)
334
- .attr('fill', fillColor)
335
- .attr('stroke', solidColor)
336
- .attr('stroke-width', 2);
337
-
338
- if (hasDesc) {
339
- // Label + descriptions vertically centered in circle
340
- const labelFont = scaledCircleLabelFont;
341
- const blockH = labelFont + 4 + wrappedDesc.length * scaledDescLineH;
342
- const startY = ln.y - blockH / 2 + labelFont;
343
-
344
- const labelText = nodeG
345
- .append('text')
346
- .attr('x', ln.x)
347
- .attr('y', startY)
348
- .attr('text-anchor', 'middle')
349
- .attr('fill', textColor)
350
- .attr('font-family', FONT_FAMILY)
351
- .attr('font-size', labelFont)
352
- .attr('font-weight', '600');
353
- renderInlineText(labelText, node.label, palette, labelFont);
354
-
355
- let descY = startY + scaledDescLineH + 4;
356
- wrappedDesc.forEach((line) => {
357
- const descText = nodeG
358
- .append('text')
359
- .attr('x', ln.x)
360
- .attr('y', descY)
361
- .attr('text-anchor', 'middle')
362
- .attr('fill', descColor)
363
- .attr('font-family', FONT_FAMILY)
364
- .attr('font-size', scaledDescFont);
365
- renderInlineText(descText, line.text, palette, DESC_FONT_SIZE);
366
- descY += scaledDescLineH;
367
- });
368
- } else {
369
- // Label centered in circle
370
- const labelFont = scaledCircleLabelFont;
371
- const labelText = nodeG
372
- .append('text')
373
- .attr('x', ln.x)
374
- .attr('y', ln.y + labelFont / 3)
375
- .attr('text-anchor', 'middle')
376
- .attr('fill', textColor)
377
- .attr('font-family', FONT_FAMILY)
378
- .attr('font-size', labelFont)
379
- .attr('font-weight', '600');
380
- renderInlineText(labelText, node.label, palette, labelFont);
381
- }
382
- } else {
383
- // ── Rectangular node shape ──
384
- const rx = 6;
385
- nodeG
386
- .append('rect')
387
- .attr('x', ln.x - nodeW / 2)
388
- .attr('y', ln.y - nodeH / 2)
389
- .attr('width', nodeW)
390
- .attr('height', nodeH)
391
- .attr('rx', rx)
392
- .attr('ry', rx)
393
- .attr('fill', fillColor)
394
- .attr('stroke', solidColor)
395
- .attr('stroke-width', 2);
396
-
397
- if (hasDesc) {
398
- // ── Described node: header + separator + description ──
399
- const headerCenterY = ln.y - nodeH / 2 + HEADER_H / 2;
400
- const labelText = nodeG
401
- .append('text')
402
- .attr('x', ln.x)
403
- .attr('y', headerCenterY + scaledNodeFont / 3)
404
- .attr('text-anchor', 'middle')
405
- .attr('fill', textColor)
406
- .attr('font-family', FONT_FAMILY)
407
- .attr('font-size', scaledNodeFont)
408
- .attr('font-weight', '600');
409
- renderInlineText(labelText, node.label, palette, scaledNodeFont);
410
-
411
- const sepY = ln.y - nodeH / 2 + HEADER_H;
412
- nodeG
413
- .append('line')
414
- .attr('x1', ln.x - nodeW / 2)
415
- .attr('y1', sepY)
416
- .attr('x2', ln.x + nodeW / 2)
417
- .attr('y2', sepY)
418
- .attr('stroke', solid ? descColor : solidColor)
419
- .attr('stroke-opacity', 0.3)
420
- .attr('stroke-width', 1);
421
-
422
- const descStartY = sepY + 4 + scaledDescFont;
423
- const descPadX = Math.max(8, 12 * layout.scale);
424
- const descX = ln.x - nodeW / 2 + descPadX;
425
- // Bullet body column — body of bullet items + their continuations
426
- // share this x so wrapped text aligns under the first word past "•".
427
- const bulletBodyX = descX + Math.max(8, 12 * layout.scale);
428
- wrappedDesc.forEach((line, li) => {
429
- const lineY = descStartY + li * scaledDescLineH;
430
- if (line.kind === 'bullet-first') {
431
- // Bullet glyph as its own text element at descX
432
- nodeG
433
- .append('text')
434
- .attr('x', descX)
435
- .attr('y', lineY)
436
- .attr('text-anchor', 'start')
437
- .attr('fill', descColor)
438
- .attr('font-family', FONT_FAMILY)
439
- .attr('font-size', scaledDescFont)
440
- .text('•');
441
- // Body text at the bullet column
442
- const bodyText = nodeG
443
- .append('text')
444
- .attr('x', bulletBodyX)
445
- .attr('y', lineY)
446
- .attr('text-anchor', 'start')
447
- .attr('fill', descColor)
448
- .attr('font-family', FONT_FAMILY)
449
- .attr('font-size', scaledDescFont);
450
- renderInlineText(bodyText, line.text, palette, DESC_FONT_SIZE);
451
- } else {
452
- const x = line.kind === 'bullet-cont' ? bulletBodyX : descX;
453
- const descText = nodeG
454
- .append('text')
455
- .attr('x', x)
456
- .attr('y', lineY)
457
- .attr('text-anchor', 'start')
458
- .attr('fill', descColor)
459
- .attr('font-family', FONT_FAMILY)
460
- .attr('font-size', scaledDescFont);
461
- renderInlineText(descText, line.text, palette, DESC_FONT_SIZE);
462
- }
463
- });
464
- } else {
465
- // ── Plain node: label centered ──
466
- const labelText = nodeG
467
- .append('text')
468
- .attr('x', ln.x)
469
- .attr('y', ln.y + scaledNodeFont / 3)
470
- .attr('text-anchor', 'middle')
471
- .attr('fill', textColor)
472
- .attr('font-family', FONT_FAMILY)
473
- .attr('font-size', scaledNodeFont)
474
- .attr('font-weight', '600');
475
- renderInlineText(labelText, node.label, palette, scaledNodeFont);
476
- }
477
- }
478
- }
479
-
480
- // ── Render edge labels (in a new group appended after nodes so they sit
481
- // on top of nodes in document order, regardless of append-time ordering). ──
482
- const labelLayer = g.append('g').attr('class', 'cycle-edge-labels');
483
- for (const { le, edge } of edgeLabelInfo) {
484
- const hasEdgeLabel = !!le.label;
485
- const hasEdgeDesc = showDescriptions && edge.description.length > 0;
486
- const { labelLines, descLines } = wrapEdgeLabelText(
487
- hasEdgeLabel ? le.label : undefined,
488
- hasEdgeDesc ? edge.description : []
489
- );
490
- if (labelLines.length === 0 && descLines.length === 0) continue;
491
- const edgeG = labelLayer.append('g').attr('class', 'cycle-edge');
492
- if (edge.lineNumber) {
493
- edgeG.attr('data-line-number', edge.lineNumber);
494
- }
495
-
496
- const normAngle =
497
- ((le.labelAngle % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
498
- const isRight = normAngle < Math.PI * 0.4 || normAngle > Math.PI * 1.6;
499
- const isLeft = normAngle > Math.PI * 0.6 && normAngle < Math.PI * 1.4;
500
- const anchor = isRight ? 'start' : isLeft ? 'end' : 'middle';
501
-
502
- const lineCount = labelLines.length + descLines.length;
503
- // Measure the widest rendered line in pixels at the scaled edge-label font
504
- // so the background box matches the actual ink (same measurer the layout
505
- // uses to size + place the label).
506
- let maxLineW = 0;
507
- for (const l of labelLines)
508
- maxLineW = Math.max(maxLineW, measureText(l, scaledEdgeLabelFont));
509
- for (const l of descLines)
510
- maxLineW = Math.max(maxLineW, measureText(l, scaledDescFont));
511
-
512
- const bgW = maxLineW + 12;
513
- const bgH = lineCount * scaledEdgeLineH + 6;
514
- const bgX = isRight
515
- ? le.labelX - 4
516
- : isLeft
517
- ? le.labelX - bgW + 4
518
- : le.labelX - bgW / 2;
519
- const bgY = le.labelY - scaledEdgeLabelFont - 2;
520
-
521
- edgeG
522
- .append('rect')
523
- .attr('x', bgX)
524
- .attr('y', bgY)
525
- .attr('width', bgW)
526
- .attr('height', bgH)
527
- .attr('rx', 3)
528
- .attr('fill', palette.bg)
529
- .attr('fill-opacity', 0.85);
530
-
531
- let textY = le.labelY;
532
- for (const line of labelLines) {
533
- const labelText = edgeG
534
- .append('text')
535
- .attr('x', le.labelX)
536
- .attr('y', textY)
537
- .attr('text-anchor', anchor)
538
- .attr('fill', palette.text)
539
- .attr('font-family', FONT_FAMILY)
540
- .attr('font-size', scaledEdgeLabelFont)
541
- .attr('font-weight', '600');
542
- renderInlineText(labelText, line, palette, scaledEdgeLabelFont);
543
- textY += scaledEdgeLineH;
544
- }
545
- for (const line of descLines) {
546
- const descText = edgeG
547
- .append('text')
548
- .attr('x', le.labelX)
549
- .attr('y', textY)
550
- .attr('text-anchor', anchor)
551
- .attr('fill', palette.textMuted)
552
- .attr('font-family', FONT_FAMILY)
553
- .attr('font-size', scaledDescFont);
554
- renderInlineText(descText, line, palette, scaledDescFont);
555
- textY += scaledEdgeLineH;
556
- }
557
- }
558
- }
559
-
560
- /**
561
- * Render for CLI/export (no click handlers).
562
- */
563
- export function renderCycleForExport(
564
- container: HTMLDivElement,
565
- parsed: ParsedCycle,
566
- palette: PaletteColors,
567
- isDark: boolean,
568
- exportDims?: D3ExportDimensions,
569
- viewState?: CompactViewState,
570
- exportMode?: boolean
571
- ): void {
572
- renderCycle(
573
- container,
574
- parsed,
575
- palette,
576
- isDark,
577
- undefined,
578
- exportDims,
579
- viewState,
580
- { ...(exportMode !== undefined && { exportMode }) }
581
- );
582
- }
583
-
584
- // ── Helpers ──────────────────────────────────────────────────
585
-
586
- function resolveNodeColor(
587
- color: string | undefined,
588
- palette: PaletteColors,
589
- defaultColor: string
590
- ): string {
591
- if (!color) return defaultColor;
592
- return resolveColor(color, palette) ?? defaultColor;
593
- }
594
-
595
- function resolveEdgeColor(
596
- edge: ParsedCycle['edges'][0],
597
- parsed: ParsedCycle,
598
- palette: PaletteColors,
599
- defaultNodeColor: string
600
- ): string {
601
- if (edge.color) {
602
- return resolveColor(edge.color, palette) ?? defaultNodeColor;
603
- }
604
- // Inherit from source node
605
- const sourceNode = parsed.nodes[edge.sourceIndex];
606
- if (sourceNode?.color) {
607
- return resolveColor(sourceNode.color, palette) ?? defaultNodeColor;
608
- }
609
- return defaultNodeColor;
610
- }
611
-
612
- /** Stable marker ID for a (color, strokeWidth) pair. */
613
- function arrowMarkerId(color: string, strokeWidth: number): string {
614
- return `cycle-arrow-${color.replace('#', '')}-w${strokeWidth}`;
615
- }
616
-
617
- /**
618
- * Create an arrowhead marker using markerUnits="strokeWidth" (SVG default)
619
- * with per-edge dimensions. The marker base automatically equals the stroke
620
- * width — no gaps or lollipop effects. Marker dimensions are computed so
621
- * the rendered arrowhead length follows a sublinear formula:
622
- *
623
- * rendered length = markerWidth × strokeWidth = arrowHeadLength(sw)
624
- * → markerWidth = arrowHeadLength(sw) / sw
625
- *
626
- * The height is fixed at 1 strokeWidth unit so the base = stroke width.
627
- */
628
- function ensureArrowMarker(
629
- defs: d3Selection.Selection<SVGDefsElement, unknown, null, undefined>,
630
- color: string,
631
- strokeWidth: number
632
- ): void {
633
- const id = arrowMarkerId(color, strokeWidth);
634
- // Marker dimensions in strokeWidth units.
635
- // Rendered size = mw × sw (length) and mh × sw (height).
636
- const mw = arrowHeadLength(strokeWidth) / strokeWidth;
637
- // Height proportional to length (½ ratio) but at least 1.5× stroke width
638
- // so the arrowhead is always visibly wider than the stroke.
639
- const mh = Math.max(1.5, mw * 0.5);
640
-
641
- defs
642
- .append('marker')
643
- .attr('id', id)
644
- .attr('viewBox', `0 0 ${mw} ${mh}`)
645
- .attr('refX', mw * 0.1)
646
- .attr('refY', mh / 2)
647
- .attr('markerWidth', mw)
648
- .attr('markerHeight', mh)
649
- .attr('orient', 'auto')
650
- .append('polygon')
651
- .attr('points', `0,0 ${mw},${mh / 2} 0,${mh}`)
652
- .attr('fill', color);
653
- }