@diagrammo/dgmo 0.8.3 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.claude/commands/dgmo-diagram-this.md +60 -0
  2. package/.claude/commands/dgmo-document-project.md +128 -0
  3. package/.claude/commands/dgmo.md +185 -50
  4. package/.cursorrules +32 -37
  5. package/.github/copilot-instructions.md +35 -44
  6. package/.windsurfrules +32 -37
  7. package/README.md +4 -4
  8. package/dist/cli.cjs +153 -153
  9. package/dist/editor.cjs +336 -0
  10. package/dist/editor.cjs.map +1 -0
  11. package/dist/editor.d.cts +27 -0
  12. package/dist/editor.d.ts +27 -0
  13. package/dist/editor.js +305 -0
  14. package/dist/editor.js.map +1 -0
  15. package/dist/index.cjs +3336 -1055
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.js +3336 -1055
  18. package/dist/index.js.map +1 -1
  19. package/docs/language-reference.md +30 -29
  20. package/gallery/fixtures/arc.dgmo +18 -0
  21. package/gallery/fixtures/area.dgmo +19 -0
  22. package/gallery/fixtures/bar-stacked.dgmo +10 -0
  23. package/gallery/fixtures/bar.dgmo +10 -0
  24. package/gallery/fixtures/c4-full.dgmo +52 -0
  25. package/gallery/fixtures/c4.dgmo +17 -0
  26. package/gallery/fixtures/chord.dgmo +12 -0
  27. package/gallery/fixtures/class-basic.dgmo +14 -0
  28. package/gallery/fixtures/class-full.dgmo +43 -0
  29. package/gallery/fixtures/doughnut.dgmo +8 -0
  30. package/gallery/fixtures/flowchart-basic.dgmo +3 -0
  31. package/gallery/fixtures/flowchart-colors.dgmo +5 -0
  32. package/gallery/fixtures/flowchart-complex.dgmo +17 -0
  33. package/gallery/fixtures/flowchart-decision.dgmo +5 -0
  34. package/gallery/fixtures/flowchart-full.dgmo +13 -0
  35. package/gallery/fixtures/flowchart-groups.dgmo +10 -0
  36. package/gallery/fixtures/flowchart-loop.dgmo +7 -0
  37. package/gallery/fixtures/flowchart-nested.dgmo +7 -0
  38. package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
  39. package/gallery/fixtures/function.dgmo +8 -0
  40. package/gallery/fixtures/funnel.dgmo +7 -0
  41. package/gallery/fixtures/gantt-full.dgmo +49 -0
  42. package/gallery/fixtures/gantt.dgmo +42 -0
  43. package/gallery/fixtures/heatmap.dgmo +8 -0
  44. package/gallery/fixtures/infra-full.dgmo +78 -0
  45. package/gallery/fixtures/infra-overload.dgmo +25 -0
  46. package/gallery/fixtures/infra.dgmo +47 -0
  47. package/gallery/fixtures/initiative-status-full.dgmo +46 -0
  48. package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
  49. package/gallery/fixtures/initiative-status.dgmo +9 -0
  50. package/gallery/fixtures/line.dgmo +19 -0
  51. package/gallery/fixtures/multi-line.dgmo +11 -0
  52. package/gallery/fixtures/org-basic.dgmo +16 -0
  53. package/gallery/fixtures/org-full.dgmo +69 -0
  54. package/gallery/fixtures/org-teams.dgmo +25 -0
  55. package/gallery/fixtures/pie.dgmo +9 -0
  56. package/gallery/fixtures/polar-area.dgmo +8 -0
  57. package/gallery/fixtures/quadrant.dgmo +18 -0
  58. package/gallery/fixtures/radar.dgmo +8 -0
  59. package/gallery/fixtures/sankey.dgmo +31 -0
  60. package/gallery/fixtures/scatter.dgmo +21 -0
  61. package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
  62. package/gallery/fixtures/sequence-tags.dgmo +41 -0
  63. package/gallery/fixtures/sequence.dgmo +35 -0
  64. package/gallery/fixtures/sitemap-basic.dgmo +12 -0
  65. package/gallery/fixtures/sitemap-full.dgmo +156 -0
  66. package/gallery/fixtures/slope.dgmo +8 -0
  67. package/gallery/fixtures/spr-eras.dgmo +62 -0
  68. package/gallery/fixtures/state.dgmo +30 -0
  69. package/gallery/fixtures/timeline-intraday.dgmo +14 -0
  70. package/gallery/fixtures/timeline.dgmo +32 -0
  71. package/gallery/fixtures/venn.dgmo +10 -0
  72. package/gallery/fixtures/wordcloud.dgmo +24 -0
  73. package/package.json +51 -2
  74. package/src/c4/layout.ts +372 -90
  75. package/src/c4/parser.ts +100 -55
  76. package/src/chart.ts +91 -28
  77. package/src/class/parser.ts +41 -12
  78. package/src/cli.ts +168 -61
  79. package/src/completion.ts +378 -183
  80. package/src/d3.ts +887 -288
  81. package/src/dgmo-mermaid.ts +16 -13
  82. package/src/dgmo-router.ts +69 -23
  83. package/src/echarts.ts +646 -153
  84. package/src/editor/dgmo.grammar +69 -0
  85. package/src/editor/dgmo.grammar.d.ts +2 -0
  86. package/src/editor/dgmo.grammar.js +18 -0
  87. package/src/editor/dgmo.grammar.terms.d.ts +5 -0
  88. package/src/editor/dgmo.grammar.terms.js +35 -0
  89. package/src/editor/highlight.ts +36 -0
  90. package/src/editor/index.ts +28 -0
  91. package/src/editor/keywords.ts +220 -0
  92. package/src/editor/tokens.ts +30 -0
  93. package/src/er/parser.ts +48 -14
  94. package/src/er/renderer.ts +112 -53
  95. package/src/gantt/calculator.ts +91 -29
  96. package/src/gantt/parser.ts +197 -71
  97. package/src/gantt/renderer.ts +1120 -350
  98. package/src/graph/flowchart-parser.ts +46 -25
  99. package/src/graph/state-parser.ts +47 -17
  100. package/src/infra/parser.ts +157 -53
  101. package/src/infra/renderer.ts +723 -271
  102. package/src/initiative-status/parser.ts +138 -44
  103. package/src/kanban/parser.ts +25 -14
  104. package/src/org/layout.ts +111 -44
  105. package/src/org/parser.ts +69 -22
  106. package/src/palettes/index.ts +3 -2
  107. package/src/sequence/parser.ts +193 -61
  108. package/src/sitemap/parser.ts +65 -29
  109. package/src/utils/arrows.ts +2 -22
  110. package/src/utils/duration.ts +39 -21
  111. package/src/utils/legend-constants.ts +0 -2
  112. package/src/utils/parsing.ts +75 -31
package/src/echarts.ts CHANGED
@@ -4,7 +4,6 @@ import { FONT_FAMILY } from './fonts';
4
4
  import { injectBranding } from './branding';
5
5
  import { renderLegendSvg } from './utils/legend-svg';
6
6
  import type { LegendGroupData } from './utils/legend-svg';
7
- import { LEGEND_HEIGHT } from './utils/legend-constants';
8
7
 
9
8
  // ============================================================
10
9
  // Types
@@ -101,7 +100,13 @@ import { parseChart } from './chart';
101
100
  import type { ParsedChart, ChartEra } from './chart';
102
101
  import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
103
102
  import { resolveColor } from './colors';
104
- import { collectIndentedValues, extractColor, measureIndent, normalizeGroupedNumber, parseFirstLine, parseSeriesNames } from './utils/parsing';
103
+ import {
104
+ collectIndentedValues,
105
+ extractColor,
106
+ measureIndent,
107
+ parseFirstLine,
108
+ parseSeriesNames,
109
+ } from './utils/parsing';
105
110
  import { parseDataRowValues } from './chart';
106
111
 
107
112
  // ============================================================
@@ -112,9 +117,17 @@ const EMPHASIS_SELF = { focus: 'self' as const, blurScope: 'global' as const };
112
117
  const EMPHASIS_LINE = {
113
118
  ...EMPHASIS_SELF,
114
119
  scale: 2.5,
115
- itemStyle: { borderWidth: 2, borderColor: '#fff', shadowBlur: 8, shadowColor: 'rgba(0,0,0,0.4)' },
120
+ itemStyle: {
121
+ borderWidth: 2,
122
+ borderColor: '#fff',
123
+ shadowBlur: 8,
124
+ shadowColor: 'rgba(0,0,0,0.4)',
125
+ },
126
+ };
127
+ const CHART_BASE: Pick<EChartsOption, 'backgroundColor' | 'animation'> = {
128
+ backgroundColor: 'transparent',
129
+ animation: false,
116
130
  };
117
- const CHART_BASE: Pick<EChartsOption, 'backgroundColor' | 'animation'> = { backgroundColor: 'transparent', animation: false };
118
131
  const CHART_BORDER_WIDTH = 2;
119
132
 
120
133
  // ============================================================
@@ -122,13 +135,26 @@ const CHART_BORDER_WIDTH = 2;
122
135
  // ============================================================
123
136
 
124
137
  const VALID_EXTENDED_TYPES = new Set<ExtendedChartType>([
125
- 'sankey', 'chord', 'function', 'scatter', 'heatmap', 'funnel',
138
+ 'sankey',
139
+ 'chord',
140
+ 'function',
141
+ 'scatter',
142
+ 'heatmap',
143
+ 'funnel',
126
144
  ]);
127
145
 
128
146
  /** Known option keywords for the extended chart parser. */
129
147
  const KNOWN_EXTENDED_OPTIONS = new Set([
130
- 'chart', 'title', 'series', 'xlabel', 'ylabel', 'sizelabel',
131
- 'no-labels', 'columns', 'rows', 'x',
148
+ 'chart',
149
+ 'title',
150
+ 'series',
151
+ 'x-label',
152
+ 'y-label',
153
+ 'size-label',
154
+ 'no-labels',
155
+ 'columns',
156
+ 'rows',
157
+ 'x',
132
158
  ]);
133
159
 
134
160
  /**
@@ -139,11 +165,14 @@ function parseScatterRow(
139
165
  line: string,
140
166
  palette: PaletteColors | undefined,
141
167
  currentCategory: string,
142
- lineNumber: number,
168
+ lineNumber: number
143
169
  ): ParsedScatterPoint | null {
144
170
  const dataRow = parseDataRowValues(line, { multiValue: true });
145
171
  if (!dataRow || dataRow.values.length < 2) return null;
146
- const { label: rawLabel, color: pointColor } = extractColor(dataRow.label, palette);
172
+ const { label: rawLabel, color: pointColor } = extractColor(
173
+ dataRow.label,
174
+ palette
175
+ );
147
176
  return {
148
177
  name: rawLabel,
149
178
  x: dataRow.values[0],
@@ -195,8 +224,16 @@ export function parseExtendedChart(
195
224
 
196
225
  // Reject legacy ## category syntax
197
226
  if (/^#{2,}\s+/.test(trimmed)) {
198
- const name = trimmed.replace(/^#{2,}\s+/, '').replace(/\s*\([^)]*\)\s*$/, '').trim();
199
- result.diagnostics.push(makeDgmoError(lineNumber, `'## ${name}' is no longer supported. Use '[${name}]' instead`));
227
+ const name = trimmed
228
+ .replace(/^#{2,}\s+/, '')
229
+ .replace(/\s*\([^)]*\)\s*$/, '')
230
+ .trim();
231
+ result.diagnostics.push(
232
+ makeDgmoError(
233
+ lineNumber,
234
+ `'## ${name}' is no longer supported. Use '[${name}]' instead`
235
+ )
236
+ );
200
237
  continue;
201
238
  }
202
239
 
@@ -208,7 +245,8 @@ export function parseExtendedChart(
208
245
  firstLineParsed = true;
209
246
  const firstLine = parseFirstLine(trimmed);
210
247
  if (firstLine) {
211
- const chartType = firstLine.chartType.toLowerCase() as ExtendedChartType;
248
+ const chartType =
249
+ firstLine.chartType.toLowerCase() as ExtendedChartType;
212
250
  if (VALID_EXTENDED_TYPES.has(chartType)) {
213
251
  result.type = chartType;
214
252
  if (firstLine.title) {
@@ -229,7 +267,11 @@ export function parseExtendedChart(
229
267
  }
230
268
  // If the first line is a single word (no spaces, no colon, no numbers),
231
269
  // treat it as an unrecognized chart type rather than falling through
232
- if (!trimmed.includes(' ') && !trimmed.includes(':') && !/\d/.test(trimmed)) {
270
+ if (
271
+ !trimmed.includes(' ') &&
272
+ !trimmed.includes(':') &&
273
+ !/\d/.test(trimmed)
274
+ ) {
233
275
  const validTypes = [...VALID_EXTENDED_TYPES];
234
276
  let msg = `Unsupported chart type: ${trimmed}. Supported types: ${validTypes.join(', ')}.`;
235
277
  const hint = suggest(trimmed.toLowerCase(), validTypes);
@@ -246,7 +288,9 @@ export function parseExtendedChart(
246
288
  const categoryMatch = trimmed.match(/^\[(.+?)\](?:\s*\(([^)]+)\))?\s*$/);
247
289
  if (categoryMatch) {
248
290
  const catName = categoryMatch[1].trim();
249
- const catColor = categoryMatch[2] ? resolveColor(categoryMatch[2].trim(), palette) : null;
291
+ const catColor = categoryMatch[2]
292
+ ? resolveColor(categoryMatch[2].trim(), palette)
293
+ : null;
250
294
  if (catColor) {
251
295
  if (!result.categoryColors) result.categoryColors = {};
252
296
  result.categoryColors[catName] = catColor;
@@ -258,17 +302,27 @@ export function parseExtendedChart(
258
302
  }
259
303
 
260
304
  // Sankey/chord link syntax: Source -> Target Value (directed) or Source -- Target Value (undirected)
261
- const arrowMatch = trimmed.match(/^(.+?)\s*(->|--)\s*(.+?)\s+(\d+(?:\.\d+)?)\s*(?:\(([^)]+)\))?\s*$/);
305
+ const arrowMatch = trimmed.match(
306
+ /^(.+?)\s*(->|--)\s*(.+?)\s+(\d+(?:\.\d+)?)\s*(?:\(([^)]+)\))?\s*$/
307
+ );
262
308
  if (arrowMatch) {
263
309
  const [, rawSource, arrow, rawTarget, val, rawLinkColor] = arrowMatch;
264
- const { label: source, color: sourceColor } = extractColor(rawSource.trim(), palette);
265
- const { label: target, color: targetColor } = extractColor(rawTarget.trim(), palette);
310
+ const { label: source, color: sourceColor } = extractColor(
311
+ rawSource.trim(),
312
+ palette
313
+ );
314
+ const { label: target, color: targetColor } = extractColor(
315
+ rawTarget.trim(),
316
+ palette
317
+ );
266
318
  if (sourceColor || targetColor) {
267
319
  if (!result.nodeColors) result.nodeColors = {};
268
320
  if (sourceColor) result.nodeColors[source] = sourceColor;
269
321
  if (targetColor) result.nodeColors[target] = targetColor;
270
322
  }
271
- const linkColor = rawLinkColor ? resolveColor(rawLinkColor.trim(), palette) : undefined;
323
+ const linkColor = rawLinkColor
324
+ ? resolveColor(rawLinkColor.trim(), palette)
325
+ : undefined;
272
326
  if (!result.links) result.links = [];
273
327
  result.links.push({
274
328
  source,
@@ -293,19 +347,34 @@ export function parseExtendedChart(
293
347
  if (sankeyStack.length > 0) {
294
348
  // Parse "TargetName value (linkColor)" or "TargetName(nodeColor) value (linkColor)"
295
349
  // Strip trailing (color) annotation before parseDataRowValues — it can't handle it
296
- const valColorMatch = trimmed.match(/(\d+(?:\.\d+)?)\s*\(([^)]+)\)\s*$/);
297
- const strippedLine = valColorMatch ? trimmed.replace(/\s*\([^)]+\)\s*$/, '') : trimmed;
350
+ const valColorMatch = trimmed.match(
351
+ /(\d+(?:\.\d+)?)\s*\(([^)]+)\)\s*$/
352
+ );
353
+ const strippedLine = valColorMatch
354
+ ? trimmed.replace(/\s*\([^)]+\)\s*$/, '')
355
+ : trimmed;
298
356
  const dataRow = parseDataRowValues(strippedLine);
299
357
  if (dataRow && dataRow.values.length === 1) {
300
358
  const source = sankeyStack.at(-1)!.name;
301
- const linkColor = valColorMatch?.[2] ? resolveColor(valColorMatch[2].trim(), palette) : undefined;
302
- const { label: target, color: targetColor } = extractColor(dataRow.label, palette);
359
+ const linkColor = valColorMatch?.[2]
360
+ ? resolveColor(valColorMatch[2].trim(), palette)
361
+ : undefined;
362
+ const { label: target, color: targetColor } = extractColor(
363
+ dataRow.label,
364
+ palette
365
+ );
303
366
  if (targetColor) {
304
367
  if (!result.nodeColors) result.nodeColors = {};
305
368
  result.nodeColors[target] = targetColor;
306
369
  }
307
370
  if (!result.links) result.links = [];
308
- result.links.push({ source, target, value: dataRow.values[0], ...(linkColor && { color: linkColor }), lineNumber });
371
+ result.links.push({
372
+ source,
373
+ target,
374
+ value: dataRow.values[0],
375
+ ...(linkColor && { color: linkColor }),
376
+ lineNumber,
377
+ });
309
378
  sankeyStack.push({ name: target, indent });
310
379
  continue;
311
380
  }
@@ -314,12 +383,17 @@ export function parseExtendedChart(
314
383
 
315
384
  // Bare label at indent 0 (or any indent without a value) = new source node
316
385
  const spaceIdx = trimmed.indexOf(' ');
317
- const hasNumericSuffix = spaceIdx >= 0 && !isNaN(parseFloat(trimmed.substring(trimmed.lastIndexOf(' ') + 1)));
386
+ const hasNumericSuffix =
387
+ spaceIdx >= 0 &&
388
+ !isNaN(parseFloat(trimmed.substring(trimmed.lastIndexOf(' ') + 1)));
318
389
  if (!hasNumericSuffix) {
319
390
  while (sankeyStack.length && sankeyStack.at(-1)!.indent >= indent) {
320
391
  sankeyStack.pop();
321
392
  }
322
- const { label: nodeName, color: nodeColor } = extractColor(trimmed, palette);
393
+ const { label: nodeName, color: nodeColor } = extractColor(
394
+ trimmed,
395
+ palette
396
+ );
323
397
  if (nodeColor) {
324
398
  if (!result.nodeColors) result.nodeColors = {};
325
399
  result.nodeColors[nodeName] = nodeColor;
@@ -331,7 +405,9 @@ export function parseExtendedChart(
331
405
 
332
406
  // Extract first token to check for known options
333
407
  const spaceIdx = trimmed.indexOf(' ');
334
- const firstToken = (spaceIdx >= 0 ? trimmed.substring(0, spaceIdx) : trimmed).toLowerCase();
408
+ const firstToken = (
409
+ spaceIdx >= 0 ? trimmed.substring(0, spaceIdx) : trimmed
410
+ ).toLowerCase();
335
411
 
336
412
  // Known option with a value
337
413
  if (KNOWN_EXTENDED_OPTIONS.has(firstToken) && spaceIdx >= 0) {
@@ -369,13 +445,25 @@ export function parseExtendedChart(
369
445
  result.seriesNames = parsed.names;
370
446
  result.seriesNameLineNumbers = parsed.nameLineNumbers;
371
447
  }
372
- if (parsed.nameColors.some(Boolean)) result.seriesNameColors = parsed.nameColors;
448
+ if (parsed.nameColors.some(Boolean))
449
+ result.seriesNameColors = parsed.nameColors;
373
450
  continue;
374
451
  }
375
452
 
376
- if (firstToken === 'xlabel') { result.xlabel = value; result.xlabelLineNumber = lineNumber; continue; }
377
- if (firstToken === 'ylabel') { result.ylabel = value; result.ylabelLineNumber = lineNumber; continue; }
378
- if (firstToken === 'sizelabel') { result.sizelabel = value; continue; }
453
+ if (firstToken === 'x-label') {
454
+ result.xlabel = value;
455
+ result.xlabelLineNumber = lineNumber;
456
+ continue;
457
+ }
458
+ if (firstToken === 'y-label') {
459
+ result.ylabel = value;
460
+ result.ylabelLineNumber = lineNumber;
461
+ continue;
462
+ }
463
+ if (firstToken === 'size-label') {
464
+ result.sizelabel = value;
465
+ continue;
466
+ }
379
467
 
380
468
  if (firstToken === 'columns') {
381
469
  if (value) {
@@ -416,8 +504,14 @@ export function parseExtendedChart(
416
504
  }
417
505
 
418
506
  // Bare boolean options
419
- if (firstToken === 'no-labels') { result.showLabels = false; continue; }
420
- if (firstToken === 'shade') { result.shade = true; continue; }
507
+ if (firstToken === 'no-labels') {
508
+ result.showLabels = false;
509
+ continue;
510
+ }
511
+ if (firstToken === 'shade') {
512
+ result.shade = true;
513
+ continue;
514
+ }
421
515
 
422
516
  // Bare keyword options (no value)
423
517
  if (firstToken === 'series' && spaceIdx === -1) {
@@ -429,7 +523,8 @@ export function parseExtendedChart(
429
523
  result.seriesNames = parsed.names;
430
524
  result.seriesNameLineNumbers = parsed.nameLineNumbers;
431
525
  }
432
- if (parsed.nameColors.some(Boolean)) result.seriesNameColors = parsed.nameColors;
526
+ if (parsed.nameColors.some(Boolean))
527
+ result.seriesNameColors = parsed.nameColors;
433
528
  continue;
434
529
  }
435
530
 
@@ -452,7 +547,10 @@ export function parseExtendedChart(
452
547
  if (result.type === 'function') {
453
548
  const colonIndex = trimmed.indexOf(':');
454
549
  if (colonIndex >= 0) {
455
- const { label: fnName, color: fnColor } = extractColor(trimmed.substring(0, colonIndex).trim(), palette);
550
+ const { label: fnName, color: fnColor } = extractColor(
551
+ trimmed.substring(0, colonIndex).trim(),
552
+ palette
553
+ );
456
554
  const fnValue = trimmed.substring(colonIndex + 1).trim();
457
555
  if (!result.functions) result.functions = [];
458
556
  result.functions.push({
@@ -468,7 +566,12 @@ export function parseExtendedChart(
468
566
  // Scatter chart: "Name x, y" or "Name x, y, size"
469
567
  if (result.type === 'scatter') {
470
568
  // Parse from right: trailing comma-separated numbers are x, y [, size]
471
- const scatterData = parseScatterRow(trimmed, palette, currentCategory, lineNumber);
569
+ const scatterData = parseScatterRow(
570
+ trimmed,
571
+ palette,
572
+ currentCategory,
573
+ lineNumber
574
+ );
472
575
  if (scatterData) {
473
576
  if (!result.scatterPoints) result.scatterPoints = [];
474
577
  result.scatterPoints.push(scatterData);
@@ -481,7 +584,11 @@ export function parseExtendedChart(
481
584
  const dataRow = parseDataRowValues(trimmed, { multiValue: true });
482
585
  if (dataRow && dataRow.values.length > 0) {
483
586
  if (!result.heatmapRows) result.heatmapRows = [];
484
- result.heatmapRows.push({ label: dataRow.label, values: dataRow.values, lineNumber });
587
+ result.heatmapRows.push({
588
+ label: dataRow.label,
589
+ values: dataRow.values,
590
+ lineNumber,
591
+ });
485
592
  continue;
486
593
  }
487
594
  }
@@ -489,14 +596,23 @@ export function parseExtendedChart(
489
596
  // Funnel / generic data point: "Label value"
490
597
  const dataRow = parseDataRowValues(trimmed);
491
598
  if (dataRow && dataRow.values.length === 1) {
492
- const { label: rawLabel, color: pointColor } = extractColor(dataRow.label, palette);
599
+ const { label: rawLabel, color: pointColor } = extractColor(
600
+ dataRow.label,
601
+ palette
602
+ );
493
603
  result.data.push({
494
604
  label: rawLabel,
495
605
  value: dataRow.values[0],
496
606
  ...(pointColor && { color: pointColor }),
497
607
  lineNumber,
498
608
  });
609
+ continue;
499
610
  }
611
+
612
+ // Catch-all: nothing matched this line
613
+ result.diagnostics.push(
614
+ makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, 'warning')
615
+ );
500
616
  }
501
617
 
502
618
  const warn = (line: number, message: string): void => {
@@ -514,21 +630,33 @@ export function parseExtendedChart(
514
630
  }
515
631
  } else if (result.type === 'function') {
516
632
  if (!result.functions || result.functions.length === 0) {
517
- warn(1, 'No functions found. Add functions in format: Name: expression');
633
+ warn(
634
+ 1,
635
+ 'No functions found. Add functions in format: Name: expression'
636
+ );
518
637
  }
519
638
  if (!result.xRange) {
520
639
  result.xRange = { min: -10, max: 10 }; // Default range
521
640
  }
522
641
  } else if (result.type === 'scatter') {
523
642
  if (!result.scatterPoints || result.scatterPoints.length === 0) {
524
- warn(1, 'No scatter points found. Add points in format: Name: x, y or Name: x, y, size');
643
+ warn(
644
+ 1,
645
+ 'No scatter points found. Add points in format: Name: x, y or Name: x, y, size'
646
+ );
525
647
  }
526
648
  } else if (result.type === 'heatmap') {
527
649
  if (!result.heatmapRows || result.heatmapRows.length === 0) {
528
- warn(1, 'No heatmap data found. Add data in format: RowLabel: val1, val2, val3');
650
+ warn(
651
+ 1,
652
+ 'No heatmap data found. Add data in format: RowLabel: val1, val2, val3'
653
+ );
529
654
  }
530
655
  if (!result.columns || result.columns.length === 0) {
531
- warn(1, 'No columns defined. Add columns in format: columns: Col1, Col2, Col3');
656
+ warn(
657
+ 1,
658
+ 'No columns defined. Add columns in format: columns: Col1, Col2, Col3'
659
+ );
532
660
  }
533
661
  } else if (result.type === 'funnel') {
534
662
  if (result.data.length === 0) {
@@ -547,15 +675,43 @@ export function parseExtendedChart(
547
675
  /**
548
676
  * Computes the shared set of theme-derived variables used by all chart option builders.
549
677
  */
550
- function buildChartCommons(parsed: { title?: string; error?: string | null }, palette: PaletteColors, isDark: boolean) {
678
+ function buildChartCommons(
679
+ parsed: { title?: string; error?: string | null },
680
+ palette: PaletteColors,
681
+ isDark: boolean
682
+ ) {
551
683
  const textColor = palette.text;
552
684
  const axisLineColor = palette.border;
553
685
  const splitLineColor = palette.border;
554
686
  const gridOpacity = isDark ? 0.7 : 0.55;
555
687
  const colors = getSeriesColors(palette);
556
- const titleConfig = parsed.title ? { text: parsed.title, left: 'center' as const, top: 8, textStyle: { color: textColor, fontSize: 20, fontWeight: 'bold' as const, fontFamily: FONT_FAMILY } } : undefined;
557
- const tooltipTheme = { backgroundColor: palette.surface, borderColor: palette.border, textStyle: { color: palette.text } };
558
- return { textColor, axisLineColor, splitLineColor, gridOpacity, colors, titleConfig, tooltipTheme };
688
+ const titleConfig = parsed.title
689
+ ? {
690
+ text: parsed.title,
691
+ left: 'center' as const,
692
+ top: 8,
693
+ textStyle: {
694
+ color: textColor,
695
+ fontSize: 20,
696
+ fontWeight: 'bold' as const,
697
+ fontFamily: FONT_FAMILY,
698
+ },
699
+ }
700
+ : undefined;
701
+ const tooltipTheme = {
702
+ backgroundColor: palette.surface,
703
+ borderColor: palette.border,
704
+ textStyle: { color: palette.text },
705
+ };
706
+ return {
707
+ textColor,
708
+ axisLineColor,
709
+ splitLineColor,
710
+ gridOpacity,
711
+ colors,
712
+ titleConfig,
713
+ tooltipTheme,
714
+ };
559
715
  }
560
716
 
561
717
  /**
@@ -573,7 +729,14 @@ export function buildExtendedChartOption(
573
729
  return {};
574
730
  }
575
731
 
576
- const { textColor, axisLineColor, gridOpacity, colors, titleConfig, tooltipTheme } = buildChartCommons(parsed, palette, isDark);
732
+ const {
733
+ textColor,
734
+ axisLineColor,
735
+ gridOpacity,
736
+ colors,
737
+ titleConfig,
738
+ tooltipTheme,
739
+ } = buildChartCommons(parsed, palette, isDark);
577
740
 
578
741
  // Sankey chart has different structure
579
742
  if (parsed.type === 'sankey') {
@@ -700,7 +863,7 @@ function buildSankeyOption(
700
863
  nodeGap: 12,
701
864
  nodeWidth: 20,
702
865
  data: nodes,
703
- links: (parsed.links ?? []).map(link => ({
866
+ links: (parsed.links ?? []).map((link) => ({
704
867
  source: link.source,
705
868
  target: link.target,
706
869
  value: link.value,
@@ -762,7 +925,11 @@ function buildChordOption(
762
925
  const stroke = colors[index % colors.length];
763
926
  return {
764
927
  name,
765
- itemStyle: { color: mix(stroke, bg, 30), borderColor: stroke, borderWidth: CHART_BORDER_WIDTH },
928
+ itemStyle: {
929
+ color: mix(stroke, bg, 30),
930
+ borderColor: stroke,
931
+ borderWidth: CHART_BORDER_WIDTH,
932
+ },
766
933
  };
767
934
  });
768
935
 
@@ -808,7 +975,9 @@ function buildChordOption(
808
975
  // Detect opposing link pairs to offset curvatures
809
976
  const pairKeys = new Set<string>();
810
977
  for (const l of allLinks) {
811
- const rev = allLinks.find((r) => r.source === l.target && r.target === l.source && r !== l);
978
+ const rev = allLinks.find(
979
+ (r) => r.source === l.target && r.target === l.source && r !== l
980
+ );
812
981
  if (rev) pairKeys.add(`${l.source}\0${l.target}`);
813
982
  }
814
983
  return allLinks.map((link) => {
@@ -816,13 +985,18 @@ function buildChordOption(
816
985
  // Offset curvature for opposing pairs: one curves more, the other less
817
986
  const baseCurve = 0.3;
818
987
  const curveness = hasOpposite
819
- ? (link.source < link.target ? baseCurve + 0.15 : baseCurve - 0.15)
988
+ ? link.source < link.target
989
+ ? baseCurve + 0.15
990
+ : baseCurve - 0.15
820
991
  : baseCurve;
821
992
  return {
822
993
  source: link.source,
823
994
  target: link.target,
824
995
  value: link.value,
825
- ...(link.directed && { symbol: ['none', 'arrow'], symbolSize: [0, 10] }),
996
+ ...(link.directed && {
997
+ symbol: ['none', 'arrow'],
998
+ symbolSize: [0, 10],
999
+ }),
826
1000
  lineStyle: {
827
1001
  width: Math.max(1, Math.min(link.value / 20, 10)),
828
1002
  color: colors[nodeNames.indexOf(link.source) % colors.length],
@@ -1000,16 +1174,18 @@ function buildFunctionOption(
1000
1174
  */
1001
1175
  export function getSimpleChartLegendGroups(
1002
1176
  parsed: ParsedChart,
1003
- colors: string[],
1177
+ colors: string[]
1004
1178
  ): LegendGroupData[] {
1005
1179
  if (!parsed.seriesNames || parsed.seriesNames.length <= 1) return [];
1006
- return [{
1007
- name: 'Series',
1008
- entries: parsed.seriesNames.map((name, i) => ({
1009
- value: name,
1010
- color: parsed.seriesNameColors?.[i] ?? colors[i % colors.length],
1011
- })),
1012
- }];
1180
+ return [
1181
+ {
1182
+ name: 'Series',
1183
+ entries: parsed.seriesNames.map((name, i) => ({
1184
+ value: name,
1185
+ color: parsed.seriesNameColors?.[i] ?? colors[i % colors.length],
1186
+ })),
1187
+ },
1188
+ ];
1013
1189
  }
1014
1190
 
1015
1191
  /**
@@ -1018,31 +1194,37 @@ export function getSimpleChartLegendGroups(
1018
1194
  */
1019
1195
  export function getExtendedChartLegendGroups(
1020
1196
  parsed: ParsedExtendedChart,
1021
- colors: string[],
1197
+ colors: string[]
1022
1198
  ): LegendGroupData[] {
1023
1199
  if (parsed.type === 'scatter') {
1024
1200
  const points = parsed.scatterPoints ?? [];
1025
- const categories = [...new Set(points.map((p) => p.category).filter(Boolean))] as string[];
1201
+ const categories = [
1202
+ ...new Set(points.map((p) => p.category).filter(Boolean)),
1203
+ ] as string[];
1026
1204
  if (categories.length === 0) return [];
1027
- return [{
1028
- name: 'Group',
1029
- entries: categories.map((cat, i) => ({
1030
- value: cat,
1031
- color: parsed.categoryColors?.[cat] ?? colors[i % colors.length],
1032
- })),
1033
- }];
1205
+ return [
1206
+ {
1207
+ name: 'Group',
1208
+ entries: categories.map((cat, i) => ({
1209
+ value: cat,
1210
+ color: parsed.categoryColors?.[cat] ?? colors[i % colors.length],
1211
+ })),
1212
+ },
1213
+ ];
1034
1214
  }
1035
1215
 
1036
1216
  if (parsed.type === 'function') {
1037
1217
  const fns = parsed.functions ?? [];
1038
1218
  if (fns.length === 0) return [];
1039
- return [{
1040
- name: 'Function',
1041
- entries: fns.map((fn, i) => ({
1042
- value: fn.name,
1043
- color: fn.color ?? colors[i % colors.length],
1044
- })),
1045
- }];
1219
+ return [
1220
+ {
1221
+ name: 'Function',
1222
+ entries: fns.map((fn, i) => ({
1223
+ value: fn.name,
1224
+ color: fn.color ?? colors[i % colors.length],
1225
+ })),
1226
+ },
1227
+ ];
1046
1228
  }
1047
1229
 
1048
1230
  return [];
@@ -1052,16 +1234,30 @@ export function getExtendedChartLegendGroups(
1052
1234
  // Scatter label collision avoidance — greedy placement algorithm
1053
1235
  // ---------------------------------------------------------------------------
1054
1236
 
1055
- interface LabelRect { x: number; y: number; w: number; h: number }
1056
- interface PointCircle { cx: number; cy: number; r: number }
1237
+ interface LabelRect {
1238
+ x: number;
1239
+ y: number;
1240
+ w: number;
1241
+ h: number;
1242
+ }
1243
+ interface PointCircle {
1244
+ cx: number;
1245
+ cy: number;
1246
+ r: number;
1247
+ }
1057
1248
 
1058
1249
  /** Axis-aligned bounding box overlap test. @internal exported for testing */
1059
1250
  export function rectsOverlap(a: LabelRect, b: LabelRect): boolean {
1060
- return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
1251
+ return (
1252
+ a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y
1253
+ );
1061
1254
  }
1062
1255
 
1063
1256
  /** Rect vs circle overlap using nearest-point-on-rect distance check. @internal exported for testing */
1064
- export function rectCircleOverlap(rect: LabelRect, circle: PointCircle): boolean {
1257
+ export function rectCircleOverlap(
1258
+ rect: LabelRect,
1259
+ circle: PointCircle
1260
+ ): boolean {
1065
1261
  const nearestX = Math.max(rect.x, Math.min(circle.cx, rect.x + rect.w));
1066
1262
  const nearestY = Math.max(rect.y, Math.min(circle.cy, rect.y + rect.h));
1067
1263
  const dx = nearestX - circle.cx;
@@ -1124,9 +1320,18 @@ export function computeScatterLabelGraphics(
1124
1320
  : pt.py + offset; // below: label top edge is offset below point center
1125
1321
 
1126
1322
  // Check chart bounds
1127
- if (labelY < chartBounds.top || labelY + labelHeight > chartBounds.bottom) break;
1128
-
1129
- const candidate: LabelRect = { x: labelX, y: labelY, w: labelWidth, h: labelHeight };
1323
+ if (
1324
+ labelY < chartBounds.top ||
1325
+ labelY + labelHeight > chartBounds.bottom
1326
+ )
1327
+ break;
1328
+
1329
+ const candidate: LabelRect = {
1330
+ x: labelX,
1331
+ y: labelY,
1332
+ w: labelWidth,
1333
+ h: labelHeight,
1334
+ };
1130
1335
 
1131
1336
  // Check collisions with all placed labels
1132
1337
  let collision = false;
@@ -1172,7 +1377,12 @@ export function computeScatterLabelGraphics(
1172
1377
  }
1173
1378
  }
1174
1379
 
1175
- const labelRect: LabelRect = { x: labelX, y: bestLabelY, w: labelWidth, h: labelHeight };
1380
+ const labelRect: LabelRect = {
1381
+ x: labelX,
1382
+ y: bestLabelY,
1383
+ w: labelWidth,
1384
+ h: labelHeight,
1385
+ };
1176
1386
  placedLabels.push(labelRect);
1177
1387
 
1178
1388
  const textY = bestLabelY + labelHeight / 2;
@@ -1262,10 +1472,11 @@ function dataToPixel(
1262
1472
  ): { px: number; py: number } {
1263
1473
  // containLabel: true shrinks the plot area — apply conservative 30px inset
1264
1474
  const inset = 30;
1265
- const gridLeftPx = gridLeftPct * chartWidth / 100 + inset;
1266
- const gridRightPx = chartWidth - gridRightPct * chartWidth / 100 - inset;
1267
- const gridTopPx = gridTopPct * chartHeight / 100 + inset;
1268
- const gridBottomPx = chartHeight - gridBottomPct * chartHeight / 100 - inset;
1475
+ const gridLeftPx = (gridLeftPct * chartWidth) / 100 + inset;
1476
+ const gridRightPx = chartWidth - (gridRightPct * chartWidth) / 100 - inset;
1477
+ const gridTopPx = (gridTopPct * chartHeight) / 100 + inset;
1478
+ const gridBottomPx =
1479
+ chartHeight - (gridBottomPct * chartHeight) / 100 - inset;
1269
1480
  const plotWidth = gridRightPx - gridLeftPx;
1270
1481
  const plotHeight = gridBottomPx - gridTopPx;
1271
1482
 
@@ -1334,7 +1545,11 @@ function buildScatterOption(
1334
1545
  name: p.name,
1335
1546
  value: hasSize ? [p.x, p.y, p.size ?? 0] : [p.x, p.y],
1336
1547
  ...(p.color && {
1337
- itemStyle: { color: mix(p.color, bg, 30), borderColor: p.color, borderWidth: CHART_BORDER_WIDTH },
1548
+ itemStyle: {
1549
+ color: mix(p.color, bg, 30),
1550
+ borderColor: p.color,
1551
+ borderWidth: CHART_BORDER_WIDTH,
1552
+ },
1338
1553
  }),
1339
1554
  }));
1340
1555
 
@@ -1345,7 +1560,11 @@ function buildScatterOption(
1345
1560
  ...(hasSize
1346
1561
  ? { symbolSize: (val: number[]) => val[2] }
1347
1562
  : { symbolSize: defaultSize }),
1348
- itemStyle: { color: mix(catColor, bg, 30), borderColor: catColor, borderWidth: CHART_BORDER_WIDTH },
1563
+ itemStyle: {
1564
+ color: mix(catColor, bg, 30),
1565
+ borderColor: catColor,
1566
+ borderWidth: CHART_BORDER_WIDTH,
1567
+ },
1349
1568
  label: labelConfig,
1350
1569
  emphasis: emphasisConfig,
1351
1570
  };
@@ -1360,7 +1579,11 @@ function buildScatterOption(
1360
1579
  ...(hasSize
1361
1580
  ? { symbolSize: p.size ?? defaultSize }
1362
1581
  : { symbolSize: defaultSize }),
1363
- itemStyle: { color: mix(stroke, bg, 30), borderColor: stroke, borderWidth: CHART_BORDER_WIDTH },
1582
+ itemStyle: {
1583
+ color: mix(stroke, bg, 30),
1584
+ borderColor: stroke,
1585
+ borderWidth: CHART_BORDER_WIDTH,
1586
+ },
1364
1587
  };
1365
1588
  });
1366
1589
 
@@ -1427,13 +1650,23 @@ function buildScatterOption(
1427
1650
  const pt = points[idx];
1428
1651
  const catIndex = pt.category ? categories.indexOf(pt.category) : -1;
1429
1652
  const catColor = pt.category
1430
- ? (parsed.categoryColors?.[pt.category] ?? colors[catIndex % colors.length])
1653
+ ? (parsed.categoryColors?.[pt.category] ??
1654
+ colors[catIndex % colors.length])
1431
1655
  : colors[idx % colors.length];
1432
1656
  const color = pt.color ?? catColor;
1433
1657
  const { px, py } = dataToPixel(
1434
- pt.x, pt.y, axisXMin, axisXMax, axisYMin, axisYMax,
1435
- gridLeft, gridRight, gridTop, gridBottom,
1436
- ECHART_EXPORT_WIDTH, ECHART_EXPORT_HEIGHT
1658
+ pt.x,
1659
+ pt.y,
1660
+ axisXMin,
1661
+ axisXMax,
1662
+ axisYMin,
1663
+ axisYMax,
1664
+ gridLeft,
1665
+ gridRight,
1666
+ gridTop,
1667
+ gridBottom,
1668
+ ECHART_EXPORT_WIDTH,
1669
+ ECHART_EXPORT_HEIGHT
1437
1670
  );
1438
1671
  labelPoints.push({ name: pt.name, px, py, color, size: pt.size });
1439
1672
  }
@@ -1441,16 +1674,26 @@ function buildScatterOption(
1441
1674
  points.forEach((pt, index) => {
1442
1675
  const color = pt.color ?? colors[index % colors.length];
1443
1676
  const { px, py } = dataToPixel(
1444
- pt.x, pt.y, axisXMin, axisXMax, axisYMin, axisYMax,
1445
- gridLeft, gridRight, gridTop, gridBottom,
1446
- ECHART_EXPORT_WIDTH, ECHART_EXPORT_HEIGHT
1677
+ pt.x,
1678
+ pt.y,
1679
+ axisXMin,
1680
+ axisXMax,
1681
+ axisYMin,
1682
+ axisYMax,
1683
+ gridLeft,
1684
+ gridRight,
1685
+ gridTop,
1686
+ gridBottom,
1687
+ ECHART_EXPORT_WIDTH,
1688
+ ECHART_EXPORT_HEIGHT
1447
1689
  );
1448
1690
  labelPoints.push({ name: pt.name, px, py, color, size: pt.size });
1449
1691
  });
1450
1692
  }
1451
1693
 
1452
- const chartBoundsTop = gridTop * ECHART_EXPORT_HEIGHT / 100;
1453
- const chartBoundsBottom = ECHART_EXPORT_HEIGHT - gridBottom * ECHART_EXPORT_HEIGHT / 100;
1694
+ const chartBoundsTop = (gridTop * ECHART_EXPORT_HEIGHT) / 100;
1695
+ const chartBoundsBottom =
1696
+ ECHART_EXPORT_HEIGHT - (gridBottom * ECHART_EXPORT_HEIGHT) / 100;
1454
1697
  graphic = computeScatterLabelGraphics(
1455
1698
  labelPoints,
1456
1699
  { top: chartBoundsTop, bottom: chartBoundsBottom },
@@ -1462,11 +1705,12 @@ function buildScatterOption(
1462
1705
 
1463
1706
  // Build legend for categorized scatter charts
1464
1707
  const categories = hasCategories
1465
- ? [...new Set(points.map((p) => p.category).filter(Boolean))] as string[]
1708
+ ? ([...new Set(points.map((p) => p.category).filter(Boolean))] as string[])
1466
1709
  : [];
1467
- const legendConfig = categories.length > 0
1468
- ? { data: categories, bottom: 10, textStyle: { color: textColor } }
1469
- : undefined;
1710
+ const legendConfig =
1711
+ categories.length > 0
1712
+ ? { data: categories, bottom: 10, textStyle: { color: textColor } }
1713
+ : undefined;
1470
1714
 
1471
1715
  return {
1472
1716
  ...CHART_BASE,
@@ -1823,7 +2067,10 @@ function makeGridAxis(
1823
2067
  const maxLabelLen = Math.max(...data.map((l) => l.length));
1824
2068
  const count = data.length;
1825
2069
  // When interval skips labels, base sizing on visible count (≈ count / step)
1826
- const step = intervalOverride != null && intervalOverride > 0 ? intervalOverride + 1 : 1;
2070
+ const step =
2071
+ intervalOverride != null && intervalOverride > 0
2072
+ ? intervalOverride + 1
2073
+ : 1;
1827
2074
  const visibleCount = Math.ceil(count / step);
1828
2075
  // Reduce font size based on density and label length
1829
2076
  if (visibleCount > 10 || maxLabelLen > 20) catFontSize = 10;
@@ -1832,7 +2079,11 @@ function makeGridAxis(
1832
2079
 
1833
2080
  // Constrain labels to their allotted slot width so ECharts wraps instead of hiding.
1834
2081
  // Skip when interval > 0 — visible labels are spread out and need no constraint.
1835
- if ((intervalOverride == null || intervalOverride === 0) && chartWidthHint && count > 0) {
2082
+ if (
2083
+ (intervalOverride == null || intervalOverride === 0) &&
2084
+ chartWidthHint &&
2085
+ count > 0
2086
+ ) {
1836
2087
  const availPerLabel = Math.floor((chartWidthHint * 0.85) / count);
1837
2088
  catLabelExtras = {
1838
2089
  width: availPerLabel,
@@ -1864,7 +2115,11 @@ function makeGridAxis(
1864
2115
  name: label,
1865
2116
  nameLocation: 'middle',
1866
2117
  nameGap: nameGapOverride ?? defaultGap,
1867
- nameTextStyle: { color: textColor, fontSize: 18, fontFamily: FONT_FAMILY },
2118
+ nameTextStyle: {
2119
+ color: textColor,
2120
+ fontSize: 18,
2121
+ fontFamily: FONT_FAMILY,
2122
+ },
1868
2123
  }),
1869
2124
  };
1870
2125
  }
@@ -1882,35 +2137,132 @@ export function buildSimpleChartOption(
1882
2137
  ): EChartsOption {
1883
2138
  if (parsed.error) return {};
1884
2139
 
1885
- const { textColor, axisLineColor, splitLineColor, gridOpacity, colors, titleConfig, tooltipTheme } = buildChartCommons(parsed, palette, isDark);
2140
+ const {
2141
+ textColor,
2142
+ axisLineColor,
2143
+ splitLineColor,
2144
+ gridOpacity,
2145
+ colors,
2146
+ titleConfig,
2147
+ tooltipTheme,
2148
+ } = buildChartCommons(parsed, palette, isDark);
1886
2149
  const bg = isDark ? palette.surface : palette.bg;
1887
2150
 
1888
2151
  switch (parsed.type) {
1889
2152
  case 'bar':
1890
- return buildBarOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, bg, titleConfig, tooltipTheme, chartWidth);
2153
+ return buildBarOption(
2154
+ parsed,
2155
+ textColor,
2156
+ axisLineColor,
2157
+ splitLineColor,
2158
+ gridOpacity,
2159
+ colors,
2160
+ bg,
2161
+ titleConfig,
2162
+ tooltipTheme,
2163
+ chartWidth
2164
+ );
1891
2165
  case 'bar-stacked':
1892
- return buildBarStackedOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, bg, titleConfig, tooltipTheme, chartWidth);
2166
+ return buildBarStackedOption(
2167
+ parsed,
2168
+ textColor,
2169
+ axisLineColor,
2170
+ splitLineColor,
2171
+ gridOpacity,
2172
+ colors,
2173
+ bg,
2174
+ titleConfig,
2175
+ tooltipTheme,
2176
+ chartWidth
2177
+ );
1893
2178
  case 'line':
1894
2179
  return parsed.seriesNames
1895
- ? buildMultiLineOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, colors, titleConfig, tooltipTheme, chartWidth)
1896
- : buildLineOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, titleConfig, tooltipTheme, chartWidth);
2180
+ ? buildMultiLineOption(
2181
+ parsed,
2182
+ palette,
2183
+ textColor,
2184
+ axisLineColor,
2185
+ splitLineColor,
2186
+ gridOpacity,
2187
+ colors,
2188
+ titleConfig,
2189
+ tooltipTheme,
2190
+ chartWidth
2191
+ )
2192
+ : buildLineOption(
2193
+ parsed,
2194
+ palette,
2195
+ textColor,
2196
+ axisLineColor,
2197
+ splitLineColor,
2198
+ gridOpacity,
2199
+ titleConfig,
2200
+ tooltipTheme,
2201
+ chartWidth
2202
+ );
1897
2203
  case 'area':
1898
- return buildAreaOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, titleConfig, tooltipTheme, chartWidth);
2204
+ return buildAreaOption(
2205
+ parsed,
2206
+ palette,
2207
+ textColor,
2208
+ axisLineColor,
2209
+ splitLineColor,
2210
+ gridOpacity,
2211
+ titleConfig,
2212
+ tooltipTheme,
2213
+ chartWidth
2214
+ );
1899
2215
  case 'pie':
1900
- return buildPieOption(parsed, textColor, getSegmentColors(palette, parsed.data.length), bg, titleConfig, tooltipTheme, false);
2216
+ return buildPieOption(
2217
+ parsed,
2218
+ textColor,
2219
+ getSegmentColors(palette, parsed.data.length),
2220
+ bg,
2221
+ titleConfig,
2222
+ tooltipTheme,
2223
+ false
2224
+ );
1901
2225
  case 'doughnut':
1902
- return buildPieOption(parsed, textColor, getSegmentColors(palette, parsed.data.length), bg, titleConfig, tooltipTheme, true);
2226
+ return buildPieOption(
2227
+ parsed,
2228
+ textColor,
2229
+ getSegmentColors(palette, parsed.data.length),
2230
+ bg,
2231
+ titleConfig,
2232
+ tooltipTheme,
2233
+ true
2234
+ );
1903
2235
  case 'radar':
1904
- return buildRadarOption(parsed, palette, isDark, textColor, gridOpacity, titleConfig, tooltipTheme);
2236
+ return buildRadarOption(
2237
+ parsed,
2238
+ palette,
2239
+ isDark,
2240
+ textColor,
2241
+ gridOpacity,
2242
+ titleConfig,
2243
+ tooltipTheme
2244
+ );
1905
2245
  case 'polar-area':
1906
- return buildPolarAreaOption(parsed, textColor, getSegmentColors(palette, parsed.data.length), bg, titleConfig, tooltipTheme);
2246
+ return buildPolarAreaOption(
2247
+ parsed,
2248
+ textColor,
2249
+ getSegmentColors(palette, parsed.data.length),
2250
+ bg,
2251
+ titleConfig,
2252
+ tooltipTheme
2253
+ );
1907
2254
  }
1908
2255
  }
1909
2256
 
1910
2257
  /**
1911
2258
  * Builds a standard chart grid object with consistent spacing rules.
1912
2259
  */
1913
- function makeChartGrid(options: { xLabel?: string; yLabel?: string; hasTitle: boolean; hasLegend?: boolean }): Record<string, unknown> {
2260
+ function makeChartGrid(options: {
2261
+ xLabel?: string;
2262
+ yLabel?: string;
2263
+ hasTitle: boolean;
2264
+ hasLegend?: boolean;
2265
+ }): Record<string, unknown> {
1914
2266
  return {
1915
2267
  left: options.yLabel ? '12%' : '3%',
1916
2268
  right: '4%',
@@ -1941,17 +2293,39 @@ function buildBarOption(
1941
2293
  const stroke = d.color ?? colors[i % colors.length];
1942
2294
  return {
1943
2295
  value: d.value,
1944
- itemStyle: { color: mix(stroke, bg, 30), borderColor: stroke, borderWidth: CHART_BORDER_WIDTH },
2296
+ itemStyle: {
2297
+ color: mix(stroke, bg, 30),
2298
+ borderColor: stroke,
2299
+ borderWidth: CHART_BORDER_WIDTH,
2300
+ },
1945
2301
  };
1946
2302
  });
1947
2303
 
1948
2304
  // When category labels are on the y-axis (horizontal bars), they can be wide —
1949
2305
  // compute a nameGap that clears the longest label so the ylabel doesn't overlap.
1950
- const hCatGap = isHorizontal && yLabel
1951
- ? Math.max(40, Math.max(...labels.map((l) => l.length)) * 8 + 16)
1952
- : undefined;
1953
- const categoryAxis = makeGridAxis('category', textColor, axisLineColor, splitLineColor, gridOpacity, isHorizontal ? yLabel : xLabel, labels, hCatGap, !isHorizontal ? chartWidth : undefined);
1954
- const valueAxis = makeGridAxis('value', textColor, axisLineColor, splitLineColor, gridOpacity, isHorizontal ? xLabel : yLabel);
2306
+ const hCatGap =
2307
+ isHorizontal && yLabel
2308
+ ? Math.max(40, Math.max(...labels.map((l) => l.length)) * 8 + 16)
2309
+ : undefined;
2310
+ const categoryAxis = makeGridAxis(
2311
+ 'category',
2312
+ textColor,
2313
+ axisLineColor,
2314
+ splitLineColor,
2315
+ gridOpacity,
2316
+ isHorizontal ? yLabel : xLabel,
2317
+ labels,
2318
+ hCatGap,
2319
+ !isHorizontal ? chartWidth : undefined
2320
+ );
2321
+ const valueAxis = makeGridAxis(
2322
+ 'value',
2323
+ textColor,
2324
+ axisLineColor,
2325
+ splitLineColor,
2326
+ gridOpacity,
2327
+ isHorizontal ? xLabel : yLabel
2328
+ );
1955
2329
 
1956
2330
  // xAxis is always the bottom axis, yAxis is always the left axis in ECharts
1957
2331
 
@@ -1999,7 +2373,8 @@ function buildMarkArea(
1999
2373
  data: eras.map((era) => {
2000
2374
  const startIdx = labels.indexOf(era.start);
2001
2375
  const endIdx = labels.indexOf(era.end);
2002
- const bandSlots = startIdx >= 0 && endIdx >= 0 ? endIdx - startIdx : Infinity;
2376
+ const bandSlots =
2377
+ startIdx >= 0 && endIdx >= 0 ? endIdx - startIdx : Infinity;
2003
2378
  const color = era.color ?? defaultColor;
2004
2379
  return [
2005
2380
  {
@@ -2033,7 +2408,8 @@ function buildLineOption(
2033
2408
  chartWidth?: number
2034
2409
  ): EChartsOption {
2035
2410
  const { xLabel, yLabel } = resolveAxisLabels(parsed);
2036
- const lineColor = parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
2411
+ const lineColor =
2412
+ parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
2037
2413
  const labels = parsed.data.map((d) => d.label);
2038
2414
  const values = parsed.data.map((d) => d.value);
2039
2415
  const eras = parsed.eras ?? [];
@@ -2049,8 +2425,26 @@ function buildLineOption(
2049
2425
  axisPointer: { type: 'line' },
2050
2426
  },
2051
2427
  grid: makeChartGrid({ xLabel, yLabel, hasTitle: !!parsed.title }),
2052
- xAxis: makeGridAxis('category', textColor, axisLineColor, splitLineColor, gridOpacity, xLabel, labels, undefined, chartWidth, interval),
2053
- yAxis: makeGridAxis('value', textColor, axisLineColor, splitLineColor, gridOpacity, yLabel),
2428
+ xAxis: makeGridAxis(
2429
+ 'category',
2430
+ textColor,
2431
+ axisLineColor,
2432
+ splitLineColor,
2433
+ gridOpacity,
2434
+ xLabel,
2435
+ labels,
2436
+ undefined,
2437
+ chartWidth,
2438
+ interval
2439
+ ),
2440
+ yAxis: makeGridAxis(
2441
+ 'value',
2442
+ textColor,
2443
+ axisLineColor,
2444
+ splitLineColor,
2445
+ gridOpacity,
2446
+ yLabel
2447
+ ),
2054
2448
  series: [
2055
2449
  {
2056
2450
  type: 'line',
@@ -2118,9 +2512,32 @@ function buildMultiLineOption(
2118
2512
  bottom: 10,
2119
2513
  textStyle: { color: textColor },
2120
2514
  },
2121
- grid: makeChartGrid({ xLabel, yLabel, hasTitle: !!parsed.title, hasLegend: true }),
2122
- xAxis: makeGridAxis('category', textColor, axisLineColor, splitLineColor, gridOpacity, xLabel, labels, undefined, chartWidth, interval),
2123
- yAxis: makeGridAxis('value', textColor, axisLineColor, splitLineColor, gridOpacity, yLabel),
2515
+ grid: makeChartGrid({
2516
+ xLabel,
2517
+ yLabel,
2518
+ hasTitle: !!parsed.title,
2519
+ hasLegend: true,
2520
+ }),
2521
+ xAxis: makeGridAxis(
2522
+ 'category',
2523
+ textColor,
2524
+ axisLineColor,
2525
+ splitLineColor,
2526
+ gridOpacity,
2527
+ xLabel,
2528
+ labels,
2529
+ undefined,
2530
+ chartWidth,
2531
+ interval
2532
+ ),
2533
+ yAxis: makeGridAxis(
2534
+ 'value',
2535
+ textColor,
2536
+ axisLineColor,
2537
+ splitLineColor,
2538
+ gridOpacity,
2539
+ yLabel
2540
+ ),
2124
2541
  series,
2125
2542
  };
2126
2543
  }
@@ -2139,7 +2556,8 @@ function buildAreaOption(
2139
2556
  chartWidth?: number
2140
2557
  ): EChartsOption {
2141
2558
  const { xLabel, yLabel } = resolveAxisLabels(parsed);
2142
- const lineColor = parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
2559
+ const lineColor =
2560
+ parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
2143
2561
  const labels = parsed.data.map((d) => d.label);
2144
2562
  const values = parsed.data.map((d) => d.value);
2145
2563
  const eras = parsed.eras ?? [];
@@ -2155,8 +2573,26 @@ function buildAreaOption(
2155
2573
  axisPointer: { type: 'line' },
2156
2574
  },
2157
2575
  grid: makeChartGrid({ xLabel, yLabel, hasTitle: !!parsed.title }),
2158
- xAxis: makeGridAxis('category', textColor, axisLineColor, splitLineColor, gridOpacity, xLabel, labels, undefined, chartWidth, interval),
2159
- yAxis: makeGridAxis('value', textColor, axisLineColor, splitLineColor, gridOpacity, yLabel),
2576
+ xAxis: makeGridAxis(
2577
+ 'category',
2578
+ textColor,
2579
+ axisLineColor,
2580
+ splitLineColor,
2581
+ gridOpacity,
2582
+ xLabel,
2583
+ labels,
2584
+ undefined,
2585
+ chartWidth,
2586
+ interval
2587
+ ),
2588
+ yAxis: makeGridAxis(
2589
+ 'value',
2590
+ textColor,
2591
+ axisLineColor,
2592
+ splitLineColor,
2593
+ gridOpacity,
2594
+ yLabel
2595
+ ),
2160
2596
  series: [
2161
2597
  {
2162
2598
  type: 'line',
@@ -2211,7 +2647,11 @@ function buildPieOption(
2211
2647
  return {
2212
2648
  name: d.label,
2213
2649
  value: d.value,
2214
- itemStyle: { color: mix(stroke, bg, 30), borderColor: stroke, borderWidth: CHART_BORDER_WIDTH },
2650
+ itemStyle: {
2651
+ color: mix(stroke, bg, 30),
2652
+ borderColor: stroke,
2653
+ borderWidth: CHART_BORDER_WIDTH,
2654
+ },
2215
2655
  };
2216
2656
  });
2217
2657
 
@@ -2253,7 +2693,8 @@ function buildRadarOption(
2253
2693
  tooltipTheme: Record<string, unknown>
2254
2694
  ): EChartsOption {
2255
2695
  const bg = isDark ? palette.surface : palette.bg;
2256
- const radarColor = parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
2696
+ const radarColor =
2697
+ parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
2257
2698
  const values = parsed.data.map((d) => d.value);
2258
2699
  const maxValue = Math.max(...values) * 1.15;
2259
2700
 
@@ -2328,7 +2769,11 @@ function buildPolarAreaOption(
2328
2769
  return {
2329
2770
  name: d.label,
2330
2771
  value: d.value,
2331
- itemStyle: { color: mix(stroke, bg, 30), borderColor: stroke, borderWidth: CHART_BORDER_WIDTH },
2772
+ itemStyle: {
2773
+ color: mix(stroke, bg, 30),
2774
+ borderColor: stroke,
2775
+ borderWidth: CHART_BORDER_WIDTH,
2776
+ },
2332
2777
  };
2333
2778
  });
2334
2779
 
@@ -2389,7 +2834,11 @@ function buildBarStackedOption(
2389
2834
  type: 'bar' as const,
2390
2835
  stack: 'total',
2391
2836
  data,
2392
- itemStyle: { color: mix(color, bg, 30), borderColor: color, borderWidth: CHART_BORDER_WIDTH },
2837
+ itemStyle: {
2838
+ color: mix(color, bg, 30),
2839
+ borderColor: color,
2840
+ borderWidth: CHART_BORDER_WIDTH,
2841
+ },
2393
2842
  label: {
2394
2843
  show: true,
2395
2844
  position: 'inside' as const,
@@ -2403,14 +2852,34 @@ function buildBarStackedOption(
2403
2852
  };
2404
2853
  });
2405
2854
 
2406
- const hCatGap = isHorizontal && yLabel
2407
- ? Math.max(40, Math.max(...labels.map((l) => l.length)) * 8 + 16)
2408
- : undefined;
2409
- const categoryAxis = makeGridAxis('category', textColor, axisLineColor, splitLineColor, gridOpacity, isHorizontal ? yLabel : xLabel, labels, hCatGap, !isHorizontal ? chartWidth : undefined);
2855
+ const hCatGap =
2856
+ isHorizontal && yLabel
2857
+ ? Math.max(40, Math.max(...labels.map((l) => l.length)) * 8 + 16)
2858
+ : undefined;
2859
+ const categoryAxis = makeGridAxis(
2860
+ 'category',
2861
+ textColor,
2862
+ axisLineColor,
2863
+ splitLineColor,
2864
+ gridOpacity,
2865
+ isHorizontal ? yLabel : xLabel,
2866
+ labels,
2867
+ hCatGap,
2868
+ !isHorizontal ? chartWidth : undefined
2869
+ );
2410
2870
  // For horizontal bars with a legend, use a smaller nameGap so the xlabel
2411
2871
  // stays close to the axis ticks rather than drifting toward the legend.
2412
2872
  const hValueGap = isHorizontal && xLabel ? 40 : undefined;
2413
- const valueAxis = makeGridAxis('value', textColor, axisLineColor, splitLineColor, gridOpacity, isHorizontal ? xLabel : yLabel, undefined, hValueGap);
2873
+ const valueAxis = makeGridAxis(
2874
+ 'value',
2875
+ textColor,
2876
+ axisLineColor,
2877
+ splitLineColor,
2878
+ gridOpacity,
2879
+ isHorizontal ? xLabel : yLabel,
2880
+ undefined,
2881
+ hValueGap
2882
+ );
2414
2883
 
2415
2884
  return {
2416
2885
  ...CHART_BASE,
@@ -2420,7 +2889,12 @@ function buildBarStackedOption(
2420
2889
  bottom: 10,
2421
2890
  textStyle: { color: textColor },
2422
2891
  },
2423
- grid: makeChartGrid({ xLabel, yLabel, hasTitle: !!parsed.title, hasLegend: true }),
2892
+ grid: makeChartGrid({
2893
+ xLabel,
2894
+ yLabel,
2895
+ hasTitle: !!parsed.title,
2896
+ hasLegend: true,
2897
+ }),
2424
2898
  xAxis: isHorizontal ? valueAxis : categoryAxis,
2425
2899
  yAxis: isHorizontal ? categoryAxis : valueAxis,
2426
2900
  series,
@@ -2436,8 +2910,15 @@ const ECHART_EXPORT_HEIGHT = 800;
2436
2910
 
2437
2911
  // Standard chart types handled by buildSimpleChartOption (via parseChart)
2438
2912
  const STANDARD_CHART_TYPES = new Set([
2439
- 'bar', 'line', 'multi-line', 'area', 'pie', 'doughnut',
2440
- 'radar', 'polar-area', 'bar-stacked',
2913
+ 'bar',
2914
+ 'line',
2915
+ 'multi-line',
2916
+ 'area',
2917
+ 'pie',
2918
+ 'doughnut',
2919
+ 'radar',
2920
+ 'polar-area',
2921
+ 'bar-stacked',
2441
2922
  ]);
2442
2923
 
2443
2924
  /**
@@ -2472,13 +2953,18 @@ export async function renderExtendedChartForExport(
2472
2953
  if (!chartType) return '';
2473
2954
 
2474
2955
  let option: EChartsOption;
2475
- let legendGroups: LegendGroupData[] = [];
2956
+ let legendGroups: LegendGroupData[] = []; // eslint-disable-line no-useless-assignment
2476
2957
  const colors = getSeriesColors(effectivePalette);
2477
2958
 
2478
2959
  if (STANDARD_CHART_TYPES.has(chartType)) {
2479
2960
  const parsed = parseChart(content, effectivePalette);
2480
2961
  if (parsed.error) return '';
2481
- option = buildSimpleChartOption(parsed, effectivePalette, isDark, ECHART_EXPORT_WIDTH);
2962
+ option = buildSimpleChartOption(
2963
+ parsed,
2964
+ effectivePalette,
2965
+ isDark,
2966
+ ECHART_EXPORT_WIDTH
2967
+ );
2482
2968
  legendGroups = getSimpleChartLegendGroups(parsed, colors);
2483
2969
  } else {
2484
2970
  const parsed = parseExtendedChart(content, effectivePalette);
@@ -2507,7 +2993,8 @@ export async function renderExtendedChartForExport(
2507
2993
 
2508
2994
  // The SSR output already includes xmlns, width, height, and viewBox.
2509
2995
  // Inject font-family and background on the root <svg> element.
2510
- const bgStyle = theme !== 'transparent' ? `background: ${effectivePalette.bg}; ` : '';
2996
+ const bgStyle =
2997
+ theme !== 'transparent' ? `background: ${effectivePalette.bg}; ` : '';
2511
2998
  let result = svgString.replace(
2512
2999
  /^<svg /,
2513
3000
  `<svg style="${bgStyle}font-family: ${FONT_FAMILY}" `
@@ -2515,13 +3002,18 @@ export async function renderExtendedChartForExport(
2515
3002
 
2516
3003
  // Inject custom legend SVG when present
2517
3004
  if (legendGroups.length > 0) {
2518
- const titleHeight = option.title && (option.title as { text?: string }).text ? 40 : 0;
3005
+ const titleHeight =
3006
+ option.title && (option.title as { text?: string }).text ? 40 : 0;
2519
3007
  const legendY = 8 + titleHeight;
2520
3008
  // In static export, expand the first group so entries are visible
2521
3009
  // Extract grid offsets for plot-area-centered legend
2522
3010
  const grid = option.grid as Record<string, unknown> | undefined;
2523
- const gridLeftPct = grid?.left ? parseFloat(String(grid.left)) : undefined;
2524
- const gridRightPct = grid?.right ? parseFloat(String(grid.right)) : undefined;
3011
+ const gridLeftPct = grid?.left
3012
+ ? parseFloat(String(grid.left))
3013
+ : undefined;
3014
+ const gridRightPct = grid?.right
3015
+ ? parseFloat(String(grid.right))
3016
+ : undefined;
2525
3017
  const { svg: legendSvgStr } = renderLegendSvg(legendGroups, {
2526
3018
  palette: effectivePalette,
2527
3019
  isDark,
@@ -2534,12 +3026,13 @@ export async function renderExtendedChartForExport(
2534
3026
  // Insert legend group right after the opening <svg ...> tag
2535
3027
  result = result.replace(
2536
3028
  /(<svg[^>]*>)/,
2537
- `$1<g transform="translate(0,${legendY})">${legendSvgStr}</g>`,
3029
+ `$1<g transform="translate(0,${legendY})">${legendSvgStr}</g>`
2538
3030
  );
2539
3031
  }
2540
3032
 
2541
3033
  if (options?.branding !== false) {
2542
- const brandColor = theme === 'transparent' ? '#888' : effectivePalette.textMuted;
3034
+ const brandColor =
3035
+ theme === 'transparent' ? '#888' : effectivePalette.textMuted;
2543
3036
  result = injectBranding(result, brandColor);
2544
3037
  }
2545
3038