@diagrammo/dgmo 0.8.3 → 0.8.5

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 (122) 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 +452 -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 +188 -185
  9. package/dist/editor.cjs +338 -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 +307 -0
  14. package/dist/editor.js.map +1 -0
  15. package/dist/highlight.cjs +560 -0
  16. package/dist/highlight.cjs.map +1 -0
  17. package/dist/highlight.d.cts +32 -0
  18. package/dist/highlight.d.ts +32 -0
  19. package/dist/highlight.js +530 -0
  20. package/dist/highlight.js.map +1 -0
  21. package/dist/index.cjs +3467 -1078
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +22 -1
  24. package/dist/index.d.ts +22 -1
  25. package/dist/index.js +3466 -1078
  26. package/dist/index.js.map +1 -1
  27. package/docs/language-reference.md +46 -37
  28. package/gallery/fixtures/arc.dgmo +18 -0
  29. package/gallery/fixtures/area.dgmo +19 -0
  30. package/gallery/fixtures/bar-stacked.dgmo +10 -0
  31. package/gallery/fixtures/bar.dgmo +10 -0
  32. package/gallery/fixtures/c4-full.dgmo +52 -0
  33. package/gallery/fixtures/c4.dgmo +17 -0
  34. package/gallery/fixtures/chord.dgmo +12 -0
  35. package/gallery/fixtures/class-basic.dgmo +14 -0
  36. package/gallery/fixtures/class-full.dgmo +43 -0
  37. package/gallery/fixtures/doughnut.dgmo +8 -0
  38. package/gallery/fixtures/flowchart-basic.dgmo +3 -0
  39. package/gallery/fixtures/flowchart-colors.dgmo +5 -0
  40. package/gallery/fixtures/flowchart-complex.dgmo +17 -0
  41. package/gallery/fixtures/flowchart-decision.dgmo +5 -0
  42. package/gallery/fixtures/flowchart-full.dgmo +13 -0
  43. package/gallery/fixtures/flowchart-groups.dgmo +10 -0
  44. package/gallery/fixtures/flowchart-loop.dgmo +7 -0
  45. package/gallery/fixtures/flowchart-nested.dgmo +7 -0
  46. package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
  47. package/gallery/fixtures/function.dgmo +8 -0
  48. package/gallery/fixtures/funnel.dgmo +7 -0
  49. package/gallery/fixtures/gantt-full.dgmo +49 -0
  50. package/gallery/fixtures/gantt.dgmo +42 -0
  51. package/gallery/fixtures/heatmap.dgmo +8 -0
  52. package/gallery/fixtures/infra-full.dgmo +78 -0
  53. package/gallery/fixtures/infra-overload.dgmo +25 -0
  54. package/gallery/fixtures/infra.dgmo +47 -0
  55. package/gallery/fixtures/initiative-status-full.dgmo +46 -0
  56. package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
  57. package/gallery/fixtures/initiative-status.dgmo +9 -0
  58. package/gallery/fixtures/line.dgmo +19 -0
  59. package/gallery/fixtures/multi-line.dgmo +11 -0
  60. package/gallery/fixtures/org-basic.dgmo +16 -0
  61. package/gallery/fixtures/org-full.dgmo +69 -0
  62. package/gallery/fixtures/org-teams.dgmo +25 -0
  63. package/gallery/fixtures/pie.dgmo +9 -0
  64. package/gallery/fixtures/polar-area.dgmo +8 -0
  65. package/gallery/fixtures/quadrant.dgmo +18 -0
  66. package/gallery/fixtures/radar.dgmo +8 -0
  67. package/gallery/fixtures/sankey.dgmo +31 -0
  68. package/gallery/fixtures/scatter.dgmo +21 -0
  69. package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
  70. package/gallery/fixtures/sequence-tags.dgmo +41 -0
  71. package/gallery/fixtures/sequence.dgmo +35 -0
  72. package/gallery/fixtures/sitemap-basic.dgmo +12 -0
  73. package/gallery/fixtures/sitemap-full.dgmo +156 -0
  74. package/gallery/fixtures/slope.dgmo +9 -0
  75. package/gallery/fixtures/spr-eras.dgmo +62 -0
  76. package/gallery/fixtures/state.dgmo +30 -0
  77. package/gallery/fixtures/timeline-intraday.dgmo +14 -0
  78. package/gallery/fixtures/timeline.dgmo +32 -0
  79. package/gallery/fixtures/venn.dgmo +10 -0
  80. package/gallery/fixtures/wordcloud.dgmo +24 -0
  81. package/package.json +71 -2
  82. package/src/c4/layout.ts +372 -90
  83. package/src/c4/parser.ts +100 -55
  84. package/src/chart.ts +91 -28
  85. package/src/class/parser.ts +41 -12
  86. package/src/cli.ts +211 -62
  87. package/src/completion.ts +378 -183
  88. package/src/d3.ts +1044 -303
  89. package/src/dgmo-mermaid.ts +16 -13
  90. package/src/dgmo-router.ts +69 -23
  91. package/src/echarts.ts +646 -153
  92. package/src/editor/dgmo.grammar +69 -0
  93. package/src/editor/dgmo.grammar.d.ts +2 -0
  94. package/src/editor/dgmo.grammar.js +18 -0
  95. package/src/editor/dgmo.grammar.terms.d.ts +5 -0
  96. package/src/editor/dgmo.grammar.terms.js +35 -0
  97. package/src/editor/highlight-api.ts +444 -0
  98. package/src/editor/highlight.ts +36 -0
  99. package/src/editor/index.ts +28 -0
  100. package/src/editor/keywords.ts +222 -0
  101. package/src/editor/tokens.ts +30 -0
  102. package/src/er/parser.ts +48 -14
  103. package/src/er/renderer.ts +112 -53
  104. package/src/gantt/calculator.ts +91 -29
  105. package/src/gantt/parser.ts +197 -71
  106. package/src/gantt/renderer.ts +1120 -350
  107. package/src/graph/flowchart-parser.ts +46 -25
  108. package/src/graph/state-parser.ts +47 -17
  109. package/src/index.ts +96 -31
  110. package/src/infra/parser.ts +157 -53
  111. package/src/infra/renderer.ts +723 -271
  112. package/src/initiative-status/parser.ts +138 -44
  113. package/src/kanban/parser.ts +25 -14
  114. package/src/org/layout.ts +111 -44
  115. package/src/org/parser.ts +69 -22
  116. package/src/palettes/index.ts +3 -2
  117. package/src/sequence/parser.ts +193 -61
  118. package/src/sitemap/parser.ts +65 -29
  119. package/src/utils/arrows.ts +2 -22
  120. package/src/utils/duration.ts +39 -21
  121. package/src/utils/legend-constants.ts +0 -2
  122. 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,
@@ -467,8 +509,17 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
467
509
  let timelineEraBlockIndent = 0;
468
510
  let inTimelineMarkerBlock = false;
469
511
  let timelineMarkerBlockIndent = 0;
512
+ let inSlopePeriodBlock = false;
470
513
  const timelineAliasMap = new Map<string, string>();
471
- const VALID_D3_TYPES = new Set(['slope', 'wordcloud', 'arc', 'timeline', 'venn', 'quadrant', 'sequence']);
514
+ const VALID_D3_TYPES = new Set([
515
+ 'slope',
516
+ 'wordcloud',
517
+ 'arc',
518
+ 'timeline',
519
+ 'venn',
520
+ 'quadrant',
521
+ 'sequence',
522
+ ]);
472
523
  let firstLineParsed = false;
473
524
 
474
525
  for (let i = 0; i < lines.length; i++) {
@@ -509,7 +560,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
509
560
  lineNumber,
510
561
  };
511
562
  if (tagBlockMatch.alias) {
512
- timelineAliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
563
+ timelineAliasMap.set(
564
+ tagBlockMatch.alias.toLowerCase(),
565
+ tagBlockMatch.name.toLowerCase()
566
+ );
513
567
  }
514
568
  result.timelineTagGroups.push(currentTimelineTagGroup);
515
569
  continue;
@@ -526,7 +580,11 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
526
580
  const { label, color } = extractColor(entryText, palette);
527
581
  if (color) {
528
582
  if (isDefault) currentTimelineTagGroup.defaultValue = label;
529
- currentTimelineTagGroup.entries.push({ value: label, color, lineNumber });
583
+ currentTimelineTagGroup.entries.push({
584
+ value: label,
585
+ color,
586
+ lineNumber,
587
+ });
530
588
  continue;
531
589
  }
532
590
  }
@@ -558,9 +616,21 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
558
616
  }
559
617
 
560
618
  // 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'));
619
+ if (
620
+ /^#{2,}\s+/.test(line) &&
621
+ (result.type === 'arc' || result.type === 'timeline')
622
+ ) {
623
+ const name = line
624
+ .replace(/^#{2,}\s+/, '')
625
+ .replace(/\s*\([^)]*\)\s*$/, '')
626
+ .trim();
627
+ result.diagnostics.push(
628
+ makeDgmoError(
629
+ lineNumber,
630
+ `'## ${name}' is no longer supported. Use '[${name}]' instead`,
631
+ 'warning'
632
+ )
633
+ );
564
634
  continue;
565
635
  }
566
636
 
@@ -570,10 +640,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
570
640
  currentTimelineGroup = null;
571
641
  }
572
642
 
573
- // Arc link line: source -> target(color): weight
643
+ // Arc link line: source -> target(color) weight
574
644
  if (result.type === 'arc') {
575
645
  const linkMatch = line.match(
576
- /^(.+?)\s*->\s*(.+?)(?:\(([^)]+)\))?\s*(?::\s*(\d+(?:\.\d+)?))?$/
646
+ /^(.+?)\s*->\s*(.+?)(?:\(([^)]+)\))?\s*(?:\s+(\d+(?:\.\d+)?))?$/
577
647
  );
578
648
  if (linkMatch) {
579
649
  const source = linkMatch[1].trim();
@@ -613,7 +683,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
613
683
  } else {
614
684
  if (line.startsWith('//')) continue;
615
685
  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*$/
686
+ /^(\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
687
  );
618
688
  if (eraEntryMatch) {
619
689
  const colorAnnotation = eraEntryMatch[4]?.trim() || null;
@@ -678,7 +748,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
678
748
 
679
749
  // Timeline era lines (inline): era YYYY->YYYY Label (color)
680
750
  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*$/
751
+ /^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
752
  );
683
753
  if (eraMatch) {
684
754
  const colorAnnotation = eraMatch[4]?.trim() || null;
@@ -696,7 +766,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
696
766
 
697
767
  // Timeline marker lines (inline): marker YYYY Label (color)
698
768
  const markerMatch = line.match(
699
- /^marker:?\s+(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
769
+ /^marker\s+(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
700
770
  );
701
771
  if (markerMatch) {
702
772
  const colorAnnotation = markerMatch[3]?.trim() || null;
@@ -719,7 +789,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
719
789
  // Supports uncertain end with ? suffix (e.g., ->3m?: fades out the last 20%)
720
790
  // Accepts both -> (hyphen) and –> (en-dash U+2013)
721
791
  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+)(.+)$/
792
+ /^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d+(?:\.\d{1,2})?)(min|[dwmyh])(\?)?\s+(.+)$/
723
793
  );
724
794
  if (durationMatch) {
725
795
  const startDate = durationMatch[1];
@@ -728,9 +798,14 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
728
798
  const unit = durationMatch[3] as 'd' | 'w' | 'm' | 'y' | 'h' | 'min';
729
799
  const endDate = addDurationToDate(startDate, amount, unit);
730
800
  const segments = durationMatch[5].split('|');
731
- const metadata = segments.length > 1
732
- ? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber, MULTIPLE_PIPE_ERROR))
733
- : {};
801
+ const metadata =
802
+ segments.length > 1
803
+ ? parsePipeMetadata(
804
+ ['', ...segments.slice(1)],
805
+ timelineAliasMap,
806
+ () => warn(lineNumber, MULTIPLE_PIPE_ERROR)
807
+ )
808
+ : {};
734
809
  result.timelineEvents.push({
735
810
  date: startDate,
736
811
  endDate,
@@ -747,13 +822,18 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
747
822
  // Also supports YYYY-MM-DD HH:MM in both start and end dates
748
823
  // Accepts both -> (hyphen) and –> (en-dash U+2013)
749
824
  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+)(.+)$/
825
+ /^(\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
826
  );
752
827
  if (rangeMatch) {
753
828
  const segments = rangeMatch[4].split('|');
754
- const metadata = segments.length > 1
755
- ? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber, MULTIPLE_PIPE_ERROR))
756
- : {};
829
+ const metadata =
830
+ segments.length > 1
831
+ ? parsePipeMetadata(
832
+ ['', ...segments.slice(1)],
833
+ timelineAliasMap,
834
+ () => warn(lineNumber, MULTIPLE_PIPE_ERROR)
835
+ )
836
+ : {};
757
837
  result.timelineEvents.push({
758
838
  date: rangeMatch[1],
759
839
  endDate: rangeMatch[2],
@@ -766,15 +846,18 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
766
846
  continue;
767
847
  }
768
848
 
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
- );
849
+ // Point event: 1718 description
850
+ const pointMatch = line.match(/^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s+(.+)$/);
773
851
  if (pointMatch) {
774
852
  const segments = pointMatch[2].split('|');
775
- const metadata = segments.length > 1
776
- ? parsePipeMetadata(['', ...segments.slice(1)], timelineAliasMap, () => warn(lineNumber, MULTIPLE_PIPE_ERROR))
777
- : {};
853
+ const metadata =
854
+ segments.length > 1
855
+ ? parsePipeMetadata(
856
+ ['', ...segments.slice(1)],
857
+ timelineAliasMap,
858
+ () => warn(lineNumber, MULTIPLE_PIPE_ERROR)
859
+ )
860
+ : {};
778
861
  result.timelineEvents.push({
779
862
  date: pointMatch[1],
780
863
  endDate: null,
@@ -799,40 +882,37 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
799
882
  if (s.alias) knownSetRefs.add(s.alias.toLowerCase());
800
883
  }
801
884
 
802
- const segments = line.split('+').map((s) => s.trim()).filter(Boolean);
885
+ const segments = line
886
+ .split('+')
887
+ .map((s) => s.trim())
888
+ .filter(Boolean);
803
889
  if (segments.length >= 2) {
804
890
  // All segments except the last are pure set references
805
891
  const rawSets = segments.slice(0, -1);
806
892
  const lastSeg = segments[segments.length - 1];
807
893
 
808
894
  // For the last segment, extract set reference and optional label.
809
- // Support deprecated colon: "SetRef: Label"
810
- const colonIdx = lastSeg.indexOf(':');
895
+ // Find where the set reference ends and label begins.
896
+ // Try progressively shorter prefixes against known set names/aliases.
897
+ const words = lastSeg.split(/\s+/);
898
+ let matchLen = 0;
899
+ for (let w = words.length; w >= 1; w--) {
900
+ const candidate = words.slice(0, w).join(' ');
901
+ if (knownSetRefs.has(candidate.toLowerCase())) {
902
+ matchLen = w;
903
+ break;
904
+ }
905
+ }
811
906
  let lastSetRef: string;
812
907
  let label: string | null;
813
- if (colonIdx >= 0) {
814
- lastSetRef = lastSeg.substring(0, colonIdx).trim();
815
- label = lastSeg.substring(colonIdx + 1).trim() || null;
908
+ if (matchLen > 0) {
909
+ lastSetRef = words.slice(0, matchLen).join(' ');
910
+ label =
911
+ words.length > matchLen ? words.slice(matchLen).join(' ') : null;
816
912
  } 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
- }
913
+ // No known set matched assume first word is the set ref, rest is label
914
+ lastSetRef = words[0];
915
+ label = words.length > 1 ? words.slice(1).join(' ') : null;
836
916
  }
837
917
  rawSets.push(lastSetRef);
838
918
  result.vennOverlaps.push({ sets: rawSets, label, lineNumber });
@@ -841,7 +921,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
841
921
  }
842
922
 
843
923
  // Set declaration: "Name(color) alias x" / "Name alias x" / "Name(color)" / "Name"
844
- const setDeclMatch = line.match(/^([^(:]+?)(?:\(([^)]+)\))?(?:\s+alias\s+(\S+))?\s*$/i);
924
+ const setDeclMatch = line.match(
925
+ /^([^(:]+?)(?:\(([^)]+)\))?(?:\s+alias\s+(\S+))?\s*$/i
926
+ );
845
927
  if (setDeclMatch) {
846
928
  const name = setDeclMatch[1].trim();
847
929
  const colorName = setDeclMatch[2]?.trim() ?? null;
@@ -849,11 +931,17 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
849
931
  if (colorName) {
850
932
  const resolved = resolveColor(colorName, palette);
851
933
  if (resolved === null) {
852
- warn(lineNumber, `Hex colors are not supported — use named colors (blue, red, green, etc.)`);
934
+ warn(
935
+ lineNumber,
936
+ `Hex colors are not supported — use named colors (blue, red, green, etc.)`
937
+ );
853
938
  } else if (resolved.startsWith('#')) {
854
939
  color = resolved;
855
940
  } else {
856
- warn(lineNumber, `Unknown color "${colorName}" on set "${name}". Using auto-assigned color.`);
941
+ warn(
942
+ lineNumber,
943
+ `Unknown color "${colorName}" on set "${name}". Using auto-assigned color.`
944
+ );
857
945
  }
858
946
  }
859
947
  const alias = setDeclMatch[3]?.trim() ?? null;
@@ -864,8 +952,8 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
864
952
 
865
953
  // Quadrant-specific parsing
866
954
  if (result.type === 'quadrant') {
867
- // x-axis: Low, High — or indented multi-line
868
- const xAxisMatch = line.match(/^x-axis\s*:\s*(.*)/i);
955
+ // x-label Low, High — or indented multi-line
956
+ const xAxisMatch = line.match(/^x-label\s+(.*)/i);
869
957
  if (xAxisMatch) {
870
958
  const val = xAxisMatch[1].trim();
871
959
  let parts: string[];
@@ -883,8 +971,8 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
883
971
  continue;
884
972
  }
885
973
 
886
- // y-axis: Low, High — or indented multi-line
887
- const yAxisMatch = line.match(/^y-axis\s*:\s*(.*)/i);
974
+ // y-label Low, High — or indented multi-line
975
+ const yAxisMatch = line.match(/^y-label\s+(.*)/i);
888
976
  if (yAxisMatch) {
889
977
  const val = yAxisMatch[1].trim();
890
978
  let parts: string[];
@@ -902,9 +990,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
902
990
  continue;
903
991
  }
904
992
 
905
- // Quadrant position labels: top-right: Label (color)
993
+ // Quadrant position labels: top-right Label (color)
906
994
  const quadrantLabelRe =
907
- /^(top-right|top-left|bottom-left|bottom-right)\s*:\s*(.+)/i;
995
+ /^(top-right|top-left|bottom-left|bottom-right)\s+(.+)/i;
908
996
  const quadrantMatch = line.match(quadrantLabelRe);
909
997
  if (quadrantMatch) {
910
998
  const position = quadrantMatch[1].toLowerCase();
@@ -926,9 +1014,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
926
1014
  continue;
927
1015
  }
928
1016
 
929
- // Data points: Label: x, y
1017
+ // Data points: Label x, y
930
1018
  const pointMatch = line.match(
931
- /^(.+?):\s*([0-9]*\.?[0-9]+)\s*,\s*([0-9]*\.?[0-9]+)\s*$/
1019
+ /^(.+?)\s+([0-9]*\.?[0-9]+)\s*,\s*([0-9]*\.?[0-9]+)\s*$/
932
1020
  );
933
1021
  if (pointMatch) {
934
1022
  const label = pointMatch[1].trim();
@@ -957,7 +1045,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
957
1045
  const firstToken = line.substring(0, spaceIdx).toLowerCase();
958
1046
  const restValue = line.substring(spaceIdx + 1).trim();
959
1047
 
960
- if (firstToken === 'chart' && VALID_D3_TYPES.has(restValue.toLowerCase())) {
1048
+ if (
1049
+ firstToken === 'chart' &&
1050
+ VALID_D3_TYPES.has(restValue.toLowerCase())
1051
+ ) {
961
1052
  result.type = restValue.toLowerCase() as ParsedVisualization['type'];
962
1053
  continue;
963
1054
  }
@@ -1009,6 +1100,164 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1009
1100
  }
1010
1101
  }
1011
1102
 
1103
+ // ── Slope chart: period directive + right-scan data rows ──
1104
+ if (result.type === 'slope') {
1105
+ // Period block: indented lines inside `period` block
1106
+ // (blank lines are pre-filtered at loop top, so only non-indented lines close the block)
1107
+ if (inSlopePeriodBlock) {
1108
+ if (indent > 0) {
1109
+ result.periods.push(line);
1110
+ continue;
1111
+ }
1112
+ // Non-indented line → close block, fall through to process normally
1113
+ inSlopePeriodBlock = false;
1114
+ }
1115
+
1116
+ // Period directive: `period Label1 Label2` or bare `period` (block open)
1117
+ // Only accept before data rows start (F4: prevent keyword shadowing labels)
1118
+ if (result.data.length === 0) {
1119
+ const periodMatch = line.match(/^period\b(.*)$/i);
1120
+ if (periodMatch) {
1121
+ if (result.periods.length > 0 && !inSlopePeriodBlock) {
1122
+ // F5: warn on duplicate period directives
1123
+ warn(
1124
+ lineNumber,
1125
+ `Duplicate 'period' directive — periods are already defined`
1126
+ );
1127
+ }
1128
+ const rest = periodMatch[1].trim();
1129
+ if (rest) {
1130
+ // One-line: `period 1715 1725`
1131
+ const periodLabels = rest.split(/\s+/);
1132
+ result.periods.push(...periodLabels);
1133
+ } else {
1134
+ // Block open: bare `period`
1135
+ inSlopePeriodBlock = true;
1136
+ }
1137
+ continue;
1138
+ }
1139
+ }
1140
+
1141
+ // Migration error: bare period line (old syntax — comma-separated, no keyword)
1142
+ // F1: Only fire when ALL comma-separated tokens are short (≤20 chars) and non-empty
1143
+ if (
1144
+ result.periods.length === 0 &&
1145
+ line.includes(',') &&
1146
+ !line.includes(':')
1147
+ ) {
1148
+ const tokens = line
1149
+ .split(',')
1150
+ .map((t) => t.trim())
1151
+ .filter(Boolean);
1152
+ const looksLikePeriods =
1153
+ tokens.length >= 2 && tokens.every((t) => t.length <= 20);
1154
+ if (looksLikePeriods) {
1155
+ return fail(
1156
+ lineNumber,
1157
+ `Period lines require the 'period' keyword — use 'period ${tokens.join(' ')}'`
1158
+ );
1159
+ }
1160
+ }
1161
+
1162
+ // Migration error: old colon syntax in data rows
1163
+ // F2: Only fire when content after colon is predominantly numeric (old "Label: val1, val2" pattern)
1164
+ if (line.includes(':')) {
1165
+ const colonPos = line.indexOf(':');
1166
+ const afterColon = line.substring(colonPos + 1).trim();
1167
+ const numericTokens = afterColon
1168
+ .split(/[,\s]+/)
1169
+ .filter((v) => /^-?\d/.test(v));
1170
+ // Only trigger if most tokens after the colon are numeric (old data pattern)
1171
+ if (numericTokens.length >= 1) {
1172
+ const allTokens = afterColon.split(/[,\s]+/).filter(Boolean);
1173
+ if (numericTokens.length >= allTokens.length * 0.5) {
1174
+ const label = line.substring(0, colonPos).trim();
1175
+ return fail(
1176
+ lineNumber,
1177
+ `Colons are no longer used in slope data rows — use '${label} ${numericTokens.join(' ')}'`
1178
+ );
1179
+ }
1180
+ }
1181
+ }
1182
+
1183
+ // Right-scan data row parsing (requires periods to be known)
1184
+ if (result.periods.length >= 2) {
1185
+ const P = result.periods.length;
1186
+ const tokens = line.split(/\s+/);
1187
+ const values: number[] = [];
1188
+
1189
+ // Scan from right, capped at P values
1190
+ let rightIdx = tokens.length - 1;
1191
+ while (rightIdx >= 0 && values.length < P) {
1192
+ const raw = tokens[rightIdx].replace(/,/g, '');
1193
+ const num = parseFloat(raw);
1194
+ if (!isNaN(num) && /^-?\d/.test(raw)) {
1195
+ values.unshift(num);
1196
+ rightIdx--;
1197
+ } else {
1198
+ break;
1199
+ }
1200
+ }
1201
+
1202
+ if (values.length < P) {
1203
+ warn(
1204
+ lineNumber,
1205
+ `Data row has ${values.length} numeric value(s) but ${P} period(s) are defined — expected ${P} values`
1206
+ );
1207
+ continue;
1208
+ }
1209
+
1210
+ // Remaining left tokens = label
1211
+ const labelTokens = tokens.slice(0, rightIdx + 1);
1212
+ const joinedLabel = labelTokens.join(' ');
1213
+
1214
+ if (!joinedLabel) {
1215
+ warn(
1216
+ lineNumber,
1217
+ `Data row has no label — add a label before the numeric values`
1218
+ );
1219
+ continue;
1220
+ }
1221
+
1222
+ // Color annotation: `Label (color)` → extract color
1223
+ const colorMatch = joinedLabel.match(/^(.+?)\(([^)]+)\)\s*$/);
1224
+ const labelPart = colorMatch ? colorMatch[1].trim() : joinedLabel;
1225
+ const colorPart = colorMatch
1226
+ ? resolveColor(colorMatch[2].trim(), palette)
1227
+ : null;
1228
+
1229
+ if (!labelPart) {
1230
+ warn(
1231
+ lineNumber,
1232
+ `Data row has no label — add a label before the numeric values`
1233
+ );
1234
+ continue;
1235
+ }
1236
+
1237
+ // F3: Warn on purely numeric labels — likely a mistake
1238
+ if (/^\d[\d,.]*$/.test(labelPart)) {
1239
+ warn(
1240
+ lineNumber,
1241
+ `Label '${labelPart}' looks numeric — this may indicate too many values or a missing label`
1242
+ );
1243
+ }
1244
+
1245
+ result.data.push({
1246
+ label: labelPart,
1247
+ values,
1248
+ color: colorPart,
1249
+ lineNumber,
1250
+ });
1251
+ continue;
1252
+ }
1253
+
1254
+ // If we get here in a slope chart, it's an unrecognized line
1255
+ if (firstLineParsed) {
1256
+ warn(lineNumber, `Unexpected line: '${line}'.`);
1257
+ }
1258
+ continue;
1259
+ }
1260
+
1012
1261
  // ── Colon-separated metadata / options (legacy + data lines) ──
1013
1262
  const colonIndex = line.indexOf(':');
1014
1263
 
@@ -1114,9 +1363,14 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1114
1363
  } else if (colonIndex === -1) {
1115
1364
  // Try "word weight" or "multi-word-label weight" space-separated format
1116
1365
  const lastSpace = line.lastIndexOf(' ');
1117
- const maybeWeight = lastSpace >= 0 ? parseFloat(line.substring(lastSpace + 1)) : NaN;
1366
+ const maybeWeight =
1367
+ lastSpace >= 0 ? parseFloat(line.substring(lastSpace + 1)) : NaN;
1118
1368
  if (lastSpace >= 0 && !isNaN(maybeWeight) && maybeWeight > 0) {
1119
- result.words.push({ text: line.substring(0, lastSpace).trim(), weight: maybeWeight, lineNumber });
1369
+ result.words.push({
1370
+ text: line.substring(0, lastSpace).trim(),
1371
+ weight: maybeWeight,
1372
+ lineNumber,
1373
+ });
1120
1374
  } else {
1121
1375
  freeformLines.push(line);
1122
1376
  }
@@ -1127,29 +1381,22 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1127
1381
  continue;
1128
1382
  }
1129
1383
 
1130
- // Period line: comma-separated labels with no colon before first comma
1131
- // e.g., "2020, 2024" or "Q1 2023, Q2 2023, Q3 2023"
1132
- if (
1133
- result.periods.length === 0 &&
1134
- line.includes(',') &&
1135
- !line.includes(':')
1136
- ) {
1137
- const periods = line
1138
- .split(',')
1139
- .map((p) => p.trim())
1140
- .filter(Boolean);
1141
- if (periods.length >= 2) {
1142
- result.periods = periods;
1143
- continue;
1144
- }
1384
+ // Catch-all: nothing matched this line
1385
+ // Skip on first line chart type suggestion is handled post-loop
1386
+ if (firstLineParsed) {
1387
+ warn(lineNumber, `Unexpected line: '${line}'.`);
1145
1388
  }
1146
1389
  }
1147
1390
 
1148
1391
  // Validation
1149
1392
  if (!result.type) {
1150
1393
  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);
1394
+ const firstNonEmpty =
1395
+ lines.find((l) => l.trim() && !l.trim().startsWith('//'))?.trim() ?? '';
1396
+ const hint = suggest(
1397
+ firstNonEmpty.split(/\s/)[0].toLowerCase(),
1398
+ validD3Types
1399
+ );
1153
1400
  let msg = `Unsupported chart type: "${firstNonEmpty.split(/\s/)[0]}". Supported types: ${validD3Types.join(', ')}`;
1154
1401
  if (hint) msg += `. ${hint}`;
1155
1402
  return fail(1, msg);
@@ -1166,7 +1413,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1166
1413
  result.words = tokenizeFreeformText(freeformLines.join(' '));
1167
1414
  }
1168
1415
  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');
1416
+ warn(
1417
+ 1,
1418
+ 'No words found. Add words as "word weight" (space-separated), one per line, or paste freeform text'
1419
+ );
1170
1420
  }
1171
1421
  // Apply max word limit (words are already sorted by weight desc for freeform)
1172
1422
  if (
@@ -1183,12 +1433,18 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1183
1433
 
1184
1434
  if (result.type === 'arc') {
1185
1435
  if (result.links.length === 0) {
1186
- warn(1, 'No links found. Add links as "Source -> Target: weight" (e.g., "Alice -> Bob: 5")');
1436
+ warn(
1437
+ 1,
1438
+ 'No links found. Add links as "Source -> Target weight" (e.g., "Alice -> Bob 5")'
1439
+ );
1187
1440
  }
1188
1441
  // Validate arc ordering vs groups
1189
1442
  if (result.arcNodeGroups.length > 0) {
1190
1443
  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.`);
1444
+ warn(
1445
+ 1,
1446
+ `Cannot use "order ${result.arcOrder}" with [Group] headers. Use "order group" or remove group headers.`
1447
+ );
1192
1448
  result.arcOrder = 'group';
1193
1449
  }
1194
1450
  if (result.arcOrder === 'appearance') {
@@ -1200,15 +1456,19 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1200
1456
 
1201
1457
  if (result.type === 'timeline') {
1202
1458
  if (result.timelineEvents.length === 0) {
1203
- warn(1, 'No events found. Add events as "YYYY: description" or "YYYY->YYYY: description"');
1459
+ warn(
1460
+ 1,
1461
+ 'No events found. Add events as "YYYY: description" or "YYYY->YYYY: description"'
1462
+ );
1204
1463
  }
1205
1464
  // Validate tag values and inject defaults
1206
1465
  if (result.timelineTagGroups.length > 0) {
1207
1466
  validateTagValues(
1208
1467
  result.timelineEvents,
1209
1468
  result.timelineTagGroups,
1210
- (line, msg) => result.diagnostics.push(makeDgmoError(line, msg, 'warning')),
1211
- suggest,
1469
+ (line, msg) =>
1470
+ result.diagnostics.push(makeDgmoError(line, msg, 'warning')),
1471
+ suggest
1212
1472
  );
1213
1473
  for (const group of result.timelineTagGroups) {
1214
1474
  if (!group.defaultValue) continue;
@@ -1226,7 +1486,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1226
1486
 
1227
1487
  if (result.type === 'venn') {
1228
1488
  if (result.vennSets.length < 2) {
1229
- return fail(1, 'At least 2 sets are required. Add set names (e.g., "Apples", "Oranges")');
1489
+ return fail(
1490
+ 1,
1491
+ 'At least 2 sets are required. Add set names (e.g., "Apples", "Oranges")'
1492
+ );
1230
1493
  }
1231
1494
  if (result.vennSets.length > 3) {
1232
1495
  return fail(1, 'Venn diagrams support 2–3 sets');
@@ -1240,7 +1503,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1240
1503
  if (s.alias) aliasLower.set(s.alias.toLowerCase(), s.name);
1241
1504
  }
1242
1505
  const resolveSetRef = (ref: string): string | null =>
1243
- setNameLower.get(ref.toLowerCase()) ?? aliasLower.get(ref.toLowerCase()) ?? null;
1506
+ setNameLower.get(ref.toLowerCase()) ??
1507
+ aliasLower.get(ref.toLowerCase()) ??
1508
+ null;
1244
1509
 
1245
1510
  // Resolve intersection set references; drop invalid ones with a diagnostic
1246
1511
  const validOverlaps: VennOverlap[] = [];
@@ -1250,8 +1515,16 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1250
1515
  for (const ref of ov.sets) {
1251
1516
  const resolved = resolveSetRef(ref);
1252
1517
  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]);
1518
+ result.diagnostics.push(
1519
+ makeDgmoError(
1520
+ ov.lineNumber,
1521
+ `Intersection references unknown set or alias "${ref}"`
1522
+ )
1523
+ );
1524
+ if (!result.error)
1525
+ result.error = formatDgmoError(
1526
+ result.diagnostics[result.diagnostics.length - 1]
1527
+ );
1255
1528
  valid = false;
1256
1529
  break;
1257
1530
  }
@@ -1265,24 +1538,36 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
1265
1538
 
1266
1539
  if (result.type === 'quadrant') {
1267
1540
  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")');
1541
+ warn(
1542
+ 1,
1543
+ 'No data points found. Add points as "Label x, y" (e.g., "Item A 0.5, 0.7")'
1544
+ );
1269
1545
  }
1270
1546
  return result;
1271
1547
  }
1272
1548
 
1273
1549
  // Slope chart validation
1274
1550
  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")');
1551
+ return fail(
1552
+ 1,
1553
+ "Missing 'period' directive. Add 'period 2020 2024' before data rows (minimum 2 periods required)"
1554
+ );
1276
1555
  }
1277
1556
 
1278
1557
  if (result.data.length === 0) {
1279
- warn(1, 'No data lines found. Add data as "Label: value1, value2" (e.g., "Apple: 25, 35")');
1558
+ warn(
1559
+ 1,
1560
+ "No data lines found. Add data as 'Label value1 value2' (e.g., 'Blackbeard 40 4')"
1561
+ );
1280
1562
  }
1281
1563
 
1282
1564
  // Validate value counts match period count — warn and skip mismatched items
1283
1565
  for (const item of result.data) {
1284
1566
  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`);
1567
+ warn(
1568
+ item.lineNumber,
1569
+ `Data item "${item.label}" has ${item.values.length} value(s) but ${result.periods.length} period(s) are defined`
1570
+ );
1286
1571
  }
1287
1572
  }
1288
1573
  result.data = result.data.filter(
@@ -1523,7 +1808,14 @@ export function renderSlopeChart(
1523
1808
  const tooltip = createTooltip(container, palette, isDark);
1524
1809
 
1525
1810
  // Title
1526
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
1811
+ renderChartTitle(
1812
+ svg,
1813
+ title,
1814
+ parsed.titleLineNumber,
1815
+ width,
1816
+ textColor,
1817
+ onClickItem
1818
+ );
1527
1819
 
1528
1820
  // Period column headers
1529
1821
  for (const period of periods) {
@@ -1592,13 +1884,23 @@ export function renderSlopeChart(
1592
1884
  wrappedLines = lines;
1593
1885
  }
1594
1886
  const lineHeight = SLOPE_LABEL_FONT_SIZE * 1.2;
1595
- const labelHeight = labelLineCount === 1
1596
- ? SLOPE_LABEL_FONT_SIZE
1597
- : labelLineCount * lineHeight;
1887
+ const labelHeight =
1888
+ labelLineCount === 1
1889
+ ? SLOPE_LABEL_FONT_SIZE
1890
+ : labelLineCount * lineHeight;
1598
1891
 
1599
1892
  return {
1600
- item, idx, color, firstVal, lastVal, tipHtml,
1601
- lastX, labelText, maxChars, wrappedLines, labelHeight,
1893
+ item,
1894
+ idx,
1895
+ color,
1896
+ firstVal,
1897
+ lastVal,
1898
+ tipHtml,
1899
+ lastX,
1900
+ labelText,
1901
+ maxChars,
1902
+ wrappedLines,
1903
+ labelHeight,
1602
1904
  };
1603
1905
  });
1604
1906
 
@@ -1610,7 +1912,10 @@ export function renderSlopeChart(
1610
1912
  naturalY: yScale(item.values[pi]),
1611
1913
  height: leftLabelHeight,
1612
1914
  }));
1613
- leftLabelCollisions.set(pi, resolveVerticalCollisions(entries, 4, innerHeight));
1915
+ leftLabelCollisions.set(
1916
+ pi,
1917
+ resolveVerticalCollisions(entries, 4, innerHeight)
1918
+ );
1614
1919
  }
1615
1920
 
1616
1921
  // --- Resolve right-side label collisions ---
@@ -1618,7 +1923,11 @@ export function renderSlopeChart(
1618
1923
  naturalY: yScale(si.lastVal),
1619
1924
  height: Math.max(si.labelHeight, SLOPE_LABEL_FONT_SIZE * 1.4),
1620
1925
  }));
1621
- const rightAdjustedY = resolveVerticalCollisions(rightEntries, 4, innerHeight);
1926
+ const rightAdjustedY = resolveVerticalCollisions(
1927
+ rightEntries,
1928
+ 4,
1929
+ innerHeight
1930
+ );
1622
1931
 
1623
1932
  // Render each data series
1624
1933
  data.forEach((item, idx) => {
@@ -1632,7 +1941,8 @@ export function renderSlopeChart(
1632
1941
  .attr('data-line-number', String(item.lineNumber));
1633
1942
 
1634
1943
  // Line
1635
- seriesG.append('path')
1944
+ seriesG
1945
+ .append('path')
1636
1946
  .datum(item.values)
1637
1947
  .attr('fill', 'none')
1638
1948
  .attr('stroke', color)
@@ -1640,7 +1950,8 @@ export function renderSlopeChart(
1640
1950
  .attr('d', lineGen);
1641
1951
 
1642
1952
  // Invisible wider path for easier hover targeting
1643
- seriesG.append('path')
1953
+ seriesG
1954
+ .append('path')
1644
1955
  .datum(item.values)
1645
1956
  .attr('fill', 'none')
1646
1957
  .attr('stroke', 'transparent')
@@ -1664,7 +1975,8 @@ export function renderSlopeChart(
1664
1975
  const y = yScale(val);
1665
1976
 
1666
1977
  // Point circle
1667
- seriesG.append('circle')
1978
+ seriesG
1979
+ .append('circle')
1668
1980
  .attr('cx', x)
1669
1981
  .attr('cy', y)
1670
1982
  .attr('r', 4)
@@ -1688,7 +2000,8 @@ export function renderSlopeChart(
1688
2000
  const isLast = i === periods.length - 1;
1689
2001
  if (!isLast) {
1690
2002
  const adjustedY = leftLabelCollisions.get(i)![idx];
1691
- seriesG.append('text')
2003
+ seriesG
2004
+ .append('text')
1692
2005
  .attr('x', isFirst ? x - 10 : x)
1693
2006
  .attr('y', adjustedY)
1694
2007
  .attr('dy', '0.35em')
@@ -1920,7 +2233,14 @@ export function renderArcDiagram(
1920
2233
  .attr('transform', `translate(${margin.left},${margin.top})`);
1921
2234
 
1922
2235
  // Title
1923
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
2236
+ renderChartTitle(
2237
+ svg,
2238
+ title,
2239
+ parsed.titleLineNumber,
2240
+ width,
2241
+ textColor,
2242
+ onClickItem
2243
+ );
1924
2244
 
1925
2245
  // Build adjacency map for hover interactions
1926
2246
  const neighbors = new Map<string, Set<string>>();
@@ -2097,13 +2417,18 @@ export function renderArcDiagram(
2097
2417
  const y = yScale(node)!;
2098
2418
  const nodeColor = nodeColorMap.get(node) ?? textColor;
2099
2419
  // 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);
2420
+ const nodeLink = links.find(
2421
+ (l) => l.source === node || l.target === node
2422
+ );
2101
2423
 
2102
2424
  const nodeG = g
2103
2425
  .append('g')
2104
2426
  .attr('class', 'arc-node')
2105
2427
  .attr('data-node', node)
2106
- .attr('data-line-number', nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null)
2428
+ .attr(
2429
+ 'data-line-number',
2430
+ nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null
2431
+ )
2107
2432
  .style('cursor', 'pointer')
2108
2433
  .on('mouseenter', () => handleMouseEnter(node))
2109
2434
  .on('mouseleave', handleMouseLeave)
@@ -2232,13 +2557,18 @@ export function renderArcDiagram(
2232
2557
  const x = xScale(node)!;
2233
2558
  const nodeColor = nodeColorMap.get(node) ?? textColor;
2234
2559
  // 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);
2560
+ const nodeLink = links.find(
2561
+ (l) => l.source === node || l.target === node
2562
+ );
2236
2563
 
2237
2564
  const nodeG = g
2238
2565
  .append('g')
2239
2566
  .attr('class', 'arc-node')
2240
2567
  .attr('data-node', node)
2241
- .attr('data-line-number', nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null)
2568
+ .attr(
2569
+ 'data-line-number',
2570
+ nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null
2571
+ )
2242
2572
  .style('cursor', 'pointer')
2243
2573
  .on('mouseenter', () => handleMouseEnter(node))
2244
2574
  .on('mouseleave', handleMouseLeave)
@@ -2633,7 +2963,11 @@ export function computeTimeTicks(
2633
2963
  // Iterate from the start hour boundary
2634
2964
  const startDate = fractionalYearToDate(domainMin);
2635
2965
  // Round down to nearest step boundary
2636
- startDate.setMinutes(Math.floor(startDate.getMinutes() / stepMin) * stepMin, 0, 0);
2966
+ startDate.setMinutes(
2967
+ Math.floor(startDate.getMinutes() / stepMin) * stepMin,
2968
+ 0,
2969
+ 0
2970
+ );
2637
2971
 
2638
2972
  while (true) {
2639
2973
  const val = dateToFractionalYear(startDate);
@@ -2659,7 +2993,12 @@ export function computeTimeTicks(
2659
2993
 
2660
2994
  const startDate = fractionalYearToDate(domainMin);
2661
2995
  // Round down to nearest step boundary
2662
- startDate.setHours(Math.floor(startDate.getHours() / stepHour) * stepHour, 0, 0, 0);
2996
+ startDate.setHours(
2997
+ Math.floor(startDate.getHours() / stepHour) * stepHour,
2998
+ 0,
2999
+ 0,
3000
+ 0
3001
+ );
2663
3002
 
2664
3003
  while (true) {
2665
3004
  const val = dateToFractionalYear(startDate);
@@ -3072,7 +3411,10 @@ export function renderTimeline(
3072
3411
  exportDims?: D3ExportDimensions,
3073
3412
  activeTagGroup?: string | null,
3074
3413
  swimlaneTagGroup?: string | null,
3075
- onTagStateChange?: (activeTagGroup: string | null, swimlaneTagGroup: string | null) => void,
3414
+ onTagStateChange?: (
3415
+ activeTagGroup: string | null,
3416
+ swimlaneTagGroup: string | null
3417
+ ) => void,
3076
3418
  viewMode?: boolean
3077
3419
  ): void {
3078
3420
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
@@ -3091,7 +3433,11 @@ export function renderTimeline(
3091
3433
  if (timelineEvents.length === 0) return;
3092
3434
 
3093
3435
  // When sort: tag is set and no explicit swimlane param, use the default
3094
- if (swimlaneTagGroup == null && timelineSort === 'tag' && parsed.timelineDefaultSwimlaneTG) {
3436
+ if (
3437
+ swimlaneTagGroup == null &&
3438
+ timelineSort === 'tag' &&
3439
+ parsed.timelineDefaultSwimlaneTG
3440
+ ) {
3095
3441
  swimlaneTagGroup = parsed.timelineDefaultSwimlaneTG;
3096
3442
  }
3097
3443
 
@@ -3143,12 +3489,8 @@ export function renderTimeline(
3143
3489
 
3144
3490
  // Order lanes by earliest event date
3145
3491
  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
- );
3492
+ const aMin = Math.min(...a[1].map((e) => parseTimelineDate(e.date)));
3493
+ const bMin = Math.min(...b[1].map((e) => parseTimelineDate(e.date)));
3152
3494
  return aMin - bMin;
3153
3495
  });
3154
3496
 
@@ -3170,7 +3512,11 @@ export function renderTimeline(
3170
3512
  function eventColor(ev: TimelineEvent): string {
3171
3513
  // Tag color takes priority when a tag group is active
3172
3514
  if (effectiveColorTG) {
3173
- const tagColor = resolveTagColor(ev.metadata, parsed.timelineTagGroups, effectiveColorTG);
3515
+ const tagColor = resolveTagColor(
3516
+ ev.metadata,
3517
+ parsed.timelineTagGroups,
3518
+ effectiveColorTG
3519
+ );
3174
3520
  if (tagColor) return tagColor;
3175
3521
  }
3176
3522
  if (ev.group && groupColorMap.has(ev.group)) {
@@ -3281,16 +3627,23 @@ export function renderTimeline(
3281
3627
  el.attr('opacity', val === tagValue ? 1 : FADE_OPACITY);
3282
3628
  });
3283
3629
  g.selectAll<SVGGElement, unknown>('.tl-legend-item, .tl-lane-header').attr(
3284
- 'opacity', FADE_OPACITY
3630
+ 'opacity',
3631
+ FADE_OPACITY
3632
+ );
3633
+ g.selectAll<SVGGElement, unknown>('.tl-marker').attr(
3634
+ 'opacity',
3635
+ FADE_OPACITY
3285
3636
  );
3286
- g.selectAll<SVGGElement, unknown>('.tl-marker').attr('opacity', FADE_OPACITY);
3287
3637
  // Fade legend entry dots/labels that don't match (keep group pill visible)
3288
3638
  g.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry').each(function () {
3289
3639
  const el = d3Selection.select(this);
3290
3640
  const entryValue = el.attr('data-legend-entry');
3291
3641
  if (entryValue === '__group__') return; // keep group pill at full opacity
3292
3642
  const entryGroup = el.attr('data-tag-group');
3293
- el.attr('opacity', entryGroup === tagKey && entryValue === tagValue ? 1 : FADE_OPACITY);
3643
+ el.attr(
3644
+ 'opacity',
3645
+ entryGroup === tagKey && entryValue === tagValue ? 1 : FADE_OPACITY
3646
+ );
3294
3647
  });
3295
3648
  }
3296
3649
 
@@ -3311,7 +3664,8 @@ export function renderTimeline(
3311
3664
  // VERTICAL orientation (time flows top→bottom)
3312
3665
  // ================================================================
3313
3666
  if (isVertical) {
3314
- const useGroupedVertical = tagLanes != null ||
3667
+ const useGroupedVertical =
3668
+ tagLanes != null ||
3315
3669
  (timelineSort === 'group' && timelineGroups.length > 0);
3316
3670
  if (useGroupedVertical) {
3317
3671
  // === GROUPED: one column/lane per group, vertical ===
@@ -3370,7 +3724,14 @@ export function renderTimeline(
3370
3724
  .append('g')
3371
3725
  .attr('transform', `translate(${margin.left},${margin.top})`);
3372
3726
 
3373
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
3727
+ renderChartTitle(
3728
+ svg,
3729
+ title,
3730
+ parsed.titleLineNumber,
3731
+ width,
3732
+ textColor,
3733
+ onClickItem
3734
+ );
3374
3735
 
3375
3736
  renderEras(
3376
3737
  g,
@@ -3620,7 +3981,14 @@ export function renderTimeline(
3620
3981
  .append('g')
3621
3982
  .attr('transform', `translate(${margin.left},${margin.top})`);
3622
3983
 
3623
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
3984
+ renderChartTitle(
3985
+ svg,
3986
+ title,
3987
+ parsed.titleLineNumber,
3988
+ width,
3989
+ textColor,
3990
+ onClickItem
3991
+ );
3624
3992
 
3625
3993
  renderEras(
3626
3994
  g,
@@ -3746,8 +4114,7 @@ export function renderTimeline(
3746
4114
  if (ev.uncertain) {
3747
4115
  const gradientId = `uncertain-v-${ev.lineNumber}`;
3748
4116
  const strokeGradientId = `uncertain-v-s-${ev.lineNumber}`;
3749
- const defs =
3750
- svg.select('defs').node() || svg.append('defs').node();
4117
+ const defs = svg.select('defs').node() || svg.append('defs').node();
3751
4118
  const defsEl = d3Selection.select(defs as Element);
3752
4119
  defsEl
3753
4120
  .append('linearGradient')
@@ -3861,8 +4228,8 @@ export function renderTimeline(
3861
4228
  const BAR_H = 22; // range bar thickness (tall enough for text inside)
3862
4229
  const GROUP_GAP = 12; // vertical gap between group swim-lanes
3863
4230
 
3864
- const useGroupedHorizontal = tagLanes != null ||
3865
- (timelineSort === 'group' && timelineGroups.length > 0);
4231
+ const useGroupedHorizontal =
4232
+ tagLanes != null || (timelineSort === 'group' && timelineGroups.length > 0);
3866
4233
  if (useGroupedHorizontal) {
3867
4234
  // === GROUPED: swim-lanes stacked vertically, events on own rows ===
3868
4235
  let lanes: Lane[];
@@ -3895,7 +4262,11 @@ export function renderTimeline(
3895
4262
  // Group-sorted doesn't need legend space (group names shown on left)
3896
4263
  const baseTopMargin = title ? 50 : 20;
3897
4264
  const margin = {
3898
- top: baseTopMargin + (timelineScale ? 40 : 0) + markerMargin + tagLegendReserve,
4265
+ top:
4266
+ baseTopMargin +
4267
+ (timelineScale ? 40 : 0) +
4268
+ markerMargin +
4269
+ tagLegendReserve,
3899
4270
  right: 40,
3900
4271
  bottom: 40 + scaleMargin,
3901
4272
  left: dynamicLeftMargin,
@@ -3921,7 +4292,14 @@ export function renderTimeline(
3921
4292
  .append('g')
3922
4293
  .attr('transform', `translate(${margin.left},${margin.top})`);
3923
4294
 
3924
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
4295
+ renderChartTitle(
4296
+ svg,
4297
+ title,
4298
+ parsed.titleLineNumber,
4299
+ width,
4300
+ textColor,
4301
+ onClickItem
4302
+ );
3925
4303
 
3926
4304
  renderEras(
3927
4305
  g,
@@ -4222,7 +4600,14 @@ export function renderTimeline(
4222
4600
  .append('g')
4223
4601
  .attr('transform', `translate(${margin.left},${margin.top})`);
4224
4602
 
4225
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
4603
+ renderChartTitle(
4604
+ svg,
4605
+ title,
4606
+ parsed.titleLineNumber,
4607
+ width,
4608
+ textColor,
4609
+ onClickItem
4610
+ );
4226
4611
 
4227
4612
  renderEras(
4228
4613
  g,
@@ -4500,13 +4885,17 @@ export function renderTimeline(
4500
4885
  expandedWidth: number;
4501
4886
  };
4502
4887
  const legendGroups: LegendGroup[] = parsed.timelineTagGroups.map((g) => {
4503
- const pillW = measureLegendText(g.name, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
4888
+ const pillW =
4889
+ measureLegendText(g.name, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
4504
4890
  // Expanded: pill + icon (unless viewMode) + entries
4505
4891
  const iconSpace = viewMode ? 8 : LG_ICON_W + 4;
4506
4892
  let entryX = LG_CAPSULE_PAD + pillW + iconSpace;
4507
4893
  for (const entry of g.entries) {
4508
4894
  const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
4509
- entryX = textX + measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) + LG_ENTRY_TRAIL;
4895
+ entryX =
4896
+ textX +
4897
+ measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) +
4898
+ LG_ENTRY_TRAIL;
4510
4899
  }
4511
4900
  return {
4512
4901
  group: g,
@@ -4526,7 +4915,8 @@ export function renderTimeline(
4526
4915
  y: number,
4527
4916
  isSwimActive: boolean
4528
4917
  ) {
4529
- const iconG = parent.append('g')
4918
+ const iconG = parent
4919
+ .append('g')
4530
4920
  .attr('class', 'tl-swimlane-icon')
4531
4921
  .attr('transform', `translate(${x}, ${y})`)
4532
4922
  .style('cursor', 'pointer');
@@ -4539,7 +4929,8 @@ export function renderTimeline(
4539
4929
  { y: 8, w: 6 },
4540
4930
  ];
4541
4931
  for (const bar of bars) {
4542
- iconG.append('rect')
4932
+ iconG
4933
+ .append('rect')
4543
4934
  .attr('x', 0)
4544
4935
  .attr('y', bar.y)
4545
4936
  .attr('width', bar.w)
@@ -4554,8 +4945,16 @@ export function renderTimeline(
4554
4945
  /** Full re-render with updated swimlane state */
4555
4946
  function relayout() {
4556
4947
  renderTimeline(
4557
- container, parsed, palette, isDark, onClickItem, exportDims,
4558
- currentActiveGroup, currentSwimlaneGroup, onTagStateChange, viewMode
4948
+ container,
4949
+ parsed,
4950
+ palette,
4951
+ isDark,
4952
+ onClickItem,
4953
+ exportDims,
4954
+ currentActiveGroup,
4955
+ currentSwimlaneGroup,
4956
+ onTagStateChange,
4957
+ viewMode
4559
4958
  );
4560
4959
  }
4561
4960
 
@@ -4565,7 +4964,8 @@ export function renderTimeline(
4565
4964
  mainSvg.selectAll('.tl-tag-legend-container').remove();
4566
4965
 
4567
4966
  // Effective color source: explicit color group > swimlane group
4568
- const effectiveColorKey = (currentActiveGroup ?? currentSwimlaneGroup)?.toLowerCase() ?? null;
4967
+ const effectiveColorKey =
4968
+ (currentActiveGroup ?? currentSwimlaneGroup)?.toLowerCase() ?? null;
4569
4969
 
4570
4970
  // In view mode, only show the color-driving tag group (expanded, non-interactive).
4571
4971
  // Skip the swimlane group if it's separate from the color group (lane headers already label it).
@@ -4580,32 +4980,43 @@ export function renderTimeline(
4580
4980
  if (visibleGroups.length === 0) return;
4581
4981
 
4582
4982
  // 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;
4983
+ const totalW =
4984
+ visibleGroups.reduce((s, lg) => {
4985
+ const isActive =
4986
+ viewMode ||
4987
+ (currentActiveGroup != null &&
4988
+ lg.group.name.toLowerCase() ===
4989
+ currentActiveGroup.toLowerCase());
4990
+ return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
4991
+ }, 0) +
4992
+ (visibleGroups.length - 1) * LG_GROUP_GAP;
4589
4993
 
4590
4994
  let cx = (width - totalW) / 2;
4591
4995
 
4592
4996
  // Legend container for data-legend-active attribute
4593
- const legendContainer = mainSvg.append('g')
4997
+ const legendContainer = mainSvg
4998
+ .append('g')
4594
4999
  .attr('class', 'tl-tag-legend-container');
4595
5000
  if (currentActiveGroup) {
4596
- legendContainer.attr('data-legend-active', currentActiveGroup.toLowerCase());
5001
+ legendContainer.attr(
5002
+ 'data-legend-active',
5003
+ currentActiveGroup.toLowerCase()
5004
+ );
4597
5005
  }
4598
5006
 
4599
5007
  for (const lg of visibleGroups) {
4600
5008
  const groupKey = lg.group.name.toLowerCase();
4601
- const isActive = viewMode ||
5009
+ const isActive =
5010
+ viewMode ||
4602
5011
  (currentActiveGroup != null &&
4603
5012
  currentActiveGroup.toLowerCase() === groupKey);
4604
- const isSwimActive = currentSwimlaneGroup != null &&
5013
+ const isSwimActive =
5014
+ currentSwimlaneGroup != null &&
4605
5015
  currentSwimlaneGroup.toLowerCase() === groupKey;
4606
5016
 
4607
5017
  const pillLabel = lg.group.name;
4608
- const pillWidth = measureLegendText(pillLabel, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
5018
+ const pillWidth =
5019
+ measureLegendText(pillLabel, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
4609
5020
 
4610
5021
  const gEl = legendContainer
4611
5022
  .append('g')
@@ -4616,19 +5027,19 @@ export function renderTimeline(
4616
5027
  .attr('data-legend-entry', '__group__');
4617
5028
 
4618
5029
  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
- });
5030
+ gEl.style('cursor', 'pointer').on('click', () => {
5031
+ currentActiveGroup =
5032
+ currentActiveGroup === groupKey ? null : groupKey;
5033
+ drawLegend();
5034
+ recolorEvents();
5035
+ onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
5036
+ });
4627
5037
  }
4628
5038
 
4629
5039
  // Outer capsule background (active only)
4630
5040
  if (isActive) {
4631
- gEl.append('rect')
5041
+ gEl
5042
+ .append('rect')
4632
5043
  .attr('width', lg.expandedWidth)
4633
5044
  .attr('height', LG_HEIGHT)
4634
5045
  .attr('rx', LG_HEIGHT / 2)
@@ -4640,7 +5051,8 @@ export function renderTimeline(
4640
5051
  const pillH = LG_HEIGHT - (isActive ? LG_CAPSULE_PAD * 2 : 0);
4641
5052
 
4642
5053
  // Pill background
4643
- gEl.append('rect')
5054
+ gEl
5055
+ .append('rect')
4644
5056
  .attr('x', pillXOff)
4645
5057
  .attr('y', pillYOff)
4646
5058
  .attr('width', pillWidth)
@@ -4650,7 +5062,8 @@ export function renderTimeline(
4650
5062
 
4651
5063
  // Active pill border
4652
5064
  if (isActive) {
4653
- gEl.append('rect')
5065
+ gEl
5066
+ .append('rect')
4654
5067
  .attr('x', pillXOff)
4655
5068
  .attr('y', pillYOff)
4656
5069
  .attr('width', pillWidth)
@@ -4662,7 +5075,8 @@ export function renderTimeline(
4662
5075
  }
4663
5076
 
4664
5077
  // Pill text
4665
- gEl.append('text')
5078
+ gEl
5079
+ .append('text')
4666
5080
  .attr('x', pillXOff + pillWidth / 2)
4667
5081
  .attr('y', LG_HEIGHT / 2 + LG_PILL_FONT_SIZE / 2 - 2)
4668
5082
  .attr('font-size', LG_PILL_FONT_SIZE)
@@ -4684,7 +5098,8 @@ export function renderTimeline(
4684
5098
  .attr('data-swimlane-toggle', groupKey)
4685
5099
  .on('click', (event: MouseEvent) => {
4686
5100
  event.stopPropagation();
4687
- currentSwimlaneGroup = currentSwimlaneGroup === groupKey ? null : groupKey;
5101
+ currentSwimlaneGroup =
5102
+ currentSwimlaneGroup === groupKey ? null : groupKey;
4688
5103
  onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
4689
5104
  relayout();
4690
5105
  });
@@ -4697,7 +5112,8 @@ export function renderTimeline(
4697
5112
  const tagKey = lg.group.name.toLowerCase();
4698
5113
  const tagVal = entry.value.toLowerCase();
4699
5114
 
4700
- const entryG = gEl.append('g')
5115
+ const entryG = gEl
5116
+ .append('g')
4701
5117
  .attr('class', 'tl-tag-legend-entry')
4702
5118
  .attr('data-tag-group', tagKey)
4703
5119
  .attr('data-legend-entry', tagVal);
@@ -4708,18 +5124,24 @@ export function renderTimeline(
4708
5124
  .on('mouseenter', (event: MouseEvent) => {
4709
5125
  event.stopPropagation();
4710
5126
  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
- });
5127
+ mainSvg
5128
+ .selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
5129
+ .each(function () {
5130
+ const el = d3Selection.select(this);
5131
+ const ev = el.attr('data-legend-entry');
5132
+ if (ev === '__group__') return;
5133
+ const eg = el.attr('data-tag-group');
5134
+ el.attr(
5135
+ 'opacity',
5136
+ eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY
5137
+ );
5138
+ });
4718
5139
  })
4719
5140
  .on('mouseleave', (event: MouseEvent) => {
4720
5141
  event.stopPropagation();
4721
5142
  fadeReset(mainG);
4722
- mainSvg.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
5143
+ mainSvg
5144
+ .selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
4723
5145
  .attr('opacity', 1);
4724
5146
  })
4725
5147
  .on('click', (event: MouseEvent) => {
@@ -4727,14 +5149,16 @@ export function renderTimeline(
4727
5149
  });
4728
5150
  }
4729
5151
 
4730
- entryG.append('circle')
5152
+ entryG
5153
+ .append('circle')
4731
5154
  .attr('cx', entryX + LG_DOT_R)
4732
5155
  .attr('cy', LG_HEIGHT / 2)
4733
5156
  .attr('r', LG_DOT_R)
4734
5157
  .attr('fill', entry.color);
4735
5158
 
4736
5159
  const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
4737
- entryG.append('text')
5160
+ entryG
5161
+ .append('text')
4738
5162
  .attr('x', textX)
4739
5163
  .attr('y', LG_HEIGHT / 2 + LG_ENTRY_FONT_SIZE / 2 - 1)
4740
5164
  .attr('font-size', LG_ENTRY_FONT_SIZE)
@@ -4742,7 +5166,10 @@ export function renderTimeline(
4742
5166
  .attr('fill', palette.textMuted)
4743
5167
  .text(entry.value);
4744
5168
 
4745
- entryX = textX + measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) + LG_ENTRY_TRAIL;
5169
+ entryX =
5170
+ textX +
5171
+ measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) +
5172
+ LG_ENTRY_TRAIL;
4746
5173
  }
4747
5174
  }
4748
5175
 
@@ -4767,16 +5194,27 @@ export function renderTimeline(
4767
5194
  let color: string;
4768
5195
  if (colorTG) {
4769
5196
  const tagColor = resolveTagColor(
4770
- ev.metadata, parsed.timelineTagGroups, colorTG
5197
+ ev.metadata,
5198
+ parsed.timelineTagGroups,
5199
+ colorTG
4771
5200
  );
4772
- color = tagColor ?? (ev.group && groupColorMap.has(ev.group)
4773
- ? groupColorMap.get(ev.group)! : textColor);
5201
+ color =
5202
+ tagColor ??
5203
+ (ev.group && groupColorMap.has(ev.group)
5204
+ ? groupColorMap.get(ev.group)!
5205
+ : textColor);
4774
5206
  } else {
4775
- color = ev.group && groupColorMap.has(ev.group)
4776
- ? groupColorMap.get(ev.group)! : textColor;
5207
+ color =
5208
+ ev.group && groupColorMap.has(ev.group)
5209
+ ? groupColorMap.get(ev.group)!
5210
+ : textColor;
4777
5211
  }
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);
5212
+ el.selectAll('rect')
5213
+ .attr('fill', mix(color, bg, 30))
5214
+ .attr('stroke', color);
5215
+ el.selectAll('circle:not(.tl-event-point-outline)')
5216
+ .attr('fill', mix(color, bg, 30))
5217
+ .attr('stroke', color);
4780
5218
  });
4781
5219
  }
4782
5220
 
@@ -4833,7 +5271,14 @@ export function renderWordCloud(
4833
5271
 
4834
5272
  const rotateFn = getRotateFn(cloudOptions.rotate);
4835
5273
 
4836
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
5274
+ renderChartTitle(
5275
+ svg,
5276
+ title,
5277
+ parsed.titleLineNumber,
5278
+ width,
5279
+ textColor,
5280
+ onClickItem
5281
+ );
4837
5282
 
4838
5283
  const g = svg
4839
5284
  .append('g')
@@ -5140,7 +5585,8 @@ export function renderVenn(
5140
5585
  const labelTextPad = 4;
5141
5586
 
5142
5587
  for (let i = 0; i < n; i++) {
5143
- const estimatedWidth = vennSets[i].name.length * 8.5 + stubLen + edgePad + labelTextPad;
5588
+ const estimatedWidth =
5589
+ vennSets[i].name.length * 8.5 + stubLen + edgePad + labelTextPad;
5144
5590
  const dx = rawCircles[i].x - clusterCx;
5145
5591
  const dy = rawCircles[i].y - clusterCy;
5146
5592
  if (Math.abs(dx) >= Math.abs(dy)) {
@@ -5167,13 +5613,27 @@ export function renderVenn(
5167
5613
  const scaledR = circles[0].r;
5168
5614
 
5169
5615
  // Suppress WebKit focus ring on interactive SVG elements
5170
- svg.append('style').text('circle:focus, circle:focus-visible { outline: none !important; }');
5616
+ svg
5617
+ .append('style')
5618
+ .text('circle:focus, circle:focus-visible { outline: none !important; }');
5171
5619
 
5172
5620
  // Title
5173
- renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
5621
+ renderChartTitle(
5622
+ svg,
5623
+ title,
5624
+ parsed.titleLineNumber,
5625
+ width,
5626
+ textColor,
5627
+ onClickItem
5628
+ );
5174
5629
 
5175
5630
  // ── Semi-transparent filled circles (non-interactive) ──
5176
- const circleEls: d3Selection.Selection<SVGCircleElement, unknown, null, undefined>[] = [];
5631
+ const circleEls: d3Selection.Selection<
5632
+ SVGCircleElement,
5633
+ unknown,
5634
+ null,
5635
+ undefined
5636
+ >[] = [];
5177
5637
  const circleGroup = svg.append('g');
5178
5638
  circles.forEach((c, i) => {
5179
5639
  const el = circleGroup
@@ -5200,10 +5660,13 @@ export function renderVenn(
5200
5660
 
5201
5661
  // Individual circle clipPaths
5202
5662
  circles.forEach((c, i) => {
5203
- defs.append('clipPath')
5663
+ defs
5664
+ .append('clipPath')
5204
5665
  .attr('id', `vcp-${i}`)
5205
5666
  .append('circle')
5206
- .attr('cx', c.x).attr('cy', c.y).attr('r', c.r);
5667
+ .attr('cx', c.x)
5668
+ .attr('cy', c.y)
5669
+ .attr('r', c.r);
5207
5670
  });
5208
5671
 
5209
5672
  // All region index-sets: exclusive then intersection subsets
@@ -5215,57 +5678,79 @@ export function renderVenn(
5215
5678
  }
5216
5679
 
5217
5680
  const overlayGroup = svg.append('g').style('pointer-events', 'none');
5218
- const overlayEls = new Map<string, d3Selection.Selection<SVGRectElement, unknown, null, undefined>>();
5681
+ const overlayEls = new Map<
5682
+ string,
5683
+ d3Selection.Selection<SVGRectElement, unknown, null, undefined>
5684
+ >();
5219
5685
 
5220
5686
  for (const idxs of regionIdxSets) {
5221
5687
  const key = idxs.join('-');
5222
- const excluded = Array.from({ length: n }, (_, j) => j).filter(j => !idxs.includes(j));
5688
+ const excluded = Array.from({ length: n }, (_, j) => j).filter(
5689
+ (j) => !idxs.includes(j)
5690
+ );
5223
5691
 
5224
5692
  // Build nested clipPath for intersection of all idxs
5225
5693
  let clipId = `vcp-${idxs[0]}`;
5226
5694
  for (let k = 1; k < idxs.length; k++) {
5227
5695
  const nestedId = `vcp-n-${idxs.slice(0, k + 1).join('-')}`;
5228
5696
  const ci = idxs[k];
5229
- defs.append('clipPath')
5697
+ defs
5698
+ .append('clipPath')
5230
5699
  .attr('id', nestedId)
5231
5700
  .append('circle')
5232
- .attr('cx', circles[ci].x).attr('cy', circles[ci].y).attr('r', circles[ci].r)
5701
+ .attr('cx', circles[ci].x)
5702
+ .attr('cy', circles[ci].y)
5703
+ .attr('r', circles[ci].r)
5233
5704
  .attr('clip-path', `url(#${clipId})`);
5234
5705
  clipId = nestedId;
5235
5706
  }
5236
5707
 
5237
5708
  // Determine line number for this region (for editor sync)
5238
- let regionLineNumber: number | null = null;
5709
+ let regionLineNumber: number | null = null; // eslint-disable-line no-useless-assignment
5239
5710
  if (idxs.length === 1) {
5240
5711
  regionLineNumber = vennSets[idxs[0]].lineNumber;
5241
5712
  } else {
5242
- const sortedNames = idxs.map(i => vennSets[i].name).sort();
5713
+ const sortedNames = idxs.map((i) => vennSets[i].name).sort();
5243
5714
  const ov = vennOverlaps.find(
5244
- (o) => o.sets.length === sortedNames.length && o.sets.every((s, k) => s === sortedNames[k])
5715
+ (o) =>
5716
+ o.sets.length === sortedNames.length &&
5717
+ o.sets.every((s, k) => s === sortedNames[k])
5245
5718
  );
5246
5719
  regionLineNumber = ov?.lineNumber ?? null;
5247
5720
  }
5248
5721
 
5249
- const el = overlayGroup.append('rect')
5250
- .attr('x', 0).attr('y', 0)
5251
- .attr('width', width).attr('height', height)
5722
+ const el = overlayGroup
5723
+ .append('rect')
5724
+ .attr('x', 0)
5725
+ .attr('y', 0)
5726
+ .attr('width', width)
5727
+ .attr('height', height)
5252
5728
  .attr('fill', 'white')
5253
5729
  .attr('fill-opacity', 0)
5254
5730
  .attr('class', 'venn-region-overlay')
5255
- .attr('data-line-number', regionLineNumber != null ? String(regionLineNumber) : '0')
5731
+ .attr(
5732
+ 'data-line-number',
5733
+ regionLineNumber != null ? String(regionLineNumber) : '0'
5734
+ )
5256
5735
  .attr('clip-path', `url(#${clipId})`);
5257
5736
 
5258
5737
  if (excluded.length > 0) {
5259
5738
  // Mask subtracts excluded circles so only the exact region shape highlights
5260
5739
  const maskId = `vvm-${key}`;
5261
5740
  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)
5741
+ mask
5742
+ .append('rect')
5743
+ .attr('x', 0)
5744
+ .attr('y', 0)
5745
+ .attr('width', width)
5746
+ .attr('height', height)
5265
5747
  .attr('fill', 'white');
5266
5748
  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)
5749
+ mask
5750
+ .append('circle')
5751
+ .attr('cx', circles[j].x)
5752
+ .attr('cy', circles[j].y)
5753
+ .attr('r', circles[j].r)
5269
5754
  .attr('fill', 'black');
5270
5755
  }
5271
5756
  el.attr('mask', `url(#${maskId})`);
@@ -5276,10 +5761,12 @@ export function renderVenn(
5276
5761
 
5277
5762
  const showRegionOverlay = (idxs: number[]) => {
5278
5763
  const key = [...idxs].sort((a, b) => a - b).join('-');
5279
- overlayEls.forEach((el, k) => el.attr('fill-opacity', k === key ? 0 : 0.55));
5764
+ overlayEls.forEach((el, k) =>
5765
+ el.attr('fill-opacity', k === key ? 0 : 0.55)
5766
+ );
5280
5767
  };
5281
5768
  const hideAllOverlays = () => {
5282
- overlayEls.forEach(el => el.attr('fill-opacity', 0));
5769
+ overlayEls.forEach((el) => el.attr('fill-opacity', 0));
5283
5770
  };
5284
5771
 
5285
5772
  // ── Labels ──
@@ -5288,7 +5775,9 @@ export function renderVenn(
5288
5775
 
5289
5776
  function exclusiveHSpan(px: number, py: number, ci: number): number {
5290
5777
  const dy = py - circles[ci].y;
5291
- const halfChord = Math.sqrt(Math.max(0, circles[ci].r * circles[ci].r - dy * dy));
5778
+ const halfChord = Math.sqrt(
5779
+ Math.max(0, circles[ci].r * circles[ci].r - dy * dy)
5780
+ );
5292
5781
  let left = circles[ci].x - halfChord;
5293
5782
  let right = circles[ci].x + halfChord;
5294
5783
  for (let j = 0; j < n; j++) {
@@ -5319,11 +5808,14 @@ export function renderVenn(
5319
5808
  const centroid = regionCentroid(circles, inside);
5320
5809
 
5321
5810
  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)));
5811
+ const fitFont = Math.min(
5812
+ MAX_FONT,
5813
+ Math.max(MIN_FONT, (availW - INTERNAL_PAD * 2) / (text.length * CH_RATIO))
5814
+ );
5324
5815
  const estTextW = text.length * CH_RATIO * fitFont;
5325
5816
 
5326
- const fitsInside = estTextW + INTERNAL_PAD * 2 < availW &&
5817
+ const fitsInside =
5818
+ estTextW + INTERNAL_PAD * 2 < availW &&
5327
5819
  pointInCircle({ x: centroid.x, y: centroid.y - fitFont / 2 }, c) &&
5328
5820
  pointInCircle({ x: centroid.x, y: centroid.y + fitFont / 2 }, c);
5329
5821
 
@@ -5342,7 +5834,13 @@ export function renderVenn(
5342
5834
  let dx = c.x - gcx;
5343
5835
  let dy = c.y - gcy;
5344
5836
  const mag = Math.sqrt(dx * dx + dy * dy);
5345
- if (mag < 1e-6) { dx = 1; dy = 0; } else { dx /= mag; dy /= mag; }
5837
+ if (mag < 1e-6) {
5838
+ dx = 1;
5839
+ dy = 0;
5840
+ } else {
5841
+ dx /= mag;
5842
+ dy /= mag;
5843
+ }
5346
5844
 
5347
5845
  const exitX = c.x + dx * c.r;
5348
5846
  const exitY = c.y + dy * c.r;
@@ -5353,8 +5851,10 @@ export function renderVenn(
5353
5851
 
5354
5852
  labelGroup
5355
5853
  .append('line')
5356
- .attr('x1', edgeX).attr('y1', edgeY)
5357
- .attr('x2', stubEndX).attr('y2', stubEndY)
5854
+ .attr('x1', edgeX)
5855
+ .attr('y1', edgeY)
5856
+ .attr('x2', stubEndX)
5857
+ .attr('y2', stubEndY)
5358
5858
  .attr('stroke', textColor)
5359
5859
  .attr('stroke-width', 1);
5360
5860
 
@@ -5381,7 +5881,8 @@ export function renderVenn(
5381
5881
 
5382
5882
  // ── Overlap labels (inline at region centroid) ──
5383
5883
  function overlapHSpan(py: number, idxs: number[]): number {
5384
- let left = -Infinity, right = Infinity;
5884
+ let left = -Infinity,
5885
+ right = Infinity;
5385
5886
  for (const ci of idxs) {
5386
5887
  const dy = py - circles[ci].y;
5387
5888
  if (Math.abs(dy) >= circles[ci].r) return 0;
@@ -5411,8 +5912,13 @@ export function renderVenn(
5411
5912
  const inside = circles.map((_, j) => idxs.includes(j));
5412
5913
  const centroid = regionCentroid(circles, inside);
5413
5914
  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)));
5915
+ const fitFont = Math.min(
5916
+ MAX_FONT,
5917
+ Math.max(
5918
+ MIN_FONT,
5919
+ (availW - INTERNAL_PAD * 2) / (ov.label.length * CH_RATIO)
5920
+ )
5921
+ );
5416
5922
  labelGroup
5417
5923
  .append('text')
5418
5924
  .attr('x', centroid.x)
@@ -5441,11 +5947,16 @@ export function renderVenn(
5441
5947
  .attr('data-line-number', String(vennSets[i].lineNumber))
5442
5948
  .style('cursor', onClickItem ? 'pointer' : 'default')
5443
5949
  .style('outline', 'none')
5444
- .on('mouseenter', () => { showRegionOverlay([i]); })
5445
- .on('mouseleave', () => { hideAllOverlays(); })
5950
+ .on('mouseenter', () => {
5951
+ showRegionOverlay([i]);
5952
+ })
5953
+ .on('mouseleave', () => {
5954
+ hideAllOverlays();
5955
+ })
5446
5956
  .on('click', function () {
5447
5957
  (this as SVGElement).blur?.();
5448
- if (onClickItem && vennSets[i].lineNumber) onClickItem(vennSets[i].lineNumber);
5958
+ if (onClickItem && vennSets[i].lineNumber)
5959
+ onClickItem(vennSets[i].lineNumber);
5449
5960
  });
5450
5961
  });
5451
5962
 
@@ -5454,14 +5965,23 @@ export function renderVenn(
5454
5965
 
5455
5966
  const subsets: { idxs: number[]; sets: string[] }[] = [];
5456
5967
  if (n === 2) {
5457
- subsets.push({ idxs: [0, 1], sets: [vennSets[0].name, vennSets[1].name].sort() });
5968
+ subsets.push({
5969
+ idxs: [0, 1],
5970
+ sets: [vennSets[0].name, vennSets[1].name].sort(),
5971
+ });
5458
5972
  } else {
5459
5973
  for (let a = 0; a < n; a++) {
5460
5974
  for (let b = a + 1; b < n; b++) {
5461
- subsets.push({ idxs: [a, b], sets: [vennSets[a].name, vennSets[b].name].sort() });
5975
+ subsets.push({
5976
+ idxs: [a, b],
5977
+ sets: [vennSets[a].name, vennSets[b].name].sort(),
5978
+ });
5462
5979
  }
5463
5980
  }
5464
- subsets.push({ idxs: [0, 1, 2], sets: [vennSets[0].name, vennSets[1].name, vennSets[2].name].sort() });
5981
+ subsets.push({
5982
+ idxs: [0, 1, 2],
5983
+ sets: [vennSets[0].name, vennSets[1].name, vennSets[2].name].sort(),
5984
+ });
5465
5985
  }
5466
5986
 
5467
5987
  for (const subset of subsets) {
@@ -5469,7 +5989,8 @@ export function renderVenn(
5469
5989
  const inside = circles.map((_, j) => idxs.includes(j));
5470
5990
  const centroid = regionCentroid(circles, inside);
5471
5991
  const declaredOv = vennOverlaps.find(
5472
- (ov) => ov.sets.length === sets.length && ov.sets.every((s, k) => s === sets[k])
5992
+ (ov) =>
5993
+ ov.sets.length === sets.length && ov.sets.every((s, k) => s === sets[k])
5473
5994
  );
5474
5995
  hoverGroup
5475
5996
  .append('circle')
@@ -5482,8 +6003,12 @@ export function renderVenn(
5482
6003
  .attr('data-line-number', declaredOv ? String(declaredOv.lineNumber) : '')
5483
6004
  .style('cursor', onClickItem && declaredOv ? 'pointer' : 'default')
5484
6005
  .style('outline', 'none')
5485
- .on('mouseenter', () => { showRegionOverlay(idxs); })
5486
- .on('mouseleave', () => { hideAllOverlays(); })
6006
+ .on('mouseenter', () => {
6007
+ showRegionOverlay(idxs);
6008
+ })
6009
+ .on('mouseleave', () => {
6010
+ hideAllOverlays();
6011
+ })
5487
6012
  .on('click', function () {
5488
6013
  (this as SVGElement).blur?.();
5489
6014
  if (onClickItem && declaredOv) onClickItem(declaredOv.lineNumber);
@@ -5542,7 +6067,12 @@ export function renderQuadrant(
5542
6067
  // Margins
5543
6068
  const hasXAxis = !!quadrantXAxis;
5544
6069
  const hasYAxis = !!quadrantYAxis;
5545
- const margin = { top: title ? 60 : 30, right: 30, bottom: hasXAxis ? 70 : 40, left: hasYAxis ? 80 : 40 };
6070
+ const margin = {
6071
+ top: title ? 60 : 30,
6072
+ right: 30,
6073
+ bottom: hasXAxis ? 70 : 40,
6074
+ left: hasYAxis ? 80 : 40,
6075
+ };
5546
6076
  const chartWidth = width - margin.left - margin.right;
5547
6077
  const chartHeight = height - margin.top - margin.bottom;
5548
6078
 
@@ -5554,7 +6084,14 @@ export function renderQuadrant(
5554
6084
  const tooltip = createTooltip(container, palette, isDark);
5555
6085
 
5556
6086
  // Title
5557
- renderChartTitle(svg, title, quadrantTitleLineNumber, width, textColor, onClickItem);
6087
+ renderChartTitle(
6088
+ svg,
6089
+ title,
6090
+ quadrantTitleLineNumber,
6091
+ width,
6092
+ textColor,
6093
+ onClickItem
6094
+ );
5558
6095
 
5559
6096
  // Chart group (translated by margins)
5560
6097
  const chartG = svg
@@ -5565,12 +6102,21 @@ export function renderQuadrant(
5565
6102
  const mixHex = (a: string, b: string, pct: number): string => {
5566
6103
  const parse = (h: string) => {
5567
6104
  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)];
6105
+ const f = r.length === 3 ? r[0] + r[0] + r[1] + r[1] + r[2] + r[2] : r;
6106
+ return [
6107
+ parseInt(f.substring(0, 2), 16),
6108
+ parseInt(f.substring(2, 4), 16),
6109
+ parseInt(f.substring(4, 6), 16),
6110
+ ];
5570
6111
  };
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)}`;
6112
+ const [ar, ag, ab] = parse(a),
6113
+ [br, bg, bb] = parse(b),
6114
+ t = pct / 100;
6115
+ const c = (x: number, y: number) =>
6116
+ Math.round(x * t + y * (1 - t))
6117
+ .toString(16)
6118
+ .padStart(2, '0');
6119
+ return `#${c(ar, br)}${c(ag, bg)}${c(ab, bb)}`;
5574
6120
  };
5575
6121
 
5576
6122
  const bg = isDark ? palette.surface : palette.bg;
@@ -5687,7 +6233,11 @@ export function renderQuadrant(
5687
6233
  fontSize: number;
5688
6234
  }
5689
6235
 
5690
- const quadrantLabelLayout = (text: string, qw: number, qh: number): QuadrantLabelLayout => {
6236
+ const quadrantLabelLayout = (
6237
+ text: string,
6238
+ qw: number,
6239
+ qh: number
6240
+ ): QuadrantLabelLayout => {
5691
6241
  const availW = qw - LABEL_PAD;
5692
6242
  const availH = qh - LABEL_PAD;
5693
6243
  const words = text.split(/\s+/);
@@ -5695,7 +6245,10 @@ export function renderQuadrant(
5695
6245
  // Try single line first
5696
6246
  if (estTextWidth(text, LABEL_MAX_FONT) <= availW) {
5697
6247
  const fs = Math.min(LABEL_MAX_FONT, availH);
5698
- return { lines: [text], fontSize: Math.max(LABEL_MIN_FONT, Math.round(fs)) };
6248
+ return {
6249
+ lines: [text],
6250
+ fontSize: Math.max(LABEL_MIN_FONT, Math.round(fs)),
6251
+ };
5699
6252
  }
5700
6253
 
5701
6254
  // Try wrapping into 2+ lines: greedily pack words so each line fits availW
@@ -5742,7 +6295,10 @@ export function renderQuadrant(
5742
6295
  const qh = chartHeight / 2;
5743
6296
  const quadrantDefsWithLabel = quadrantDefs.filter((d) => d.label !== null);
5744
6297
  const labelLayouts = new Map(
5745
- quadrantDefsWithLabel.map((d) => [d.label!.text, quadrantLabelLayout(d.label!.text, qw, qh)])
6298
+ quadrantDefsWithLabel.map((d) => [
6299
+ d.label!.text,
6300
+ quadrantLabelLayout(d.label!.text, qw, qh),
6301
+ ])
5746
6302
  );
5747
6303
 
5748
6304
  const quadrantLabelTexts = chartG
@@ -5807,7 +6363,10 @@ export function renderQuadrant(
5807
6363
  .attr('text-anchor', 'middle')
5808
6364
  .attr('fill', textColor)
5809
6365
  .attr('font-size', '18px')
5810
- .attr('data-line-number', quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null)
6366
+ .attr(
6367
+ 'data-line-number',
6368
+ quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null
6369
+ )
5811
6370
  .style(
5812
6371
  'cursor',
5813
6372
  onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
@@ -5823,7 +6382,10 @@ export function renderQuadrant(
5823
6382
  .attr('text-anchor', 'middle')
5824
6383
  .attr('fill', textColor)
5825
6384
  .attr('font-size', '18px')
5826
- .attr('data-line-number', quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null)
6385
+ .attr(
6386
+ 'data-line-number',
6387
+ quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null
6388
+ )
5827
6389
  .style(
5828
6390
  'cursor',
5829
6391
  onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
@@ -5859,7 +6421,10 @@ export function renderQuadrant(
5859
6421
  .attr('fill', textColor)
5860
6422
  .attr('font-size', '18px')
5861
6423
  .attr('transform', `rotate(-90, 22, ${yMidBottom})`)
5862
- .attr('data-line-number', quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null)
6424
+ .attr(
6425
+ 'data-line-number',
6426
+ quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null
6427
+ )
5863
6428
  .style(
5864
6429
  'cursor',
5865
6430
  onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
@@ -5876,7 +6441,10 @@ export function renderQuadrant(
5876
6441
  .attr('fill', textColor)
5877
6442
  .attr('font-size', '18px')
5878
6443
  .attr('transform', `rotate(-90, 22, ${yMidTop})`)
5879
- .attr('data-line-number', quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null)
6444
+ .attr(
6445
+ 'data-line-number',
6446
+ quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null
6447
+ )
5880
6448
  .style(
5881
6449
  'cursor',
5882
6450
  onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
@@ -5935,7 +6503,9 @@ export function renderQuadrant(
5935
6503
  const pointColor =
5936
6504
  quadDef?.label?.color ?? defaultColors[quadDef?.colorIdx ?? 0];
5937
6505
 
5938
- const pointG = pointsG.append('g').attr('class', 'point-group')
6506
+ const pointG = pointsG
6507
+ .append('g')
6508
+ .attr('class', 'point-group')
5939
6509
  .attr('data-line-number', String(point.lineNumber));
5940
6510
 
5941
6511
  // Circle with white fill and colored border for visibility on opaque quadrants
@@ -6024,7 +6594,10 @@ const EXPORT_HEIGHT = 800;
6024
6594
  /**
6025
6595
  * Resolves the palette for export, falling back to Nord light/dark.
6026
6596
  */
6027
- async function resolveExportPalette(theme: string, palette?: PaletteColors): Promise<PaletteColors> {
6597
+ async function resolveExportPalette(
6598
+ theme: string,
6599
+ palette?: PaletteColors
6600
+ ): Promise<PaletteColors> {
6028
6601
  if (palette) return palette;
6029
6602
  const { getPalette } = await import('./palettes');
6030
6603
  return theme === 'dark' ? getPalette('nord').dark : getPalette('nord').light;
@@ -6086,7 +6659,13 @@ export async function renderForExport(
6086
6659
  hiddenAttributes?: Set<string>;
6087
6660
  swimlaneTagGroup?: string | null;
6088
6661
  },
6089
- options?: { branding?: boolean; c4Level?: 'context' | 'containers' | 'components' | 'deployment'; c4System?: string; c4Container?: string; tagGroup?: string }
6662
+ options?: {
6663
+ branding?: boolean;
6664
+ c4Level?: 'context' | 'containers' | 'components' | 'deployment';
6665
+ c4System?: string;
6666
+ c4Container?: string;
6667
+ tagGroup?: string;
6668
+ }
6090
6669
  ): Promise<string> {
6091
6670
  // Flowchart and org chart use their own parser pipelines — intercept before parseVisualization()
6092
6671
  const { parseDgmoChartType } = await import('./dgmo-router');
@@ -6106,7 +6685,8 @@ export async function renderForExport(
6106
6685
 
6107
6686
  // Apply interactive collapse state when provided
6108
6687
  const collapsedNodes = orgExportState?.collapsedNodes;
6109
- const activeTagGroup = orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
6688
+ const activeTagGroup =
6689
+ orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
6110
6690
  const hiddenAttributes = orgExportState?.hiddenAttributes;
6111
6691
 
6112
6692
  const { parsed: effectiveParsed, hiddenCounts } =
@@ -6128,7 +6708,17 @@ export async function renderForExport(
6128
6708
  const exportHeight = orgLayout.height + PADDING * 2 + titleOffset;
6129
6709
  const container = createExportContainer(exportWidth, exportHeight);
6130
6710
 
6131
- renderOrg(container, effectiveParsed, orgLayout, effectivePalette, isDark, undefined, { width: exportWidth, height: exportHeight }, activeTagGroup, hiddenAttributes);
6711
+ renderOrg(
6712
+ container,
6713
+ effectiveParsed,
6714
+ orgLayout,
6715
+ effectivePalette,
6716
+ isDark,
6717
+ undefined,
6718
+ { width: exportWidth, height: exportHeight },
6719
+ activeTagGroup,
6720
+ hiddenAttributes
6721
+ );
6132
6722
  return finalizeSvgExport(container, theme, effectivePalette, options);
6133
6723
  }
6134
6724
 
@@ -6146,7 +6736,8 @@ export async function renderForExport(
6146
6736
 
6147
6737
  // Apply interactive collapse state when provided
6148
6738
  const collapsedNodes = orgExportState?.collapsedNodes;
6149
- const activeTagGroup = orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
6739
+ const activeTagGroup =
6740
+ orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
6150
6741
  const hiddenAttributes = orgExportState?.hiddenAttributes;
6151
6742
 
6152
6743
  const { parsed: effectiveParsed, hiddenCounts } =
@@ -6159,7 +6750,7 @@ export async function renderForExport(
6159
6750
  hiddenCounts.size > 0 ? hiddenCounts : undefined,
6160
6751
  activeTagGroup,
6161
6752
  hiddenAttributes,
6162
- true,
6753
+ true
6163
6754
  );
6164
6755
 
6165
6756
  const PADDING = 20;
@@ -6168,7 +6759,17 @@ export async function renderForExport(
6168
6759
  const exportHeight = sitemapLayout.height + PADDING * 2 + titleOffset;
6169
6760
  const container = createExportContainer(exportWidth, exportHeight);
6170
6761
 
6171
- renderSitemap(container, effectiveParsed, sitemapLayout, effectivePalette, isDark, undefined, { width: exportWidth, height: exportHeight }, activeTagGroup, hiddenAttributes);
6762
+ renderSitemap(
6763
+ container,
6764
+ effectiveParsed,
6765
+ sitemapLayout,
6766
+ effectivePalette,
6767
+ isDark,
6768
+ undefined,
6769
+ { width: exportWidth, height: exportHeight },
6770
+ activeTagGroup,
6771
+ hiddenAttributes
6772
+ );
6172
6773
  return finalizeSvgExport(container, theme, effectivePalette, options);
6173
6774
  }
6174
6775
 
@@ -6186,7 +6787,15 @@ export async function renderForExport(
6186
6787
  container.style.left = '-9999px';
6187
6788
  document.body.appendChild(container);
6188
6789
 
6189
- renderKanban(container, kanbanParsed, effectivePalette, theme === 'dark', undefined, undefined, options?.tagGroup);
6790
+ renderKanban(
6791
+ container,
6792
+ kanbanParsed,
6793
+ effectivePalette,
6794
+ theme === 'dark',
6795
+ undefined,
6796
+ undefined,
6797
+ options?.tagGroup
6798
+ );
6190
6799
  return finalizeSvgExport(container, theme, effectivePalette, options);
6191
6800
  }
6192
6801
 
@@ -6206,7 +6815,15 @@ export async function renderForExport(
6206
6815
  const exportHeight = classLayout.height + PADDING * 2 + titleOffset;
6207
6816
  const container = createExportContainer(exportWidth, exportHeight);
6208
6817
 
6209
- renderClassDiagram(container, classParsed, classLayout, effectivePalette, theme === 'dark', undefined, { width: exportWidth, height: exportHeight });
6818
+ renderClassDiagram(
6819
+ container,
6820
+ classParsed,
6821
+ classLayout,
6822
+ effectivePalette,
6823
+ theme === 'dark',
6824
+ undefined,
6825
+ { width: exportWidth, height: exportHeight }
6826
+ );
6210
6827
  return finalizeSvgExport(container, theme, effectivePalette, options);
6211
6828
  }
6212
6829
 
@@ -6226,14 +6843,26 @@ export async function renderForExport(
6226
6843
  const exportHeight = erLayout.height + PADDING * 2 + titleOffset;
6227
6844
  const container = createExportContainer(exportWidth, exportHeight);
6228
6845
 
6229
- renderERDiagram(container, erParsed, erLayout, effectivePalette, theme === 'dark', undefined, { width: exportWidth, height: exportHeight }, options?.tagGroup);
6846
+ renderERDiagram(
6847
+ container,
6848
+ erParsed,
6849
+ erLayout,
6850
+ effectivePalette,
6851
+ theme === 'dark',
6852
+ undefined,
6853
+ { width: exportWidth, height: exportHeight },
6854
+ options?.tagGroup
6855
+ );
6230
6856
  return finalizeSvgExport(container, theme, effectivePalette, options);
6231
6857
  }
6232
6858
 
6233
6859
  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');
6860
+ const { parseInitiativeStatus } =
6861
+ await import('./initiative-status/parser');
6862
+ const { layoutInitiativeStatus } =
6863
+ await import('./initiative-status/layout');
6864
+ const { renderInitiativeStatus } =
6865
+ await import('./initiative-status/renderer');
6237
6866
 
6238
6867
  const effectivePalette = await resolveExportPalette(theme, palette);
6239
6868
  const isParsed = parseInitiativeStatus(content);
@@ -6246,14 +6875,27 @@ export async function renderForExport(
6246
6875
  const exportHeight = isLayout.height + PADDING * 2 + titleOffset;
6247
6876
  const container = createExportContainer(exportWidth, exportHeight);
6248
6877
 
6249
- renderInitiativeStatus(container, isParsed, isLayout, effectivePalette, theme === 'dark', { exportDims: { width: exportWidth, height: exportHeight } });
6878
+ renderInitiativeStatus(
6879
+ container,
6880
+ isParsed,
6881
+ isLayout,
6882
+ effectivePalette,
6883
+ theme === 'dark',
6884
+ { exportDims: { width: exportWidth, height: exportHeight } }
6885
+ );
6250
6886
  return finalizeSvgExport(container, theme, effectivePalette, options);
6251
6887
  }
6252
6888
 
6253
6889
  if (detectedType === 'c4') {
6254
6890
  const { parseC4 } = await import('./c4/parser');
6255
- const { layoutC4Context, layoutC4Containers, layoutC4Components, layoutC4Deployment } = await import('./c4/layout');
6256
- const { renderC4Context, renderC4Containers } = await import('./c4/renderer');
6891
+ const {
6892
+ layoutC4Context,
6893
+ layoutC4Containers,
6894
+ layoutC4Components,
6895
+ layoutC4Deployment,
6896
+ } = await import('./c4/layout');
6897
+ const { renderC4Context, renderC4Containers } =
6898
+ await import('./c4/renderer');
6257
6899
 
6258
6900
  const effectivePalette = await resolveExportPalette(theme, palette);
6259
6901
  const c4Parsed = parseC4(content, effectivePalette);
@@ -6264,13 +6906,14 @@ export async function renderForExport(
6264
6906
  const c4System = options?.c4System;
6265
6907
  const c4Container = options?.c4Container;
6266
6908
 
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);
6909
+ const c4Layout =
6910
+ c4Level === 'deployment'
6911
+ ? layoutC4Deployment(c4Parsed)
6912
+ : c4Level === 'components' && c4System && c4Container
6913
+ ? layoutC4Components(c4Parsed, c4System, c4Container)
6914
+ : c4Level === 'containers' && c4System
6915
+ ? layoutC4Containers(c4Parsed, c4System)
6916
+ : layoutC4Context(c4Parsed);
6274
6917
 
6275
6918
  if (c4Layout.nodes.length === 0) return '';
6276
6919
 
@@ -6280,11 +6923,23 @@ export async function renderForExport(
6280
6923
  const exportHeight = c4Layout.height + PADDING * 2 + titleOffset;
6281
6924
  const container = createExportContainer(exportWidth, exportHeight);
6282
6925
 
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);
6926
+ const renderFn =
6927
+ c4Level === 'deployment' ||
6928
+ (c4Level === 'components' && c4System && c4Container) ||
6929
+ (c4Level === 'containers' && c4System)
6930
+ ? renderC4Containers
6931
+ : renderC4Context;
6932
+
6933
+ renderFn(
6934
+ container,
6935
+ c4Parsed,
6936
+ c4Layout,
6937
+ effectivePalette,
6938
+ theme === 'dark',
6939
+ undefined,
6940
+ { width: exportWidth, height: exportHeight },
6941
+ options?.tagGroup
6942
+ );
6288
6943
  return finalizeSvgExport(container, theme, effectivePalette, options);
6289
6944
  }
6290
6945
 
@@ -6300,7 +6955,15 @@ export async function renderForExport(
6300
6955
  const layout = layoutGraph(fcParsed);
6301
6956
  const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
6302
6957
 
6303
- renderFlowchart(container, fcParsed, layout, effectivePalette, theme === 'dark', undefined, { width: EXPORT_WIDTH, height: EXPORT_HEIGHT });
6958
+ renderFlowchart(
6959
+ container,
6960
+ fcParsed,
6961
+ layout,
6962
+ effectivePalette,
6963
+ theme === 'dark',
6964
+ undefined,
6965
+ { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
6966
+ );
6304
6967
  return finalizeSvgExport(container, theme, effectivePalette, options);
6305
6968
  }
6306
6969
 
@@ -6308,7 +6971,8 @@ export async function renderForExport(
6308
6971
  const { parseInfra } = await import('./infra/parser');
6309
6972
  const { computeInfra } = await import('./infra/compute');
6310
6973
  const { layoutInfra } = await import('./infra/layout');
6311
- const { renderInfra, computeInfraLegendGroups } = await import('./infra/renderer');
6974
+ const { renderInfra, computeInfraLegendGroups } =
6975
+ await import('./infra/renderer');
6312
6976
 
6313
6977
  const effectivePalette = await resolveExportPalette(theme, palette);
6314
6978
  const infraParsed = parseInfra(content);
@@ -6319,13 +6983,30 @@ export async function renderForExport(
6319
6983
  const activeTagGroup = options?.tagGroup ?? null;
6320
6984
 
6321
6985
  const titleOffset = infraParsed.title ? 40 : 0;
6322
- const legendGroups = computeInfraLegendGroups(infraLayout.nodes, infraParsed.tagGroups, effectivePalette);
6986
+ const legendGroups = computeInfraLegendGroups(
6987
+ infraLayout.nodes,
6988
+ infraParsed.tagGroups,
6989
+ effectivePalette
6990
+ );
6323
6991
  const legendOffset = legendGroups.length > 0 ? 28 : 0;
6324
6992
  const exportWidth = infraLayout.width;
6325
6993
  const exportHeight = infraLayout.height + titleOffset + legendOffset;
6326
6994
  const container = createExportContainer(exportWidth, exportHeight);
6327
6995
 
6328
- renderInfra(container, infraLayout, effectivePalette, theme === 'dark', infraParsed.title, infraParsed.titleLineNumber, infraParsed.tagGroups, activeTagGroup, false, null, null, true);
6996
+ renderInfra(
6997
+ container,
6998
+ infraLayout,
6999
+ effectivePalette,
7000
+ theme === 'dark',
7001
+ infraParsed.title,
7002
+ infraParsed.titleLineNumber,
7003
+ infraParsed.tagGroups,
7004
+ activeTagGroup,
7005
+ false,
7006
+ null,
7007
+ null,
7008
+ true
7009
+ );
6329
7010
  // Restore explicit pixel dimensions for resvg (renderer uses 100%/viewBox for app scaling)
6330
7011
  const infraSvg = container.querySelector('svg');
6331
7012
  if (infraSvg) {
@@ -6349,7 +7030,14 @@ export async function renderForExport(
6349
7030
  const EXPORT_H = 800;
6350
7031
  const container = createExportContainer(EXPORT_W, EXPORT_H);
6351
7032
 
6352
- renderGantt(container, resolved, effectivePalette, theme === 'dark', undefined, { width: EXPORT_W, height: EXPORT_H });
7033
+ renderGantt(
7034
+ container,
7035
+ resolved,
7036
+ effectivePalette,
7037
+ theme === 'dark',
7038
+ undefined,
7039
+ { width: EXPORT_W, height: EXPORT_H }
7040
+ );
6353
7041
  return finalizeSvgExport(container, theme, effectivePalette, options);
6354
7042
  }
6355
7043
 
@@ -6365,7 +7053,15 @@ export async function renderForExport(
6365
7053
  const layout = layoutGraph(stateParsed);
6366
7054
  const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
6367
7055
 
6368
- renderState(container, stateParsed, layout, effectivePalette, theme === 'dark', undefined, { width: EXPORT_WIDTH, height: EXPORT_HEIGHT });
7056
+ renderState(
7057
+ container,
7058
+ stateParsed,
7059
+ layout,
7060
+ effectivePalette,
7061
+ theme === 'dark',
7062
+ undefined,
7063
+ { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
7064
+ );
6369
7065
  return finalizeSvgExport(container, theme, effectivePalette, options);
6370
7066
  }
6371
7067
 
@@ -6391,30 +7087,75 @@ export async function renderForExport(
6391
7087
  const effectivePalette = await resolveExportPalette(theme, palette);
6392
7088
  const isDark = theme === 'dark';
6393
7089
  const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
6394
- const dims: D3ExportDimensions = { width: EXPORT_WIDTH, height: EXPORT_HEIGHT };
7090
+ const dims: D3ExportDimensions = {
7091
+ width: EXPORT_WIDTH,
7092
+ height: EXPORT_HEIGHT,
7093
+ };
6395
7094
 
6396
7095
  if (parsed.type === 'sequence') {
6397
7096
  const { parseSequenceDgmo } = await import('./sequence/parser');
6398
7097
  const { renderSequenceDiagram } = await import('./sequence/renderer');
6399
7098
  const seqParsed = parseSequenceDgmo(content);
6400
7099
  if (seqParsed.error || seqParsed.participants.length === 0) return '';
6401
- renderSequenceDiagram(container, seqParsed, effectivePalette, isDark, undefined, {
6402
- exportWidth: EXPORT_WIDTH,
6403
- activeTagGroup: options?.tagGroup,
6404
- });
7100
+ renderSequenceDiagram(
7101
+ container,
7102
+ seqParsed,
7103
+ effectivePalette,
7104
+ isDark,
7105
+ undefined,
7106
+ {
7107
+ exportWidth: EXPORT_WIDTH,
7108
+ activeTagGroup: options?.tagGroup,
7109
+ }
7110
+ );
6405
7111
  } else if (parsed.type === 'wordcloud') {
6406
- await renderWordCloudAsync(container, parsed, effectivePalette, isDark, dims);
7112
+ await renderWordCloudAsync(
7113
+ container,
7114
+ parsed,
7115
+ effectivePalette,
7116
+ isDark,
7117
+ dims
7118
+ );
6407
7119
  } else if (parsed.type === 'arc') {
6408
- renderArcDiagram(container, parsed, effectivePalette, isDark, undefined, dims);
7120
+ renderArcDiagram(
7121
+ container,
7122
+ parsed,
7123
+ effectivePalette,
7124
+ isDark,
7125
+ undefined,
7126
+ dims
7127
+ );
6409
7128
  } else if (parsed.type === 'timeline') {
6410
- renderTimeline(container, parsed, effectivePalette, isDark, undefined, dims,
6411
- orgExportState?.activeTagGroup ?? options?.tagGroup, orgExportState?.swimlaneTagGroup);
7129
+ renderTimeline(
7130
+ container,
7131
+ parsed,
7132
+ effectivePalette,
7133
+ isDark,
7134
+ undefined,
7135
+ dims,
7136
+ orgExportState?.activeTagGroup ?? options?.tagGroup,
7137
+ orgExportState?.swimlaneTagGroup
7138
+ );
6412
7139
  } else if (parsed.type === 'venn') {
6413
7140
  renderVenn(container, parsed, effectivePalette, isDark, undefined, dims);
6414
7141
  } else if (parsed.type === 'quadrant') {
6415
- renderQuadrant(container, parsed, effectivePalette, isDark, undefined, dims);
7142
+ renderQuadrant(
7143
+ container,
7144
+ parsed,
7145
+ effectivePalette,
7146
+ isDark,
7147
+ undefined,
7148
+ dims
7149
+ );
6416
7150
  } else {
6417
- renderSlopeChart(container, parsed, effectivePalette, isDark, undefined, dims);
7151
+ renderSlopeChart(
7152
+ container,
7153
+ parsed,
7154
+ effectivePalette,
7155
+ isDark,
7156
+ undefined,
7157
+ dims
7158
+ );
6418
7159
  }
6419
7160
 
6420
7161
  return finalizeSvgExport(container, theme, effectivePalette, options);