@diagrammo/dgmo 0.8.3 → 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 (112) 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 +153 -153
  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 +3336 -1055
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.js +3336 -1055
  18. package/dist/index.js.map +1 -1
  19. package/docs/language-reference.md +30 -29
  20. package/gallery/fixtures/arc.dgmo +18 -0
  21. package/gallery/fixtures/area.dgmo +19 -0
  22. package/gallery/fixtures/bar-stacked.dgmo +10 -0
  23. package/gallery/fixtures/bar.dgmo +10 -0
  24. package/gallery/fixtures/c4-full.dgmo +52 -0
  25. package/gallery/fixtures/c4.dgmo +17 -0
  26. package/gallery/fixtures/chord.dgmo +12 -0
  27. package/gallery/fixtures/class-basic.dgmo +14 -0
  28. package/gallery/fixtures/class-full.dgmo +43 -0
  29. package/gallery/fixtures/doughnut.dgmo +8 -0
  30. package/gallery/fixtures/flowchart-basic.dgmo +3 -0
  31. package/gallery/fixtures/flowchart-colors.dgmo +5 -0
  32. package/gallery/fixtures/flowchart-complex.dgmo +17 -0
  33. package/gallery/fixtures/flowchart-decision.dgmo +5 -0
  34. package/gallery/fixtures/flowchart-full.dgmo +13 -0
  35. package/gallery/fixtures/flowchart-groups.dgmo +10 -0
  36. package/gallery/fixtures/flowchart-loop.dgmo +7 -0
  37. package/gallery/fixtures/flowchart-nested.dgmo +7 -0
  38. package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
  39. package/gallery/fixtures/function.dgmo +8 -0
  40. package/gallery/fixtures/funnel.dgmo +7 -0
  41. package/gallery/fixtures/gantt-full.dgmo +49 -0
  42. package/gallery/fixtures/gantt.dgmo +42 -0
  43. package/gallery/fixtures/heatmap.dgmo +8 -0
  44. package/gallery/fixtures/infra-full.dgmo +78 -0
  45. package/gallery/fixtures/infra-overload.dgmo +25 -0
  46. package/gallery/fixtures/infra.dgmo +47 -0
  47. package/gallery/fixtures/initiative-status-full.dgmo +46 -0
  48. package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
  49. package/gallery/fixtures/initiative-status.dgmo +9 -0
  50. package/gallery/fixtures/line.dgmo +19 -0
  51. package/gallery/fixtures/multi-line.dgmo +11 -0
  52. package/gallery/fixtures/org-basic.dgmo +16 -0
  53. package/gallery/fixtures/org-full.dgmo +69 -0
  54. package/gallery/fixtures/org-teams.dgmo +25 -0
  55. package/gallery/fixtures/pie.dgmo +9 -0
  56. package/gallery/fixtures/polar-area.dgmo +8 -0
  57. package/gallery/fixtures/quadrant.dgmo +18 -0
  58. package/gallery/fixtures/radar.dgmo +8 -0
  59. package/gallery/fixtures/sankey.dgmo +31 -0
  60. package/gallery/fixtures/scatter.dgmo +21 -0
  61. package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
  62. package/gallery/fixtures/sequence-tags.dgmo +41 -0
  63. package/gallery/fixtures/sequence.dgmo +35 -0
  64. package/gallery/fixtures/sitemap-basic.dgmo +12 -0
  65. package/gallery/fixtures/sitemap-full.dgmo +156 -0
  66. package/gallery/fixtures/slope.dgmo +8 -0
  67. package/gallery/fixtures/spr-eras.dgmo +62 -0
  68. package/gallery/fixtures/state.dgmo +30 -0
  69. package/gallery/fixtures/timeline-intraday.dgmo +14 -0
  70. package/gallery/fixtures/timeline.dgmo +32 -0
  71. package/gallery/fixtures/venn.dgmo +10 -0
  72. package/gallery/fixtures/wordcloud.dgmo +24 -0
  73. package/package.json +51 -2
  74. package/src/c4/layout.ts +372 -90
  75. package/src/c4/parser.ts +100 -55
  76. package/src/chart.ts +91 -28
  77. package/src/class/parser.ts +41 -12
  78. package/src/cli.ts +168 -61
  79. package/src/completion.ts +378 -183
  80. package/src/d3.ts +887 -288
  81. package/src/dgmo-mermaid.ts +16 -13
  82. package/src/dgmo-router.ts +69 -23
  83. package/src/echarts.ts +646 -153
  84. package/src/editor/dgmo.grammar +69 -0
  85. package/src/editor/dgmo.grammar.d.ts +2 -0
  86. package/src/editor/dgmo.grammar.js +18 -0
  87. package/src/editor/dgmo.grammar.terms.d.ts +5 -0
  88. package/src/editor/dgmo.grammar.terms.js +35 -0
  89. package/src/editor/highlight.ts +36 -0
  90. package/src/editor/index.ts +28 -0
  91. package/src/editor/keywords.ts +220 -0
  92. package/src/editor/tokens.ts +30 -0
  93. package/src/er/parser.ts +48 -14
  94. package/src/er/renderer.ts +112 -53
  95. package/src/gantt/calculator.ts +91 -29
  96. package/src/gantt/parser.ts +197 -71
  97. package/src/gantt/renderer.ts +1120 -350
  98. package/src/graph/flowchart-parser.ts +46 -25
  99. package/src/graph/state-parser.ts +47 -17
  100. package/src/infra/parser.ts +157 -53
  101. package/src/infra/renderer.ts +723 -271
  102. package/src/initiative-status/parser.ts +138 -44
  103. package/src/kanban/parser.ts +25 -14
  104. package/src/org/layout.ts +111 -44
  105. package/src/org/parser.ts +69 -22
  106. package/src/palettes/index.ts +3 -2
  107. package/src/sequence/parser.ts +193 -61
  108. package/src/sitemap/parser.ts +65 -29
  109. package/src/utils/arrows.ts +2 -22
  110. package/src/utils/duration.ts +39 -21
  111. package/src/utils/legend-constants.ts +0 -2
  112. package/src/utils/parsing.ts +75 -31
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, parseFirstLine, parsePipeMetadata, MULTIPLE_PIPE_ERROR } 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,
@@ -468,7 +510,15 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
468
510
  let inTimelineMarkerBlock = false;
469
511
  let timelineMarkerBlockIndent = 0;
470
512
  const timelineAliasMap = new Map<string, string>();
471
- 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
+ ]);
472
522
  let firstLineParsed = false;
473
523
 
474
524
  for (let i = 0; i < lines.length; i++) {
@@ -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();
@@ -613,7 +682,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
613
682
  } else {
614
683
  if (line.startsWith('//')) continue;
615
684
  const eraEntryMatch = line.match(
616
- /^(\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*\(([^)]+)\))?\s*$/
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*$/
617
686
  );
618
687
  if (eraEntryMatch) {
619
688
  const colorAnnotation = eraEntryMatch[4]?.trim() || null;
@@ -678,7 +747,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
678
747
 
679
748
  // Timeline era lines (inline): era YYYY->YYYY Label (color)
680
749
  const eraMatch = line.match(
681
- /^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*\(([^)]+)\))?\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*$/
682
751
  );
683
752
  if (eraMatch) {
684
753
  const colorAnnotation = eraMatch[4]?.trim() || null;
@@ -696,7 +765,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
696
765
 
697
766
  // Timeline marker lines (inline): marker YYYY Label (color)
698
767
  const markerMatch = line.match(
699
- /^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*$/
700
769
  );
701
770
  if (markerMatch) {
702
771
  const colorAnnotation = markerMatch[3]?.trim() || null;
@@ -719,7 +788,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
719
788
  // Supports uncertain end with ? suffix (e.g., ->3m?: fades out the last 20%)
720
789
  // Accepts both -> (hyphen) and –> (en-dash U+2013)
721
790
  const durationMatch = line.match(
722
- /^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\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+(.+)$/
723
792
  );
724
793
  if (durationMatch) {
725
794
  const startDate = durationMatch[1];
@@ -728,9 +797,14 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
728
797
  const unit = durationMatch[3] as 'd' | 'w' | 'm' | 'y' | 'h' | 'min';
729
798
  const endDate = addDurationToDate(startDate, amount, unit);
730
799
  const segments = durationMatch[5].split('|');
731
- const metadata = segments.length > 1
732
- ? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber, MULTIPLE_PIPE_ERROR))
733
- : {};
800
+ const metadata =
801
+ segments.length > 1
802
+ ? parsePipeMetadata(
803
+ ['', ...segments.slice(1)],
804
+ timelineAliasMap,
805
+ () => warn(lineNumber, MULTIPLE_PIPE_ERROR)
806
+ )
807
+ : {};
734
808
  result.timelineEvents.push({
735
809
  date: startDate,
736
810
  endDate,
@@ -747,13 +821,18 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
747
821
  // Also supports YYYY-MM-DD HH:MM in both start and end dates
748
822
  // Accepts both -> (hyphen) and –> (en-dash U+2013)
749
823
  const rangeMatch = line.match(
750
- /^(\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+)(.+)$/
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+(.+)$/
751
825
  );
752
826
  if (rangeMatch) {
753
827
  const segments = rangeMatch[4].split('|');
754
- const metadata = segments.length > 1
755
- ? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber, MULTIPLE_PIPE_ERROR))
756
- : {};
828
+ const metadata =
829
+ segments.length > 1
830
+ ? parsePipeMetadata(
831
+ ['', ...segments.slice(1)],
832
+ timelineAliasMap,
833
+ () => warn(lineNumber, MULTIPLE_PIPE_ERROR)
834
+ )
835
+ : {};
757
836
  result.timelineEvents.push({
758
837
  date: rangeMatch[1],
759
838
  endDate: rangeMatch[2],
@@ -766,15 +845,18 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
766
845
  continue;
767
846
  }
768
847
 
769
- // Point event: 1718 description (or legacy 1718: description)
770
- const pointMatch = line.match(
771
- /^(\d{4}(?:-\d{2})?(?:-\d{2})?)(?:\s*:\s*|\s+)(.+)$/
772
- );
848
+ // Point event: 1718 description
849
+ const pointMatch = line.match(/^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s+(.+)$/);
773
850
  if (pointMatch) {
774
851
  const segments = pointMatch[2].split('|');
775
- const metadata = segments.length > 1
776
- ? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber, MULTIPLE_PIPE_ERROR))
777
- : {};
852
+ const metadata =
853
+ segments.length > 1
854
+ ? parsePipeMetadata(
855
+ ['', ...segments.slice(1)],
856
+ timelineAliasMap,
857
+ () => warn(lineNumber, MULTIPLE_PIPE_ERROR)
858
+ )
859
+ : {};
778
860
  result.timelineEvents.push({
779
861
  date: pointMatch[1],
780
862
  endDate: null,
@@ -799,40 +881,37 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
799
881
  if (s.alias) knownSetRefs.add(s.alias.toLowerCase());
800
882
  }
801
883
 
802
- const segments = line.split('+').map((s) => s.trim()).filter(Boolean);
884
+ const segments = line
885
+ .split('+')
886
+ .map((s) => s.trim())
887
+ .filter(Boolean);
803
888
  if (segments.length >= 2) {
804
889
  // All segments except the last are pure set references
805
890
  const rawSets = segments.slice(0, -1);
806
891
  const lastSeg = segments[segments.length - 1];
807
892
 
808
893
  // For the last segment, extract set reference and optional label.
809
- // Support deprecated colon: "SetRef: Label"
810
- const colonIdx = lastSeg.indexOf(':');
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
+ }
811
905
  let lastSetRef: string;
812
906
  let label: string | null;
813
- if (colonIdx >= 0) {
814
- lastSetRef = lastSeg.substring(0, colonIdx).trim();
815
- label = lastSeg.substring(colonIdx + 1).trim() || null;
907
+ if (matchLen > 0) {
908
+ lastSetRef = words.slice(0, matchLen).join(' ');
909
+ label =
910
+ words.length > matchLen ? words.slice(matchLen).join(' ') : null;
816
911
  } else {
817
- // No colonfind where the set reference ends and label begins.
818
- // Try progressively shorter prefixes against known set names/aliases.
819
- const words = lastSeg.split(/\s+/);
820
- let matchLen = 0;
821
- for (let w = words.length; w >= 1; w--) {
822
- const candidate = words.slice(0, w).join(' ');
823
- if (knownSetRefs.has(candidate.toLowerCase())) {
824
- matchLen = w;
825
- break;
826
- }
827
- }
828
- if (matchLen > 0) {
829
- lastSetRef = words.slice(0, matchLen).join(' ');
830
- label = words.length > matchLen ? words.slice(matchLen).join(' ') : null;
831
- } else {
832
- // No known set matched — assume first word is the set ref, rest is label
833
- lastSetRef = words[0];
834
- label = words.length > 1 ? words.slice(1).join(' ') : null;
835
- }
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;
836
915
  }
837
916
  rawSets.push(lastSetRef);
838
917
  result.vennOverlaps.push({ sets: rawSets, label, lineNumber });
@@ -841,7 +920,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
841
920
  }
842
921
 
843
922
  // Set declaration: "Name(color) alias x" / "Name alias x" / "Name(color)" / "Name"
844
- const setDeclMatch = line.match(/^([^(:]+?)(?:\(([^)]+)\))?(?:\s+alias\s+(\S+))?\s*$/i);
923
+ const setDeclMatch = line.match(
924
+ /^([^(:]+?)(?:\(([^)]+)\))?(?:\s+alias\s+(\S+))?\s*$/i
925
+ );
845
926
  if (setDeclMatch) {
846
927
  const name = setDeclMatch[1].trim();
847
928
  const colorName = setDeclMatch[2]?.trim() ?? null;
@@ -849,11 +930,17 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
849
930
  if (colorName) {
850
931
  const resolved = resolveColor(colorName, palette);
851
932
  if (resolved === null) {
852
- 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
+ );
853
937
  } else if (resolved.startsWith('#')) {
854
938
  color = resolved;
855
939
  } else {
856
- 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
+ );
857
944
  }
858
945
  }
859
946
  const alias = setDeclMatch[3]?.trim() ?? null;
@@ -864,8 +951,8 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
864
951
 
865
952
  // Quadrant-specific parsing
866
953
  if (result.type === 'quadrant') {
867
- // x-axis: Low, High — or indented multi-line
868
- 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);
869
956
  if (xAxisMatch) {
870
957
  const val = xAxisMatch[1].trim();
871
958
  let parts: string[];
@@ -883,8 +970,8 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
883
970
  continue;
884
971
  }
885
972
 
886
- // y-axis: Low, High — or indented multi-line
887
- 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);
888
975
  if (yAxisMatch) {
889
976
  const val = yAxisMatch[1].trim();
890
977
  let parts: string[];
@@ -902,9 +989,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
902
989
  continue;
903
990
  }
904
991
 
905
- // Quadrant position labels: top-right: Label (color)
992
+ // Quadrant position labels: top-right Label (color)
906
993
  const quadrantLabelRe =
907
- /^(top-right|top-left|bottom-left|bottom-right)\s*:\s*(.+)/i;
994
+ /^(top-right|top-left|bottom-left|bottom-right)\s+(.+)/i;
908
995
  const quadrantMatch = line.match(quadrantLabelRe);
909
996
  if (quadrantMatch) {
910
997
  const position = quadrantMatch[1].toLowerCase();
@@ -926,9 +1013,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
926
1013
  continue;
927
1014
  }
928
1015
 
929
- // Data points: Label: x, y
1016
+ // Data points: Label x, y
930
1017
  const pointMatch = line.match(
931
- /^(.+?):\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*$/
932
1019
  );
933
1020
  if (pointMatch) {
934
1021
  const label = pointMatch[1].trim();
@@ -957,7 +1044,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
957
1044
  const firstToken = line.substring(0, spaceIdx).toLowerCase();
958
1045
  const restValue = line.substring(spaceIdx + 1).trim();
959
1046
 
960
- if (firstToken === 'chart' && VALID_D3_TYPES.has(restValue.toLowerCase())) {
1047
+ if (
1048
+ firstToken === 'chart' &&
1049
+ VALID_D3_TYPES.has(restValue.toLowerCase())
1050
+ ) {
961
1051
  result.type = restValue.toLowerCase() as ParsedVisualization['type'];
962
1052
  continue;
963
1053
  }
@@ -1114,9 +1204,14 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1114
1204
  } else if (colonIndex === -1) {
1115
1205
  // Try "word weight" or "multi-word-label weight" space-separated format
1116
1206
  const lastSpace = line.lastIndexOf(' ');
1117
- const maybeWeight = lastSpace >= 0 ? parseFloat(line.substring(lastSpace + 1)) : NaN;
1207
+ const maybeWeight =
1208
+ lastSpace >= 0 ? parseFloat(line.substring(lastSpace + 1)) : NaN;
1118
1209
  if (lastSpace >= 0 && !isNaN(maybeWeight) && maybeWeight > 0) {
1119
- 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
+ });
1120
1215
  } else {
1121
1216
  freeformLines.push(line);
1122
1217
  }
@@ -1143,13 +1238,23 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1143
1238
  continue;
1144
1239
  }
1145
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
+ }
1146
1247
  }
1147
1248
 
1148
1249
  // Validation
1149
1250
  if (!result.type) {
1150
1251
  const validD3Types = [...VALID_D3_TYPES];
1151
- const firstNonEmpty = lines.find(l => l.trim() && !l.trim().startsWith('//'))?.trim() ?? '';
1152
- 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
+ );
1153
1258
  let msg = `Unsupported chart type: "${firstNonEmpty.split(/\s/)[0]}". Supported types: ${validD3Types.join(', ')}`;
1154
1259
  if (hint) msg += `. ${hint}`;
1155
1260
  return fail(1, msg);
@@ -1166,7 +1271,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1166
1271
  result.words = tokenizeFreeformText(freeformLines.join(' '));
1167
1272
  }
1168
1273
  if (result.words.length === 0) {
1169
- warn(1, 'No words found. Add words as "word weight" (space-separated), 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
+ );
1170
1278
  }
1171
1279
  // Apply max word limit (words are already sorted by weight desc for freeform)
1172
1280
  if (
@@ -1183,12 +1291,18 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1183
1291
 
1184
1292
  if (result.type === 'arc') {
1185
1293
  if (result.links.length === 0) {
1186
- 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
+ );
1187
1298
  }
1188
1299
  // Validate arc ordering vs groups
1189
1300
  if (result.arcNodeGroups.length > 0) {
1190
1301
  if (result.arcOrder === 'name' || result.arcOrder === 'degree') {
1191
- 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
+ );
1192
1306
  result.arcOrder = 'group';
1193
1307
  }
1194
1308
  if (result.arcOrder === 'appearance') {
@@ -1200,15 +1314,19 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1200
1314
 
1201
1315
  if (result.type === 'timeline') {
1202
1316
  if (result.timelineEvents.length === 0) {
1203
- 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
+ );
1204
1321
  }
1205
1322
  // Validate tag values and inject defaults
1206
1323
  if (result.timelineTagGroups.length > 0) {
1207
1324
  validateTagValues(
1208
1325
  result.timelineEvents,
1209
1326
  result.timelineTagGroups,
1210
- (line, msg) => result.diagnostics.push(makeDgmoError(line, msg, 'warning')),
1211
- suggest,
1327
+ (line, msg) =>
1328
+ result.diagnostics.push(makeDgmoError(line, msg, 'warning')),
1329
+ suggest
1212
1330
  );
1213
1331
  for (const group of result.timelineTagGroups) {
1214
1332
  if (!group.defaultValue) continue;
@@ -1226,7 +1344,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1226
1344
 
1227
1345
  if (result.type === 'venn') {
1228
1346
  if (result.vennSets.length < 2) {
1229
- 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
+ );
1230
1351
  }
1231
1352
  if (result.vennSets.length > 3) {
1232
1353
  return fail(1, 'Venn diagrams support 2–3 sets');
@@ -1240,7 +1361,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1240
1361
  if (s.alias) aliasLower.set(s.alias.toLowerCase(), s.name);
1241
1362
  }
1242
1363
  const resolveSetRef = (ref: string): string | null =>
1243
- setNameLower.get(ref.toLowerCase()) ?? aliasLower.get(ref.toLowerCase()) ?? null;
1364
+ setNameLower.get(ref.toLowerCase()) ??
1365
+ aliasLower.get(ref.toLowerCase()) ??
1366
+ null;
1244
1367
 
1245
1368
  // Resolve intersection set references; drop invalid ones with a diagnostic
1246
1369
  const validOverlaps: VennOverlap[] = [];
@@ -1250,8 +1373,16 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1250
1373
  for (const ref of ov.sets) {
1251
1374
  const resolved = resolveSetRef(ref);
1252
1375
  if (!resolved) {
1253
- result.diagnostics.push(makeDgmoError(ov.lineNumber, `Intersection references unknown set or alias "${ref}"`));
1254
- 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
+ );
1255
1386
  valid = false;
1256
1387
  break;
1257
1388
  }
@@ -1265,24 +1396,36 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1265
1396
 
1266
1397
  if (result.type === 'quadrant') {
1267
1398
  if (result.quadrantPoints.length === 0) {
1268
- 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
+ );
1269
1403
  }
1270
1404
  return result;
1271
1405
  }
1272
1406
 
1273
1407
  // Slope chart validation
1274
1408
  if (result.periods.length < 2) {
1275
- 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
+ );
1276
1413
  }
1277
1414
 
1278
1415
  if (result.data.length === 0) {
1279
- 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
+ );
1280
1420
  }
1281
1421
 
1282
1422
  // Validate value counts match period count — warn and skip mismatched items
1283
1423
  for (const item of result.data) {
1284
1424
  if (item.values.length !== result.periods.length) {
1285
- 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
+ );
1286
1429
  }
1287
1430
  }
1288
1431
  result.data = result.data.filter(
@@ -1523,7 +1666,14 @@ export function renderSlopeChart(
1523
1666
  const tooltip = createTooltip(container, palette, isDark);
1524
1667
 
1525
1668
  // Title
1526
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
1669
+ renderChartTitle(
1670
+ svg,
1671
+ title,
1672
+ parsed.titleLineNumber,
1673
+ width,
1674
+ textColor,
1675
+ onClickItem
1676
+ );
1527
1677
 
1528
1678
  // Period column headers
1529
1679
  for (const period of periods) {
@@ -1592,13 +1742,23 @@ export function renderSlopeChart(
1592
1742
  wrappedLines = lines;
1593
1743
  }
1594
1744
  const lineHeight = SLOPE_LABEL_FONT_SIZE * 1.2;
1595
- const labelHeight = labelLineCount === 1
1596
- ? SLOPE_LABEL_FONT_SIZE
1597
- : labelLineCount * lineHeight;
1745
+ const labelHeight =
1746
+ labelLineCount === 1
1747
+ ? SLOPE_LABEL_FONT_SIZE
1748
+ : labelLineCount * lineHeight;
1598
1749
 
1599
1750
  return {
1600
- item, idx, color, firstVal, lastVal, tipHtml,
1601
- 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,
1602
1762
  };
1603
1763
  });
1604
1764
 
@@ -1610,7 +1770,10 @@ export function renderSlopeChart(
1610
1770
  naturalY: yScale(item.values[pi]),
1611
1771
  height: leftLabelHeight,
1612
1772
  }));
1613
- leftLabelCollisions.set(pi, resolveVerticalCollisions(entries, 4, innerHeight));
1773
+ leftLabelCollisions.set(
1774
+ pi,
1775
+ resolveVerticalCollisions(entries, 4, innerHeight)
1776
+ );
1614
1777
  }
1615
1778
 
1616
1779
  // --- Resolve right-side label collisions ---
@@ -1618,7 +1781,11 @@ export function renderSlopeChart(
1618
1781
  naturalY: yScale(si.lastVal),
1619
1782
  height: Math.max(si.labelHeight, SLOPE_LABEL_FONT_SIZE * 1.4),
1620
1783
  }));
1621
- const rightAdjustedY = resolveVerticalCollisions(rightEntries, 4, innerHeight);
1784
+ const rightAdjustedY = resolveVerticalCollisions(
1785
+ rightEntries,
1786
+ 4,
1787
+ innerHeight
1788
+ );
1622
1789
 
1623
1790
  // Render each data series
1624
1791
  data.forEach((item, idx) => {
@@ -1632,7 +1799,8 @@ export function renderSlopeChart(
1632
1799
  .attr('data-line-number', String(item.lineNumber));
1633
1800
 
1634
1801
  // Line
1635
- seriesG.append('path')
1802
+ seriesG
1803
+ .append('path')
1636
1804
  .datum(item.values)
1637
1805
  .attr('fill', 'none')
1638
1806
  .attr('stroke', color)
@@ -1640,7 +1808,8 @@ export function renderSlopeChart(
1640
1808
  .attr('d', lineGen);
1641
1809
 
1642
1810
  // Invisible wider path for easier hover targeting
1643
- seriesG.append('path')
1811
+ seriesG
1812
+ .append('path')
1644
1813
  .datum(item.values)
1645
1814
  .attr('fill', 'none')
1646
1815
  .attr('stroke', 'transparent')
@@ -1664,7 +1833,8 @@ export function renderSlopeChart(
1664
1833
  const y = yScale(val);
1665
1834
 
1666
1835
  // Point circle
1667
- seriesG.append('circle')
1836
+ seriesG
1837
+ .append('circle')
1668
1838
  .attr('cx', x)
1669
1839
  .attr('cy', y)
1670
1840
  .attr('r', 4)
@@ -1688,7 +1858,8 @@ export function renderSlopeChart(
1688
1858
  const isLast = i === periods.length - 1;
1689
1859
  if (!isLast) {
1690
1860
  const adjustedY = leftLabelCollisions.get(i)![idx];
1691
- seriesG.append('text')
1861
+ seriesG
1862
+ .append('text')
1692
1863
  .attr('x', isFirst ? x - 10 : x)
1693
1864
  .attr('y', adjustedY)
1694
1865
  .attr('dy', '0.35em')
@@ -1920,7 +2091,14 @@ export function renderArcDiagram(
1920
2091
  .attr('transform', `translate(${margin.left},${margin.top})`);
1921
2092
 
1922
2093
  // Title
1923
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
2094
+ renderChartTitle(
2095
+ svg,
2096
+ title,
2097
+ parsed.titleLineNumber,
2098
+ width,
2099
+ textColor,
2100
+ onClickItem
2101
+ );
1924
2102
 
1925
2103
  // Build adjacency map for hover interactions
1926
2104
  const neighbors = new Map<string, Set<string>>();
@@ -2097,13 +2275,18 @@ export function renderArcDiagram(
2097
2275
  const y = yScale(node)!;
2098
2276
  const nodeColor = nodeColorMap.get(node) ?? textColor;
2099
2277
  // Find the first link involving this node (for line number and click target)
2100
- 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
+ );
2101
2281
 
2102
2282
  const nodeG = g
2103
2283
  .append('g')
2104
2284
  .attr('class', 'arc-node')
2105
2285
  .attr('data-node', node)
2106
- .attr('data-line-number', nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null)
2286
+ .attr(
2287
+ 'data-line-number',
2288
+ nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null
2289
+ )
2107
2290
  .style('cursor', 'pointer')
2108
2291
  .on('mouseenter', () => handleMouseEnter(node))
2109
2292
  .on('mouseleave', handleMouseLeave)
@@ -2232,13 +2415,18 @@ export function renderArcDiagram(
2232
2415
  const x = xScale(node)!;
2233
2416
  const nodeColor = nodeColorMap.get(node) ?? textColor;
2234
2417
  // Find the first link involving this node (for line number and click target)
2235
- 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
+ );
2236
2421
 
2237
2422
  const nodeG = g
2238
2423
  .append('g')
2239
2424
  .attr('class', 'arc-node')
2240
2425
  .attr('data-node', node)
2241
- .attr('data-line-number', nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null)
2426
+ .attr(
2427
+ 'data-line-number',
2428
+ nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null
2429
+ )
2242
2430
  .style('cursor', 'pointer')
2243
2431
  .on('mouseenter', () => handleMouseEnter(node))
2244
2432
  .on('mouseleave', handleMouseLeave)
@@ -2633,7 +2821,11 @@ export function computeTimeTicks(
2633
2821
  // Iterate from the start hour boundary
2634
2822
  const startDate = fractionalYearToDate(domainMin);
2635
2823
  // Round down to nearest step boundary
2636
- 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
+ );
2637
2829
 
2638
2830
  while (true) {
2639
2831
  const val = dateToFractionalYear(startDate);
@@ -2659,7 +2851,12 @@ export function computeTimeTicks(
2659
2851
 
2660
2852
  const startDate = fractionalYearToDate(domainMin);
2661
2853
  // Round down to nearest step boundary
2662
- 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
+ );
2663
2860
 
2664
2861
  while (true) {
2665
2862
  const val = dateToFractionalYear(startDate);
@@ -3072,7 +3269,10 @@ export function renderTimeline(
3072
3269
  exportDims?: D3ExportDimensions,
3073
3270
  activeTagGroup?: string | null,
3074
3271
  swimlaneTagGroup?: string | null,
3075
- onTagStateChange?: (activeTagGroup: string | null, swimlaneTagGroup: string | null) => void,
3272
+ onTagStateChange?: (
3273
+ activeTagGroup: string | null,
3274
+ swimlaneTagGroup: string | null
3275
+ ) => void,
3076
3276
  viewMode?: boolean
3077
3277
  ): void {
3078
3278
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
@@ -3091,7 +3291,11 @@ export function renderTimeline(
3091
3291
  if (timelineEvents.length === 0) return;
3092
3292
 
3093
3293
  // When sort: tag is set and no explicit swimlane param, use the default
3094
- if (swimlaneTagGroup == null && timelineSort === 'tag' && parsed.timelineDefaultSwimlaneTG) {
3294
+ if (
3295
+ swimlaneTagGroup == null &&
3296
+ timelineSort === 'tag' &&
3297
+ parsed.timelineDefaultSwimlaneTG
3298
+ ) {
3095
3299
  swimlaneTagGroup = parsed.timelineDefaultSwimlaneTG;
3096
3300
  }
3097
3301
 
@@ -3143,12 +3347,8 @@ export function renderTimeline(
3143
3347
 
3144
3348
  // Order lanes by earliest event date
3145
3349
  const laneEntries = [...buckets.entries()].sort((a, b) => {
3146
- const aMin = Math.min(
3147
- ...a[1].map((e) => parseTimelineDate(e.date))
3148
- );
3149
- const bMin = Math.min(
3150
- ...b[1].map((e) => parseTimelineDate(e.date))
3151
- );
3350
+ const aMin = Math.min(...a[1].map((e) => parseTimelineDate(e.date)));
3351
+ const bMin = Math.min(...b[1].map((e) => parseTimelineDate(e.date)));
3152
3352
  return aMin - bMin;
3153
3353
  });
3154
3354
 
@@ -3170,7 +3370,11 @@ export function renderTimeline(
3170
3370
  function eventColor(ev: TimelineEvent): string {
3171
3371
  // Tag color takes priority when a tag group is active
3172
3372
  if (effectiveColorTG) {
3173
- const tagColor = resolveTagColor(ev.metadata, parsed.timelineTagGroups, effectiveColorTG);
3373
+ const tagColor = resolveTagColor(
3374
+ ev.metadata,
3375
+ parsed.timelineTagGroups,
3376
+ effectiveColorTG
3377
+ );
3174
3378
  if (tagColor) return tagColor;
3175
3379
  }
3176
3380
  if (ev.group && groupColorMap.has(ev.group)) {
@@ -3281,16 +3485,23 @@ export function renderTimeline(
3281
3485
  el.attr('opacity', val === tagValue ? 1 : FADE_OPACITY);
3282
3486
  });
3283
3487
  g.selectAll<SVGGElement, unknown>('.tl-legend-item, .tl-lane-header').attr(
3284
- 'opacity', FADE_OPACITY
3488
+ 'opacity',
3489
+ FADE_OPACITY
3490
+ );
3491
+ g.selectAll<SVGGElement, unknown>('.tl-marker').attr(
3492
+ 'opacity',
3493
+ FADE_OPACITY
3285
3494
  );
3286
- g.selectAll<SVGGElement, unknown>('.tl-marker').attr('opacity', FADE_OPACITY);
3287
3495
  // Fade legend entry dots/labels that don't match (keep group pill visible)
3288
3496
  g.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry').each(function () {
3289
3497
  const el = d3Selection.select(this);
3290
3498
  const entryValue = el.attr('data-legend-entry');
3291
3499
  if (entryValue === '__group__') return; // keep group pill at full opacity
3292
3500
  const entryGroup = el.attr('data-tag-group');
3293
- el.attr('opacity', entryGroup === tagKey && entryValue === tagValue ? 1 : FADE_OPACITY);
3501
+ el.attr(
3502
+ 'opacity',
3503
+ entryGroup === tagKey && entryValue === tagValue ? 1 : FADE_OPACITY
3504
+ );
3294
3505
  });
3295
3506
  }
3296
3507
 
@@ -3311,7 +3522,8 @@ export function renderTimeline(
3311
3522
  // VERTICAL orientation (time flows top→bottom)
3312
3523
  // ================================================================
3313
3524
  if (isVertical) {
3314
- const useGroupedVertical = tagLanes != null ||
3525
+ const useGroupedVertical =
3526
+ tagLanes != null ||
3315
3527
  (timelineSort === 'group' && timelineGroups.length > 0);
3316
3528
  if (useGroupedVertical) {
3317
3529
  // === GROUPED: one column/lane per group, vertical ===
@@ -3370,7 +3582,14 @@ export function renderTimeline(
3370
3582
  .append('g')
3371
3583
  .attr('transform', `translate(${margin.left},${margin.top})`);
3372
3584
 
3373
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
3585
+ renderChartTitle(
3586
+ svg,
3587
+ title,
3588
+ parsed.titleLineNumber,
3589
+ width,
3590
+ textColor,
3591
+ onClickItem
3592
+ );
3374
3593
 
3375
3594
  renderEras(
3376
3595
  g,
@@ -3620,7 +3839,14 @@ export function renderTimeline(
3620
3839
  .append('g')
3621
3840
  .attr('transform', `translate(${margin.left},${margin.top})`);
3622
3841
 
3623
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
3842
+ renderChartTitle(
3843
+ svg,
3844
+ title,
3845
+ parsed.titleLineNumber,
3846
+ width,
3847
+ textColor,
3848
+ onClickItem
3849
+ );
3624
3850
 
3625
3851
  renderEras(
3626
3852
  g,
@@ -3746,8 +3972,7 @@ export function renderTimeline(
3746
3972
  if (ev.uncertain) {
3747
3973
  const gradientId = `uncertain-v-${ev.lineNumber}`;
3748
3974
  const strokeGradientId = `uncertain-v-s-${ev.lineNumber}`;
3749
- const defs =
3750
- svg.select('defs').node() || svg.append('defs').node();
3975
+ const defs = svg.select('defs').node() || svg.append('defs').node();
3751
3976
  const defsEl = d3Selection.select(defs as Element);
3752
3977
  defsEl
3753
3978
  .append('linearGradient')
@@ -3861,8 +4086,8 @@ export function renderTimeline(
3861
4086
  const BAR_H = 22; // range bar thickness (tall enough for text inside)
3862
4087
  const GROUP_GAP = 12; // vertical gap between group swim-lanes
3863
4088
 
3864
- const useGroupedHorizontal = tagLanes != null ||
3865
- (timelineSort === 'group' && timelineGroups.length > 0);
4089
+ const useGroupedHorizontal =
4090
+ tagLanes != null || (timelineSort === 'group' && timelineGroups.length > 0);
3866
4091
  if (useGroupedHorizontal) {
3867
4092
  // === GROUPED: swim-lanes stacked vertically, events on own rows ===
3868
4093
  let lanes: Lane[];
@@ -3895,7 +4120,11 @@ export function renderTimeline(
3895
4120
  // Group-sorted doesn't need legend space (group names shown on left)
3896
4121
  const baseTopMargin = title ? 50 : 20;
3897
4122
  const margin = {
3898
- top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
4123
+ top:
4124
+ baseTopMargin +
4125
+ (timelineScale ? 40 : 0) +
4126
+ markerMargin +
4127
+ tagLegendReserve,
3899
4128
  right: 40,
3900
4129
  bottom: 40 + scaleMargin,
3901
4130
  left: dynamicLeftMargin,
@@ -3921,7 +4150,14 @@ export function renderTimeline(
3921
4150
  .append('g')
3922
4151
  .attr('transform', `translate(${margin.left},${margin.top})`);
3923
4152
 
3924
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
4153
+ renderChartTitle(
4154
+ svg,
4155
+ title,
4156
+ parsed.titleLineNumber,
4157
+ width,
4158
+ textColor,
4159
+ onClickItem
4160
+ );
3925
4161
 
3926
4162
  renderEras(
3927
4163
  g,
@@ -4222,7 +4458,14 @@ export function renderTimeline(
4222
4458
  .append('g')
4223
4459
  .attr('transform', `translate(${margin.left},${margin.top})`);
4224
4460
 
4225
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
4461
+ renderChartTitle(
4462
+ svg,
4463
+ title,
4464
+ parsed.titleLineNumber,
4465
+ width,
4466
+ textColor,
4467
+ onClickItem
4468
+ );
4226
4469
 
4227
4470
  renderEras(
4228
4471
  g,
@@ -4500,13 +4743,17 @@ export function renderTimeline(
4500
4743
  expandedWidth: number;
4501
4744
  };
4502
4745
  const legendGroups: LegendGroup[] = parsed.timelineTagGroups.map((g) => {
4503
- 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;
4504
4748
  // Expanded: pill + icon (unless viewMode) + entries
4505
4749
  const iconSpace = viewMode ? 8 : LG_ICON_W + 4;
4506
4750
  let entryX = LG_CAPSULE_PAD + pillW + iconSpace;
4507
4751
  for (const entry of g.entries) {
4508
4752
  const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
4509
- 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;
4510
4757
  }
4511
4758
  return {
4512
4759
  group: g,
@@ -4526,7 +4773,8 @@ export function renderTimeline(
4526
4773
  y: number,
4527
4774
  isSwimActive: boolean
4528
4775
  ) {
4529
- const iconG = parent.append('g')
4776
+ const iconG = parent
4777
+ .append('g')
4530
4778
  .attr('class', 'tl-swimlane-icon')
4531
4779
  .attr('transform', `translate(${x}, ${y})`)
4532
4780
  .style('cursor', 'pointer');
@@ -4539,7 +4787,8 @@ export function renderTimeline(
4539
4787
  { y: 8, w: 6 },
4540
4788
  ];
4541
4789
  for (const bar of bars) {
4542
- iconG.append('rect')
4790
+ iconG
4791
+ .append('rect')
4543
4792
  .attr('x', 0)
4544
4793
  .attr('y', bar.y)
4545
4794
  .attr('width', bar.w)
@@ -4554,8 +4803,16 @@ export function renderTimeline(
4554
4803
  /** Full re-render with updated swimlane state */
4555
4804
  function relayout() {
4556
4805
  renderTimeline(
4557
- container, parsed, palette, isDark, onClickItem, exportDims,
4558
- currentActiveGroup, currentSwimlaneGroup, onTagStateChange, viewMode
4806
+ container,
4807
+ parsed,
4808
+ palette,
4809
+ isDark,
4810
+ onClickItem,
4811
+ exportDims,
4812
+ currentActiveGroup,
4813
+ currentSwimlaneGroup,
4814
+ onTagStateChange,
4815
+ viewMode
4559
4816
  );
4560
4817
  }
4561
4818
 
@@ -4565,7 +4822,8 @@ export function renderTimeline(
4565
4822
  mainSvg.selectAll('.tl-tag-legend-container').remove();
4566
4823
 
4567
4824
  // Effective color source: explicit color group > swimlane group
4568
- const effectiveColorKey = (currentActiveGroup ?? currentSwimlaneGroup)?.toLowerCase() ?? null;
4825
+ const effectiveColorKey =
4826
+ (currentActiveGroup ?? currentSwimlaneGroup)?.toLowerCase() ?? null;
4569
4827
 
4570
4828
  // In view mode, only show the color-driving tag group (expanded, non-interactive).
4571
4829
  // Skip the swimlane group if it's separate from the color group (lane headers already label it).
@@ -4580,32 +4838,43 @@ export function renderTimeline(
4580
4838
  if (visibleGroups.length === 0) return;
4581
4839
 
4582
4840
  // Compute total width and center horizontally in SVG
4583
- const totalW = visibleGroups.reduce((s, lg) => {
4584
- const isActive = viewMode ||
4585
- (currentActiveGroup != null &&
4586
- lg.group.name.toLowerCase() === currentActiveGroup.toLowerCase());
4587
- return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
4588
- }, 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;
4589
4851
 
4590
4852
  let cx = (width - totalW) / 2;
4591
4853
 
4592
4854
  // Legend container for data-legend-active attribute
4593
- const legendContainer = mainSvg.append('g')
4855
+ const legendContainer = mainSvg
4856
+ .append('g')
4594
4857
  .attr('class', 'tl-tag-legend-container');
4595
4858
  if (currentActiveGroup) {
4596
- legendContainer.attr('data-legend-active', currentActiveGroup.toLowerCase());
4859
+ legendContainer.attr(
4860
+ 'data-legend-active',
4861
+ currentActiveGroup.toLowerCase()
4862
+ );
4597
4863
  }
4598
4864
 
4599
4865
  for (const lg of visibleGroups) {
4600
4866
  const groupKey = lg.group.name.toLowerCase();
4601
- const isActive = viewMode ||
4867
+ const isActive =
4868
+ viewMode ||
4602
4869
  (currentActiveGroup != null &&
4603
4870
  currentActiveGroup.toLowerCase() === groupKey);
4604
- const isSwimActive = currentSwimlaneGroup != null &&
4871
+ const isSwimActive =
4872
+ currentSwimlaneGroup != null &&
4605
4873
  currentSwimlaneGroup.toLowerCase() === groupKey;
4606
4874
 
4607
4875
  const pillLabel = lg.group.name;
4608
- const pillWidth = measureLegendText(pillLabel, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
4876
+ const pillWidth =
4877
+ measureLegendText(pillLabel, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
4609
4878
 
4610
4879
  const gEl = legendContainer
4611
4880
  .append('g')
@@ -4616,19 +4885,19 @@ export function renderTimeline(
4616
4885
  .attr('data-legend-entry', '__group__');
4617
4886
 
4618
4887
  if (!viewMode) {
4619
- gEl
4620
- .style('cursor', 'pointer')
4621
- .on('click', () => {
4622
- currentActiveGroup = currentActiveGroup === groupKey ? null : groupKey;
4623
- drawLegend();
4624
- recolorEvents();
4625
- onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
4626
- });
4888
+ gEl.style('cursor', 'pointer').on('click', () => {
4889
+ currentActiveGroup =
4890
+ currentActiveGroup === groupKey ? null : groupKey;
4891
+ drawLegend();
4892
+ recolorEvents();
4893
+ onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
4894
+ });
4627
4895
  }
4628
4896
 
4629
4897
  // Outer capsule background (active only)
4630
4898
  if (isActive) {
4631
- gEl.append('rect')
4899
+ gEl
4900
+ .append('rect')
4632
4901
  .attr('width', lg.expandedWidth)
4633
4902
  .attr('height', LG_HEIGHT)
4634
4903
  .attr('rx', LG_HEIGHT / 2)
@@ -4640,7 +4909,8 @@ export function renderTimeline(
4640
4909
  const pillH = LG_HEIGHT - (isActive ? LG_CAPSULE_PAD * 2 : 0);
4641
4910
 
4642
4911
  // Pill background
4643
- gEl.append('rect')
4912
+ gEl
4913
+ .append('rect')
4644
4914
  .attr('x', pillXOff)
4645
4915
  .attr('y', pillYOff)
4646
4916
  .attr('width', pillWidth)
@@ -4650,7 +4920,8 @@ export function renderTimeline(
4650
4920
 
4651
4921
  // Active pill border
4652
4922
  if (isActive) {
4653
- gEl.append('rect')
4923
+ gEl
4924
+ .append('rect')
4654
4925
  .attr('x', pillXOff)
4655
4926
  .attr('y', pillYOff)
4656
4927
  .attr('width', pillWidth)
@@ -4662,7 +4933,8 @@ export function renderTimeline(
4662
4933
  }
4663
4934
 
4664
4935
  // Pill text
4665
- gEl.append('text')
4936
+ gEl
4937
+ .append('text')
4666
4938
  .attr('x', pillXOff + pillWidth / 2)
4667
4939
  .attr('y', LG_HEIGHT / 2 + LG_PILL_FONT_SIZE / 2 - 2)
4668
4940
  .attr('font-size', LG_PILL_FONT_SIZE)
@@ -4684,7 +4956,8 @@ export function renderTimeline(
4684
4956
  .attr('data-swimlane-toggle', groupKey)
4685
4957
  .on('click', (event: MouseEvent) => {
4686
4958
  event.stopPropagation();
4687
- currentSwimlaneGroup = currentSwimlaneGroup === groupKey ? null : groupKey;
4959
+ currentSwimlaneGroup =
4960
+ currentSwimlaneGroup === groupKey ? null : groupKey;
4688
4961
  onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
4689
4962
  relayout();
4690
4963
  });
@@ -4697,7 +4970,8 @@ export function renderTimeline(
4697
4970
  const tagKey = lg.group.name.toLowerCase();
4698
4971
  const tagVal = entry.value.toLowerCase();
4699
4972
 
4700
- const entryG = gEl.append('g')
4973
+ const entryG = gEl
4974
+ .append('g')
4701
4975
  .attr('class', 'tl-tag-legend-entry')
4702
4976
  .attr('data-tag-group', tagKey)
4703
4977
  .attr('data-legend-entry', tagVal);
@@ -4708,18 +4982,24 @@ export function renderTimeline(
4708
4982
  .on('mouseenter', (event: MouseEvent) => {
4709
4983
  event.stopPropagation();
4710
4984
  fadeToTagValue(mainG, tagKey, tagVal);
4711
- mainSvg.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry').each(function () {
4712
- const el = d3Selection.select(this);
4713
- const ev = el.attr('data-legend-entry');
4714
- if (ev === '__group__') return;
4715
- const eg = el.attr('data-tag-group');
4716
- el.attr('opacity', eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY);
4717
- });
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
+ });
4718
4997
  })
4719
4998
  .on('mouseleave', (event: MouseEvent) => {
4720
4999
  event.stopPropagation();
4721
5000
  fadeReset(mainG);
4722
- mainSvg.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
5001
+ mainSvg
5002
+ .selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
4723
5003
  .attr('opacity', 1);
4724
5004
  })
4725
5005
  .on('click', (event: MouseEvent) => {
@@ -4727,14 +5007,16 @@ export function renderTimeline(
4727
5007
  });
4728
5008
  }
4729
5009
 
4730
- entryG.append('circle')
5010
+ entryG
5011
+ .append('circle')
4731
5012
  .attr('cx', entryX + LG_DOT_R)
4732
5013
  .attr('cy', LG_HEIGHT / 2)
4733
5014
  .attr('r', LG_DOT_R)
4734
5015
  .attr('fill', entry.color);
4735
5016
 
4736
5017
  const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
4737
- entryG.append('text')
5018
+ entryG
5019
+ .append('text')
4738
5020
  .attr('x', textX)
4739
5021
  .attr('y', LG_HEIGHT / 2 + LG_ENTRY_FONT_SIZE / 2 - 1)
4740
5022
  .attr('font-size', LG_ENTRY_FONT_SIZE)
@@ -4742,7 +5024,10 @@ export function renderTimeline(
4742
5024
  .attr('fill', palette.textMuted)
4743
5025
  .text(entry.value);
4744
5026
 
4745
- 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;
4746
5031
  }
4747
5032
  }
4748
5033
 
@@ -4767,16 +5052,27 @@ export function renderTimeline(
4767
5052
  let color: string;
4768
5053
  if (colorTG) {
4769
5054
  const tagColor = resolveTagColor(
4770
- ev.metadata, parsed.timelineTagGroups, colorTG
5055
+ ev.metadata,
5056
+ parsed.timelineTagGroups,
5057
+ colorTG
4771
5058
  );
4772
- color = tagColor ?? (ev.group && groupColorMap.has(ev.group)
4773
- ? groupColorMap.get(ev.group)! : textColor);
5059
+ color =
5060
+ tagColor ??
5061
+ (ev.group && groupColorMap.has(ev.group)
5062
+ ? groupColorMap.get(ev.group)!
5063
+ : textColor);
4774
5064
  } else {
4775
- color = ev.group && groupColorMap.has(ev.group)
4776
- ? groupColorMap.get(ev.group)! : textColor;
5065
+ color =
5066
+ ev.group && groupColorMap.has(ev.group)
5067
+ ? groupColorMap.get(ev.group)!
5068
+ : textColor;
4777
5069
  }
4778
- el.selectAll('rect').attr('fill', mix(color, bg, 30)).attr('stroke', color);
4779
- 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);
4780
5076
  });
4781
5077
  }
4782
5078
 
@@ -4833,7 +5129,14 @@ export function renderWordCloud(
4833
5129
 
4834
5130
  const rotateFn = getRotateFn(cloudOptions.rotate);
4835
5131
 
4836
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
5132
+ renderChartTitle(
5133
+ svg,
5134
+ title,
5135
+ parsed.titleLineNumber,
5136
+ width,
5137
+ textColor,
5138
+ onClickItem
5139
+ );
4837
5140
 
4838
5141
  const g = svg
4839
5142
  .append('g')
@@ -5140,7 +5443,8 @@ export function renderVenn(
5140
5443
  const labelTextPad = 4;
5141
5444
 
5142
5445
  for (let i = 0; i < n; i++) {
5143
- const estimatedWidth = vennSets[i].name.length * 8.5 + stubLen + edgePad + labelTextPad;
5446
+ const estimatedWidth =
5447
+ vennSets[i].name.length * 8.5 + stubLen + edgePad + labelTextPad;
5144
5448
  const dx = rawCircles[i].x - clusterCx;
5145
5449
  const dy = rawCircles[i].y - clusterCy;
5146
5450
  if (Math.abs(dx) >= Math.abs(dy)) {
@@ -5167,13 +5471,27 @@ export function renderVenn(
5167
5471
  const scaledR = circles[0].r;
5168
5472
 
5169
5473
  // Suppress WebKit focus ring on interactive SVG elements
5170
- 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; }');
5171
5477
 
5172
5478
  // Title
5173
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
5479
+ renderChartTitle(
5480
+ svg,
5481
+ title,
5482
+ parsed.titleLineNumber,
5483
+ width,
5484
+ textColor,
5485
+ onClickItem
5486
+ );
5174
5487
 
5175
5488
  // ── Semi-transparent filled circles (non-interactive) ──
5176
- const circleEls: d3Selection.Selection<SVGCircleElement, unknown, null, undefined>[] = [];
5489
+ const circleEls: d3Selection.Selection<
5490
+ SVGCircleElement,
5491
+ unknown,
5492
+ null,
5493
+ undefined
5494
+ >[] = [];
5177
5495
  const circleGroup = svg.append('g');
5178
5496
  circles.forEach((c, i) => {
5179
5497
  const el = circleGroup
@@ -5200,10 +5518,13 @@ export function renderVenn(
5200
5518
 
5201
5519
  // Individual circle clipPaths
5202
5520
  circles.forEach((c, i) => {
5203
- defs.append('clipPath')
5521
+ defs
5522
+ .append('clipPath')
5204
5523
  .attr('id', `vcp-${i}`)
5205
5524
  .append('circle')
5206
- .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);
5207
5528
  });
5208
5529
 
5209
5530
  // All region index-sets: exclusive then intersection subsets
@@ -5215,57 +5536,79 @@ export function renderVenn(
5215
5536
  }
5216
5537
 
5217
5538
  const overlayGroup = svg.append('g').style('pointer-events', 'none');
5218
- 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
+ >();
5219
5543
 
5220
5544
  for (const idxs of regionIdxSets) {
5221
5545
  const key = idxs.join('-');
5222
- 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
+ );
5223
5549
 
5224
5550
  // Build nested clipPath for intersection of all idxs
5225
5551
  let clipId = `vcp-${idxs[0]}`;
5226
5552
  for (let k = 1; k < idxs.length; k++) {
5227
5553
  const nestedId = `vcp-n-${idxs.slice(0, k + 1).join('-')}`;
5228
5554
  const ci = idxs[k];
5229
- defs.append('clipPath')
5555
+ defs
5556
+ .append('clipPath')
5230
5557
  .attr('id', nestedId)
5231
5558
  .append('circle')
5232
- .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)
5233
5562
  .attr('clip-path', `url(#${clipId})`);
5234
5563
  clipId = nestedId;
5235
5564
  }
5236
5565
 
5237
5566
  // Determine line number for this region (for editor sync)
5238
- let regionLineNumber: number | null = null;
5567
+ let regionLineNumber: number | null = null; // eslint-disable-line no-useless-assignment
5239
5568
  if (idxs.length === 1) {
5240
5569
  regionLineNumber = vennSets[idxs[0]].lineNumber;
5241
5570
  } else {
5242
- const sortedNames = idxs.map(i => vennSets[i].name).sort();
5571
+ const sortedNames = idxs.map((i) => vennSets[i].name).sort();
5243
5572
  const ov = vennOverlaps.find(
5244
- (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])
5245
5576
  );
5246
5577
  regionLineNumber = ov?.lineNumber ?? null;
5247
5578
  }
5248
5579
 
5249
- const el = overlayGroup.append('rect')
5250
- .attr('x', 0).attr('y', 0)
5251
- .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)
5252
5586
  .attr('fill', 'white')
5253
5587
  .attr('fill-opacity', 0)
5254
5588
  .attr('class', 'venn-region-overlay')
5255
- .attr('data-line-number', regionLineNumber != null ? String(regionLineNumber) : '0')
5589
+ .attr(
5590
+ 'data-line-number',
5591
+ regionLineNumber != null ? String(regionLineNumber) : '0'
5592
+ )
5256
5593
  .attr('clip-path', `url(#${clipId})`);
5257
5594
 
5258
5595
  if (excluded.length > 0) {
5259
5596
  // Mask subtracts excluded circles so only the exact region shape highlights
5260
5597
  const maskId = `vvm-${key}`;
5261
5598
  const mask = defs.append('mask').attr('id', maskId);
5262
- mask.append('rect')
5263
- .attr('x', 0).attr('y', 0)
5264
- .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)
5265
5605
  .attr('fill', 'white');
5266
5606
  for (const j of excluded) {
5267
- mask.append('circle')
5268
- .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)
5269
5612
  .attr('fill', 'black');
5270
5613
  }
5271
5614
  el.attr('mask', `url(#${maskId})`);
@@ -5276,10 +5619,12 @@ export function renderVenn(
5276
5619
 
5277
5620
  const showRegionOverlay = (idxs: number[]) => {
5278
5621
  const key = [...idxs].sort((a, b) => a - b).join('-');
5279
- 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
+ );
5280
5625
  };
5281
5626
  const hideAllOverlays = () => {
5282
- overlayEls.forEach(el => el.attr('fill-opacity', 0));
5627
+ overlayEls.forEach((el) => el.attr('fill-opacity', 0));
5283
5628
  };
5284
5629
 
5285
5630
  // ── Labels ──
@@ -5288,7 +5633,9 @@ export function renderVenn(
5288
5633
 
5289
5634
  function exclusiveHSpan(px: number, py: number, ci: number): number {
5290
5635
  const dy = py - circles[ci].y;
5291
- 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
+ );
5292
5639
  let left = circles[ci].x - halfChord;
5293
5640
  let right = circles[ci].x + halfChord;
5294
5641
  for (let j = 0; j < n; j++) {
@@ -5319,11 +5666,14 @@ export function renderVenn(
5319
5666
  const centroid = regionCentroid(circles, inside);
5320
5667
 
5321
5668
  const availW = exclusiveHSpan(centroid.x, centroid.y, i);
5322
- const fitFont = Math.min(MAX_FONT, Math.max(MIN_FONT,
5323
- (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
+ );
5324
5673
  const estTextW = text.length * CH_RATIO * fitFont;
5325
5674
 
5326
- const fitsInside = estTextW + INTERNAL_PAD * 2 < availW &&
5675
+ const fitsInside =
5676
+ estTextW + INTERNAL_PAD * 2 < availW &&
5327
5677
  pointInCircle({ x: centroid.x, y: centroid.y - fitFont / 2 }, c) &&
5328
5678
  pointInCircle({ x: centroid.x, y: centroid.y + fitFont / 2 }, c);
5329
5679
 
@@ -5342,7 +5692,13 @@ export function renderVenn(
5342
5692
  let dx = c.x - gcx;
5343
5693
  let dy = c.y - gcy;
5344
5694
  const mag = Math.sqrt(dx * dx + dy * dy);
5345
- 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
+ }
5346
5702
 
5347
5703
  const exitX = c.x + dx * c.r;
5348
5704
  const exitY = c.y + dy * c.r;
@@ -5353,8 +5709,10 @@ export function renderVenn(
5353
5709
 
5354
5710
  labelGroup
5355
5711
  .append('line')
5356
- .attr('x1', edgeX).attr('y1', edgeY)
5357
- .attr('x2', stubEndX).attr('y2', stubEndY)
5712
+ .attr('x1', edgeX)
5713
+ .attr('y1', edgeY)
5714
+ .attr('x2', stubEndX)
5715
+ .attr('y2', stubEndY)
5358
5716
  .attr('stroke', textColor)
5359
5717
  .attr('stroke-width', 1);
5360
5718
 
@@ -5381,7 +5739,8 @@ export function renderVenn(
5381
5739
 
5382
5740
  // ── Overlap labels (inline at region centroid) ──
5383
5741
  function overlapHSpan(py: number, idxs: number[]): number {
5384
- let left = -Infinity, right = Infinity;
5742
+ let left = -Infinity,
5743
+ right = Infinity;
5385
5744
  for (const ci of idxs) {
5386
5745
  const dy = py - circles[ci].y;
5387
5746
  if (Math.abs(dy) >= circles[ci].r) return 0;
@@ -5411,8 +5770,13 @@ export function renderVenn(
5411
5770
  const inside = circles.map((_, j) => idxs.includes(j));
5412
5771
  const centroid = regionCentroid(circles, inside);
5413
5772
  const availW = overlapHSpan(centroid.y, idxs);
5414
- const fitFont = Math.min(MAX_FONT, Math.max(MIN_FONT,
5415
- (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
+ );
5416
5780
  labelGroup
5417
5781
  .append('text')
5418
5782
  .attr('x', centroid.x)
@@ -5441,11 +5805,16 @@ export function renderVenn(
5441
5805
  .attr('data-line-number', String(vennSets[i].lineNumber))
5442
5806
  .style('cursor', onClickItem ? 'pointer' : 'default')
5443
5807
  .style('outline', 'none')
5444
- .on('mouseenter', () => { showRegionOverlay([i]); })
5445
- .on('mouseleave', () => { hideAllOverlays(); })
5808
+ .on('mouseenter', () => {
5809
+ showRegionOverlay([i]);
5810
+ })
5811
+ .on('mouseleave', () => {
5812
+ hideAllOverlays();
5813
+ })
5446
5814
  .on('click', function () {
5447
5815
  (this as SVGElement).blur?.();
5448
- if (onClickItem && vennSets[i].lineNumber) onClickItem(vennSets[i].lineNumber);
5816
+ if (onClickItem && vennSets[i].lineNumber)
5817
+ onClickItem(vennSets[i].lineNumber);
5449
5818
  });
5450
5819
  });
5451
5820
 
@@ -5454,14 +5823,23 @@ export function renderVenn(
5454
5823
 
5455
5824
  const subsets: { idxs: number[]; sets: string[] }[] = [];
5456
5825
  if (n === 2) {
5457
- 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
+ });
5458
5830
  } else {
5459
5831
  for (let a = 0; a < n; a++) {
5460
5832
  for (let b = a + 1; b < n; b++) {
5461
- 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
+ });
5462
5837
  }
5463
5838
  }
5464
- 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
+ });
5465
5843
  }
5466
5844
 
5467
5845
  for (const subset of subsets) {
@@ -5469,7 +5847,8 @@ export function renderVenn(
5469
5847
  const inside = circles.map((_, j) => idxs.includes(j));
5470
5848
  const centroid = regionCentroid(circles, inside);
5471
5849
  const declaredOv = vennOverlaps.find(
5472
- (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])
5473
5852
  );
5474
5853
  hoverGroup
5475
5854
  .append('circle')
@@ -5482,8 +5861,12 @@ export function renderVenn(
5482
5861
  .attr('data-line-number', declaredOv ? String(declaredOv.lineNumber) : '')
5483
5862
  .style('cursor', onClickItem && declaredOv ? 'pointer' : 'default')
5484
5863
  .style('outline', 'none')
5485
- .on('mouseenter', () => { showRegionOverlay(idxs); })
5486
- .on('mouseleave', () => { hideAllOverlays(); })
5864
+ .on('mouseenter', () => {
5865
+ showRegionOverlay(idxs);
5866
+ })
5867
+ .on('mouseleave', () => {
5868
+ hideAllOverlays();
5869
+ })
5487
5870
  .on('click', function () {
5488
5871
  (this as SVGElement).blur?.();
5489
5872
  if (onClickItem && declaredOv) onClickItem(declaredOv.lineNumber);
@@ -5542,7 +5925,12 @@ export function renderQuadrant(
5542
5925
  // Margins
5543
5926
  const hasXAxis = !!quadrantXAxis;
5544
5927
  const hasYAxis = !!quadrantYAxis;
5545
- 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
+ };
5546
5934
  const chartWidth = width - margin.left - margin.right;
5547
5935
  const chartHeight = height - margin.top - margin.bottom;
5548
5936
 
@@ -5554,7 +5942,14 @@ export function renderQuadrant(
5554
5942
  const tooltip = createTooltip(container, palette, isDark);
5555
5943
 
5556
5944
  // Title
5557
- renderChartTitle(svg, title, quadrantTitleLineNumber, width, textColor, onClickItem);
5945
+ renderChartTitle(
5946
+ svg,
5947
+ title,
5948
+ quadrantTitleLineNumber,
5949
+ width,
5950
+ textColor,
5951
+ onClickItem
5952
+ );
5558
5953
 
5559
5954
  // Chart group (translated by margins)
5560
5955
  const chartG = svg
@@ -5565,12 +5960,21 @@ export function renderQuadrant(
5565
5960
  const mixHex = (a: string, b: string, pct: number): string => {
5566
5961
  const parse = (h: string) => {
5567
5962
  const r = h.replace('#', '');
5568
- const f = r.length === 3 ? r[0]+r[0]+r[1]+r[1]+r[2]+r[2] : r;
5569
- 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
+ ];
5570
5969
  };
5571
- const [ar,ag,ab] = parse(a), [br,bg,bb] = parse(b), t = pct/100;
5572
- const c = (x: number, y: number) => Math.round(x*t + y*(1-t)).toString(16).padStart(2,'0');
5573
- 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)}`;
5574
5978
  };
5575
5979
 
5576
5980
  const bg = isDark ? palette.surface : palette.bg;
@@ -5687,7 +6091,11 @@ export function renderQuadrant(
5687
6091
  fontSize: number;
5688
6092
  }
5689
6093
 
5690
- const quadrantLabelLayout = (text: string, qw: number, qh: number): QuadrantLabelLayout => {
6094
+ const quadrantLabelLayout = (
6095
+ text: string,
6096
+ qw: number,
6097
+ qh: number
6098
+ ): QuadrantLabelLayout => {
5691
6099
  const availW = qw - LABEL_PAD;
5692
6100
  const availH = qh - LABEL_PAD;
5693
6101
  const words = text.split(/\s+/);
@@ -5695,7 +6103,10 @@ export function renderQuadrant(
5695
6103
  // Try single line first
5696
6104
  if (estTextWidth(text, LABEL_MAX_FONT) <= availW) {
5697
6105
  const fs = Math.min(LABEL_MAX_FONT, availH);
5698
- 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
+ };
5699
6110
  }
5700
6111
 
5701
6112
  // Try wrapping into 2+ lines: greedily pack words so each line fits availW
@@ -5742,7 +6153,10 @@ export function renderQuadrant(
5742
6153
  const qh = chartHeight / 2;
5743
6154
  const quadrantDefsWithLabel = quadrantDefs.filter((d) => d.label !== null);
5744
6155
  const labelLayouts = new Map(
5745
- 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
+ ])
5746
6160
  );
5747
6161
 
5748
6162
  const quadrantLabelTexts = chartG
@@ -5807,7 +6221,10 @@ export function renderQuadrant(
5807
6221
  .attr('text-anchor', 'middle')
5808
6222
  .attr('fill', textColor)
5809
6223
  .attr('font-size', '18px')
5810
- .attr('data-line-number', quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null)
6224
+ .attr(
6225
+ 'data-line-number',
6226
+ quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null
6227
+ )
5811
6228
  .style(
5812
6229
  'cursor',
5813
6230
  onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
@@ -5823,7 +6240,10 @@ export function renderQuadrant(
5823
6240
  .attr('text-anchor', 'middle')
5824
6241
  .attr('fill', textColor)
5825
6242
  .attr('font-size', '18px')
5826
- .attr('data-line-number', quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null)
6243
+ .attr(
6244
+ 'data-line-number',
6245
+ quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null
6246
+ )
5827
6247
  .style(
5828
6248
  'cursor',
5829
6249
  onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
@@ -5859,7 +6279,10 @@ export function renderQuadrant(
5859
6279
  .attr('fill', textColor)
5860
6280
  .attr('font-size', '18px')
5861
6281
  .attr('transform', `rotate(-90, 22, ${yMidBottom})`)
5862
- .attr('data-line-number', quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null)
6282
+ .attr(
6283
+ 'data-line-number',
6284
+ quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null
6285
+ )
5863
6286
  .style(
5864
6287
  'cursor',
5865
6288
  onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
@@ -5876,7 +6299,10 @@ export function renderQuadrant(
5876
6299
  .attr('fill', textColor)
5877
6300
  .attr('font-size', '18px')
5878
6301
  .attr('transform', `rotate(-90, 22, ${yMidTop})`)
5879
- .attr('data-line-number', quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null)
6302
+ .attr(
6303
+ 'data-line-number',
6304
+ quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null
6305
+ )
5880
6306
  .style(
5881
6307
  'cursor',
5882
6308
  onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
@@ -5935,7 +6361,9 @@ export function renderQuadrant(
5935
6361
  const pointColor =
5936
6362
  quadDef?.label?.color ?? defaultColors[quadDef?.colorIdx ?? 0];
5937
6363
 
5938
- const pointG = pointsG.append('g').attr('class', 'point-group')
6364
+ const pointG = pointsG
6365
+ .append('g')
6366
+ .attr('class', 'point-group')
5939
6367
  .attr('data-line-number', String(point.lineNumber));
5940
6368
 
5941
6369
  // Circle with white fill and colored border for visibility on opaque quadrants
@@ -6024,7 +6452,10 @@ const EXPORT_HEIGHT = 800;
6024
6452
  /**
6025
6453
  * Resolves the palette for export, falling back to Nord light/dark.
6026
6454
  */
6027
- async function resolveExportPalette(theme: string, palette?: PaletteColors): Promise<PaletteColors> {
6455
+ async function resolveExportPalette(
6456
+ theme: string,
6457
+ palette?: PaletteColors
6458
+ ): Promise<PaletteColors> {
6028
6459
  if (palette) return palette;
6029
6460
  const { getPalette } = await import('./palettes');
6030
6461
  return theme === 'dark' ? getPalette('nord').dark : getPalette('nord').light;
@@ -6086,7 +6517,13 @@ export async function renderForExport(
6086
6517
  hiddenAttributes?: Set<string>;
6087
6518
  swimlaneTagGroup?: string | null;
6088
6519
  },
6089
- 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
+ }
6090
6527
  ): Promise<string> {
6091
6528
  // Flowchart and org chart use their own parser pipelines — intercept before parseVisualization()
6092
6529
  const { parseDgmoChartType } = await import('./dgmo-router');
@@ -6106,7 +6543,8 @@ export async function renderForExport(
6106
6543
 
6107
6544
  // Apply interactive collapse state when provided
6108
6545
  const collapsedNodes = orgExportState?.collapsedNodes;
6109
- const activeTagGroup = orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
6546
+ const activeTagGroup =
6547
+ orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
6110
6548
  const hiddenAttributes = orgExportState?.hiddenAttributes;
6111
6549
 
6112
6550
  const { parsed: effectiveParsed, hiddenCounts } =
@@ -6128,7 +6566,17 @@ export async function renderForExport(
6128
6566
  const exportHeight = orgLayout.height + PADDING * 2 + titleOffset;
6129
6567
  const container = createExportContainer(exportWidth, exportHeight);
6130
6568
 
6131
- 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
+ );
6132
6580
  return finalizeSvgExport(container, theme, effectivePalette, options);
6133
6581
  }
6134
6582
 
@@ -6146,7 +6594,8 @@ export async function renderForExport(
6146
6594
 
6147
6595
  // Apply interactive collapse state when provided
6148
6596
  const collapsedNodes = orgExportState?.collapsedNodes;
6149
- const activeTagGroup = orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
6597
+ const activeTagGroup =
6598
+ orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
6150
6599
  const hiddenAttributes = orgExportState?.hiddenAttributes;
6151
6600
 
6152
6601
  const { parsed: effectiveParsed, hiddenCounts } =
@@ -6159,7 +6608,7 @@ export async function renderForExport(
6159
6608
  hiddenCounts.size > 0 ? hiddenCounts : undefined,
6160
6609
  activeTagGroup,
6161
6610
  hiddenAttributes,
6162
- true,
6611
+ true
6163
6612
  );
6164
6613
 
6165
6614
  const PADDING = 20;
@@ -6168,7 +6617,17 @@ export async function renderForExport(
6168
6617
  const exportHeight = sitemapLayout.height + PADDING * 2 + titleOffset;
6169
6618
  const container = createExportContainer(exportWidth, exportHeight);
6170
6619
 
6171
- 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
+ );
6172
6631
  return finalizeSvgExport(container, theme, effectivePalette, options);
6173
6632
  }
6174
6633
 
@@ -6186,7 +6645,15 @@ export async function renderForExport(
6186
6645
  container.style.left = '-9999px';
6187
6646
  document.body.appendChild(container);
6188
6647
 
6189
- 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
+ );
6190
6657
  return finalizeSvgExport(container, theme, effectivePalette, options);
6191
6658
  }
6192
6659
 
@@ -6206,7 +6673,15 @@ export async function renderForExport(
6206
6673
  const exportHeight = classLayout.height + PADDING * 2 + titleOffset;
6207
6674
  const container = createExportContainer(exportWidth, exportHeight);
6208
6675
 
6209
- 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
+ );
6210
6685
  return finalizeSvgExport(container, theme, effectivePalette, options);
6211
6686
  }
6212
6687
 
@@ -6226,14 +6701,26 @@ export async function renderForExport(
6226
6701
  const exportHeight = erLayout.height + PADDING * 2 + titleOffset;
6227
6702
  const container = createExportContainer(exportWidth, exportHeight);
6228
6703
 
6229
- 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
+ );
6230
6714
  return finalizeSvgExport(container, theme, effectivePalette, options);
6231
6715
  }
6232
6716
 
6233
6717
  if (detectedType === 'initiative-status') {
6234
- const { parseInitiativeStatus } = await import('./initiative-status/parser');
6235
- const { layoutInitiativeStatus } = await import('./initiative-status/layout');
6236
- 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');
6237
6724
 
6238
6725
  const effectivePalette = await resolveExportPalette(theme, palette);
6239
6726
  const isParsed = parseInitiativeStatus(content);
@@ -6246,14 +6733,27 @@ export async function renderForExport(
6246
6733
  const exportHeight = isLayout.height + PADDING * 2 + titleOffset;
6247
6734
  const container = createExportContainer(exportWidth, exportHeight);
6248
6735
 
6249
- 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
+ );
6250
6744
  return finalizeSvgExport(container, theme, effectivePalette, options);
6251
6745
  }
6252
6746
 
6253
6747
  if (detectedType === 'c4') {
6254
6748
  const { parseC4 } = await import('./c4/parser');
6255
- const { layoutC4Context, layoutC4Containers, layoutC4Components, layoutC4Deployment } = await import('./c4/layout');
6256
- 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');
6257
6757
 
6258
6758
  const effectivePalette = await resolveExportPalette(theme, palette);
6259
6759
  const c4Parsed = parseC4(content, effectivePalette);
@@ -6264,13 +6764,14 @@ export async function renderForExport(
6264
6764
  const c4System = options?.c4System;
6265
6765
  const c4Container = options?.c4Container;
6266
6766
 
6267
- const c4Layout = c4Level === 'deployment'
6268
- ? layoutC4Deployment(c4Parsed)
6269
- : c4Level === 'components' && c4System && c4Container
6270
- ? layoutC4Components(c4Parsed, c4System, c4Container)
6271
- : c4Level === 'containers' && c4System
6272
- ? layoutC4Containers(c4Parsed, c4System)
6273
- : 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);
6274
6775
 
6275
6776
  if (c4Layout.nodes.length === 0) return '';
6276
6777
 
@@ -6280,11 +6781,23 @@ export async function renderForExport(
6280
6781
  const exportHeight = c4Layout.height + PADDING * 2 + titleOffset;
6281
6782
  const container = createExportContainer(exportWidth, exportHeight);
6282
6783
 
6283
- const renderFn = c4Level === 'deployment' || (c4Level === 'components' && c4System && c4Container) || (c4Level === 'containers' && c4System)
6284
- ? renderC4Containers
6285
- : renderC4Context;
6286
-
6287
- 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
+ );
6288
6801
  return finalizeSvgExport(container, theme, effectivePalette, options);
6289
6802
  }
6290
6803
 
@@ -6300,7 +6813,15 @@ export async function renderForExport(
6300
6813
  const layout = layoutGraph(fcParsed);
6301
6814
  const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
6302
6815
 
6303
- 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
+ );
6304
6825
  return finalizeSvgExport(container, theme, effectivePalette, options);
6305
6826
  }
6306
6827
 
@@ -6308,7 +6829,8 @@ export async function renderForExport(
6308
6829
  const { parseInfra } = await import('./infra/parser');
6309
6830
  const { computeInfra } = await import('./infra/compute');
6310
6831
  const { layoutInfra } = await import('./infra/layout');
6311
- const { renderInfra, computeInfraLegendGroups } = await import('./infra/renderer');
6832
+ const { renderInfra, computeInfraLegendGroups } =
6833
+ await import('./infra/renderer');
6312
6834
 
6313
6835
  const effectivePalette = await resolveExportPalette(theme, palette);
6314
6836
  const infraParsed = parseInfra(content);
@@ -6319,13 +6841,30 @@ export async function renderForExport(
6319
6841
  const activeTagGroup = options?.tagGroup ?? null;
6320
6842
 
6321
6843
  const titleOffset = infraParsed.title ? 40 : 0;
6322
- const legendGroups = computeInfraLegendGroups(infraLayout.nodes, infraParsed.tagGroups, effectivePalette);
6844
+ const legendGroups = computeInfraLegendGroups(
6845
+ infraLayout.nodes,
6846
+ infraParsed.tagGroups,
6847
+ effectivePalette
6848
+ );
6323
6849
  const legendOffset = legendGroups.length > 0 ? 28 : 0;
6324
6850
  const exportWidth = infraLayout.width;
6325
6851
  const exportHeight = infraLayout.height + titleOffset + legendOffset;
6326
6852
  const container = createExportContainer(exportWidth, exportHeight);
6327
6853
 
6328
- 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
+ );
6329
6868
  // Restore explicit pixel dimensions for resvg (renderer uses 100%/viewBox for app scaling)
6330
6869
  const infraSvg = container.querySelector('svg');
6331
6870
  if (infraSvg) {
@@ -6349,7 +6888,14 @@ export async function renderForExport(
6349
6888
  const EXPORT_H = 800;
6350
6889
  const container = createExportContainer(EXPORT_W, EXPORT_H);
6351
6890
 
6352
- 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
+ );
6353
6899
  return finalizeSvgExport(container, theme, effectivePalette, options);
6354
6900
  }
6355
6901
 
@@ -6365,7 +6911,15 @@ export async function renderForExport(
6365
6911
  const layout = layoutGraph(stateParsed);
6366
6912
  const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
6367
6913
 
6368
- 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
+ );
6369
6923
  return finalizeSvgExport(container, theme, effectivePalette, options);
6370
6924
  }
6371
6925
 
@@ -6391,30 +6945,75 @@ export async function renderForExport(
6391
6945
  const effectivePalette = await resolveExportPalette(theme, palette);
6392
6946
  const isDark = theme === 'dark';
6393
6947
  const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
6394
- const dims: D3ExportDimensions = { width: EXPORT_WIDTH, height: EXPORT_HEIGHT };
6948
+ const dims: D3ExportDimensions = {
6949
+ width: EXPORT_WIDTH,
6950
+ height: EXPORT_HEIGHT,
6951
+ };
6395
6952
 
6396
6953
  if (parsed.type === 'sequence') {
6397
6954
  const { parseSequenceDgmo } = await import('./sequence/parser');
6398
6955
  const { renderSequenceDiagram } = await import('./sequence/renderer');
6399
6956
  const seqParsed = parseSequenceDgmo(content);
6400
6957
  if (seqParsed.error || seqParsed.participants.length === 0) return '';
6401
- renderSequenceDiagram(container, seqParsed, effectivePalette, isDark, undefined, {
6402
- exportWidth: EXPORT_WIDTH,
6403
- activeTagGroup: options?.tagGroup,
6404
- });
6958
+ renderSequenceDiagram(
6959
+ container,
6960
+ seqParsed,
6961
+ effectivePalette,
6962
+ isDark,
6963
+ undefined,
6964
+ {
6965
+ exportWidth: EXPORT_WIDTH,
6966
+ activeTagGroup: options?.tagGroup,
6967
+ }
6968
+ );
6405
6969
  } else if (parsed.type === 'wordcloud') {
6406
- await renderWordCloudAsync(container, parsed, effectivePalette, isDark, dims);
6970
+ await renderWordCloudAsync(
6971
+ container,
6972
+ parsed,
6973
+ effectivePalette,
6974
+ isDark,
6975
+ dims
6976
+ );
6407
6977
  } else if (parsed.type === 'arc') {
6408
- renderArcDiagram(container, parsed, effectivePalette, isDark, undefined, dims);
6978
+ renderArcDiagram(
6979
+ container,
6980
+ parsed,
6981
+ effectivePalette,
6982
+ isDark,
6983
+ undefined,
6984
+ dims
6985
+ );
6409
6986
  } else if (parsed.type === 'timeline') {
6410
- renderTimeline(container, parsed, effectivePalette, isDark, undefined, dims,
6411
- 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
+ );
6412
6997
  } else if (parsed.type === 'venn') {
6413
6998
  renderVenn(container, parsed, effectivePalette, isDark, undefined, dims);
6414
6999
  } else if (parsed.type === 'quadrant') {
6415
- renderQuadrant(container, parsed, effectivePalette, isDark, undefined, dims);
7000
+ renderQuadrant(
7001
+ container,
7002
+ parsed,
7003
+ effectivePalette,
7004
+ isDark,
7005
+ undefined,
7006
+ dims
7007
+ );
6416
7008
  } else {
6417
- renderSlopeChart(container, parsed, effectivePalette, isDark, undefined, dims);
7009
+ renderSlopeChart(
7010
+ container,
7011
+ parsed,
7012
+ effectivePalette,
7013
+ isDark,
7014
+ undefined,
7015
+ dims
7016
+ );
6418
7017
  }
6419
7018
 
6420
7019
  return finalizeSvgExport(container, theme, effectivePalette, options);