@diagrammo/dgmo 0.8.2 → 0.8.4

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 (120) hide show
  1. package/.claude/commands/dgmo-diagram-this.md +60 -0
  2. package/.claude/commands/dgmo-document-project.md +128 -0
  3. package/.claude/commands/dgmo.md +185 -50
  4. package/.cursorrules +32 -37
  5. package/.github/copilot-instructions.md +35 -44
  6. package/.windsurfrules +32 -37
  7. package/README.md +4 -4
  8. package/dist/cli.cjs +189 -194
  9. package/dist/editor.cjs +336 -0
  10. package/dist/editor.cjs.map +1 -0
  11. package/dist/editor.d.cts +27 -0
  12. package/dist/editor.d.ts +27 -0
  13. package/dist/editor.js +305 -0
  14. package/dist/editor.js.map +1 -0
  15. package/dist/index.cjs +3699 -1564
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +7 -6
  18. package/dist/index.d.ts +7 -6
  19. package/dist/index.js +3699 -1564
  20. package/dist/index.js.map +1 -1
  21. package/docs/language-reference.md +822 -1060
  22. package/gallery/fixtures/arc.dgmo +18 -0
  23. package/gallery/fixtures/area.dgmo +19 -0
  24. package/gallery/fixtures/bar-stacked.dgmo +10 -0
  25. package/gallery/fixtures/bar.dgmo +10 -0
  26. package/gallery/fixtures/c4-full.dgmo +52 -0
  27. package/gallery/fixtures/c4.dgmo +17 -0
  28. package/gallery/fixtures/chord.dgmo +12 -0
  29. package/gallery/fixtures/class-basic.dgmo +14 -0
  30. package/gallery/fixtures/class-full.dgmo +43 -0
  31. package/gallery/fixtures/doughnut.dgmo +8 -0
  32. package/gallery/fixtures/flowchart-basic.dgmo +3 -0
  33. package/gallery/fixtures/flowchart-colors.dgmo +5 -0
  34. package/gallery/fixtures/flowchart-complex.dgmo +17 -0
  35. package/gallery/fixtures/flowchart-decision.dgmo +5 -0
  36. package/gallery/fixtures/flowchart-full.dgmo +13 -0
  37. package/gallery/fixtures/flowchart-groups.dgmo +10 -0
  38. package/gallery/fixtures/flowchart-loop.dgmo +7 -0
  39. package/gallery/fixtures/flowchart-nested.dgmo +7 -0
  40. package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
  41. package/gallery/fixtures/function.dgmo +8 -0
  42. package/gallery/fixtures/funnel.dgmo +7 -0
  43. package/gallery/fixtures/gantt-full.dgmo +49 -0
  44. package/gallery/fixtures/gantt.dgmo +42 -0
  45. package/gallery/fixtures/heatmap.dgmo +8 -0
  46. package/gallery/fixtures/infra-full.dgmo +78 -0
  47. package/gallery/fixtures/infra-overload.dgmo +25 -0
  48. package/gallery/fixtures/infra.dgmo +47 -0
  49. package/gallery/fixtures/initiative-status-full.dgmo +46 -0
  50. package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
  51. package/gallery/fixtures/initiative-status.dgmo +9 -0
  52. package/gallery/fixtures/line.dgmo +19 -0
  53. package/gallery/fixtures/multi-line.dgmo +11 -0
  54. package/gallery/fixtures/org-basic.dgmo +16 -0
  55. package/gallery/fixtures/org-full.dgmo +69 -0
  56. package/gallery/fixtures/org-teams.dgmo +25 -0
  57. package/gallery/fixtures/pie.dgmo +9 -0
  58. package/gallery/fixtures/polar-area.dgmo +8 -0
  59. package/gallery/fixtures/quadrant.dgmo +18 -0
  60. package/gallery/fixtures/radar.dgmo +8 -0
  61. package/gallery/fixtures/sankey.dgmo +31 -0
  62. package/gallery/fixtures/scatter.dgmo +21 -0
  63. package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
  64. package/gallery/fixtures/sequence-tags.dgmo +41 -0
  65. package/gallery/fixtures/sequence.dgmo +35 -0
  66. package/gallery/fixtures/sitemap-basic.dgmo +12 -0
  67. package/gallery/fixtures/sitemap-full.dgmo +156 -0
  68. package/gallery/fixtures/slope.dgmo +8 -0
  69. package/gallery/fixtures/spr-eras.dgmo +62 -0
  70. package/gallery/fixtures/state.dgmo +30 -0
  71. package/gallery/fixtures/timeline-intraday.dgmo +14 -0
  72. package/gallery/fixtures/timeline.dgmo +32 -0
  73. package/gallery/fixtures/venn.dgmo +10 -0
  74. package/gallery/fixtures/wordcloud.dgmo +24 -0
  75. package/package.json +51 -2
  76. package/src/c4/layout.ts +372 -90
  77. package/src/c4/parser.ts +113 -62
  78. package/src/chart.ts +149 -64
  79. package/src/class/parser.ts +84 -28
  80. package/src/class/renderer.ts +2 -2
  81. package/src/cli.ts +179 -77
  82. package/src/completion.ts +381 -182
  83. package/src/d3.ts +1026 -428
  84. package/src/dgmo-mermaid.ts +16 -13
  85. package/src/dgmo-router.ts +70 -24
  86. package/src/echarts.ts +682 -169
  87. package/src/editor/dgmo.grammar +69 -0
  88. package/src/editor/dgmo.grammar.d.ts +2 -0
  89. package/src/editor/dgmo.grammar.js +18 -0
  90. package/src/editor/dgmo.grammar.terms.d.ts +5 -0
  91. package/src/editor/dgmo.grammar.terms.js +35 -0
  92. package/src/editor/highlight.ts +36 -0
  93. package/src/editor/index.ts +28 -0
  94. package/src/editor/keywords.ts +220 -0
  95. package/src/editor/tokens.ts +30 -0
  96. package/src/er/parser.ts +55 -29
  97. package/src/er/renderer.ts +112 -53
  98. package/src/gantt/calculator.ts +91 -29
  99. package/src/gantt/parser.ts +291 -97
  100. package/src/gantt/renderer.ts +1120 -350
  101. package/src/graph/flowchart-parser.ts +48 -75
  102. package/src/graph/state-parser.ts +54 -27
  103. package/src/infra/parser.ts +161 -177
  104. package/src/infra/renderer.ts +723 -271
  105. package/src/infra/types.ts +0 -1
  106. package/src/initiative-status/parser.ts +144 -56
  107. package/src/kanban/parser.ts +27 -19
  108. package/src/org/layout.ts +111 -44
  109. package/src/org/parser.ts +71 -27
  110. package/src/org/resolver.ts +3 -3
  111. package/src/palettes/index.ts +3 -2
  112. package/src/render.ts +1 -2
  113. package/src/sequence/parser.ts +209 -100
  114. package/src/sitemap/parser.ts +73 -44
  115. package/src/utils/arrows.ts +2 -22
  116. package/src/utils/duration.ts +39 -21
  117. package/src/utils/legend-constants.ts +0 -2
  118. package/src/utils/parsing.ts +82 -72
  119. package/src/utils/tag-groups.ts +4 -41
  120. package/src/infra/serialize.ts +0 -67
package/src/d3.ts CHANGED
@@ -182,8 +182,18 @@ import { getSeriesColors } from './palettes';
182
182
  import { mix } from './palettes/color-utils';
183
183
  import type { DgmoError } from './diagnostics';
184
184
  import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
185
- import { collectIndentedValues, extractColor, normalizeDirection, parseFirstLine, parsePipeMetadata, MULTIPLE_PIPE_WARNING } from './utils/parsing';
186
- import { matchTagBlockHeading, validateTagValues, resolveTagColor } from './utils/tag-groups';
185
+ import {
186
+ collectIndentedValues,
187
+ extractColor,
188
+ parseFirstLine,
189
+ parsePipeMetadata,
190
+ MULTIPLE_PIPE_ERROR,
191
+ } from './utils/parsing';
192
+ import {
193
+ matchTagBlockHeading,
194
+ validateTagValues,
195
+ resolveTagColor,
196
+ } from './utils/tag-groups';
187
197
  import type { TagGroup } from './utils/tag-groups';
188
198
  import {
189
199
  LEGEND_HEIGHT as TL_LEGEND_HEIGHT,
@@ -197,7 +207,11 @@ import {
197
207
  LEGEND_GROUP_GAP as TL_LEGEND_GROUP_GAP,
198
208
  measureLegendText,
199
209
  } from './utils/legend-constants';
200
- import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT, TITLE_Y } from './utils/title-constants';
210
+ import {
211
+ TITLE_FONT_SIZE,
212
+ TITLE_FONT_WEIGHT,
213
+ TITLE_Y,
214
+ } from './utils/title-constants';
201
215
 
202
216
  // ============================================================
203
217
  // Shared Rendering Helpers
@@ -215,7 +229,8 @@ function renderChartTitle(
215
229
  onClickItem?: (lineNumber: number) => void
216
230
  ): void {
217
231
  if (!title) return;
218
- const titleEl = svg.append('text')
232
+ const titleEl = svg
233
+ .append('text')
219
234
  .attr('class', 'chart-title')
220
235
  .attr('x', width / 2)
221
236
  .attr('y', TITLE_Y)
@@ -230,8 +245,12 @@ function renderChartTitle(
230
245
  if (onClickItem) {
231
246
  titleEl
232
247
  .on('click', () => onClickItem(titleLineNumber))
233
- .on('mouseenter', function () { d3Selection.select(this).attr('opacity', 0.7); })
234
- .on('mouseleave', function () { d3Selection.select(this).attr('opacity', 1); });
248
+ .on('mouseenter', function () {
249
+ d3Selection.select(this).attr('opacity', 0.7);
250
+ })
251
+ .on('mouseleave', function () {
252
+ d3Selection.select(this).attr('opacity', 1);
253
+ });
235
254
  }
236
255
  }
237
256
  }
@@ -244,7 +263,15 @@ function initD3Chart(
244
263
  container: HTMLDivElement,
245
264
  palette: PaletteColors,
246
265
  exportDims?: D3ExportDimensions
247
- ): { svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>; width: number; height: number; textColor: string; mutedColor: string; bgColor: string; colors: string[] } | null {
266
+ ): {
267
+ svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>;
268
+ width: number;
269
+ height: number;
270
+ textColor: string;
271
+ mutedColor: string;
272
+ bgColor: string;
273
+ colors: string[];
274
+ } | null {
248
275
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
249
276
  const width = exportDims?.width ?? container.clientWidth;
250
277
  const height = exportDims?.height ?? container.clientHeight;
@@ -253,7 +280,12 @@ function initD3Chart(
253
280
  const mutedColor = palette.border;
254
281
  const bgColor = palette.bg;
255
282
  const colors = getSeriesColors(palette);
256
- const svg = d3Selection.select(container).append('svg').attr('width', width).attr('height', height).style('background', bgColor);
283
+ const svg = d3Selection
284
+ .select(container)
285
+ .append('svg')
286
+ .attr('width', width)
287
+ .attr('height', height)
288
+ .style('background', bgColor);
257
289
  return { svg, width, height, textColor, mutedColor, bgColor, colors };
258
290
  }
259
291
 
@@ -285,7 +317,9 @@ export function parseTimelineDate(s: string): number {
285
317
  const year = parts[0];
286
318
  const month = parts.length >= 2 ? parts[1] : 1;
287
319
  const day = parts.length >= 3 ? parts[2] : 1;
288
- return year + (month - 1) / 12 + (day - 1) / 365 + hour / 8760 + minute / 525600;
320
+ return (
321
+ year + (month - 1) / 12 + (day - 1) / 365 + hour / 8760 + minute / 525600
322
+ );
289
323
  }
290
324
 
291
325
  /** Convert a fractional year number back to a Date (inverse of parseTimelineDate). */
@@ -307,8 +341,13 @@ function fractionalYearToDate(frac: number): Date {
307
341
 
308
342
  /** Convert a Date to a fractional year number. */
309
343
  function dateToFractionalYear(d: Date): number {
310
- return d.getFullYear() + d.getMonth() / 12 + (d.getDate() - 1) / 365
311
- + d.getHours() / 8760 + d.getMinutes() / 525600;
344
+ return (
345
+ d.getFullYear() +
346
+ d.getMonth() / 12 +
347
+ (d.getDate() - 1) / 365 +
348
+ d.getHours() / 8760 +
349
+ d.getMinutes() / 525600
350
+ );
312
351
  }
313
352
 
314
353
  /**
@@ -404,7 +443,10 @@ export function addDurationToDate(
404
443
  /**
405
444
  * Parses D3 chart text format into structured data.
406
445
  */
407
- export function parseVisualization(content: string, palette?: PaletteColors): ParsedVisualization {
446
+ export function parseVisualization(
447
+ content: string,
448
+ palette?: PaletteColors
449
+ ): ParsedVisualization {
408
450
  const result: ParsedVisualization = {
409
451
  type: null,
410
452
  title: null,
@@ -463,8 +505,20 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
463
505
  let currentArcGroup: string | null = null;
464
506
  let currentTimelineGroup: string | null = null;
465
507
  let currentTimelineTagGroup: TagGroup | null = null;
508
+ let inTimelineEraBlock = false;
509
+ let timelineEraBlockIndent = 0;
510
+ let inTimelineMarkerBlock = false;
511
+ let timelineMarkerBlockIndent = 0;
466
512
  const timelineAliasMap = new Map<string, string>();
467
- const VALID_D3_TYPES = new Set(['slope', 'wordcloud', 'arc', 'timeline', 'venn', 'quadrant', 'sequence']);
513
+ const VALID_D3_TYPES = new Set([
514
+ 'slope',
515
+ 'wordcloud',
516
+ 'arc',
517
+ 'timeline',
518
+ 'venn',
519
+ 'quadrant',
520
+ 'sequence',
521
+ ]);
468
522
  let firstLineParsed = false;
469
523
 
470
524
  for (let i = 0; i < lines.length; i++) {
@@ -494,14 +548,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
494
548
  // Not a bare chart type — fall through to normal parsing
495
549
  }
496
550
 
497
- // Timeline tag group heading: `tag: Name [alias X]`
551
+ // Timeline tag group heading: `tag Name [alias X]`
498
552
  if (result.type === 'timeline' && indent === 0) {
499
553
  const tagBlockMatch = matchTagBlockHeading(line);
500
554
  if (tagBlockMatch) {
501
- if (tagBlockMatch.deprecated) {
502
- result.diagnostics.push(makeDgmoError(lineNumber,
503
- `'## ${tagBlockMatch.name}' is deprecated for tag groups — use 'tag: ${tagBlockMatch.name}' instead`, 'warning'));
504
- }
505
555
  currentTimelineTagGroup = {
506
556
  name: tagBlockMatch.name,
507
557
  alias: tagBlockMatch.alias,
@@ -509,7 +559,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
509
559
  lineNumber,
510
560
  };
511
561
  if (tagBlockMatch.alias) {
512
- timelineAliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
562
+ timelineAliasMap.set(
563
+ tagBlockMatch.alias.toLowerCase(),
564
+ tagBlockMatch.name.toLowerCase()
565
+ );
513
566
  }
514
567
  result.timelineTagGroups.push(currentTimelineTagGroup);
515
568
  continue;
@@ -526,7 +579,11 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
526
579
  const { label, color } = extractColor(entryText, palette);
527
580
  if (color) {
528
581
  if (isDefault) currentTimelineTagGroup.defaultValue = label;
529
- currentTimelineTagGroup.entries.push({ value: label, color, lineNumber });
582
+ currentTimelineTagGroup.entries.push({
583
+ value: label,
584
+ color,
585
+ lineNumber,
586
+ });
530
587
  continue;
531
588
  }
532
589
  }
@@ -558,9 +615,21 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
558
615
  }
559
616
 
560
617
  // Reject legacy ## group syntax
561
- if (/^#{2,}\s+/.test(line) && (result.type === 'arc' || result.type === 'timeline')) {
562
- const name = line.replace(/^#{2,}\s+/, '').replace(/\s*\([^)]*\)\s*$/, '').trim();
563
- result.diagnostics.push(makeDgmoError(lineNumber, `'## ${name}' is no longer supported. Use '[${name}]' instead`, 'warning'));
618
+ if (
619
+ /^#{2,}\s+/.test(line) &&
620
+ (result.type === 'arc' || result.type === 'timeline')
621
+ ) {
622
+ const name = line
623
+ .replace(/^#{2,}\s+/, '')
624
+ .replace(/\s*\([^)]*\)\s*$/, '')
625
+ .trim();
626
+ result.diagnostics.push(
627
+ makeDgmoError(
628
+ lineNumber,
629
+ `'## ${name}' is no longer supported. Use '[${name}]' instead`,
630
+ 'warning'
631
+ )
632
+ );
564
633
  continue;
565
634
  }
566
635
 
@@ -570,10 +639,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
570
639
  currentTimelineGroup = null;
571
640
  }
572
641
 
573
- // Arc link line: source -> target(color): weight
642
+ // Arc link line: source -> target(color) weight
574
643
  if (result.type === 'arc') {
575
644
  const linkMatch = line.match(
576
- /^(.+?)\s*->\s*(.+?)(?:\(([^)]+)\))?\s*(?::\s*(\d+(?:\.\d+)?))?$/
645
+ /^(.+?)\s*->\s*(.+?)(?:\(([^)]+)\))?\s*(?:\s+(\d+(?:\.\d+)?))?$/
577
646
  );
578
647
  if (linkMatch) {
579
648
  const source = linkMatch[1].trim();
@@ -605,10 +674,80 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
605
674
  }
606
675
  }
607
676
 
608
- // Timeline era lines: era YYYY->YYYY Label (color)
677
+ // Timeline era block entries (indented under bare `era`)
678
+ if (result.type === 'timeline' && inTimelineEraBlock) {
679
+ if (indent <= timelineEraBlockIndent) {
680
+ inTimelineEraBlock = false;
681
+ // fall through to process this line normally
682
+ } else {
683
+ if (line.startsWith('//')) continue;
684
+ const eraEntryMatch = line.match(
685
+ /^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
686
+ );
687
+ if (eraEntryMatch) {
688
+ const colorAnnotation = eraEntryMatch[4]?.trim() || null;
689
+ result.timelineEras.push({
690
+ startDate: eraEntryMatch[1],
691
+ endDate: eraEntryMatch[2],
692
+ label: eraEntryMatch[3].trim(),
693
+ color: colorAnnotation
694
+ ? resolveColor(colorAnnotation, palette)
695
+ : null,
696
+ lineNumber,
697
+ });
698
+ } else {
699
+ warn(lineNumber, `Unrecognized era entry: "${line}"`);
700
+ }
701
+ continue;
702
+ }
703
+ }
704
+
705
+ // Timeline marker block entries (indented under bare `marker`)
706
+ if (result.type === 'timeline' && inTimelineMarkerBlock) {
707
+ if (indent <= timelineMarkerBlockIndent) {
708
+ inTimelineMarkerBlock = false;
709
+ // fall through to process this line normally
710
+ } else {
711
+ if (line.startsWith('//')) continue;
712
+ const markerEntryMatch = line.match(
713
+ /^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
714
+ );
715
+ if (markerEntryMatch) {
716
+ const colorAnnotation = markerEntryMatch[3]?.trim() || null;
717
+ result.timelineMarkers.push({
718
+ date: markerEntryMatch[1],
719
+ label: markerEntryMatch[2].trim(),
720
+ color: colorAnnotation
721
+ ? resolveColor(colorAnnotation, palette)
722
+ : null,
723
+ lineNumber,
724
+ });
725
+ } else {
726
+ warn(lineNumber, `Unrecognized marker entry: "${line}"`);
727
+ }
728
+ continue;
729
+ }
730
+ }
731
+
732
+ // Timeline era/marker block starters and inline forms
609
733
  if (result.type === 'timeline') {
734
+ // Bare `era` keyword starts a block
735
+ if (line.toLowerCase() === 'era') {
736
+ inTimelineEraBlock = true;
737
+ timelineEraBlockIndent = indent;
738
+ continue;
739
+ }
740
+
741
+ // Bare `marker` keyword starts a block
742
+ if (line.toLowerCase() === 'marker') {
743
+ inTimelineMarkerBlock = true;
744
+ timelineMarkerBlockIndent = indent;
745
+ continue;
746
+ }
747
+
748
+ // Timeline era lines (inline): era YYYY->YYYY Label (color)
610
749
  const eraMatch = line.match(
611
- /^era\s+(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*->\s*(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*:?\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
750
+ /^era\s+(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
612
751
  );
613
752
  if (eraMatch) {
614
753
  const colorAnnotation = eraMatch[4]?.trim() || null;
@@ -624,9 +763,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
624
763
  continue;
625
764
  }
626
765
 
627
- // Timeline marker lines: marker YYYY Label (color)
766
+ // Timeline marker lines (inline): marker YYYY Label (color)
628
767
  const markerMatch = line.match(
629
- /^marker:?\s+(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
768
+ /^marker\s+(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
630
769
  );
631
770
  if (markerMatch) {
632
771
  const colorAnnotation = markerMatch[3]?.trim() || null;
@@ -647,8 +786,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
647
786
  // Duration event: 2026-07-15->30d: description (d=days, w=weeks, m=months, y=years, h=hours, min=minutes)
648
787
  // Supports decimals up to 2 places (e.g., 1.25y = 1 year 3 months)
649
788
  // Supports uncertain end with ? suffix (e.g., ->3m?: fades out the last 20%)
789
+ // Accepts both -> (hyphen) and –> (en-dash U+2013)
650
790
  const durationMatch = line.match(
651
- /^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*->\s*(\d+(?:\.\d{1,2})?)(min|[dwmyh])(\?)?(?:\s*:\s*|\s+)(.+)$/
791
+ /^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d+(?:\.\d{1,2})?)(min|[dwmyh])(\?)?\s+(.+)$/
652
792
  );
653
793
  if (durationMatch) {
654
794
  const startDate = durationMatch[1];
@@ -657,9 +797,14 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
657
797
  const unit = durationMatch[3] as 'd' | 'w' | 'm' | 'y' | 'h' | 'min';
658
798
  const endDate = addDurationToDate(startDate, amount, unit);
659
799
  const segments = durationMatch[5].split('|');
660
- const metadata = segments.length > 1
661
- ? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber, MULTIPLE_PIPE_WARNING))
662
- : {};
800
+ const metadata =
801
+ segments.length > 1
802
+ ? parsePipeMetadata(
803
+ ['', ...segments.slice(1)],
804
+ timelineAliasMap,
805
+ () => warn(lineNumber, MULTIPLE_PIPE_ERROR)
806
+ )
807
+ : {};
663
808
  result.timelineEvents.push({
664
809
  date: startDate,
665
810
  endDate,
@@ -673,14 +818,21 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
673
818
  }
674
819
 
675
820
  // Range event: 1655->1667 description (supports uncertain end: 1655->1667?)
821
+ // Also supports YYYY-MM-DD HH:MM in both start and end dates
822
+ // Accepts both -> (hyphen) and –> (en-dash U+2013)
676
823
  const rangeMatch = line.match(
677
- /^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s*->\s*(\d{4}(?:-\d{2})?(?:-\d{2})?)(\?)?(?:\s*:\s*|\s+)(.+)$/
824
+ /^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)(\?)?\s+(.+)$/
678
825
  );
679
826
  if (rangeMatch) {
680
827
  const segments = rangeMatch[4].split('|');
681
- const metadata = segments.length > 1
682
- ? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber, MULTIPLE_PIPE_WARNING))
683
- : {};
828
+ const metadata =
829
+ segments.length > 1
830
+ ? parsePipeMetadata(
831
+ ['', ...segments.slice(1)],
832
+ timelineAliasMap,
833
+ () => warn(lineNumber, MULTIPLE_PIPE_ERROR)
834
+ )
835
+ : {};
684
836
  result.timelineEvents.push({
685
837
  date: rangeMatch[1],
686
838
  endDate: rangeMatch[2],
@@ -693,15 +845,18 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
693
845
  continue;
694
846
  }
695
847
 
696
- // Point event: 1718 description (or legacy 1718: description)
697
- const pointMatch = line.match(
698
- /^(\d{4}(?:-\d{2})?(?:-\d{2})?)(?:\s*:\s*|\s+)(.+)$/
699
- );
848
+ // Point event: 1718 description
849
+ const pointMatch = line.match(/^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s+(.+)$/);
700
850
  if (pointMatch) {
701
851
  const segments = pointMatch[2].split('|');
702
- const metadata = segments.length > 1
703
- ? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber, MULTIPLE_PIPE_WARNING))
704
- : {};
852
+ const metadata =
853
+ segments.length > 1
854
+ ? parsePipeMetadata(
855
+ ['', ...segments.slice(1)],
856
+ timelineAliasMap,
857
+ () => warn(lineNumber, MULTIPLE_PIPE_ERROR)
858
+ )
859
+ : {};
705
860
  result.timelineEvents.push({
706
861
  date: pointMatch[1],
707
862
  endDate: null,
@@ -716,27 +871,58 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
716
871
 
717
872
  // Venn diagram DSL
718
873
  if (result.type === 'venn') {
719
- // Intersection line: "A + B: Label" / "A + B" / "A + B + C: Label"
874
+ // Intersection line: "A + B Label" / "A + B" / "A + B + C Label"
875
+ // Also accepts deprecated colon syntax: "A + B: Label"
720
876
  if (/\+/.test(line)) {
721
- const colonIdx = line.indexOf(':');
722
- let setsPart: string;
723
- let label: string | null;
724
- if (colonIdx >= 0) {
725
- setsPart = line.substring(0, colonIdx).trim();
726
- label = line.substring(colonIdx + 1).trim() || null;
727
- } else {
728
- setsPart = line.trim();
729
- label = null;
877
+ // Build lookup of known set names and aliases for label extraction
878
+ const knownSetRefs = new Set<string>();
879
+ for (const s of result.vennSets) {
880
+ knownSetRefs.add(s.name.toLowerCase());
881
+ if (s.alias) knownSetRefs.add(s.alias.toLowerCase());
730
882
  }
731
- const rawSets = setsPart.split('+').map((s) => s.trim()).filter(Boolean);
732
- if (rawSets.length >= 2) {
883
+
884
+ const segments = line
885
+ .split('+')
886
+ .map((s) => s.trim())
887
+ .filter(Boolean);
888
+ if (segments.length >= 2) {
889
+ // All segments except the last are pure set references
890
+ const rawSets = segments.slice(0, -1);
891
+ const lastSeg = segments[segments.length - 1];
892
+
893
+ // For the last segment, extract set reference and optional label.
894
+ // Find where the set reference ends and label begins.
895
+ // Try progressively shorter prefixes against known set names/aliases.
896
+ const words = lastSeg.split(/\s+/);
897
+ let matchLen = 0;
898
+ for (let w = words.length; w >= 1; w--) {
899
+ const candidate = words.slice(0, w).join(' ');
900
+ if (knownSetRefs.has(candidate.toLowerCase())) {
901
+ matchLen = w;
902
+ break;
903
+ }
904
+ }
905
+ let lastSetRef: string;
906
+ let label: string | null;
907
+ if (matchLen > 0) {
908
+ lastSetRef = words.slice(0, matchLen).join(' ');
909
+ label =
910
+ words.length > matchLen ? words.slice(matchLen).join(' ') : null;
911
+ } else {
912
+ // No known set matched — assume first word is the set ref, rest is label
913
+ lastSetRef = words[0];
914
+ label = words.length > 1 ? words.slice(1).join(' ') : null;
915
+ }
916
+ rawSets.push(lastSetRef);
733
917
  result.vennOverlaps.push({ sets: rawSets, label, lineNumber });
734
918
  continue;
735
919
  }
736
920
  }
737
921
 
738
922
  // Set declaration: "Name(color) alias x" / "Name alias x" / "Name(color)" / "Name"
739
- const setDeclMatch = line.match(/^([^(:]+?)(?:\(([^)]+)\))?(?:\s+alias\s+(\S+))?\s*$/i);
923
+ const setDeclMatch = line.match(
924
+ /^([^(:]+?)(?:\(([^)]+)\))?(?:\s+alias\s+(\S+))?\s*$/i
925
+ );
740
926
  if (setDeclMatch) {
741
927
  const name = setDeclMatch[1].trim();
742
928
  const colorName = setDeclMatch[2]?.trim() ?? null;
@@ -744,11 +930,17 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
744
930
  if (colorName) {
745
931
  const resolved = resolveColor(colorName, palette);
746
932
  if (resolved === null) {
747
- warn(lineNumber, `Hex colors are not supported — use named colors (blue, red, green, etc.)`);
933
+ warn(
934
+ lineNumber,
935
+ `Hex colors are not supported — use named colors (blue, red, green, etc.)`
936
+ );
748
937
  } else if (resolved.startsWith('#')) {
749
938
  color = resolved;
750
939
  } else {
751
- warn(lineNumber, `Unknown color "${colorName}" on set "${name}". Using auto-assigned color.`);
940
+ warn(
941
+ lineNumber,
942
+ `Unknown color "${colorName}" on set "${name}". Using auto-assigned color.`
943
+ );
752
944
  }
753
945
  }
754
946
  const alias = setDeclMatch[3]?.trim() ?? null;
@@ -759,8 +951,8 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
759
951
 
760
952
  // Quadrant-specific parsing
761
953
  if (result.type === 'quadrant') {
762
- // x-axis: Low, High — or indented multi-line
763
- const xAxisMatch = line.match(/^x-axis\s*:\s*(.*)/i);
954
+ // x-label Low, High — or indented multi-line
955
+ const xAxisMatch = line.match(/^x-label\s+(.*)/i);
764
956
  if (xAxisMatch) {
765
957
  const val = xAxisMatch[1].trim();
766
958
  let parts: string[];
@@ -778,8 +970,8 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
778
970
  continue;
779
971
  }
780
972
 
781
- // y-axis: Low, High — or indented multi-line
782
- const yAxisMatch = line.match(/^y-axis\s*:\s*(.*)/i);
973
+ // y-label Low, High — or indented multi-line
974
+ const yAxisMatch = line.match(/^y-label\s+(.*)/i);
783
975
  if (yAxisMatch) {
784
976
  const val = yAxisMatch[1].trim();
785
977
  let parts: string[];
@@ -797,9 +989,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
797
989
  continue;
798
990
  }
799
991
 
800
- // Quadrant position labels: top-right: Label (color)
992
+ // Quadrant position labels: top-right Label (color)
801
993
  const quadrantLabelRe =
802
- /^(top-right|top-left|bottom-left|bottom-right)\s*:\s*(.+)/i;
994
+ /^(top-right|top-left|bottom-left|bottom-right)\s+(.+)/i;
803
995
  const quadrantMatch = line.match(quadrantLabelRe);
804
996
  if (quadrantMatch) {
805
997
  const position = quadrantMatch[1].toLowerCase();
@@ -821,9 +1013,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
821
1013
  continue;
822
1014
  }
823
1015
 
824
- // Data points: Label: x, y
1016
+ // Data points: Label x, y
825
1017
  const pointMatch = line.match(
826
- /^(.+?):\s*([0-9]*\.?[0-9]+)\s*,\s*([0-9]*\.?[0-9]+)\s*$/
1018
+ /^(.+?)\s+([0-9]*\.?[0-9]+)\s*,\s*([0-9]*\.?[0-9]+)\s*$/
827
1019
  );
828
1020
  if (pointMatch) {
829
1021
  const label = pointMatch[1].trim();
@@ -852,7 +1044,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
852
1044
  const firstToken = line.substring(0, spaceIdx).toLowerCase();
853
1045
  const restValue = line.substring(spaceIdx + 1).trim();
854
1046
 
855
- if (firstToken === 'chart' && VALID_D3_TYPES.has(restValue.toLowerCase())) {
1047
+ if (
1048
+ firstToken === 'chart' &&
1049
+ VALID_D3_TYPES.has(restValue.toLowerCase())
1050
+ ) {
856
1051
  result.type = restValue.toLowerCase() as ParsedVisualization['type'];
857
1052
  continue;
858
1053
  }
@@ -866,20 +1061,6 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
866
1061
  continue;
867
1062
  }
868
1063
 
869
- if (firstToken === 'orientation' || firstToken === 'direction') {
870
- if (result.type === 'arc' || result.type === 'timeline') {
871
- const vLower = restValue.toLowerCase();
872
- if (vLower === 'horizontal' || vLower === 'vertical') {
873
- result.orientation = vLower;
874
- } else {
875
- const dir = normalizeDirection(restValue);
876
- if (dir === 'LR') result.orientation = 'horizontal';
877
- else if (dir === 'TB') result.orientation = 'vertical';
878
- }
879
- }
880
- continue;
881
- }
882
-
883
1064
  if (firstToken === 'order') {
884
1065
  const v = restValue.toLowerCase();
885
1066
  if (v === 'name' || v === 'group' || v === 'degree') {
@@ -888,29 +1069,6 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
888
1069
  continue;
889
1070
  }
890
1071
 
891
- if (firstToken === 'sort') {
892
- const vLower = restValue.toLowerCase();
893
- if (vLower === 'time' || vLower === 'group') {
894
- result.timelineSort = vLower;
895
- } else if (vLower === 'tag' || vLower.startsWith('tag:')) {
896
- result.timelineSort = 'tag';
897
- if (vLower.startsWith('tag:')) {
898
- const groupRef = restValue.substring(4).trim();
899
- if (groupRef) {
900
- result.timelineDefaultSwimlaneTG = groupRef;
901
- }
902
- }
903
- }
904
- continue;
905
- }
906
-
907
- if (firstToken === 'swimlanes') {
908
- const v = restValue.toLowerCase();
909
- if (v === 'on') result.timelineSwimlanes = true;
910
- else if (v === 'off') result.timelineSwimlanes = false;
911
- continue;
912
- }
913
-
914
1072
  if (firstToken === 'rotate') {
915
1073
  const v = restValue.toLowerCase();
916
1074
  if (v === 'none' || v === 'mixed' || v === 'angled') {
@@ -951,23 +1109,6 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
951
1109
  // Check for color annotation in raw key: "Label(color)"
952
1110
  const colorMatch = rawKey.match(/^(.+?)\(([^)]+)\)\s*$/);
953
1111
 
954
- if (key === 'chart') {
955
- const value = line
956
- .substring(colonIndex + 1)
957
- .trim()
958
- .toLowerCase();
959
- if (VALID_D3_TYPES.has(value)) {
960
- result.type = value as ParsedVisualization['type'];
961
- } else {
962
- const validD3Types = [...VALID_D3_TYPES];
963
- let msg = `Unsupported chart type: ${value}. Supported types: ${validD3Types.join(', ')}`;
964
- const hint = suggest(value, validD3Types);
965
- if (hint) msg += `. ${hint}`;
966
- return fail(lineNumber, msg);
967
- }
968
- continue;
969
- }
970
-
971
1112
  if (key === 'title') {
972
1113
  result.title = line.substring(colonIndex + 1).trim();
973
1114
  result.titleLineNumber = lineNumber;
@@ -977,23 +1118,6 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
977
1118
  continue;
978
1119
  }
979
1120
 
980
- if (key === 'orientation' || key === 'direction') {
981
- // Only arc and timeline support orientation
982
- if (result.type === 'arc' || result.type === 'timeline') {
983
- const raw = line.substring(colonIndex + 1).trim();
984
- // Accept horizontal/vertical directly, or LR/TB via normalizeDirection
985
- const vLower = raw.toLowerCase();
986
- if (vLower === 'horizontal' || vLower === 'vertical') {
987
- result.orientation = vLower;
988
- } else {
989
- const dir = normalizeDirection(raw);
990
- if (dir === 'LR') result.orientation = 'horizontal';
991
- else if (dir === 'TB') result.orientation = 'vertical';
992
- }
993
- }
994
- continue;
995
- }
996
-
997
1121
  if (key === 'order') {
998
1122
  const v = line
999
1123
  .substring(colonIndex + 1)
@@ -1005,39 +1129,6 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1005
1129
  continue;
1006
1130
  }
1007
1131
 
1008
- if (key === 'sort') {
1009
- const v = line
1010
- .substring(colonIndex + 1)
1011
- .trim();
1012
- const vLower = v.toLowerCase();
1013
- if (vLower === 'time' || vLower === 'group') {
1014
- result.timelineSort = vLower;
1015
- } else if (vLower === 'tag' || vLower.startsWith('tag:')) {
1016
- result.timelineSort = 'tag';
1017
- if (vLower.startsWith('tag:')) {
1018
- // Extract group name (preserving original case for display)
1019
- const groupRef = v.substring(4).trim();
1020
- if (groupRef) {
1021
- result.timelineDefaultSwimlaneTG = groupRef;
1022
- }
1023
- }
1024
- }
1025
- continue;
1026
- }
1027
-
1028
- if (key === 'swimlanes') {
1029
- const v = line
1030
- .substring(colonIndex + 1)
1031
- .trim()
1032
- .toLowerCase();
1033
- if (v === 'on') {
1034
- result.timelineSwimlanes = true;
1035
- } else if (v === 'off') {
1036
- result.timelineSwimlanes = false;
1037
- }
1038
- continue;
1039
- }
1040
-
1041
1132
  if (key === 'rotate') {
1042
1133
  const v = line
1043
1134
  .substring(colonIndex + 1)
@@ -1092,22 +1183,16 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1092
1183
  }
1093
1184
 
1094
1185
  if (allNumeric && numericValues.length > 0) {
1095
- // For wordcloud, single numeric value = word weight
1096
- if (result.type === 'wordcloud' && numericValues.length === 1) {
1097
- result.words.push({
1098
- text: labelPart,
1099
- weight: numericValues[0],
1100
- lineNumber,
1101
- });
1102
- } else {
1186
+ // Wordcloud does not use colon data format — skip to freeform handling
1187
+ if (result.type !== 'wordcloud') {
1103
1188
  result.data.push({
1104
1189
  label: labelPart,
1105
1190
  values: numericValues,
1106
1191
  color: colorPart,
1107
1192
  lineNumber,
1108
1193
  });
1194
+ continue;
1109
1195
  }
1110
- continue;
1111
1196
  }
1112
1197
  }
1113
1198
 
@@ -1119,9 +1204,14 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1119
1204
  } else if (colonIndex === -1) {
1120
1205
  // Try "word weight" or "multi-word-label weight" space-separated format
1121
1206
  const lastSpace = line.lastIndexOf(' ');
1122
- const maybeWeight = lastSpace >= 0 ? parseFloat(line.substring(lastSpace + 1)) : NaN;
1207
+ const maybeWeight =
1208
+ lastSpace >= 0 ? parseFloat(line.substring(lastSpace + 1)) : NaN;
1123
1209
  if (lastSpace >= 0 && !isNaN(maybeWeight) && maybeWeight > 0) {
1124
- result.words.push({ text: line.substring(0, lastSpace).trim(), weight: maybeWeight, lineNumber });
1210
+ result.words.push({
1211
+ text: line.substring(0, lastSpace).trim(),
1212
+ weight: maybeWeight,
1213
+ lineNumber,
1214
+ });
1125
1215
  } else {
1126
1216
  freeformLines.push(line);
1127
1217
  }
@@ -1148,13 +1238,23 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1148
1238
  continue;
1149
1239
  }
1150
1240
  }
1241
+
1242
+ // Catch-all: nothing matched this line
1243
+ // Skip on first line — chart type suggestion is handled post-loop
1244
+ if (firstLineParsed) {
1245
+ warn(lineNumber, `Unexpected line: '${line}'.`);
1246
+ }
1151
1247
  }
1152
1248
 
1153
1249
  // Validation
1154
1250
  if (!result.type) {
1155
1251
  const validD3Types = [...VALID_D3_TYPES];
1156
- const firstNonEmpty = lines.find(l => l.trim() && !l.trim().startsWith('//'))?.trim() ?? '';
1157
- const hint = suggest(firstNonEmpty.split(/\s/)[0].toLowerCase(), validD3Types);
1252
+ const firstNonEmpty =
1253
+ lines.find((l) => l.trim() && !l.trim().startsWith('//'))?.trim() ?? '';
1254
+ const hint = suggest(
1255
+ firstNonEmpty.split(/\s/)[0].toLowerCase(),
1256
+ validD3Types
1257
+ );
1158
1258
  let msg = `Unsupported chart type: "${firstNonEmpty.split(/\s/)[0]}". Supported types: ${validD3Types.join(', ')}`;
1159
1259
  if (hint) msg += `. ${hint}`;
1160
1260
  return fail(1, msg);
@@ -1171,7 +1271,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1171
1271
  result.words = tokenizeFreeformText(freeformLines.join(' '));
1172
1272
  }
1173
1273
  if (result.words.length === 0) {
1174
- warn(1, 'No words found. Add words as "word: weight", one per line, or paste freeform text');
1274
+ warn(
1275
+ 1,
1276
+ 'No words found. Add words as "word weight" (space-separated), one per line, or paste freeform text'
1277
+ );
1175
1278
  }
1176
1279
  // Apply max word limit (words are already sorted by weight desc for freeform)
1177
1280
  if (
@@ -1188,12 +1291,18 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1188
1291
 
1189
1292
  if (result.type === 'arc') {
1190
1293
  if (result.links.length === 0) {
1191
- warn(1, 'No links found. Add links as "Source -> Target: weight" (e.g., "Alice -> Bob: 5")');
1294
+ warn(
1295
+ 1,
1296
+ 'No links found. Add links as "Source -> Target weight" (e.g., "Alice -> Bob 5")'
1297
+ );
1192
1298
  }
1193
1299
  // Validate arc ordering vs groups
1194
1300
  if (result.arcNodeGroups.length > 0) {
1195
1301
  if (result.arcOrder === 'name' || result.arcOrder === 'degree') {
1196
- warn(1, `Cannot use "order: ${result.arcOrder}" with [Group] headers. Use "order: group" or remove group headers.`);
1302
+ warn(
1303
+ 1,
1304
+ `Cannot use "order ${result.arcOrder}" with [Group] headers. Use "order group" or remove group headers.`
1305
+ );
1197
1306
  result.arcOrder = 'group';
1198
1307
  }
1199
1308
  if (result.arcOrder === 'appearance') {
@@ -1205,15 +1314,19 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1205
1314
 
1206
1315
  if (result.type === 'timeline') {
1207
1316
  if (result.timelineEvents.length === 0) {
1208
- warn(1, 'No events found. Add events as "YYYY: description" or "YYYY->YYYY: description"');
1317
+ warn(
1318
+ 1,
1319
+ 'No events found. Add events as "YYYY: description" or "YYYY->YYYY: description"'
1320
+ );
1209
1321
  }
1210
1322
  // Validate tag values and inject defaults
1211
1323
  if (result.timelineTagGroups.length > 0) {
1212
1324
  validateTagValues(
1213
1325
  result.timelineEvents,
1214
1326
  result.timelineTagGroups,
1215
- (line, msg) => result.diagnostics.push(makeDgmoError(line, msg, 'warning')),
1216
- suggest,
1327
+ (line, msg) =>
1328
+ result.diagnostics.push(makeDgmoError(line, msg, 'warning')),
1329
+ suggest
1217
1330
  );
1218
1331
  for (const group of result.timelineTagGroups) {
1219
1332
  if (!group.defaultValue) continue;
@@ -1226,35 +1339,15 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1226
1339
  }
1227
1340
  }
1228
1341
 
1229
- // Resolve sort: tag default swimlane group
1230
- if (result.timelineSort === 'tag') {
1231
- if (result.timelineTagGroups.length === 0) {
1232
- warn(1, '"sort: tag" requires at least one tag group definition');
1233
- result.timelineSort = 'time';
1234
- } else if (result.timelineDefaultSwimlaneTG) {
1235
- // Resolve alias → full group name
1236
- const ref = result.timelineDefaultSwimlaneTG.toLowerCase();
1237
- const match = result.timelineTagGroups.find(
1238
- (g) => g.name.toLowerCase() === ref || g.alias?.toLowerCase() === ref
1239
- );
1240
- if (match) {
1241
- result.timelineDefaultSwimlaneTG = match.name;
1242
- } else {
1243
- warn(1, `"sort: tag:${result.timelineDefaultSwimlaneTG}" — no tag group matches "${result.timelineDefaultSwimlaneTG}"`);
1244
- result.timelineDefaultSwimlaneTG = result.timelineTagGroups[0].name;
1245
- }
1246
- } else {
1247
- // Default to first tag group
1248
- result.timelineDefaultSwimlaneTG = result.timelineTagGroups[0].name;
1249
- }
1250
- }
1251
-
1252
1342
  return result;
1253
1343
  }
1254
1344
 
1255
1345
  if (result.type === 'venn') {
1256
1346
  if (result.vennSets.length < 2) {
1257
- return fail(1, 'At least 2 sets are required. Add set names (e.g., "Apples", "Oranges")');
1347
+ return fail(
1348
+ 1,
1349
+ 'At least 2 sets are required. Add set names (e.g., "Apples", "Oranges")'
1350
+ );
1258
1351
  }
1259
1352
  if (result.vennSets.length > 3) {
1260
1353
  return fail(1, 'Venn diagrams support 2–3 sets');
@@ -1268,7 +1361,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1268
1361
  if (s.alias) aliasLower.set(s.alias.toLowerCase(), s.name);
1269
1362
  }
1270
1363
  const resolveSetRef = (ref: string): string | null =>
1271
- setNameLower.get(ref.toLowerCase()) ?? aliasLower.get(ref.toLowerCase()) ?? null;
1364
+ setNameLower.get(ref.toLowerCase()) ??
1365
+ aliasLower.get(ref.toLowerCase()) ??
1366
+ null;
1272
1367
 
1273
1368
  // Resolve intersection set references; drop invalid ones with a diagnostic
1274
1369
  const validOverlaps: VennOverlap[] = [];
@@ -1278,8 +1373,16 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1278
1373
  for (const ref of ov.sets) {
1279
1374
  const resolved = resolveSetRef(ref);
1280
1375
  if (!resolved) {
1281
- result.diagnostics.push(makeDgmoError(ov.lineNumber, `Intersection references unknown set or alias "${ref}"`));
1282
- if (!result.error) result.error = formatDgmoError(result.diagnostics[result.diagnostics.length - 1]);
1376
+ result.diagnostics.push(
1377
+ makeDgmoError(
1378
+ ov.lineNumber,
1379
+ `Intersection references unknown set or alias "${ref}"`
1380
+ )
1381
+ );
1382
+ if (!result.error)
1383
+ result.error = formatDgmoError(
1384
+ result.diagnostics[result.diagnostics.length - 1]
1385
+ );
1283
1386
  valid = false;
1284
1387
  break;
1285
1388
  }
@@ -1293,24 +1396,36 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1293
1396
 
1294
1397
  if (result.type === 'quadrant') {
1295
1398
  if (result.quadrantPoints.length === 0) {
1296
- warn(1, 'No data points found. Add points as "Label: x, y" (e.g., "Item A: 0.5, 0.7")');
1399
+ warn(
1400
+ 1,
1401
+ 'No data points found. Add points as "Label x, y" (e.g., "Item A 0.5, 0.7")'
1402
+ );
1297
1403
  }
1298
1404
  return result;
1299
1405
  }
1300
1406
 
1301
1407
  // Slope chart validation
1302
1408
  if (result.periods.length < 2) {
1303
- return fail(1, 'Missing or invalid periods line. Provide at least 2 comma-separated period labels (e.g., "2020, 2024")');
1409
+ return fail(
1410
+ 1,
1411
+ 'Missing or invalid periods line. Provide at least 2 comma-separated period labels (e.g., "2020, 2024")'
1412
+ );
1304
1413
  }
1305
1414
 
1306
1415
  if (result.data.length === 0) {
1307
- warn(1, 'No data lines found. Add data as "Label: value1, value2" (e.g., "Apple: 25, 35")');
1416
+ warn(
1417
+ 1,
1418
+ 'No data lines found. Add data as "Label: value1, value2" (e.g., "Apple: 25, 35")'
1419
+ );
1308
1420
  }
1309
1421
 
1310
1422
  // Validate value counts match period count — warn and skip mismatched items
1311
1423
  for (const item of result.data) {
1312
1424
  if (item.values.length !== result.periods.length) {
1313
- warn(item.lineNumber, `Data item "${item.label}" has ${item.values.length} value(s) but ${result.periods.length} period(s) are defined`);
1425
+ warn(
1426
+ item.lineNumber,
1427
+ `Data item "${item.label}" has ${item.values.length} value(s) but ${result.periods.length} period(s) are defined`
1428
+ );
1314
1429
  }
1315
1430
  }
1316
1431
  result.data = result.data.filter(
@@ -1551,7 +1666,14 @@ export function renderSlopeChart(
1551
1666
  const tooltip = createTooltip(container, palette, isDark);
1552
1667
 
1553
1668
  // Title
1554
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
1669
+ renderChartTitle(
1670
+ svg,
1671
+ title,
1672
+ parsed.titleLineNumber,
1673
+ width,
1674
+ textColor,
1675
+ onClickItem
1676
+ );
1555
1677
 
1556
1678
  // Period column headers
1557
1679
  for (const period of periods) {
@@ -1620,13 +1742,23 @@ export function renderSlopeChart(
1620
1742
  wrappedLines = lines;
1621
1743
  }
1622
1744
  const lineHeight = SLOPE_LABEL_FONT_SIZE * 1.2;
1623
- const labelHeight = labelLineCount === 1
1624
- ? SLOPE_LABEL_FONT_SIZE
1625
- : labelLineCount * lineHeight;
1745
+ const labelHeight =
1746
+ labelLineCount === 1
1747
+ ? SLOPE_LABEL_FONT_SIZE
1748
+ : labelLineCount * lineHeight;
1626
1749
 
1627
1750
  return {
1628
- item, idx, color, firstVal, lastVal, tipHtml,
1629
- lastX, labelText, maxChars, wrappedLines, labelHeight,
1751
+ item,
1752
+ idx,
1753
+ color,
1754
+ firstVal,
1755
+ lastVal,
1756
+ tipHtml,
1757
+ lastX,
1758
+ labelText,
1759
+ maxChars,
1760
+ wrappedLines,
1761
+ labelHeight,
1630
1762
  };
1631
1763
  });
1632
1764
 
@@ -1638,7 +1770,10 @@ export function renderSlopeChart(
1638
1770
  naturalY: yScale(item.values[pi]),
1639
1771
  height: leftLabelHeight,
1640
1772
  }));
1641
- leftLabelCollisions.set(pi, resolveVerticalCollisions(entries, 4, innerHeight));
1773
+ leftLabelCollisions.set(
1774
+ pi,
1775
+ resolveVerticalCollisions(entries, 4, innerHeight)
1776
+ );
1642
1777
  }
1643
1778
 
1644
1779
  // --- Resolve right-side label collisions ---
@@ -1646,7 +1781,11 @@ export function renderSlopeChart(
1646
1781
  naturalY: yScale(si.lastVal),
1647
1782
  height: Math.max(si.labelHeight, SLOPE_LABEL_FONT_SIZE * 1.4),
1648
1783
  }));
1649
- const rightAdjustedY = resolveVerticalCollisions(rightEntries, 4, innerHeight);
1784
+ const rightAdjustedY = resolveVerticalCollisions(
1785
+ rightEntries,
1786
+ 4,
1787
+ innerHeight
1788
+ );
1650
1789
 
1651
1790
  // Render each data series
1652
1791
  data.forEach((item, idx) => {
@@ -1660,7 +1799,8 @@ export function renderSlopeChart(
1660
1799
  .attr('data-line-number', String(item.lineNumber));
1661
1800
 
1662
1801
  // Line
1663
- seriesG.append('path')
1802
+ seriesG
1803
+ .append('path')
1664
1804
  .datum(item.values)
1665
1805
  .attr('fill', 'none')
1666
1806
  .attr('stroke', color)
@@ -1668,7 +1808,8 @@ export function renderSlopeChart(
1668
1808
  .attr('d', lineGen);
1669
1809
 
1670
1810
  // Invisible wider path for easier hover targeting
1671
- seriesG.append('path')
1811
+ seriesG
1812
+ .append('path')
1672
1813
  .datum(item.values)
1673
1814
  .attr('fill', 'none')
1674
1815
  .attr('stroke', 'transparent')
@@ -1692,7 +1833,8 @@ export function renderSlopeChart(
1692
1833
  const y = yScale(val);
1693
1834
 
1694
1835
  // Point circle
1695
- seriesG.append('circle')
1836
+ seriesG
1837
+ .append('circle')
1696
1838
  .attr('cx', x)
1697
1839
  .attr('cy', y)
1698
1840
  .attr('r', 4)
@@ -1716,7 +1858,8 @@ export function renderSlopeChart(
1716
1858
  const isLast = i === periods.length - 1;
1717
1859
  if (!isLast) {
1718
1860
  const adjustedY = leftLabelCollisions.get(i)![idx];
1719
- seriesG.append('text')
1861
+ seriesG
1862
+ .append('text')
1720
1863
  .attr('x', isFirst ? x - 10 : x)
1721
1864
  .attr('y', adjustedY)
1722
1865
  .attr('dy', '0.35em')
@@ -1948,7 +2091,14 @@ export function renderArcDiagram(
1948
2091
  .attr('transform', `translate(${margin.left},${margin.top})`);
1949
2092
 
1950
2093
  // Title
1951
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
2094
+ renderChartTitle(
2095
+ svg,
2096
+ title,
2097
+ parsed.titleLineNumber,
2098
+ width,
2099
+ textColor,
2100
+ onClickItem
2101
+ );
1952
2102
 
1953
2103
  // Build adjacency map for hover interactions
1954
2104
  const neighbors = new Map<string, Set<string>>();
@@ -2125,13 +2275,18 @@ export function renderArcDiagram(
2125
2275
  const y = yScale(node)!;
2126
2276
  const nodeColor = nodeColorMap.get(node) ?? textColor;
2127
2277
  // Find the first link involving this node (for line number and click target)
2128
- const nodeLink = links.find((l) => l.source === node || l.target === node);
2278
+ const nodeLink = links.find(
2279
+ (l) => l.source === node || l.target === node
2280
+ );
2129
2281
 
2130
2282
  const nodeG = g
2131
2283
  .append('g')
2132
2284
  .attr('class', 'arc-node')
2133
2285
  .attr('data-node', node)
2134
- .attr('data-line-number', nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null)
2286
+ .attr(
2287
+ 'data-line-number',
2288
+ nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null
2289
+ )
2135
2290
  .style('cursor', 'pointer')
2136
2291
  .on('mouseenter', () => handleMouseEnter(node))
2137
2292
  .on('mouseleave', handleMouseLeave)
@@ -2260,13 +2415,18 @@ export function renderArcDiagram(
2260
2415
  const x = xScale(node)!;
2261
2416
  const nodeColor = nodeColorMap.get(node) ?? textColor;
2262
2417
  // Find the first link involving this node (for line number and click target)
2263
- const nodeLink = links.find((l) => l.source === node || l.target === node);
2418
+ const nodeLink = links.find(
2419
+ (l) => l.source === node || l.target === node
2420
+ );
2264
2421
 
2265
2422
  const nodeG = g
2266
2423
  .append('g')
2267
2424
  .attr('class', 'arc-node')
2268
2425
  .attr('data-node', node)
2269
- .attr('data-line-number', nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null)
2426
+ .attr(
2427
+ 'data-line-number',
2428
+ nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null
2429
+ )
2270
2430
  .style('cursor', 'pointer')
2271
2431
  .on('mouseenter', () => handleMouseEnter(node))
2272
2432
  .on('mouseleave', handleMouseLeave)
@@ -2570,6 +2730,26 @@ export function formatDateLabel(dateStr: string): string {
2570
2730
  return `${month} ${day}, ${year}${timeSuffix}`;
2571
2731
  }
2572
2732
 
2733
+ /**
2734
+ * Formats a boundary label for the time axis.
2735
+ * When both boundaries fall on the same calendar day and have a time component,
2736
+ * returns just the time (e.g. "12:15") to avoid collisions with regular ticks.
2737
+ * Otherwise falls back to the full formatDateLabel.
2738
+ */
2739
+ function formatBoundaryLabel(dateStr: string, otherDateStr: string): string {
2740
+ const spaceIdx = dateStr.indexOf(' ');
2741
+ const otherSpaceIdx = otherDateStr.indexOf(' ');
2742
+ // Both must have time components and share the same date portion
2743
+ if (spaceIdx !== -1 && otherSpaceIdx !== -1) {
2744
+ const datePart = dateStr.slice(0, spaceIdx);
2745
+ const otherDatePart = otherDateStr.slice(0, otherSpaceIdx);
2746
+ if (datePart === otherDatePart) {
2747
+ return dateStr.slice(spaceIdx + 1); // just "HH:MM"
2748
+ }
2749
+ }
2750
+ return formatDateLabel(dateStr);
2751
+ }
2752
+
2573
2753
  /**
2574
2754
  * Computes adaptive tick marks for a timeline scale.
2575
2755
  * - Multi-year spans → year ticks
@@ -2641,7 +2821,11 @@ export function computeTimeTicks(
2641
2821
  // Iterate from the start hour boundary
2642
2822
  const startDate = fractionalYearToDate(domainMin);
2643
2823
  // Round down to nearest step boundary
2644
- startDate.setMinutes(Math.floor(startDate.getMinutes() / stepMin) * stepMin, 0, 0);
2824
+ startDate.setMinutes(
2825
+ Math.floor(startDate.getMinutes() / stepMin) * stepMin,
2826
+ 0,
2827
+ 0
2828
+ );
2645
2829
 
2646
2830
  while (true) {
2647
2831
  const val = dateToFractionalYear(startDate);
@@ -2662,19 +2846,31 @@ export function computeTimeTicks(
2662
2846
  else if (spanHours > 24) stepHour = 3;
2663
2847
  else if (spanHours > 12) stepHour = 2;
2664
2848
 
2849
+ // For single-day spans, just show HH:MM without the date prefix
2850
+ const singleDay = spanHours <= 24;
2851
+
2665
2852
  const startDate = fractionalYearToDate(domainMin);
2666
2853
  // Round down to nearest step boundary
2667
- startDate.setHours(Math.floor(startDate.getHours() / stepHour) * stepHour, 0, 0, 0);
2854
+ startDate.setHours(
2855
+ Math.floor(startDate.getHours() / stepHour) * stepHour,
2856
+ 0,
2857
+ 0,
2858
+ 0
2859
+ );
2668
2860
 
2669
2861
  while (true) {
2670
2862
  const val = dateToFractionalYear(startDate);
2671
2863
  if (val > domainMax) break;
2672
2864
  if (val >= domainMin) {
2673
- const mon = MONTH_ABBR[startDate.getMonth()];
2674
- const d = startDate.getDate();
2675
2865
  const hh = String(startDate.getHours()).padStart(2, '0');
2676
2866
  const mm = String(startDate.getMinutes()).padStart(2, '0');
2677
- ticks.push({ pos: scale(val), label: `${mon} ${d} ${hh}:${mm}` });
2867
+ if (singleDay) {
2868
+ ticks.push({ pos: scale(val), label: `${hh}:${mm}` });
2869
+ } else {
2870
+ const mon = MONTH_ABBR[startDate.getMonth()];
2871
+ const d = startDate.getDate();
2872
+ ticks.push({ pos: scale(val), label: `${mon} ${d} ${hh}:${mm}` });
2873
+ }
2678
2874
  }
2679
2875
  startDate.setHours(startDate.getHours() + stepHour);
2680
2876
  }
@@ -3073,7 +3269,10 @@ export function renderTimeline(
3073
3269
  exportDims?: D3ExportDimensions,
3074
3270
  activeTagGroup?: string | null,
3075
3271
  swimlaneTagGroup?: string | null,
3076
- onTagStateChange?: (activeTagGroup: string | null, swimlaneTagGroup: string | null) => void,
3272
+ onTagStateChange?: (
3273
+ activeTagGroup: string | null,
3274
+ swimlaneTagGroup: string | null
3275
+ ) => void,
3077
3276
  viewMode?: boolean
3078
3277
  ): void {
3079
3278
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
@@ -3092,7 +3291,11 @@ export function renderTimeline(
3092
3291
  if (timelineEvents.length === 0) return;
3093
3292
 
3094
3293
  // When sort: tag is set and no explicit swimlane param, use the default
3095
- if (swimlaneTagGroup == null && timelineSort === 'tag' && parsed.timelineDefaultSwimlaneTG) {
3294
+ if (
3295
+ swimlaneTagGroup == null &&
3296
+ timelineSort === 'tag' &&
3297
+ parsed.timelineDefaultSwimlaneTG
3298
+ ) {
3096
3299
  swimlaneTagGroup = parsed.timelineDefaultSwimlaneTG;
3097
3300
  }
3098
3301
 
@@ -3144,12 +3347,8 @@ export function renderTimeline(
3144
3347
 
3145
3348
  // Order lanes by earliest event date
3146
3349
  const laneEntries = [...buckets.entries()].sort((a, b) => {
3147
- const aMin = Math.min(
3148
- ...a[1].map((e) => parseTimelineDate(e.date))
3149
- );
3150
- const bMin = Math.min(
3151
- ...b[1].map((e) => parseTimelineDate(e.date))
3152
- );
3350
+ const aMin = Math.min(...a[1].map((e) => parseTimelineDate(e.date)));
3351
+ const bMin = Math.min(...b[1].map((e) => parseTimelineDate(e.date)));
3153
3352
  return aMin - bMin;
3154
3353
  });
3155
3354
 
@@ -3171,7 +3370,11 @@ export function renderTimeline(
3171
3370
  function eventColor(ev: TimelineEvent): string {
3172
3371
  // Tag color takes priority when a tag group is active
3173
3372
  if (effectiveColorTG) {
3174
- const tagColor = resolveTagColor(ev.metadata, parsed.timelineTagGroups, effectiveColorTG);
3373
+ const tagColor = resolveTagColor(
3374
+ ev.metadata,
3375
+ parsed.timelineTagGroups,
3376
+ effectiveColorTG
3377
+ );
3175
3378
  if (tagColor) return tagColor;
3176
3379
  }
3177
3380
  if (ev.group && groupColorMap.has(ev.group)) {
@@ -3282,16 +3485,23 @@ export function renderTimeline(
3282
3485
  el.attr('opacity', val === tagValue ? 1 : FADE_OPACITY);
3283
3486
  });
3284
3487
  g.selectAll<SVGGElement, unknown>('.tl-legend-item, .tl-lane-header').attr(
3285
- 'opacity', FADE_OPACITY
3488
+ 'opacity',
3489
+ FADE_OPACITY
3490
+ );
3491
+ g.selectAll<SVGGElement, unknown>('.tl-marker').attr(
3492
+ 'opacity',
3493
+ FADE_OPACITY
3286
3494
  );
3287
- g.selectAll<SVGGElement, unknown>('.tl-marker').attr('opacity', FADE_OPACITY);
3288
3495
  // Fade legend entry dots/labels that don't match (keep group pill visible)
3289
3496
  g.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry').each(function () {
3290
3497
  const el = d3Selection.select(this);
3291
3498
  const entryValue = el.attr('data-legend-entry');
3292
3499
  if (entryValue === '__group__') return; // keep group pill at full opacity
3293
3500
  const entryGroup = el.attr('data-tag-group');
3294
- el.attr('opacity', entryGroup === tagKey && entryValue === tagValue ? 1 : FADE_OPACITY);
3501
+ el.attr(
3502
+ 'opacity',
3503
+ entryGroup === tagKey && entryValue === tagValue ? 1 : FADE_OPACITY
3504
+ );
3295
3505
  });
3296
3506
  }
3297
3507
 
@@ -3312,7 +3522,8 @@ export function renderTimeline(
3312
3522
  // VERTICAL orientation (time flows top→bottom)
3313
3523
  // ================================================================
3314
3524
  if (isVertical) {
3315
- const useGroupedVertical = tagLanes != null ||
3525
+ const useGroupedVertical =
3526
+ tagLanes != null ||
3316
3527
  (timelineSort === 'group' && timelineGroups.length > 0);
3317
3528
  if (useGroupedVertical) {
3318
3529
  // === GROUPED: one column/lane per group, vertical ===
@@ -3371,7 +3582,14 @@ export function renderTimeline(
3371
3582
  .append('g')
3372
3583
  .attr('transform', `translate(${margin.left},${margin.top})`);
3373
3584
 
3374
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
3585
+ renderChartTitle(
3586
+ svg,
3587
+ title,
3588
+ parsed.titleLineNumber,
3589
+ width,
3590
+ textColor,
3591
+ onClickItem
3592
+ );
3375
3593
 
3376
3594
  renderEras(
3377
3595
  g,
@@ -3409,8 +3627,8 @@ export function renderTimeline(
3409
3627
  textColor,
3410
3628
  minDate,
3411
3629
  maxDate,
3412
- formatDateLabel(earliestStartDateStr),
3413
- formatDateLabel(latestEndDateStr)
3630
+ formatBoundaryLabel(earliestStartDateStr, latestEndDateStr),
3631
+ formatBoundaryLabel(latestEndDateStr, earliestStartDateStr)
3414
3632
  );
3415
3633
  }
3416
3634
 
@@ -3621,7 +3839,14 @@ export function renderTimeline(
3621
3839
  .append('g')
3622
3840
  .attr('transform', `translate(${margin.left},${margin.top})`);
3623
3841
 
3624
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
3842
+ renderChartTitle(
3843
+ svg,
3844
+ title,
3845
+ parsed.titleLineNumber,
3846
+ width,
3847
+ textColor,
3848
+ onClickItem
3849
+ );
3625
3850
 
3626
3851
  renderEras(
3627
3852
  g,
@@ -3659,8 +3884,8 @@ export function renderTimeline(
3659
3884
  textColor,
3660
3885
  minDate,
3661
3886
  maxDate,
3662
- formatDateLabel(earliestStartDateStr),
3663
- formatDateLabel(latestEndDateStr)
3887
+ formatBoundaryLabel(earliestStartDateStr, latestEndDateStr),
3888
+ formatBoundaryLabel(latestEndDateStr, earliestStartDateStr)
3664
3889
  );
3665
3890
  }
3666
3891
 
@@ -3747,8 +3972,7 @@ export function renderTimeline(
3747
3972
  if (ev.uncertain) {
3748
3973
  const gradientId = `uncertain-v-${ev.lineNumber}`;
3749
3974
  const strokeGradientId = `uncertain-v-s-${ev.lineNumber}`;
3750
- const defs =
3751
- svg.select('defs').node() || svg.append('defs').node();
3975
+ const defs = svg.select('defs').node() || svg.append('defs').node();
3752
3976
  const defsEl = d3Selection.select(defs as Element);
3753
3977
  defsEl
3754
3978
  .append('linearGradient')
@@ -3862,8 +4086,8 @@ export function renderTimeline(
3862
4086
  const BAR_H = 22; // range bar thickness (tall enough for text inside)
3863
4087
  const GROUP_GAP = 12; // vertical gap between group swim-lanes
3864
4088
 
3865
- const useGroupedHorizontal = tagLanes != null ||
3866
- (timelineSort === 'group' && timelineGroups.length > 0);
4089
+ const useGroupedHorizontal =
4090
+ tagLanes != null || (timelineSort === 'group' && timelineGroups.length > 0);
3867
4091
  if (useGroupedHorizontal) {
3868
4092
  // === GROUPED: swim-lanes stacked vertically, events on own rows ===
3869
4093
  let lanes: Lane[];
@@ -3896,7 +4120,11 @@ export function renderTimeline(
3896
4120
  // Group-sorted doesn't need legend space (group names shown on left)
3897
4121
  const baseTopMargin = title ? 50 : 20;
3898
4122
  const margin = {
3899
- top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
4123
+ top:
4124
+ baseTopMargin +
4125
+ (timelineScale ? 40 : 0) +
4126
+ markerMargin +
4127
+ tagLegendReserve,
3900
4128
  right: 40,
3901
4129
  bottom: 40 + scaleMargin,
3902
4130
  left: dynamicLeftMargin,
@@ -3922,7 +4150,14 @@ export function renderTimeline(
3922
4150
  .append('g')
3923
4151
  .attr('transform', `translate(${margin.left},${margin.top})`);
3924
4152
 
3925
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
4153
+ renderChartTitle(
4154
+ svg,
4155
+ title,
4156
+ parsed.titleLineNumber,
4157
+ width,
4158
+ textColor,
4159
+ onClickItem
4160
+ );
3926
4161
 
3927
4162
  renderEras(
3928
4163
  g,
@@ -3960,8 +4195,8 @@ export function renderTimeline(
3960
4195
  textColor,
3961
4196
  minDate,
3962
4197
  maxDate,
3963
- formatDateLabel(earliestStartDateStr),
3964
- formatDateLabel(latestEndDateStr)
4198
+ formatBoundaryLabel(earliestStartDateStr, latestEndDateStr),
4199
+ formatBoundaryLabel(latestEndDateStr, earliestStartDateStr)
3965
4200
  );
3966
4201
  }
3967
4202
 
@@ -4223,7 +4458,14 @@ export function renderTimeline(
4223
4458
  .append('g')
4224
4459
  .attr('transform', `translate(${margin.left},${margin.top})`);
4225
4460
 
4226
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
4461
+ renderChartTitle(
4462
+ svg,
4463
+ title,
4464
+ parsed.titleLineNumber,
4465
+ width,
4466
+ textColor,
4467
+ onClickItem
4468
+ );
4227
4469
 
4228
4470
  renderEras(
4229
4471
  g,
@@ -4261,8 +4503,8 @@ export function renderTimeline(
4261
4503
  textColor,
4262
4504
  minDate,
4263
4505
  maxDate,
4264
- formatDateLabel(earliestStartDateStr),
4265
- formatDateLabel(latestEndDateStr)
4506
+ formatBoundaryLabel(earliestStartDateStr, latestEndDateStr),
4507
+ formatBoundaryLabel(latestEndDateStr, earliestStartDateStr)
4266
4508
  );
4267
4509
  }
4268
4510
 
@@ -4501,13 +4743,17 @@ export function renderTimeline(
4501
4743
  expandedWidth: number;
4502
4744
  };
4503
4745
  const legendGroups: LegendGroup[] = parsed.timelineTagGroups.map((g) => {
4504
- const pillW = measureLegendText(g.name, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
4746
+ const pillW =
4747
+ measureLegendText(g.name, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
4505
4748
  // Expanded: pill + icon (unless viewMode) + entries
4506
4749
  const iconSpace = viewMode ? 8 : LG_ICON_W + 4;
4507
4750
  let entryX = LG_CAPSULE_PAD + pillW + iconSpace;
4508
4751
  for (const entry of g.entries) {
4509
4752
  const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
4510
- entryX = textX + measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) + LG_ENTRY_TRAIL;
4753
+ entryX =
4754
+ textX +
4755
+ measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) +
4756
+ LG_ENTRY_TRAIL;
4511
4757
  }
4512
4758
  return {
4513
4759
  group: g,
@@ -4527,7 +4773,8 @@ export function renderTimeline(
4527
4773
  y: number,
4528
4774
  isSwimActive: boolean
4529
4775
  ) {
4530
- const iconG = parent.append('g')
4776
+ const iconG = parent
4777
+ .append('g')
4531
4778
  .attr('class', 'tl-swimlane-icon')
4532
4779
  .attr('transform', `translate(${x}, ${y})`)
4533
4780
  .style('cursor', 'pointer');
@@ -4540,7 +4787,8 @@ export function renderTimeline(
4540
4787
  { y: 8, w: 6 },
4541
4788
  ];
4542
4789
  for (const bar of bars) {
4543
- iconG.append('rect')
4790
+ iconG
4791
+ .append('rect')
4544
4792
  .attr('x', 0)
4545
4793
  .attr('y', bar.y)
4546
4794
  .attr('width', bar.w)
@@ -4555,8 +4803,16 @@ export function renderTimeline(
4555
4803
  /** Full re-render with updated swimlane state */
4556
4804
  function relayout() {
4557
4805
  renderTimeline(
4558
- container, parsed, palette, isDark, onClickItem, exportDims,
4559
- currentActiveGroup, currentSwimlaneGroup, onTagStateChange, viewMode
4806
+ container,
4807
+ parsed,
4808
+ palette,
4809
+ isDark,
4810
+ onClickItem,
4811
+ exportDims,
4812
+ currentActiveGroup,
4813
+ currentSwimlaneGroup,
4814
+ onTagStateChange,
4815
+ viewMode
4560
4816
  );
4561
4817
  }
4562
4818
 
@@ -4566,7 +4822,8 @@ export function renderTimeline(
4566
4822
  mainSvg.selectAll('.tl-tag-legend-container').remove();
4567
4823
 
4568
4824
  // Effective color source: explicit color group > swimlane group
4569
- const effectiveColorKey = (currentActiveGroup ?? currentSwimlaneGroup)?.toLowerCase() ?? null;
4825
+ const effectiveColorKey =
4826
+ (currentActiveGroup ?? currentSwimlaneGroup)?.toLowerCase() ?? null;
4570
4827
 
4571
4828
  // In view mode, only show the color-driving tag group (expanded, non-interactive).
4572
4829
  // Skip the swimlane group if it's separate from the color group (lane headers already label it).
@@ -4581,32 +4838,43 @@ export function renderTimeline(
4581
4838
  if (visibleGroups.length === 0) return;
4582
4839
 
4583
4840
  // Compute total width and center horizontally in SVG
4584
- const totalW = visibleGroups.reduce((s, lg) => {
4585
- const isActive = viewMode ||
4586
- (currentActiveGroup != null &&
4587
- lg.group.name.toLowerCase() === currentActiveGroup.toLowerCase());
4588
- return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
4589
- }, 0) + (visibleGroups.length - 1) * LG_GROUP_GAP;
4841
+ const totalW =
4842
+ visibleGroups.reduce((s, lg) => {
4843
+ const isActive =
4844
+ viewMode ||
4845
+ (currentActiveGroup != null &&
4846
+ lg.group.name.toLowerCase() ===
4847
+ currentActiveGroup.toLowerCase());
4848
+ return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
4849
+ }, 0) +
4850
+ (visibleGroups.length - 1) * LG_GROUP_GAP;
4590
4851
 
4591
4852
  let cx = (width - totalW) / 2;
4592
4853
 
4593
4854
  // Legend container for data-legend-active attribute
4594
- const legendContainer = mainSvg.append('g')
4855
+ const legendContainer = mainSvg
4856
+ .append('g')
4595
4857
  .attr('class', 'tl-tag-legend-container');
4596
4858
  if (currentActiveGroup) {
4597
- legendContainer.attr('data-legend-active', currentActiveGroup.toLowerCase());
4859
+ legendContainer.attr(
4860
+ 'data-legend-active',
4861
+ currentActiveGroup.toLowerCase()
4862
+ );
4598
4863
  }
4599
4864
 
4600
4865
  for (const lg of visibleGroups) {
4601
4866
  const groupKey = lg.group.name.toLowerCase();
4602
- const isActive = viewMode ||
4867
+ const isActive =
4868
+ viewMode ||
4603
4869
  (currentActiveGroup != null &&
4604
4870
  currentActiveGroup.toLowerCase() === groupKey);
4605
- const isSwimActive = currentSwimlaneGroup != null &&
4871
+ const isSwimActive =
4872
+ currentSwimlaneGroup != null &&
4606
4873
  currentSwimlaneGroup.toLowerCase() === groupKey;
4607
4874
 
4608
4875
  const pillLabel = lg.group.name;
4609
- const pillWidth = measureLegendText(pillLabel, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
4876
+ const pillWidth =
4877
+ measureLegendText(pillLabel, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
4610
4878
 
4611
4879
  const gEl = legendContainer
4612
4880
  .append('g')
@@ -4617,19 +4885,19 @@ export function renderTimeline(
4617
4885
  .attr('data-legend-entry', '__group__');
4618
4886
 
4619
4887
  if (!viewMode) {
4620
- gEl
4621
- .style('cursor', 'pointer')
4622
- .on('click', () => {
4623
- currentActiveGroup = currentActiveGroup === groupKey ? null : groupKey;
4624
- drawLegend();
4625
- recolorEvents();
4626
- onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
4627
- });
4888
+ gEl.style('cursor', 'pointer').on('click', () => {
4889
+ currentActiveGroup =
4890
+ currentActiveGroup === groupKey ? null : groupKey;
4891
+ drawLegend();
4892
+ recolorEvents();
4893
+ onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
4894
+ });
4628
4895
  }
4629
4896
 
4630
4897
  // Outer capsule background (active only)
4631
4898
  if (isActive) {
4632
- gEl.append('rect')
4899
+ gEl
4900
+ .append('rect')
4633
4901
  .attr('width', lg.expandedWidth)
4634
4902
  .attr('height', LG_HEIGHT)
4635
4903
  .attr('rx', LG_HEIGHT / 2)
@@ -4641,7 +4909,8 @@ export function renderTimeline(
4641
4909
  const pillH = LG_HEIGHT - (isActive ? LG_CAPSULE_PAD * 2 : 0);
4642
4910
 
4643
4911
  // Pill background
4644
- gEl.append('rect')
4912
+ gEl
4913
+ .append('rect')
4645
4914
  .attr('x', pillXOff)
4646
4915
  .attr('y', pillYOff)
4647
4916
  .attr('width', pillWidth)
@@ -4651,7 +4920,8 @@ export function renderTimeline(
4651
4920
 
4652
4921
  // Active pill border
4653
4922
  if (isActive) {
4654
- gEl.append('rect')
4923
+ gEl
4924
+ .append('rect')
4655
4925
  .attr('x', pillXOff)
4656
4926
  .attr('y', pillYOff)
4657
4927
  .attr('width', pillWidth)
@@ -4663,7 +4933,8 @@ export function renderTimeline(
4663
4933
  }
4664
4934
 
4665
4935
  // Pill text
4666
- gEl.append('text')
4936
+ gEl
4937
+ .append('text')
4667
4938
  .attr('x', pillXOff + pillWidth / 2)
4668
4939
  .attr('y', LG_HEIGHT / 2 + LG_PILL_FONT_SIZE / 2 - 2)
4669
4940
  .attr('font-size', LG_PILL_FONT_SIZE)
@@ -4685,7 +4956,8 @@ export function renderTimeline(
4685
4956
  .attr('data-swimlane-toggle', groupKey)
4686
4957
  .on('click', (event: MouseEvent) => {
4687
4958
  event.stopPropagation();
4688
- currentSwimlaneGroup = currentSwimlaneGroup === groupKey ? null : groupKey;
4959
+ currentSwimlaneGroup =
4960
+ currentSwimlaneGroup === groupKey ? null : groupKey;
4689
4961
  onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
4690
4962
  relayout();
4691
4963
  });
@@ -4698,7 +4970,8 @@ export function renderTimeline(
4698
4970
  const tagKey = lg.group.name.toLowerCase();
4699
4971
  const tagVal = entry.value.toLowerCase();
4700
4972
 
4701
- const entryG = gEl.append('g')
4973
+ const entryG = gEl
4974
+ .append('g')
4702
4975
  .attr('class', 'tl-tag-legend-entry')
4703
4976
  .attr('data-tag-group', tagKey)
4704
4977
  .attr('data-legend-entry', tagVal);
@@ -4709,18 +4982,24 @@ export function renderTimeline(
4709
4982
  .on('mouseenter', (event: MouseEvent) => {
4710
4983
  event.stopPropagation();
4711
4984
  fadeToTagValue(mainG, tagKey, tagVal);
4712
- mainSvg.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry').each(function () {
4713
- const el = d3Selection.select(this);
4714
- const ev = el.attr('data-legend-entry');
4715
- if (ev === '__group__') return;
4716
- const eg = el.attr('data-tag-group');
4717
- el.attr('opacity', eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY);
4718
- });
4985
+ mainSvg
4986
+ .selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
4987
+ .each(function () {
4988
+ const el = d3Selection.select(this);
4989
+ const ev = el.attr('data-legend-entry');
4990
+ if (ev === '__group__') return;
4991
+ const eg = el.attr('data-tag-group');
4992
+ el.attr(
4993
+ 'opacity',
4994
+ eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY
4995
+ );
4996
+ });
4719
4997
  })
4720
4998
  .on('mouseleave', (event: MouseEvent) => {
4721
4999
  event.stopPropagation();
4722
5000
  fadeReset(mainG);
4723
- mainSvg.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
5001
+ mainSvg
5002
+ .selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
4724
5003
  .attr('opacity', 1);
4725
5004
  })
4726
5005
  .on('click', (event: MouseEvent) => {
@@ -4728,14 +5007,16 @@ export function renderTimeline(
4728
5007
  });
4729
5008
  }
4730
5009
 
4731
- entryG.append('circle')
5010
+ entryG
5011
+ .append('circle')
4732
5012
  .attr('cx', entryX + LG_DOT_R)
4733
5013
  .attr('cy', LG_HEIGHT / 2)
4734
5014
  .attr('r', LG_DOT_R)
4735
5015
  .attr('fill', entry.color);
4736
5016
 
4737
5017
  const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
4738
- entryG.append('text')
5018
+ entryG
5019
+ .append('text')
4739
5020
  .attr('x', textX)
4740
5021
  .attr('y', LG_HEIGHT / 2 + LG_ENTRY_FONT_SIZE / 2 - 1)
4741
5022
  .attr('font-size', LG_ENTRY_FONT_SIZE)
@@ -4743,7 +5024,10 @@ export function renderTimeline(
4743
5024
  .attr('fill', palette.textMuted)
4744
5025
  .text(entry.value);
4745
5026
 
4746
- entryX = textX + measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) + LG_ENTRY_TRAIL;
5027
+ entryX =
5028
+ textX +
5029
+ measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) +
5030
+ LG_ENTRY_TRAIL;
4747
5031
  }
4748
5032
  }
4749
5033
 
@@ -4768,16 +5052,27 @@ export function renderTimeline(
4768
5052
  let color: string;
4769
5053
  if (colorTG) {
4770
5054
  const tagColor = resolveTagColor(
4771
- ev.metadata, parsed.timelineTagGroups, colorTG
5055
+ ev.metadata,
5056
+ parsed.timelineTagGroups,
5057
+ colorTG
4772
5058
  );
4773
- color = tagColor ?? (ev.group && groupColorMap.has(ev.group)
4774
- ? groupColorMap.get(ev.group)! : textColor);
5059
+ color =
5060
+ tagColor ??
5061
+ (ev.group && groupColorMap.has(ev.group)
5062
+ ? groupColorMap.get(ev.group)!
5063
+ : textColor);
4775
5064
  } else {
4776
- color = ev.group && groupColorMap.has(ev.group)
4777
- ? groupColorMap.get(ev.group)! : textColor;
5065
+ color =
5066
+ ev.group && groupColorMap.has(ev.group)
5067
+ ? groupColorMap.get(ev.group)!
5068
+ : textColor;
4778
5069
  }
4779
- el.selectAll('rect').attr('fill', mix(color, bg, 30)).attr('stroke', color);
4780
- el.selectAll('circle:not(.tl-event-point-outline)').attr('fill', mix(color, bg, 30)).attr('stroke', color);
5070
+ el.selectAll('rect')
5071
+ .attr('fill', mix(color, bg, 30))
5072
+ .attr('stroke', color);
5073
+ el.selectAll('circle:not(.tl-event-point-outline)')
5074
+ .attr('fill', mix(color, bg, 30))
5075
+ .attr('stroke', color);
4781
5076
  });
4782
5077
  }
4783
5078
 
@@ -4834,7 +5129,14 @@ export function renderWordCloud(
4834
5129
 
4835
5130
  const rotateFn = getRotateFn(cloudOptions.rotate);
4836
5131
 
4837
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
5132
+ renderChartTitle(
5133
+ svg,
5134
+ title,
5135
+ parsed.titleLineNumber,
5136
+ width,
5137
+ textColor,
5138
+ onClickItem
5139
+ );
4838
5140
 
4839
5141
  const g = svg
4840
5142
  .append('g')
@@ -5141,7 +5443,8 @@ export function renderVenn(
5141
5443
  const labelTextPad = 4;
5142
5444
 
5143
5445
  for (let i = 0; i < n; i++) {
5144
- const estimatedWidth = vennSets[i].name.length * 8.5 + stubLen + edgePad + labelTextPad;
5446
+ const estimatedWidth =
5447
+ vennSets[i].name.length * 8.5 + stubLen + edgePad + labelTextPad;
5145
5448
  const dx = rawCircles[i].x - clusterCx;
5146
5449
  const dy = rawCircles[i].y - clusterCy;
5147
5450
  if (Math.abs(dx) >= Math.abs(dy)) {
@@ -5168,13 +5471,27 @@ export function renderVenn(
5168
5471
  const scaledR = circles[0].r;
5169
5472
 
5170
5473
  // Suppress WebKit focus ring on interactive SVG elements
5171
- svg.append('style').text('circle:focus, circle:focus-visible { outline: none !important; }');
5474
+ svg
5475
+ .append('style')
5476
+ .text('circle:focus, circle:focus-visible { outline: none !important; }');
5172
5477
 
5173
5478
  // Title
5174
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
5479
+ renderChartTitle(
5480
+ svg,
5481
+ title,
5482
+ parsed.titleLineNumber,
5483
+ width,
5484
+ textColor,
5485
+ onClickItem
5486
+ );
5175
5487
 
5176
5488
  // ── Semi-transparent filled circles (non-interactive) ──
5177
- const circleEls: d3Selection.Selection<SVGCircleElement, unknown, null, undefined>[] = [];
5489
+ const circleEls: d3Selection.Selection<
5490
+ SVGCircleElement,
5491
+ unknown,
5492
+ null,
5493
+ undefined
5494
+ >[] = [];
5178
5495
  const circleGroup = svg.append('g');
5179
5496
  circles.forEach((c, i) => {
5180
5497
  const el = circleGroup
@@ -5201,10 +5518,13 @@ export function renderVenn(
5201
5518
 
5202
5519
  // Individual circle clipPaths
5203
5520
  circles.forEach((c, i) => {
5204
- defs.append('clipPath')
5521
+ defs
5522
+ .append('clipPath')
5205
5523
  .attr('id', `vcp-${i}`)
5206
5524
  .append('circle')
5207
- .attr('cx', c.x).attr('cy', c.y).attr('r', c.r);
5525
+ .attr('cx', c.x)
5526
+ .attr('cy', c.y)
5527
+ .attr('r', c.r);
5208
5528
  });
5209
5529
 
5210
5530
  // All region index-sets: exclusive then intersection subsets
@@ -5216,57 +5536,79 @@ export function renderVenn(
5216
5536
  }
5217
5537
 
5218
5538
  const overlayGroup = svg.append('g').style('pointer-events', 'none');
5219
- const overlayEls = new Map<string, d3Selection.Selection<SVGRectElement, unknown, null, undefined>>();
5539
+ const overlayEls = new Map<
5540
+ string,
5541
+ d3Selection.Selection<SVGRectElement, unknown, null, undefined>
5542
+ >();
5220
5543
 
5221
5544
  for (const idxs of regionIdxSets) {
5222
5545
  const key = idxs.join('-');
5223
- const excluded = Array.from({ length: n }, (_, j) => j).filter(j => !idxs.includes(j));
5546
+ const excluded = Array.from({ length: n }, (_, j) => j).filter(
5547
+ (j) => !idxs.includes(j)
5548
+ );
5224
5549
 
5225
5550
  // Build nested clipPath for intersection of all idxs
5226
5551
  let clipId = `vcp-${idxs[0]}`;
5227
5552
  for (let k = 1; k < idxs.length; k++) {
5228
5553
  const nestedId = `vcp-n-${idxs.slice(0, k + 1).join('-')}`;
5229
5554
  const ci = idxs[k];
5230
- defs.append('clipPath')
5555
+ defs
5556
+ .append('clipPath')
5231
5557
  .attr('id', nestedId)
5232
5558
  .append('circle')
5233
- .attr('cx', circles[ci].x).attr('cy', circles[ci].y).attr('r', circles[ci].r)
5559
+ .attr('cx', circles[ci].x)
5560
+ .attr('cy', circles[ci].y)
5561
+ .attr('r', circles[ci].r)
5234
5562
  .attr('clip-path', `url(#${clipId})`);
5235
5563
  clipId = nestedId;
5236
5564
  }
5237
5565
 
5238
5566
  // Determine line number for this region (for editor sync)
5239
- let regionLineNumber: number | null = null;
5567
+ let regionLineNumber: number | null = null; // eslint-disable-line no-useless-assignment
5240
5568
  if (idxs.length === 1) {
5241
5569
  regionLineNumber = vennSets[idxs[0]].lineNumber;
5242
5570
  } else {
5243
- const sortedNames = idxs.map(i => vennSets[i].name).sort();
5571
+ const sortedNames = idxs.map((i) => vennSets[i].name).sort();
5244
5572
  const ov = vennOverlaps.find(
5245
- (o) => o.sets.length === sortedNames.length && o.sets.every((s, k) => s === sortedNames[k])
5573
+ (o) =>
5574
+ o.sets.length === sortedNames.length &&
5575
+ o.sets.every((s, k) => s === sortedNames[k])
5246
5576
  );
5247
5577
  regionLineNumber = ov?.lineNumber ?? null;
5248
5578
  }
5249
5579
 
5250
- const el = overlayGroup.append('rect')
5251
- .attr('x', 0).attr('y', 0)
5252
- .attr('width', width).attr('height', height)
5580
+ const el = overlayGroup
5581
+ .append('rect')
5582
+ .attr('x', 0)
5583
+ .attr('y', 0)
5584
+ .attr('width', width)
5585
+ .attr('height', height)
5253
5586
  .attr('fill', 'white')
5254
5587
  .attr('fill-opacity', 0)
5255
5588
  .attr('class', 'venn-region-overlay')
5256
- .attr('data-line-number', regionLineNumber != null ? String(regionLineNumber) : '0')
5589
+ .attr(
5590
+ 'data-line-number',
5591
+ regionLineNumber != null ? String(regionLineNumber) : '0'
5592
+ )
5257
5593
  .attr('clip-path', `url(#${clipId})`);
5258
5594
 
5259
5595
  if (excluded.length > 0) {
5260
5596
  // Mask subtracts excluded circles so only the exact region shape highlights
5261
5597
  const maskId = `vvm-${key}`;
5262
5598
  const mask = defs.append('mask').attr('id', maskId);
5263
- mask.append('rect')
5264
- .attr('x', 0).attr('y', 0)
5265
- .attr('width', width).attr('height', height)
5599
+ mask
5600
+ .append('rect')
5601
+ .attr('x', 0)
5602
+ .attr('y', 0)
5603
+ .attr('width', width)
5604
+ .attr('height', height)
5266
5605
  .attr('fill', 'white');
5267
5606
  for (const j of excluded) {
5268
- mask.append('circle')
5269
- .attr('cx', circles[j].x).attr('cy', circles[j].y).attr('r', circles[j].r)
5607
+ mask
5608
+ .append('circle')
5609
+ .attr('cx', circles[j].x)
5610
+ .attr('cy', circles[j].y)
5611
+ .attr('r', circles[j].r)
5270
5612
  .attr('fill', 'black');
5271
5613
  }
5272
5614
  el.attr('mask', `url(#${maskId})`);
@@ -5277,10 +5619,12 @@ export function renderVenn(
5277
5619
 
5278
5620
  const showRegionOverlay = (idxs: number[]) => {
5279
5621
  const key = [...idxs].sort((a, b) => a - b).join('-');
5280
- overlayEls.forEach((el, k) => el.attr('fill-opacity', k === key ? 0 : 0.55));
5622
+ overlayEls.forEach((el, k) =>
5623
+ el.attr('fill-opacity', k === key ? 0 : 0.55)
5624
+ );
5281
5625
  };
5282
5626
  const hideAllOverlays = () => {
5283
- overlayEls.forEach(el => el.attr('fill-opacity', 0));
5627
+ overlayEls.forEach((el) => el.attr('fill-opacity', 0));
5284
5628
  };
5285
5629
 
5286
5630
  // ── Labels ──
@@ -5289,7 +5633,9 @@ export function renderVenn(
5289
5633
 
5290
5634
  function exclusiveHSpan(px: number, py: number, ci: number): number {
5291
5635
  const dy = py - circles[ci].y;
5292
- const halfChord = Math.sqrt(Math.max(0, circles[ci].r * circles[ci].r - dy * dy));
5636
+ const halfChord = Math.sqrt(
5637
+ Math.max(0, circles[ci].r * circles[ci].r - dy * dy)
5638
+ );
5293
5639
  let left = circles[ci].x - halfChord;
5294
5640
  let right = circles[ci].x + halfChord;
5295
5641
  for (let j = 0; j < n; j++) {
@@ -5320,11 +5666,14 @@ export function renderVenn(
5320
5666
  const centroid = regionCentroid(circles, inside);
5321
5667
 
5322
5668
  const availW = exclusiveHSpan(centroid.x, centroid.y, i);
5323
- const fitFont = Math.min(MAX_FONT, Math.max(MIN_FONT,
5324
- (availW - INTERNAL_PAD * 2) / (text.length * CH_RATIO)));
5669
+ const fitFont = Math.min(
5670
+ MAX_FONT,
5671
+ Math.max(MIN_FONT, (availW - INTERNAL_PAD * 2) / (text.length * CH_RATIO))
5672
+ );
5325
5673
  const estTextW = text.length * CH_RATIO * fitFont;
5326
5674
 
5327
- const fitsInside = estTextW + INTERNAL_PAD * 2 < availW &&
5675
+ const fitsInside =
5676
+ estTextW + INTERNAL_PAD * 2 < availW &&
5328
5677
  pointInCircle({ x: centroid.x, y: centroid.y - fitFont / 2 }, c) &&
5329
5678
  pointInCircle({ x: centroid.x, y: centroid.y + fitFont / 2 }, c);
5330
5679
 
@@ -5343,7 +5692,13 @@ export function renderVenn(
5343
5692
  let dx = c.x - gcx;
5344
5693
  let dy = c.y - gcy;
5345
5694
  const mag = Math.sqrt(dx * dx + dy * dy);
5346
- if (mag < 1e-6) { dx = 1; dy = 0; } else { dx /= mag; dy /= mag; }
5695
+ if (mag < 1e-6) {
5696
+ dx = 1;
5697
+ dy = 0;
5698
+ } else {
5699
+ dx /= mag;
5700
+ dy /= mag;
5701
+ }
5347
5702
 
5348
5703
  const exitX = c.x + dx * c.r;
5349
5704
  const exitY = c.y + dy * c.r;
@@ -5354,8 +5709,10 @@ export function renderVenn(
5354
5709
 
5355
5710
  labelGroup
5356
5711
  .append('line')
5357
- .attr('x1', edgeX).attr('y1', edgeY)
5358
- .attr('x2', stubEndX).attr('y2', stubEndY)
5712
+ .attr('x1', edgeX)
5713
+ .attr('y1', edgeY)
5714
+ .attr('x2', stubEndX)
5715
+ .attr('y2', stubEndY)
5359
5716
  .attr('stroke', textColor)
5360
5717
  .attr('stroke-width', 1);
5361
5718
 
@@ -5382,7 +5739,8 @@ export function renderVenn(
5382
5739
 
5383
5740
  // ── Overlap labels (inline at region centroid) ──
5384
5741
  function overlapHSpan(py: number, idxs: number[]): number {
5385
- let left = -Infinity, right = Infinity;
5742
+ let left = -Infinity,
5743
+ right = Infinity;
5386
5744
  for (const ci of idxs) {
5387
5745
  const dy = py - circles[ci].y;
5388
5746
  if (Math.abs(dy) >= circles[ci].r) return 0;
@@ -5412,8 +5770,13 @@ export function renderVenn(
5412
5770
  const inside = circles.map((_, j) => idxs.includes(j));
5413
5771
  const centroid = regionCentroid(circles, inside);
5414
5772
  const availW = overlapHSpan(centroid.y, idxs);
5415
- const fitFont = Math.min(MAX_FONT, Math.max(MIN_FONT,
5416
- (availW - INTERNAL_PAD * 2) / (ov.label.length * CH_RATIO)));
5773
+ const fitFont = Math.min(
5774
+ MAX_FONT,
5775
+ Math.max(
5776
+ MIN_FONT,
5777
+ (availW - INTERNAL_PAD * 2) / (ov.label.length * CH_RATIO)
5778
+ )
5779
+ );
5417
5780
  labelGroup
5418
5781
  .append('text')
5419
5782
  .attr('x', centroid.x)
@@ -5442,11 +5805,16 @@ export function renderVenn(
5442
5805
  .attr('data-line-number', String(vennSets[i].lineNumber))
5443
5806
  .style('cursor', onClickItem ? 'pointer' : 'default')
5444
5807
  .style('outline', 'none')
5445
- .on('mouseenter', () => { showRegionOverlay([i]); })
5446
- .on('mouseleave', () => { hideAllOverlays(); })
5808
+ .on('mouseenter', () => {
5809
+ showRegionOverlay([i]);
5810
+ })
5811
+ .on('mouseleave', () => {
5812
+ hideAllOverlays();
5813
+ })
5447
5814
  .on('click', function () {
5448
5815
  (this as SVGElement).blur?.();
5449
- if (onClickItem && vennSets[i].lineNumber) onClickItem(vennSets[i].lineNumber);
5816
+ if (onClickItem && vennSets[i].lineNumber)
5817
+ onClickItem(vennSets[i].lineNumber);
5450
5818
  });
5451
5819
  });
5452
5820
 
@@ -5455,14 +5823,23 @@ export function renderVenn(
5455
5823
 
5456
5824
  const subsets: { idxs: number[]; sets: string[] }[] = [];
5457
5825
  if (n === 2) {
5458
- subsets.push({ idxs: [0, 1], sets: [vennSets[0].name, vennSets[1].name].sort() });
5826
+ subsets.push({
5827
+ idxs: [0, 1],
5828
+ sets: [vennSets[0].name, vennSets[1].name].sort(),
5829
+ });
5459
5830
  } else {
5460
5831
  for (let a = 0; a < n; a++) {
5461
5832
  for (let b = a + 1; b < n; b++) {
5462
- subsets.push({ idxs: [a, b], sets: [vennSets[a].name, vennSets[b].name].sort() });
5833
+ subsets.push({
5834
+ idxs: [a, b],
5835
+ sets: [vennSets[a].name, vennSets[b].name].sort(),
5836
+ });
5463
5837
  }
5464
5838
  }
5465
- subsets.push({ idxs: [0, 1, 2], sets: [vennSets[0].name, vennSets[1].name, vennSets[2].name].sort() });
5839
+ subsets.push({
5840
+ idxs: [0, 1, 2],
5841
+ sets: [vennSets[0].name, vennSets[1].name, vennSets[2].name].sort(),
5842
+ });
5466
5843
  }
5467
5844
 
5468
5845
  for (const subset of subsets) {
@@ -5470,7 +5847,8 @@ export function renderVenn(
5470
5847
  const inside = circles.map((_, j) => idxs.includes(j));
5471
5848
  const centroid = regionCentroid(circles, inside);
5472
5849
  const declaredOv = vennOverlaps.find(
5473
- (ov) => ov.sets.length === sets.length && ov.sets.every((s, k) => s === sets[k])
5850
+ (ov) =>
5851
+ ov.sets.length === sets.length && ov.sets.every((s, k) => s === sets[k])
5474
5852
  );
5475
5853
  hoverGroup
5476
5854
  .append('circle')
@@ -5483,8 +5861,12 @@ export function renderVenn(
5483
5861
  .attr('data-line-number', declaredOv ? String(declaredOv.lineNumber) : '')
5484
5862
  .style('cursor', onClickItem && declaredOv ? 'pointer' : 'default')
5485
5863
  .style('outline', 'none')
5486
- .on('mouseenter', () => { showRegionOverlay(idxs); })
5487
- .on('mouseleave', () => { hideAllOverlays(); })
5864
+ .on('mouseenter', () => {
5865
+ showRegionOverlay(idxs);
5866
+ })
5867
+ .on('mouseleave', () => {
5868
+ hideAllOverlays();
5869
+ })
5488
5870
  .on('click', function () {
5489
5871
  (this as SVGElement).blur?.();
5490
5872
  if (onClickItem && declaredOv) onClickItem(declaredOv.lineNumber);
@@ -5543,7 +5925,12 @@ export function renderQuadrant(
5543
5925
  // Margins
5544
5926
  const hasXAxis = !!quadrantXAxis;
5545
5927
  const hasYAxis = !!quadrantYAxis;
5546
- const margin = { top: title ? 60 : 30, right: 30, bottom: hasXAxis ? 70 : 40, left: hasYAxis ? 80 : 40 };
5928
+ const margin = {
5929
+ top: title ? 60 : 30,
5930
+ right: 30,
5931
+ bottom: hasXAxis ? 70 : 40,
5932
+ left: hasYAxis ? 80 : 40,
5933
+ };
5547
5934
  const chartWidth = width - margin.left - margin.right;
5548
5935
  const chartHeight = height - margin.top - margin.bottom;
5549
5936
 
@@ -5555,7 +5942,14 @@ export function renderQuadrant(
5555
5942
  const tooltip = createTooltip(container, palette, isDark);
5556
5943
 
5557
5944
  // Title
5558
- renderChartTitle(svg, title, quadrantTitleLineNumber, width, textColor, onClickItem);
5945
+ renderChartTitle(
5946
+ svg,
5947
+ title,
5948
+ quadrantTitleLineNumber,
5949
+ width,
5950
+ textColor,
5951
+ onClickItem
5952
+ );
5559
5953
 
5560
5954
  // Chart group (translated by margins)
5561
5955
  const chartG = svg
@@ -5566,12 +5960,21 @@ export function renderQuadrant(
5566
5960
  const mixHex = (a: string, b: string, pct: number): string => {
5567
5961
  const parse = (h: string) => {
5568
5962
  const r = h.replace('#', '');
5569
- const f = r.length === 3 ? r[0]+r[0]+r[1]+r[1]+r[2]+r[2] : r;
5570
- return [parseInt(f.substring(0,2),16), parseInt(f.substring(2,4),16), parseInt(f.substring(4,6),16)];
5963
+ const f = r.length === 3 ? r[0] + r[0] + r[1] + r[1] + r[2] + r[2] : r;
5964
+ return [
5965
+ parseInt(f.substring(0, 2), 16),
5966
+ parseInt(f.substring(2, 4), 16),
5967
+ parseInt(f.substring(4, 6), 16),
5968
+ ];
5571
5969
  };
5572
- const [ar,ag,ab] = parse(a), [br,bg,bb] = parse(b), t = pct/100;
5573
- const c = (x: number, y: number) => Math.round(x*t + y*(1-t)).toString(16).padStart(2,'0');
5574
- return `#${c(ar,br)}${c(ag,bg)}${c(ab,bb)}`;
5970
+ const [ar, ag, ab] = parse(a),
5971
+ [br, bg, bb] = parse(b),
5972
+ t = pct / 100;
5973
+ const c = (x: number, y: number) =>
5974
+ Math.round(x * t + y * (1 - t))
5975
+ .toString(16)
5976
+ .padStart(2, '0');
5977
+ return `#${c(ar, br)}${c(ag, bg)}${c(ab, bb)}`;
5575
5978
  };
5576
5979
 
5577
5980
  const bg = isDark ? palette.surface : palette.bg;
@@ -5688,7 +6091,11 @@ export function renderQuadrant(
5688
6091
  fontSize: number;
5689
6092
  }
5690
6093
 
5691
- const quadrantLabelLayout = (text: string, qw: number, qh: number): QuadrantLabelLayout => {
6094
+ const quadrantLabelLayout = (
6095
+ text: string,
6096
+ qw: number,
6097
+ qh: number
6098
+ ): QuadrantLabelLayout => {
5692
6099
  const availW = qw - LABEL_PAD;
5693
6100
  const availH = qh - LABEL_PAD;
5694
6101
  const words = text.split(/\s+/);
@@ -5696,7 +6103,10 @@ export function renderQuadrant(
5696
6103
  // Try single line first
5697
6104
  if (estTextWidth(text, LABEL_MAX_FONT) <= availW) {
5698
6105
  const fs = Math.min(LABEL_MAX_FONT, availH);
5699
- return { lines: [text], fontSize: Math.max(LABEL_MIN_FONT, Math.round(fs)) };
6106
+ return {
6107
+ lines: [text],
6108
+ fontSize: Math.max(LABEL_MIN_FONT, Math.round(fs)),
6109
+ };
5700
6110
  }
5701
6111
 
5702
6112
  // Try wrapping into 2+ lines: greedily pack words so each line fits availW
@@ -5743,7 +6153,10 @@ export function renderQuadrant(
5743
6153
  const qh = chartHeight / 2;
5744
6154
  const quadrantDefsWithLabel = quadrantDefs.filter((d) => d.label !== null);
5745
6155
  const labelLayouts = new Map(
5746
- quadrantDefsWithLabel.map((d) => [d.label!.text, quadrantLabelLayout(d.label!.text, qw, qh)])
6156
+ quadrantDefsWithLabel.map((d) => [
6157
+ d.label!.text,
6158
+ quadrantLabelLayout(d.label!.text, qw, qh),
6159
+ ])
5747
6160
  );
5748
6161
 
5749
6162
  const quadrantLabelTexts = chartG
@@ -5808,7 +6221,10 @@ export function renderQuadrant(
5808
6221
  .attr('text-anchor', 'middle')
5809
6222
  .attr('fill', textColor)
5810
6223
  .attr('font-size', '18px')
5811
- .attr('data-line-number', quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null)
6224
+ .attr(
6225
+ 'data-line-number',
6226
+ quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null
6227
+ )
5812
6228
  .style(
5813
6229
  'cursor',
5814
6230
  onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
@@ -5824,7 +6240,10 @@ export function renderQuadrant(
5824
6240
  .attr('text-anchor', 'middle')
5825
6241
  .attr('fill', textColor)
5826
6242
  .attr('font-size', '18px')
5827
- .attr('data-line-number', quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null)
6243
+ .attr(
6244
+ 'data-line-number',
6245
+ quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null
6246
+ )
5828
6247
  .style(
5829
6248
  'cursor',
5830
6249
  onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
@@ -5860,7 +6279,10 @@ export function renderQuadrant(
5860
6279
  .attr('fill', textColor)
5861
6280
  .attr('font-size', '18px')
5862
6281
  .attr('transform', `rotate(-90, 22, ${yMidBottom})`)
5863
- .attr('data-line-number', quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null)
6282
+ .attr(
6283
+ 'data-line-number',
6284
+ quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null
6285
+ )
5864
6286
  .style(
5865
6287
  'cursor',
5866
6288
  onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
@@ -5877,7 +6299,10 @@ export function renderQuadrant(
5877
6299
  .attr('fill', textColor)
5878
6300
  .attr('font-size', '18px')
5879
6301
  .attr('transform', `rotate(-90, 22, ${yMidTop})`)
5880
- .attr('data-line-number', quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null)
6302
+ .attr(
6303
+ 'data-line-number',
6304
+ quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null
6305
+ )
5881
6306
  .style(
5882
6307
  'cursor',
5883
6308
  onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
@@ -5936,7 +6361,9 @@ export function renderQuadrant(
5936
6361
  const pointColor =
5937
6362
  quadDef?.label?.color ?? defaultColors[quadDef?.colorIdx ?? 0];
5938
6363
 
5939
- const pointG = pointsG.append('g').attr('class', 'point-group')
6364
+ const pointG = pointsG
6365
+ .append('g')
6366
+ .attr('class', 'point-group')
5940
6367
  .attr('data-line-number', String(point.lineNumber));
5941
6368
 
5942
6369
  // Circle with white fill and colored border for visibility on opaque quadrants
@@ -6025,7 +6452,10 @@ const EXPORT_HEIGHT = 800;
6025
6452
  /**
6026
6453
  * Resolves the palette for export, falling back to Nord light/dark.
6027
6454
  */
6028
- async function resolveExportPalette(theme: string, palette?: PaletteColors): Promise<PaletteColors> {
6455
+ async function resolveExportPalette(
6456
+ theme: string,
6457
+ palette?: PaletteColors
6458
+ ): Promise<PaletteColors> {
6029
6459
  if (palette) return palette;
6030
6460
  const { getPalette } = await import('./palettes');
6031
6461
  return theme === 'dark' ? getPalette('nord').dark : getPalette('nord').light;
@@ -6087,7 +6517,13 @@ export async function renderForExport(
6087
6517
  hiddenAttributes?: Set<string>;
6088
6518
  swimlaneTagGroup?: string | null;
6089
6519
  },
6090
- options?: { branding?: boolean; c4Level?: 'context' | 'containers' | 'components' | 'deployment'; c4System?: string; c4Container?: string; tagGroup?: string }
6520
+ options?: {
6521
+ branding?: boolean;
6522
+ c4Level?: 'context' | 'containers' | 'components' | 'deployment';
6523
+ c4System?: string;
6524
+ c4Container?: string;
6525
+ tagGroup?: string;
6526
+ }
6091
6527
  ): Promise<string> {
6092
6528
  // Flowchart and org chart use their own parser pipelines — intercept before parseVisualization()
6093
6529
  const { parseDgmoChartType } = await import('./dgmo-router');
@@ -6107,7 +6543,8 @@ export async function renderForExport(
6107
6543
 
6108
6544
  // Apply interactive collapse state when provided
6109
6545
  const collapsedNodes = orgExportState?.collapsedNodes;
6110
- const activeTagGroup = orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
6546
+ const activeTagGroup =
6547
+ orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
6111
6548
  const hiddenAttributes = orgExportState?.hiddenAttributes;
6112
6549
 
6113
6550
  const { parsed: effectiveParsed, hiddenCounts } =
@@ -6129,7 +6566,17 @@ export async function renderForExport(
6129
6566
  const exportHeight = orgLayout.height + PADDING * 2 + titleOffset;
6130
6567
  const container = createExportContainer(exportWidth, exportHeight);
6131
6568
 
6132
- renderOrg(container, effectiveParsed, orgLayout, effectivePalette, isDark, undefined, { width: exportWidth, height: exportHeight }, activeTagGroup, hiddenAttributes);
6569
+ renderOrg(
6570
+ container,
6571
+ effectiveParsed,
6572
+ orgLayout,
6573
+ effectivePalette,
6574
+ isDark,
6575
+ undefined,
6576
+ { width: exportWidth, height: exportHeight },
6577
+ activeTagGroup,
6578
+ hiddenAttributes
6579
+ );
6133
6580
  return finalizeSvgExport(container, theme, effectivePalette, options);
6134
6581
  }
6135
6582
 
@@ -6147,7 +6594,8 @@ export async function renderForExport(
6147
6594
 
6148
6595
  // Apply interactive collapse state when provided
6149
6596
  const collapsedNodes = orgExportState?.collapsedNodes;
6150
- const activeTagGroup = orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
6597
+ const activeTagGroup =
6598
+ orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
6151
6599
  const hiddenAttributes = orgExportState?.hiddenAttributes;
6152
6600
 
6153
6601
  const { parsed: effectiveParsed, hiddenCounts } =
@@ -6160,7 +6608,7 @@ export async function renderForExport(
6160
6608
  hiddenCounts.size > 0 ? hiddenCounts : undefined,
6161
6609
  activeTagGroup,
6162
6610
  hiddenAttributes,
6163
- true,
6611
+ true
6164
6612
  );
6165
6613
 
6166
6614
  const PADDING = 20;
@@ -6169,7 +6617,17 @@ export async function renderForExport(
6169
6617
  const exportHeight = sitemapLayout.height + PADDING * 2 + titleOffset;
6170
6618
  const container = createExportContainer(exportWidth, exportHeight);
6171
6619
 
6172
- renderSitemap(container, effectiveParsed, sitemapLayout, effectivePalette, isDark, undefined, { width: exportWidth, height: exportHeight }, activeTagGroup, hiddenAttributes);
6620
+ renderSitemap(
6621
+ container,
6622
+ effectiveParsed,
6623
+ sitemapLayout,
6624
+ effectivePalette,
6625
+ isDark,
6626
+ undefined,
6627
+ { width: exportWidth, height: exportHeight },
6628
+ activeTagGroup,
6629
+ hiddenAttributes
6630
+ );
6173
6631
  return finalizeSvgExport(container, theme, effectivePalette, options);
6174
6632
  }
6175
6633
 
@@ -6187,7 +6645,15 @@ export async function renderForExport(
6187
6645
  container.style.left = '-9999px';
6188
6646
  document.body.appendChild(container);
6189
6647
 
6190
- renderKanban(container, kanbanParsed, effectivePalette, theme === 'dark', undefined, undefined, options?.tagGroup);
6648
+ renderKanban(
6649
+ container,
6650
+ kanbanParsed,
6651
+ effectivePalette,
6652
+ theme === 'dark',
6653
+ undefined,
6654
+ undefined,
6655
+ options?.tagGroup
6656
+ );
6191
6657
  return finalizeSvgExport(container, theme, effectivePalette, options);
6192
6658
  }
6193
6659
 
@@ -6207,7 +6673,15 @@ export async function renderForExport(
6207
6673
  const exportHeight = classLayout.height + PADDING * 2 + titleOffset;
6208
6674
  const container = createExportContainer(exportWidth, exportHeight);
6209
6675
 
6210
- renderClassDiagram(container, classParsed, classLayout, effectivePalette, theme === 'dark', undefined, { width: exportWidth, height: exportHeight });
6676
+ renderClassDiagram(
6677
+ container,
6678
+ classParsed,
6679
+ classLayout,
6680
+ effectivePalette,
6681
+ theme === 'dark',
6682
+ undefined,
6683
+ { width: exportWidth, height: exportHeight }
6684
+ );
6211
6685
  return finalizeSvgExport(container, theme, effectivePalette, options);
6212
6686
  }
6213
6687
 
@@ -6227,14 +6701,26 @@ export async function renderForExport(
6227
6701
  const exportHeight = erLayout.height + PADDING * 2 + titleOffset;
6228
6702
  const container = createExportContainer(exportWidth, exportHeight);
6229
6703
 
6230
- renderERDiagram(container, erParsed, erLayout, effectivePalette, theme === 'dark', undefined, { width: exportWidth, height: exportHeight }, options?.tagGroup);
6704
+ renderERDiagram(
6705
+ container,
6706
+ erParsed,
6707
+ erLayout,
6708
+ effectivePalette,
6709
+ theme === 'dark',
6710
+ undefined,
6711
+ { width: exportWidth, height: exportHeight },
6712
+ options?.tagGroup
6713
+ );
6231
6714
  return finalizeSvgExport(container, theme, effectivePalette, options);
6232
6715
  }
6233
6716
 
6234
6717
  if (detectedType === 'initiative-status') {
6235
- const { parseInitiativeStatus } = await import('./initiative-status/parser');
6236
- const { layoutInitiativeStatus } = await import('./initiative-status/layout');
6237
- const { renderInitiativeStatus } = await import('./initiative-status/renderer');
6718
+ const { parseInitiativeStatus } =
6719
+ await import('./initiative-status/parser');
6720
+ const { layoutInitiativeStatus } =
6721
+ await import('./initiative-status/layout');
6722
+ const { renderInitiativeStatus } =
6723
+ await import('./initiative-status/renderer');
6238
6724
 
6239
6725
  const effectivePalette = await resolveExportPalette(theme, palette);
6240
6726
  const isParsed = parseInitiativeStatus(content);
@@ -6247,14 +6733,27 @@ export async function renderForExport(
6247
6733
  const exportHeight = isLayout.height + PADDING * 2 + titleOffset;
6248
6734
  const container = createExportContainer(exportWidth, exportHeight);
6249
6735
 
6250
- renderInitiativeStatus(container, isParsed, isLayout, effectivePalette, theme === 'dark', { exportDims: { width: exportWidth, height: exportHeight } });
6736
+ renderInitiativeStatus(
6737
+ container,
6738
+ isParsed,
6739
+ isLayout,
6740
+ effectivePalette,
6741
+ theme === 'dark',
6742
+ { exportDims: { width: exportWidth, height: exportHeight } }
6743
+ );
6251
6744
  return finalizeSvgExport(container, theme, effectivePalette, options);
6252
6745
  }
6253
6746
 
6254
6747
  if (detectedType === 'c4') {
6255
6748
  const { parseC4 } = await import('./c4/parser');
6256
- const { layoutC4Context, layoutC4Containers, layoutC4Components, layoutC4Deployment } = await import('./c4/layout');
6257
- const { renderC4Context, renderC4Containers } = await import('./c4/renderer');
6749
+ const {
6750
+ layoutC4Context,
6751
+ layoutC4Containers,
6752
+ layoutC4Components,
6753
+ layoutC4Deployment,
6754
+ } = await import('./c4/layout');
6755
+ const { renderC4Context, renderC4Containers } =
6756
+ await import('./c4/renderer');
6258
6757
 
6259
6758
  const effectivePalette = await resolveExportPalette(theme, palette);
6260
6759
  const c4Parsed = parseC4(content, effectivePalette);
@@ -6265,13 +6764,14 @@ export async function renderForExport(
6265
6764
  const c4System = options?.c4System;
6266
6765
  const c4Container = options?.c4Container;
6267
6766
 
6268
- const c4Layout = c4Level === 'deployment'
6269
- ? layoutC4Deployment(c4Parsed)
6270
- : c4Level === 'components' && c4System && c4Container
6271
- ? layoutC4Components(c4Parsed, c4System, c4Container)
6272
- : c4Level === 'containers' && c4System
6273
- ? layoutC4Containers(c4Parsed, c4System)
6274
- : layoutC4Context(c4Parsed);
6767
+ const c4Layout =
6768
+ c4Level === 'deployment'
6769
+ ? layoutC4Deployment(c4Parsed)
6770
+ : c4Level === 'components' && c4System && c4Container
6771
+ ? layoutC4Components(c4Parsed, c4System, c4Container)
6772
+ : c4Level === 'containers' && c4System
6773
+ ? layoutC4Containers(c4Parsed, c4System)
6774
+ : layoutC4Context(c4Parsed);
6275
6775
 
6276
6776
  if (c4Layout.nodes.length === 0) return '';
6277
6777
 
@@ -6281,11 +6781,23 @@ export async function renderForExport(
6281
6781
  const exportHeight = c4Layout.height + PADDING * 2 + titleOffset;
6282
6782
  const container = createExportContainer(exportWidth, exportHeight);
6283
6783
 
6284
- const renderFn = c4Level === 'deployment' || (c4Level === 'components' && c4System && c4Container) || (c4Level === 'containers' && c4System)
6285
- ? renderC4Containers
6286
- : renderC4Context;
6287
-
6288
- renderFn(container, c4Parsed, c4Layout, effectivePalette, theme === 'dark', undefined, { width: exportWidth, height: exportHeight }, options?.tagGroup);
6784
+ const renderFn =
6785
+ c4Level === 'deployment' ||
6786
+ (c4Level === 'components' && c4System && c4Container) ||
6787
+ (c4Level === 'containers' && c4System)
6788
+ ? renderC4Containers
6789
+ : renderC4Context;
6790
+
6791
+ renderFn(
6792
+ container,
6793
+ c4Parsed,
6794
+ c4Layout,
6795
+ effectivePalette,
6796
+ theme === 'dark',
6797
+ undefined,
6798
+ { width: exportWidth, height: exportHeight },
6799
+ options?.tagGroup
6800
+ );
6289
6801
  return finalizeSvgExport(container, theme, effectivePalette, options);
6290
6802
  }
6291
6803
 
@@ -6301,7 +6813,15 @@ export async function renderForExport(
6301
6813
  const layout = layoutGraph(fcParsed);
6302
6814
  const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
6303
6815
 
6304
- renderFlowchart(container, fcParsed, layout, effectivePalette, theme === 'dark', undefined, { width: EXPORT_WIDTH, height: EXPORT_HEIGHT });
6816
+ renderFlowchart(
6817
+ container,
6818
+ fcParsed,
6819
+ layout,
6820
+ effectivePalette,
6821
+ theme === 'dark',
6822
+ undefined,
6823
+ { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
6824
+ );
6305
6825
  return finalizeSvgExport(container, theme, effectivePalette, options);
6306
6826
  }
6307
6827
 
@@ -6309,7 +6829,8 @@ export async function renderForExport(
6309
6829
  const { parseInfra } = await import('./infra/parser');
6310
6830
  const { computeInfra } = await import('./infra/compute');
6311
6831
  const { layoutInfra } = await import('./infra/layout');
6312
- const { renderInfra, computeInfraLegendGroups } = await import('./infra/renderer');
6832
+ const { renderInfra, computeInfraLegendGroups } =
6833
+ await import('./infra/renderer');
6313
6834
 
6314
6835
  const effectivePalette = await resolveExportPalette(theme, palette);
6315
6836
  const infraParsed = parseInfra(content);
@@ -6320,13 +6841,30 @@ export async function renderForExport(
6320
6841
  const activeTagGroup = options?.tagGroup ?? null;
6321
6842
 
6322
6843
  const titleOffset = infraParsed.title ? 40 : 0;
6323
- const legendGroups = computeInfraLegendGroups(infraLayout.nodes, infraParsed.tagGroups, effectivePalette);
6844
+ const legendGroups = computeInfraLegendGroups(
6845
+ infraLayout.nodes,
6846
+ infraParsed.tagGroups,
6847
+ effectivePalette
6848
+ );
6324
6849
  const legendOffset = legendGroups.length > 0 ? 28 : 0;
6325
6850
  const exportWidth = infraLayout.width;
6326
6851
  const exportHeight = infraLayout.height + titleOffset + legendOffset;
6327
6852
  const container = createExportContainer(exportWidth, exportHeight);
6328
6853
 
6329
- renderInfra(container, infraLayout, effectivePalette, theme === 'dark', infraParsed.title, infraParsed.titleLineNumber, infraParsed.tagGroups, activeTagGroup, false, null, null, true);
6854
+ renderInfra(
6855
+ container,
6856
+ infraLayout,
6857
+ effectivePalette,
6858
+ theme === 'dark',
6859
+ infraParsed.title,
6860
+ infraParsed.titleLineNumber,
6861
+ infraParsed.tagGroups,
6862
+ activeTagGroup,
6863
+ false,
6864
+ null,
6865
+ null,
6866
+ true
6867
+ );
6330
6868
  // Restore explicit pixel dimensions for resvg (renderer uses 100%/viewBox for app scaling)
6331
6869
  const infraSvg = container.querySelector('svg');
6332
6870
  if (infraSvg) {
@@ -6350,7 +6888,14 @@ export async function renderForExport(
6350
6888
  const EXPORT_H = 800;
6351
6889
  const container = createExportContainer(EXPORT_W, EXPORT_H);
6352
6890
 
6353
- renderGantt(container, resolved, effectivePalette, theme === 'dark', undefined, { width: EXPORT_W, height: EXPORT_H });
6891
+ renderGantt(
6892
+ container,
6893
+ resolved,
6894
+ effectivePalette,
6895
+ theme === 'dark',
6896
+ undefined,
6897
+ { width: EXPORT_W, height: EXPORT_H }
6898
+ );
6354
6899
  return finalizeSvgExport(container, theme, effectivePalette, options);
6355
6900
  }
6356
6901
 
@@ -6366,7 +6911,15 @@ export async function renderForExport(
6366
6911
  const layout = layoutGraph(stateParsed);
6367
6912
  const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
6368
6913
 
6369
- renderState(container, stateParsed, layout, effectivePalette, theme === 'dark', undefined, { width: EXPORT_WIDTH, height: EXPORT_HEIGHT });
6914
+ renderState(
6915
+ container,
6916
+ stateParsed,
6917
+ layout,
6918
+ effectivePalette,
6919
+ theme === 'dark',
6920
+ undefined,
6921
+ { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
6922
+ );
6370
6923
  return finalizeSvgExport(container, theme, effectivePalette, options);
6371
6924
  }
6372
6925
 
@@ -6392,30 +6945,75 @@ export async function renderForExport(
6392
6945
  const effectivePalette = await resolveExportPalette(theme, palette);
6393
6946
  const isDark = theme === 'dark';
6394
6947
  const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
6395
- const dims: D3ExportDimensions = { width: EXPORT_WIDTH, height: EXPORT_HEIGHT };
6948
+ const dims: D3ExportDimensions = {
6949
+ width: EXPORT_WIDTH,
6950
+ height: EXPORT_HEIGHT,
6951
+ };
6396
6952
 
6397
6953
  if (parsed.type === 'sequence') {
6398
6954
  const { parseSequenceDgmo } = await import('./sequence/parser');
6399
6955
  const { renderSequenceDiagram } = await import('./sequence/renderer');
6400
6956
  const seqParsed = parseSequenceDgmo(content);
6401
6957
  if (seqParsed.error || seqParsed.participants.length === 0) return '';
6402
- renderSequenceDiagram(container, seqParsed, effectivePalette, isDark, undefined, {
6403
- exportWidth: EXPORT_WIDTH,
6404
- activeTagGroup: options?.tagGroup,
6405
- });
6958
+ renderSequenceDiagram(
6959
+ container,
6960
+ seqParsed,
6961
+ effectivePalette,
6962
+ isDark,
6963
+ undefined,
6964
+ {
6965
+ exportWidth: EXPORT_WIDTH,
6966
+ activeTagGroup: options?.tagGroup,
6967
+ }
6968
+ );
6406
6969
  } else if (parsed.type === 'wordcloud') {
6407
- await renderWordCloudAsync(container, parsed, effectivePalette, isDark, dims);
6970
+ await renderWordCloudAsync(
6971
+ container,
6972
+ parsed,
6973
+ effectivePalette,
6974
+ isDark,
6975
+ dims
6976
+ );
6408
6977
  } else if (parsed.type === 'arc') {
6409
- renderArcDiagram(container, parsed, effectivePalette, isDark, undefined, dims);
6978
+ renderArcDiagram(
6979
+ container,
6980
+ parsed,
6981
+ effectivePalette,
6982
+ isDark,
6983
+ undefined,
6984
+ dims
6985
+ );
6410
6986
  } else if (parsed.type === 'timeline') {
6411
- renderTimeline(container, parsed, effectivePalette, isDark, undefined, dims,
6412
- orgExportState?.activeTagGroup ?? options?.tagGroup, orgExportState?.swimlaneTagGroup);
6987
+ renderTimeline(
6988
+ container,
6989
+ parsed,
6990
+ effectivePalette,
6991
+ isDark,
6992
+ undefined,
6993
+ dims,
6994
+ orgExportState?.activeTagGroup ?? options?.tagGroup,
6995
+ orgExportState?.swimlaneTagGroup
6996
+ );
6413
6997
  } else if (parsed.type === 'venn') {
6414
6998
  renderVenn(container, parsed, effectivePalette, isDark, undefined, dims);
6415
6999
  } else if (parsed.type === 'quadrant') {
6416
- renderQuadrant(container, parsed, effectivePalette, isDark, undefined, dims);
7000
+ renderQuadrant(
7001
+ container,
7002
+ parsed,
7003
+ effectivePalette,
7004
+ isDark,
7005
+ undefined,
7006
+ dims
7007
+ );
6417
7008
  } else {
6418
- renderSlopeChart(container, parsed, effectivePalette, isDark, undefined, dims);
7009
+ renderSlopeChart(
7010
+ container,
7011
+ parsed,
7012
+ effectivePalette,
7013
+ isDark,
7014
+ undefined,
7015
+ dims
7016
+ );
6419
7017
  }
6420
7018
 
6421
7019
  return finalizeSvgExport(container, theme, effectivePalette, options);