@diagrammo/dgmo 0.8.2 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.claude/commands/dgmo-diagram-this.md +60 -0
  2. package/.claude/commands/dgmo-document-project.md +128 -0
  3. package/.claude/commands/dgmo.md +185 -50
  4. package/.cursorrules +32 -37
  5. package/.github/copilot-instructions.md +35 -44
  6. package/.windsurfrules +32 -37
  7. package/README.md +4 -4
  8. package/dist/cli.cjs +189 -194
  9. package/dist/editor.cjs +336 -0
  10. package/dist/editor.cjs.map +1 -0
  11. package/dist/editor.d.cts +27 -0
  12. package/dist/editor.d.ts +27 -0
  13. package/dist/editor.js +305 -0
  14. package/dist/editor.js.map +1 -0
  15. package/dist/index.cjs +3699 -1564
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +7 -6
  18. package/dist/index.d.ts +7 -6
  19. package/dist/index.js +3699 -1564
  20. package/dist/index.js.map +1 -1
  21. package/docs/language-reference.md +822 -1060
  22. package/gallery/fixtures/arc.dgmo +18 -0
  23. package/gallery/fixtures/area.dgmo +19 -0
  24. package/gallery/fixtures/bar-stacked.dgmo +10 -0
  25. package/gallery/fixtures/bar.dgmo +10 -0
  26. package/gallery/fixtures/c4-full.dgmo +52 -0
  27. package/gallery/fixtures/c4.dgmo +17 -0
  28. package/gallery/fixtures/chord.dgmo +12 -0
  29. package/gallery/fixtures/class-basic.dgmo +14 -0
  30. package/gallery/fixtures/class-full.dgmo +43 -0
  31. package/gallery/fixtures/doughnut.dgmo +8 -0
  32. package/gallery/fixtures/flowchart-basic.dgmo +3 -0
  33. package/gallery/fixtures/flowchart-colors.dgmo +5 -0
  34. package/gallery/fixtures/flowchart-complex.dgmo +17 -0
  35. package/gallery/fixtures/flowchart-decision.dgmo +5 -0
  36. package/gallery/fixtures/flowchart-full.dgmo +13 -0
  37. package/gallery/fixtures/flowchart-groups.dgmo +10 -0
  38. package/gallery/fixtures/flowchart-loop.dgmo +7 -0
  39. package/gallery/fixtures/flowchart-nested.dgmo +7 -0
  40. package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
  41. package/gallery/fixtures/function.dgmo +8 -0
  42. package/gallery/fixtures/funnel.dgmo +7 -0
  43. package/gallery/fixtures/gantt-full.dgmo +49 -0
  44. package/gallery/fixtures/gantt.dgmo +42 -0
  45. package/gallery/fixtures/heatmap.dgmo +8 -0
  46. package/gallery/fixtures/infra-full.dgmo +78 -0
  47. package/gallery/fixtures/infra-overload.dgmo +25 -0
  48. package/gallery/fixtures/infra.dgmo +47 -0
  49. package/gallery/fixtures/initiative-status-full.dgmo +46 -0
  50. package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
  51. package/gallery/fixtures/initiative-status.dgmo +9 -0
  52. package/gallery/fixtures/line.dgmo +19 -0
  53. package/gallery/fixtures/multi-line.dgmo +11 -0
  54. package/gallery/fixtures/org-basic.dgmo +16 -0
  55. package/gallery/fixtures/org-full.dgmo +69 -0
  56. package/gallery/fixtures/org-teams.dgmo +25 -0
  57. package/gallery/fixtures/pie.dgmo +9 -0
  58. package/gallery/fixtures/polar-area.dgmo +8 -0
  59. package/gallery/fixtures/quadrant.dgmo +18 -0
  60. package/gallery/fixtures/radar.dgmo +8 -0
  61. package/gallery/fixtures/sankey.dgmo +31 -0
  62. package/gallery/fixtures/scatter.dgmo +21 -0
  63. package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
  64. package/gallery/fixtures/sequence-tags.dgmo +41 -0
  65. package/gallery/fixtures/sequence.dgmo +35 -0
  66. package/gallery/fixtures/sitemap-basic.dgmo +12 -0
  67. package/gallery/fixtures/sitemap-full.dgmo +156 -0
  68. package/gallery/fixtures/slope.dgmo +8 -0
  69. package/gallery/fixtures/spr-eras.dgmo +62 -0
  70. package/gallery/fixtures/state.dgmo +30 -0
  71. package/gallery/fixtures/timeline-intraday.dgmo +14 -0
  72. package/gallery/fixtures/timeline.dgmo +32 -0
  73. package/gallery/fixtures/venn.dgmo +10 -0
  74. package/gallery/fixtures/wordcloud.dgmo +24 -0
  75. package/package.json +51 -2
  76. package/src/c4/layout.ts +372 -90
  77. package/src/c4/parser.ts +113 -62
  78. package/src/chart.ts +149 -64
  79. package/src/class/parser.ts +84 -28
  80. package/src/class/renderer.ts +2 -2
  81. package/src/cli.ts +179 -77
  82. package/src/completion.ts +381 -182
  83. package/src/d3.ts +1026 -428
  84. package/src/dgmo-mermaid.ts +16 -13
  85. package/src/dgmo-router.ts +70 -24
  86. package/src/echarts.ts +682 -169
  87. package/src/editor/dgmo.grammar +69 -0
  88. package/src/editor/dgmo.grammar.d.ts +2 -0
  89. package/src/editor/dgmo.grammar.js +18 -0
  90. package/src/editor/dgmo.grammar.terms.d.ts +5 -0
  91. package/src/editor/dgmo.grammar.terms.js +35 -0
  92. package/src/editor/highlight.ts +36 -0
  93. package/src/editor/index.ts +28 -0
  94. package/src/editor/keywords.ts +220 -0
  95. package/src/editor/tokens.ts +30 -0
  96. package/src/er/parser.ts +55 -29
  97. package/src/er/renderer.ts +112 -53
  98. package/src/gantt/calculator.ts +91 -29
  99. package/src/gantt/parser.ts +291 -97
  100. package/src/gantt/renderer.ts +1120 -350
  101. package/src/graph/flowchart-parser.ts +48 -75
  102. package/src/graph/state-parser.ts +54 -27
  103. package/src/infra/parser.ts +161 -177
  104. package/src/infra/renderer.ts +723 -271
  105. package/src/infra/types.ts +0 -1
  106. package/src/initiative-status/parser.ts +144 -56
  107. package/src/kanban/parser.ts +27 -19
  108. package/src/org/layout.ts +111 -44
  109. package/src/org/parser.ts +71 -27
  110. package/src/org/resolver.ts +3 -3
  111. package/src/palettes/index.ts +3 -2
  112. package/src/render.ts +1 -2
  113. package/src/sequence/parser.ts +209 -100
  114. package/src/sitemap/parser.ts +73 -44
  115. package/src/utils/arrows.ts +2 -22
  116. package/src/utils/duration.ts +39 -21
  117. package/src/utils/legend-constants.ts +0 -2
  118. package/src/utils/parsing.ts +82 -72
  119. package/src/utils/tag-groups.ts +4 -41
  120. package/src/infra/serialize.ts +0 -67
package/src/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
@@ -82,6 +81,7 @@ export interface ParsedExtendedChart {
82
81
  ylabelLineNumber?: number;
83
82
  sizelabel?: string;
84
83
  showLabels?: boolean;
84
+ shade?: boolean;
85
85
  categoryColors?: Record<string, string>;
86
86
  categoryLineNumbers?: Record<string, number>;
87
87
  nodeColors?: Record<string, string>;
@@ -100,7 +100,13 @@ import { parseChart } from './chart';
100
100
  import type { ParsedChart, ChartEra } from './chart';
101
101
  import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
102
102
  import { resolveColor } from './colors';
103
- 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';
104
110
  import { parseDataRowValues } from './chart';
105
111
 
106
112
  // ============================================================
@@ -111,9 +117,17 @@ const EMPHASIS_SELF = { focus: 'self' as const, blurScope: 'global' as const };
111
117
  const EMPHASIS_LINE = {
112
118
  ...EMPHASIS_SELF,
113
119
  scale: 2.5,
114
- 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,
115
130
  };
116
- const CHART_BASE: Pick<EChartsOption, 'backgroundColor' | 'animation'> = { backgroundColor: 'transparent', animation: false };
117
131
  const CHART_BORDER_WIDTH = 2;
118
132
 
119
133
  // ============================================================
@@ -121,13 +135,26 @@ const CHART_BORDER_WIDTH = 2;
121
135
  // ============================================================
122
136
 
123
137
  const VALID_EXTENDED_TYPES = new Set<ExtendedChartType>([
124
- 'sankey', 'chord', 'function', 'scatter', 'heatmap', 'funnel',
138
+ 'sankey',
139
+ 'chord',
140
+ 'function',
141
+ 'scatter',
142
+ 'heatmap',
143
+ 'funnel',
125
144
  ]);
126
145
 
127
146
  /** Known option keywords for the extended chart parser. */
128
147
  const KNOWN_EXTENDED_OPTIONS = new Set([
129
- 'chart', 'title', 'series', 'xlabel', 'ylabel', 'sizelabel', 'labels',
130
- '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',
131
158
  ]);
132
159
 
133
160
  /**
@@ -138,11 +165,14 @@ function parseScatterRow(
138
165
  line: string,
139
166
  palette: PaletteColors | undefined,
140
167
  currentCategory: string,
141
- lineNumber: number,
168
+ lineNumber: number
142
169
  ): ParsedScatterPoint | null {
143
- const dataRow = parseDataRowValues(line);
170
+ const dataRow = parseDataRowValues(line, { multiValue: true });
144
171
  if (!dataRow || dataRow.values.length < 2) return null;
145
- const { label: rawLabel, color: pointColor } = extractColor(dataRow.label, palette);
172
+ const { label: rawLabel, color: pointColor } = extractColor(
173
+ dataRow.label,
174
+ palette
175
+ );
146
176
  return {
147
177
  name: rawLabel,
148
178
  x: dataRow.values[0],
@@ -194,8 +224,16 @@ export function parseExtendedChart(
194
224
 
195
225
  // Reject legacy ## category syntax
196
226
  if (/^#{2,}\s+/.test(trimmed)) {
197
- const name = trimmed.replace(/^#{2,}\s+/, '').replace(/\s*\([^)]*\)\s*$/, '').trim();
198
- 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
+ );
199
237
  continue;
200
238
  }
201
239
 
@@ -207,7 +245,8 @@ export function parseExtendedChart(
207
245
  firstLineParsed = true;
208
246
  const firstLine = parseFirstLine(trimmed);
209
247
  if (firstLine) {
210
- const chartType = firstLine.chartType.toLowerCase() as ExtendedChartType;
248
+ const chartType =
249
+ firstLine.chartType.toLowerCase() as ExtendedChartType;
211
250
  if (VALID_EXTENDED_TYPES.has(chartType)) {
212
251
  result.type = chartType;
213
252
  if (firstLine.title) {
@@ -228,7 +267,11 @@ export function parseExtendedChart(
228
267
  }
229
268
  // If the first line is a single word (no spaces, no colon, no numbers),
230
269
  // treat it as an unrecognized chart type rather than falling through
231
- if (!trimmed.includes(' ') && !trimmed.includes(':') && !/\d/.test(trimmed)) {
270
+ if (
271
+ !trimmed.includes(' ') &&
272
+ !trimmed.includes(':') &&
273
+ !/\d/.test(trimmed)
274
+ ) {
232
275
  const validTypes = [...VALID_EXTENDED_TYPES];
233
276
  let msg = `Unsupported chart type: ${trimmed}. Supported types: ${validTypes.join(', ')}.`;
234
277
  const hint = suggest(trimmed.toLowerCase(), validTypes);
@@ -245,7 +288,9 @@ export function parseExtendedChart(
245
288
  const categoryMatch = trimmed.match(/^\[(.+?)\](?:\s*\(([^)]+)\))?\s*$/);
246
289
  if (categoryMatch) {
247
290
  const catName = categoryMatch[1].trim();
248
- const catColor = categoryMatch[2] ? resolveColor(categoryMatch[2].trim(), palette) : null;
291
+ const catColor = categoryMatch[2]
292
+ ? resolveColor(categoryMatch[2].trim(), palette)
293
+ : null;
249
294
  if (catColor) {
250
295
  if (!result.categoryColors) result.categoryColors = {};
251
296
  result.categoryColors[catName] = catColor;
@@ -257,17 +302,27 @@ export function parseExtendedChart(
257
302
  }
258
303
 
259
304
  // Sankey/chord link syntax: Source -> Target Value (directed) or Source -- Target Value (undirected)
260
- 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
+ );
261
308
  if (arrowMatch) {
262
309
  const [, rawSource, arrow, rawTarget, val, rawLinkColor] = arrowMatch;
263
- const { label: source, color: sourceColor } = extractColor(rawSource.trim(), palette);
264
- 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
+ );
265
318
  if (sourceColor || targetColor) {
266
319
  if (!result.nodeColors) result.nodeColors = {};
267
320
  if (sourceColor) result.nodeColors[source] = sourceColor;
268
321
  if (targetColor) result.nodeColors[target] = targetColor;
269
322
  }
270
- const linkColor = rawLinkColor ? resolveColor(rawLinkColor.trim(), palette) : undefined;
323
+ const linkColor = rawLinkColor
324
+ ? resolveColor(rawLinkColor.trim(), palette)
325
+ : undefined;
271
326
  if (!result.links) result.links = [];
272
327
  result.links.push({
273
328
  source,
@@ -292,19 +347,34 @@ export function parseExtendedChart(
292
347
  if (sankeyStack.length > 0) {
293
348
  // Parse "TargetName value (linkColor)" or "TargetName(nodeColor) value (linkColor)"
294
349
  // Strip trailing (color) annotation before parseDataRowValues — it can't handle it
295
- const valColorMatch = trimmed.match(/(\d+(?:\.\d+)?)\s*\(([^)]+)\)\s*$/);
296
- 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;
297
356
  const dataRow = parseDataRowValues(strippedLine);
298
357
  if (dataRow && dataRow.values.length === 1) {
299
358
  const source = sankeyStack.at(-1)!.name;
300
- const linkColor = valColorMatch?.[2] ? resolveColor(valColorMatch[2].trim(), palette) : undefined;
301
- 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
+ );
302
366
  if (targetColor) {
303
367
  if (!result.nodeColors) result.nodeColors = {};
304
368
  result.nodeColors[target] = targetColor;
305
369
  }
306
370
  if (!result.links) result.links = [];
307
- 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
+ });
308
378
  sankeyStack.push({ name: target, indent });
309
379
  continue;
310
380
  }
@@ -313,12 +383,17 @@ export function parseExtendedChart(
313
383
 
314
384
  // Bare label at indent 0 (or any indent without a value) = new source node
315
385
  const spaceIdx = trimmed.indexOf(' ');
316
- 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)));
317
389
  if (!hasNumericSuffix) {
318
390
  while (sankeyStack.length && sankeyStack.at(-1)!.indent >= indent) {
319
391
  sankeyStack.pop();
320
392
  }
321
- const { label: nodeName, color: nodeColor } = extractColor(trimmed, palette);
393
+ const { label: nodeName, color: nodeColor } = extractColor(
394
+ trimmed,
395
+ palette
396
+ );
322
397
  if (nodeColor) {
323
398
  if (!result.nodeColors) result.nodeColors = {};
324
399
  result.nodeColors[nodeName] = nodeColor;
@@ -330,7 +405,9 @@ export function parseExtendedChart(
330
405
 
331
406
  // Extract first token to check for known options
332
407
  const spaceIdx = trimmed.indexOf(' ');
333
- const firstToken = (spaceIdx >= 0 ? trimmed.substring(0, spaceIdx) : trimmed).toLowerCase();
408
+ const firstToken = (
409
+ spaceIdx >= 0 ? trimmed.substring(0, spaceIdx) : trimmed
410
+ ).toLowerCase();
334
411
 
335
412
  // Known option with a value
336
413
  if (KNOWN_EXTENDED_OPTIONS.has(firstToken) && spaceIdx >= 0) {
@@ -368,22 +445,31 @@ export function parseExtendedChart(
368
445
  result.seriesNames = parsed.names;
369
446
  result.seriesNameLineNumbers = parsed.nameLineNumbers;
370
447
  }
371
- if (parsed.nameColors.some(Boolean)) result.seriesNameColors = parsed.nameColors;
448
+ if (parsed.nameColors.some(Boolean))
449
+ result.seriesNameColors = parsed.nameColors;
372
450
  continue;
373
451
  }
374
452
 
375
- if (firstToken === 'xlabel') { result.xlabel = value; result.xlabelLineNumber = lineNumber; continue; }
376
- if (firstToken === 'ylabel') { result.ylabel = value; result.ylabelLineNumber = lineNumber; continue; }
377
- if (firstToken === 'sizelabel') { result.sizelabel = value; continue; }
378
-
379
- if (firstToken === 'labels') {
380
- result.showLabels = value.toLowerCase() === 'on' || value.toLowerCase() === 'true';
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;
381
465
  continue;
382
466
  }
383
467
 
384
468
  if (firstToken === 'columns') {
385
469
  if (value) {
386
- result.columns = value.split(',').map((s) => s.trim());
470
+ result.columns = value.includes(',')
471
+ ? value.split(',').map((s) => s.trim())
472
+ : value.split(/\s+/);
387
473
  } else {
388
474
  const collected = collectIndentedValues(lines, i);
389
475
  i = collected.newIndex;
@@ -394,7 +480,9 @@ export function parseExtendedChart(
394
480
 
395
481
  if (firstToken === 'rows') {
396
482
  if (value) {
397
- result.rows = value.split(',').map((s) => s.trim());
483
+ result.rows = value.includes(',')
484
+ ? value.split(',').map((s) => s.trim())
485
+ : value.split(/\s+/);
398
486
  } else {
399
487
  const collected = collectIndentedValues(lines, i);
400
488
  i = collected.newIndex;
@@ -415,6 +503,16 @@ export function parseExtendedChart(
415
503
  }
416
504
  }
417
505
 
506
+ // Bare boolean options
507
+ if (firstToken === 'no-labels') {
508
+ result.showLabels = false;
509
+ continue;
510
+ }
511
+ if (firstToken === 'shade') {
512
+ result.shade = true;
513
+ continue;
514
+ }
515
+
418
516
  // Bare keyword options (no value)
419
517
  if (firstToken === 'series' && spaceIdx === -1) {
420
518
  const parsed = parseSeriesNames('', lines, i, palette);
@@ -425,7 +523,8 @@ export function parseExtendedChart(
425
523
  result.seriesNames = parsed.names;
426
524
  result.seriesNameLineNumbers = parsed.nameLineNumbers;
427
525
  }
428
- if (parsed.nameColors.some(Boolean)) result.seriesNameColors = parsed.nameColors;
526
+ if (parsed.nameColors.some(Boolean))
527
+ result.seriesNameColors = parsed.nameColors;
429
528
  continue;
430
529
  }
431
530
 
@@ -448,7 +547,10 @@ export function parseExtendedChart(
448
547
  if (result.type === 'function') {
449
548
  const colonIndex = trimmed.indexOf(':');
450
549
  if (colonIndex >= 0) {
451
- 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
+ );
452
554
  const fnValue = trimmed.substring(colonIndex + 1).trim();
453
555
  if (!result.functions) result.functions = [];
454
556
  result.functions.push({
@@ -464,7 +566,12 @@ export function parseExtendedChart(
464
566
  // Scatter chart: "Name x, y" or "Name x, y, size"
465
567
  if (result.type === 'scatter') {
466
568
  // Parse from right: trailing comma-separated numbers are x, y [, size]
467
- const scatterData = parseScatterRow(trimmed, palette, currentCategory, lineNumber);
569
+ const scatterData = parseScatterRow(
570
+ trimmed,
571
+ palette,
572
+ currentCategory,
573
+ lineNumber
574
+ );
468
575
  if (scatterData) {
469
576
  if (!result.scatterPoints) result.scatterPoints = [];
470
577
  result.scatterPoints.push(scatterData);
@@ -472,12 +579,16 @@ export function parseExtendedChart(
472
579
  }
473
580
  }
474
581
 
475
- // Heatmap data row: "RowLabel val1, val2, val3, ..."
582
+ // Heatmap data row: "RowLabel val1, val2, val3, ..." or "RowLabel val1 val2 val3"
476
583
  if (result.type === 'heatmap') {
477
- const dataRow = parseDataRowValues(trimmed);
584
+ const dataRow = parseDataRowValues(trimmed, { multiValue: true });
478
585
  if (dataRow && dataRow.values.length > 0) {
479
586
  if (!result.heatmapRows) result.heatmapRows = [];
480
- 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
+ });
481
592
  continue;
482
593
  }
483
594
  }
@@ -485,14 +596,23 @@ export function parseExtendedChart(
485
596
  // Funnel / generic data point: "Label value"
486
597
  const dataRow = parseDataRowValues(trimmed);
487
598
  if (dataRow && dataRow.values.length === 1) {
488
- const { label: rawLabel, color: pointColor } = extractColor(dataRow.label, palette);
599
+ const { label: rawLabel, color: pointColor } = extractColor(
600
+ dataRow.label,
601
+ palette
602
+ );
489
603
  result.data.push({
490
604
  label: rawLabel,
491
605
  value: dataRow.values[0],
492
606
  ...(pointColor && { color: pointColor }),
493
607
  lineNumber,
494
608
  });
609
+ continue;
495
610
  }
611
+
612
+ // Catch-all: nothing matched this line
613
+ result.diagnostics.push(
614
+ makeDgmoError(lineNumber, `Unexpected line: '${trimmed}'.`, 'warning')
615
+ );
496
616
  }
497
617
 
498
618
  const warn = (line: number, message: string): void => {
@@ -510,21 +630,33 @@ export function parseExtendedChart(
510
630
  }
511
631
  } else if (result.type === 'function') {
512
632
  if (!result.functions || result.functions.length === 0) {
513
- 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
+ );
514
637
  }
515
638
  if (!result.xRange) {
516
639
  result.xRange = { min: -10, max: 10 }; // Default range
517
640
  }
518
641
  } else if (result.type === 'scatter') {
519
642
  if (!result.scatterPoints || result.scatterPoints.length === 0) {
520
- 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
+ );
521
647
  }
522
648
  } else if (result.type === 'heatmap') {
523
649
  if (!result.heatmapRows || result.heatmapRows.length === 0) {
524
- 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
+ );
525
654
  }
526
655
  if (!result.columns || result.columns.length === 0) {
527
- 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
+ );
528
660
  }
529
661
  } else if (result.type === 'funnel') {
530
662
  if (result.data.length === 0) {
@@ -543,15 +675,43 @@ export function parseExtendedChart(
543
675
  /**
544
676
  * Computes the shared set of theme-derived variables used by all chart option builders.
545
677
  */
546
- 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
+ ) {
547
683
  const textColor = palette.text;
548
684
  const axisLineColor = palette.border;
549
685
  const splitLineColor = palette.border;
550
686
  const gridOpacity = isDark ? 0.7 : 0.55;
551
687
  const colors = getSeriesColors(palette);
552
- 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;
553
- const tooltipTheme = { backgroundColor: palette.surface, borderColor: palette.border, textStyle: { color: palette.text } };
554
- 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
+ };
555
715
  }
556
716
 
557
717
  /**
@@ -569,7 +729,14 @@ export function buildExtendedChartOption(
569
729
  return {};
570
730
  }
571
731
 
572
- 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);
573
740
 
574
741
  // Sankey chart has different structure
575
742
  if (parsed.type === 'sankey') {
@@ -696,7 +863,7 @@ function buildSankeyOption(
696
863
  nodeGap: 12,
697
864
  nodeWidth: 20,
698
865
  data: nodes,
699
- links: (parsed.links ?? []).map(link => ({
866
+ links: (parsed.links ?? []).map((link) => ({
700
867
  source: link.source,
701
868
  target: link.target,
702
869
  value: link.value,
@@ -758,7 +925,11 @@ function buildChordOption(
758
925
  const stroke = colors[index % colors.length];
759
926
  return {
760
927
  name,
761
- 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
+ },
762
933
  };
763
934
  });
764
935
 
@@ -804,7 +975,9 @@ function buildChordOption(
804
975
  // Detect opposing link pairs to offset curvatures
805
976
  const pairKeys = new Set<string>();
806
977
  for (const l of allLinks) {
807
- 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
+ );
808
981
  if (rev) pairKeys.add(`${l.source}\0${l.target}`);
809
982
  }
810
983
  return allLinks.map((link) => {
@@ -812,13 +985,18 @@ function buildChordOption(
812
985
  // Offset curvature for opposing pairs: one curves more, the other less
813
986
  const baseCurve = 0.3;
814
987
  const curveness = hasOpposite
815
- ? (link.source < link.target ? baseCurve + 0.15 : baseCurve - 0.15)
988
+ ? link.source < link.target
989
+ ? baseCurve + 0.15
990
+ : baseCurve - 0.15
816
991
  : baseCurve;
817
992
  return {
818
993
  source: link.source,
819
994
  target: link.target,
820
995
  value: link.value,
821
- ...(link.directed && { symbol: ['none', 'arrow'], symbolSize: [0, 10] }),
996
+ ...(link.directed && {
997
+ symbol: ['none', 'arrow'],
998
+ symbolSize: [0, 10],
999
+ }),
822
1000
  lineStyle: {
823
1001
  width: Math.max(1, Math.min(link.value / 20, 10)),
824
1002
  color: colors[nodeNames.indexOf(link.source) % colors.length],
@@ -918,6 +1096,12 @@ function buildFunctionOption(
918
1096
  itemStyle: {
919
1097
  color: fnColor,
920
1098
  },
1099
+ ...(parsed.shade && {
1100
+ areaStyle: {
1101
+ color: fnColor,
1102
+ opacity: 0.15,
1103
+ },
1104
+ }),
921
1105
  emphasis: EMPHASIS_SELF,
922
1106
  };
923
1107
  });
@@ -990,16 +1174,18 @@ function buildFunctionOption(
990
1174
  */
991
1175
  export function getSimpleChartLegendGroups(
992
1176
  parsed: ParsedChart,
993
- colors: string[],
1177
+ colors: string[]
994
1178
  ): LegendGroupData[] {
995
1179
  if (!parsed.seriesNames || parsed.seriesNames.length <= 1) return [];
996
- return [{
997
- name: 'Series',
998
- entries: parsed.seriesNames.map((name, i) => ({
999
- value: name,
1000
- color: parsed.seriesNameColors?.[i] ?? colors[i % colors.length],
1001
- })),
1002
- }];
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
+ ];
1003
1189
  }
1004
1190
 
1005
1191
  /**
@@ -1008,31 +1194,37 @@ export function getSimpleChartLegendGroups(
1008
1194
  */
1009
1195
  export function getExtendedChartLegendGroups(
1010
1196
  parsed: ParsedExtendedChart,
1011
- colors: string[],
1197
+ colors: string[]
1012
1198
  ): LegendGroupData[] {
1013
1199
  if (parsed.type === 'scatter') {
1014
1200
  const points = parsed.scatterPoints ?? [];
1015
- 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[];
1016
1204
  if (categories.length === 0) return [];
1017
- return [{
1018
- name: 'Group',
1019
- entries: categories.map((cat, i) => ({
1020
- value: cat,
1021
- color: parsed.categoryColors?.[cat] ?? colors[i % colors.length],
1022
- })),
1023
- }];
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
+ ];
1024
1214
  }
1025
1215
 
1026
1216
  if (parsed.type === 'function') {
1027
1217
  const fns = parsed.functions ?? [];
1028
1218
  if (fns.length === 0) return [];
1029
- return [{
1030
- name: 'Function',
1031
- entries: fns.map((fn, i) => ({
1032
- value: fn.name,
1033
- color: fn.color ?? colors[i % colors.length],
1034
- })),
1035
- }];
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
+ ];
1036
1228
  }
1037
1229
 
1038
1230
  return [];
@@ -1042,16 +1234,30 @@ export function getExtendedChartLegendGroups(
1042
1234
  // Scatter label collision avoidance — greedy placement algorithm
1043
1235
  // ---------------------------------------------------------------------------
1044
1236
 
1045
- interface LabelRect { x: number; y: number; w: number; h: number }
1046
- 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
+ }
1047
1248
 
1048
1249
  /** Axis-aligned bounding box overlap test. @internal exported for testing */
1049
1250
  export function rectsOverlap(a: LabelRect, b: LabelRect): boolean {
1050
- 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
+ );
1051
1254
  }
1052
1255
 
1053
1256
  /** Rect vs circle overlap using nearest-point-on-rect distance check. @internal exported for testing */
1054
- export function rectCircleOverlap(rect: LabelRect, circle: PointCircle): boolean {
1257
+ export function rectCircleOverlap(
1258
+ rect: LabelRect,
1259
+ circle: PointCircle
1260
+ ): boolean {
1055
1261
  const nearestX = Math.max(rect.x, Math.min(circle.cx, rect.x + rect.w));
1056
1262
  const nearestY = Math.max(rect.y, Math.min(circle.cy, rect.y + rect.h));
1057
1263
  const dx = nearestX - circle.cx;
@@ -1114,9 +1320,18 @@ export function computeScatterLabelGraphics(
1114
1320
  : pt.py + offset; // below: label top edge is offset below point center
1115
1321
 
1116
1322
  // Check chart bounds
1117
- if (labelY < chartBounds.top || labelY + labelHeight > chartBounds.bottom) break;
1118
-
1119
- 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
+ };
1120
1335
 
1121
1336
  // Check collisions with all placed labels
1122
1337
  let collision = false;
@@ -1162,7 +1377,12 @@ export function computeScatterLabelGraphics(
1162
1377
  }
1163
1378
  }
1164
1379
 
1165
- 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
+ };
1166
1386
  placedLabels.push(labelRect);
1167
1387
 
1168
1388
  const textY = bestLabelY + labelHeight / 2;
@@ -1252,10 +1472,11 @@ function dataToPixel(
1252
1472
  ): { px: number; py: number } {
1253
1473
  // containLabel: true shrinks the plot area — apply conservative 30px inset
1254
1474
  const inset = 30;
1255
- const gridLeftPx = gridLeftPct * chartWidth / 100 + inset;
1256
- const gridRightPx = chartWidth - gridRightPct * chartWidth / 100 - inset;
1257
- const gridTopPx = gridTopPct * chartHeight / 100 + inset;
1258
- 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;
1259
1480
  const plotWidth = gridRightPx - gridLeftPx;
1260
1481
  const plotHeight = gridBottomPx - gridTopPx;
1261
1482
 
@@ -1287,7 +1508,7 @@ function buildScatterOption(
1287
1508
  const hasCategories = points.some((p) => p.category !== undefined);
1288
1509
  const hasSize = points.some((p) => p.size !== undefined);
1289
1510
 
1290
- const showLabels = parsed.showLabels ?? false;
1511
+ const showLabels = parsed.showLabels ?? true;
1291
1512
  const labelFontSize = 11;
1292
1513
 
1293
1514
  // When showLabels is on, we render labels ourselves via graphic — disable ECharts labels
@@ -1324,7 +1545,11 @@ function buildScatterOption(
1324
1545
  name: p.name,
1325
1546
  value: hasSize ? [p.x, p.y, p.size ?? 0] : [p.x, p.y],
1326
1547
  ...(p.color && {
1327
- 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
+ },
1328
1553
  }),
1329
1554
  }));
1330
1555
 
@@ -1335,7 +1560,11 @@ function buildScatterOption(
1335
1560
  ...(hasSize
1336
1561
  ? { symbolSize: (val: number[]) => val[2] }
1337
1562
  : { symbolSize: defaultSize }),
1338
- 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
+ },
1339
1568
  label: labelConfig,
1340
1569
  emphasis: emphasisConfig,
1341
1570
  };
@@ -1350,7 +1579,11 @@ function buildScatterOption(
1350
1579
  ...(hasSize
1351
1580
  ? { symbolSize: p.size ?? defaultSize }
1352
1581
  : { symbolSize: defaultSize }),
1353
- 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
+ },
1354
1587
  };
1355
1588
  });
1356
1589
 
@@ -1417,13 +1650,23 @@ function buildScatterOption(
1417
1650
  const pt = points[idx];
1418
1651
  const catIndex = pt.category ? categories.indexOf(pt.category) : -1;
1419
1652
  const catColor = pt.category
1420
- ? (parsed.categoryColors?.[pt.category] ?? colors[catIndex % colors.length])
1653
+ ? (parsed.categoryColors?.[pt.category] ??
1654
+ colors[catIndex % colors.length])
1421
1655
  : colors[idx % colors.length];
1422
1656
  const color = pt.color ?? catColor;
1423
1657
  const { px, py } = dataToPixel(
1424
- pt.x, pt.y, axisXMin, axisXMax, axisYMin, axisYMax,
1425
- gridLeft, gridRight, gridTop, gridBottom,
1426
- 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
1427
1670
  );
1428
1671
  labelPoints.push({ name: pt.name, px, py, color, size: pt.size });
1429
1672
  }
@@ -1431,16 +1674,26 @@ function buildScatterOption(
1431
1674
  points.forEach((pt, index) => {
1432
1675
  const color = pt.color ?? colors[index % colors.length];
1433
1676
  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
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
1437
1689
  );
1438
1690
  labelPoints.push({ name: pt.name, px, py, color, size: pt.size });
1439
1691
  });
1440
1692
  }
1441
1693
 
1442
- const chartBoundsTop = gridTop * ECHART_EXPORT_HEIGHT / 100;
1443
- 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;
1444
1697
  graphic = computeScatterLabelGraphics(
1445
1698
  labelPoints,
1446
1699
  { top: chartBoundsTop, bottom: chartBoundsBottom },
@@ -1452,11 +1705,12 @@ function buildScatterOption(
1452
1705
 
1453
1706
  // Build legend for categorized scatter charts
1454
1707
  const categories = hasCategories
1455
- ? [...new Set(points.map((p) => p.category).filter(Boolean))] as string[]
1708
+ ? ([...new Set(points.map((p) => p.category).filter(Boolean))] as string[])
1456
1709
  : [];
1457
- const legendConfig = categories.length > 0
1458
- ? { data: categories, bottom: 10, textStyle: { color: textColor } }
1459
- : undefined;
1710
+ const legendConfig =
1711
+ categories.length > 0
1712
+ ? { data: categories, bottom: 10, textStyle: { color: textColor } }
1713
+ : undefined;
1460
1714
 
1461
1715
  return {
1462
1716
  ...CHART_BASE,
@@ -1813,7 +2067,10 @@ function makeGridAxis(
1813
2067
  const maxLabelLen = Math.max(...data.map((l) => l.length));
1814
2068
  const count = data.length;
1815
2069
  // When interval skips labels, base sizing on visible count (≈ count / step)
1816
- const step = intervalOverride != null && intervalOverride > 0 ? intervalOverride + 1 : 1;
2070
+ const step =
2071
+ intervalOverride != null && intervalOverride > 0
2072
+ ? intervalOverride + 1
2073
+ : 1;
1817
2074
  const visibleCount = Math.ceil(count / step);
1818
2075
  // Reduce font size based on density and label length
1819
2076
  if (visibleCount > 10 || maxLabelLen > 20) catFontSize = 10;
@@ -1822,7 +2079,11 @@ function makeGridAxis(
1822
2079
 
1823
2080
  // Constrain labels to their allotted slot width so ECharts wraps instead of hiding.
1824
2081
  // Skip when interval > 0 — visible labels are spread out and need no constraint.
1825
- if ((intervalOverride == null || intervalOverride === 0) && chartWidthHint && count > 0) {
2082
+ if (
2083
+ (intervalOverride == null || intervalOverride === 0) &&
2084
+ chartWidthHint &&
2085
+ count > 0
2086
+ ) {
1826
2087
  const availPerLabel = Math.floor((chartWidthHint * 0.85) / count);
1827
2088
  catLabelExtras = {
1828
2089
  width: availPerLabel,
@@ -1854,7 +2115,11 @@ function makeGridAxis(
1854
2115
  name: label,
1855
2116
  nameLocation: 'middle',
1856
2117
  nameGap: nameGapOverride ?? defaultGap,
1857
- nameTextStyle: { color: textColor, fontSize: 18, fontFamily: FONT_FAMILY },
2118
+ nameTextStyle: {
2119
+ color: textColor,
2120
+ fontSize: 18,
2121
+ fontFamily: FONT_FAMILY,
2122
+ },
1858
2123
  }),
1859
2124
  };
1860
2125
  }
@@ -1872,35 +2137,132 @@ export function buildSimpleChartOption(
1872
2137
  ): EChartsOption {
1873
2138
  if (parsed.error) return {};
1874
2139
 
1875
- 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);
1876
2149
  const bg = isDark ? palette.surface : palette.bg;
1877
2150
 
1878
2151
  switch (parsed.type) {
1879
2152
  case 'bar':
1880
- 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
+ );
1881
2165
  case 'bar-stacked':
1882
- 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
+ );
1883
2178
  case 'line':
1884
2179
  return parsed.seriesNames
1885
- ? buildMultiLineOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, colors, titleConfig, tooltipTheme, chartWidth)
1886
- : 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
+ );
1887
2203
  case 'area':
1888
- 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
+ );
1889
2215
  case 'pie':
1890
- 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
+ );
1891
2225
  case 'doughnut':
1892
- 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
+ );
1893
2235
  case 'radar':
1894
- 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
+ );
1895
2245
  case 'polar-area':
1896
- 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
+ );
1897
2254
  }
1898
2255
  }
1899
2256
 
1900
2257
  /**
1901
2258
  * Builds a standard chart grid object with consistent spacing rules.
1902
2259
  */
1903
- 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> {
1904
2266
  return {
1905
2267
  left: options.yLabel ? '12%' : '3%',
1906
2268
  right: '4%',
@@ -1931,17 +2293,39 @@ function buildBarOption(
1931
2293
  const stroke = d.color ?? colors[i % colors.length];
1932
2294
  return {
1933
2295
  value: d.value,
1934
- 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
+ },
1935
2301
  };
1936
2302
  });
1937
2303
 
1938
2304
  // When category labels are on the y-axis (horizontal bars), they can be wide —
1939
2305
  // compute a nameGap that clears the longest label so the ylabel doesn't overlap.
1940
- const hCatGap = isHorizontal && yLabel
1941
- ? Math.max(40, Math.max(...labels.map((l) => l.length)) * 8 + 16)
1942
- : undefined;
1943
- const categoryAxis = makeGridAxis('category', textColor, axisLineColor, splitLineColor, gridOpacity, isHorizontal ? yLabel : xLabel, labels, hCatGap, !isHorizontal ? chartWidth : undefined);
1944
- 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
+ );
1945
2329
 
1946
2330
  // xAxis is always the bottom axis, yAxis is always the left axis in ECharts
1947
2331
 
@@ -1989,7 +2373,8 @@ function buildMarkArea(
1989
2373
  data: eras.map((era) => {
1990
2374
  const startIdx = labels.indexOf(era.start);
1991
2375
  const endIdx = labels.indexOf(era.end);
1992
- const bandSlots = startIdx >= 0 && endIdx >= 0 ? endIdx - startIdx : Infinity;
2376
+ const bandSlots =
2377
+ startIdx >= 0 && endIdx >= 0 ? endIdx - startIdx : Infinity;
1993
2378
  const color = era.color ?? defaultColor;
1994
2379
  return [
1995
2380
  {
@@ -2023,7 +2408,8 @@ function buildLineOption(
2023
2408
  chartWidth?: number
2024
2409
  ): EChartsOption {
2025
2410
  const { xLabel, yLabel } = resolveAxisLabels(parsed);
2026
- const lineColor = parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
2411
+ const lineColor =
2412
+ parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
2027
2413
  const labels = parsed.data.map((d) => d.label);
2028
2414
  const values = parsed.data.map((d) => d.value);
2029
2415
  const eras = parsed.eras ?? [];
@@ -2039,8 +2425,26 @@ function buildLineOption(
2039
2425
  axisPointer: { type: 'line' },
2040
2426
  },
2041
2427
  grid: makeChartGrid({ xLabel, yLabel, hasTitle: !!parsed.title }),
2042
- xAxis: makeGridAxis('category', textColor, axisLineColor, splitLineColor, gridOpacity, xLabel, labels, undefined, chartWidth, interval),
2043
- 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
+ ),
2044
2448
  series: [
2045
2449
  {
2046
2450
  type: 'line',
@@ -2108,9 +2512,32 @@ function buildMultiLineOption(
2108
2512
  bottom: 10,
2109
2513
  textStyle: { color: textColor },
2110
2514
  },
2111
- grid: makeChartGrid({ xLabel, yLabel, hasTitle: !!parsed.title, hasLegend: true }),
2112
- xAxis: makeGridAxis('category', textColor, axisLineColor, splitLineColor, gridOpacity, xLabel, labels, undefined, chartWidth, interval),
2113
- 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
+ ),
2114
2541
  series,
2115
2542
  };
2116
2543
  }
@@ -2129,7 +2556,8 @@ function buildAreaOption(
2129
2556
  chartWidth?: number
2130
2557
  ): EChartsOption {
2131
2558
  const { xLabel, yLabel } = resolveAxisLabels(parsed);
2132
- const lineColor = parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
2559
+ const lineColor =
2560
+ parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
2133
2561
  const labels = parsed.data.map((d) => d.label);
2134
2562
  const values = parsed.data.map((d) => d.value);
2135
2563
  const eras = parsed.eras ?? [];
@@ -2145,8 +2573,26 @@ function buildAreaOption(
2145
2573
  axisPointer: { type: 'line' },
2146
2574
  },
2147
2575
  grid: makeChartGrid({ xLabel, yLabel, hasTitle: !!parsed.title }),
2148
- xAxis: makeGridAxis('category', textColor, axisLineColor, splitLineColor, gridOpacity, xLabel, labels, undefined, chartWidth, interval),
2149
- 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
+ ),
2150
2596
  series: [
2151
2597
  {
2152
2598
  type: 'line',
@@ -2165,13 +2611,23 @@ function buildAreaOption(
2165
2611
 
2166
2612
  // ── Segment label formatter ──────────────────────────────────
2167
2613
 
2168
- function segmentLabelFormatter(mode: ParsedChart['labels']): string {
2169
- switch (mode) {
2170
- case 'name': return '{b}';
2171
- case 'value': return '{b} — {c}';
2172
- case 'percent': return '{b} — {d}%';
2173
- default: return '{b} {c} ({d}%)';
2174
- }
2614
+ function segmentLabelFormatter(parsed: ParsedChart): string {
2615
+ const showName = !parsed.noLabelName;
2616
+ const showValue = !parsed.noLabelValue;
2617
+ const showPercent = !parsed.noLabelPercent;
2618
+
2619
+ const parts: string[] = [];
2620
+ if (showName) parts.push('{b}');
2621
+ if (showValue) parts.push('{c}');
2622
+ if (showPercent) parts.push('{d}%');
2623
+
2624
+ if (parts.length === 0) return '{b}'; // fallback: always show name
2625
+ if (parts.length === 1) return parts[0];
2626
+
2627
+ // Name is joined with " — ", value+percent are grouped with parens when all three
2628
+ if (showName && showValue && showPercent) return '{b} — {c} ({d}%)';
2629
+ if (showName) return '{b} — ' + parts.slice(1).join(' ');
2630
+ return parts.join(' ');
2175
2631
  }
2176
2632
 
2177
2633
  // ── Pie / Doughnut ───────────────────────────────────────────
@@ -2191,7 +2647,11 @@ function buildPieOption(
2191
2647
  return {
2192
2648
  name: d.label,
2193
2649
  value: d.value,
2194
- 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
+ },
2195
2655
  };
2196
2656
  });
2197
2657
 
@@ -2210,7 +2670,7 @@ function buildPieOption(
2210
2670
  data,
2211
2671
  label: {
2212
2672
  position: 'outside',
2213
- formatter: segmentLabelFormatter(parsed.labels),
2673
+ formatter: segmentLabelFormatter(parsed),
2214
2674
  color: textColor,
2215
2675
  fontFamily: FONT_FAMILY,
2216
2676
  },
@@ -2233,7 +2693,8 @@ function buildRadarOption(
2233
2693
  tooltipTheme: Record<string, unknown>
2234
2694
  ): EChartsOption {
2235
2695
  const bg = isDark ? palette.surface : palette.bg;
2236
- const radarColor = parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
2696
+ const radarColor =
2697
+ parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
2237
2698
  const values = parsed.data.map((d) => d.value);
2238
2699
  const maxValue = Math.max(...values) * 1.15;
2239
2700
 
@@ -2308,7 +2769,11 @@ function buildPolarAreaOption(
2308
2769
  return {
2309
2770
  name: d.label,
2310
2771
  value: d.value,
2311
- 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
+ },
2312
2777
  };
2313
2778
  });
2314
2779
 
@@ -2329,7 +2794,7 @@ function buildPolarAreaOption(
2329
2794
  data,
2330
2795
  label: {
2331
2796
  position: 'outside',
2332
- formatter: segmentLabelFormatter(parsed.labels),
2797
+ formatter: segmentLabelFormatter(parsed),
2333
2798
  color: textColor,
2334
2799
  fontFamily: FONT_FAMILY,
2335
2800
  },
@@ -2369,7 +2834,11 @@ function buildBarStackedOption(
2369
2834
  type: 'bar' as const,
2370
2835
  stack: 'total',
2371
2836
  data,
2372
- 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
+ },
2373
2842
  label: {
2374
2843
  show: true,
2375
2844
  position: 'inside' as const,
@@ -2383,14 +2852,34 @@ function buildBarStackedOption(
2383
2852
  };
2384
2853
  });
2385
2854
 
2386
- const hCatGap = isHorizontal && yLabel
2387
- ? Math.max(40, Math.max(...labels.map((l) => l.length)) * 8 + 16)
2388
- : undefined;
2389
- 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
+ );
2390
2870
  // For horizontal bars with a legend, use a smaller nameGap so the xlabel
2391
2871
  // stays close to the axis ticks rather than drifting toward the legend.
2392
2872
  const hValueGap = isHorizontal && xLabel ? 40 : undefined;
2393
- 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
+ );
2394
2883
 
2395
2884
  return {
2396
2885
  ...CHART_BASE,
@@ -2400,7 +2889,12 @@ function buildBarStackedOption(
2400
2889
  bottom: 10,
2401
2890
  textStyle: { color: textColor },
2402
2891
  },
2403
- grid: makeChartGrid({ xLabel, yLabel, hasTitle: !!parsed.title, hasLegend: true }),
2892
+ grid: makeChartGrid({
2893
+ xLabel,
2894
+ yLabel,
2895
+ hasTitle: !!parsed.title,
2896
+ hasLegend: true,
2897
+ }),
2404
2898
  xAxis: isHorizontal ? valueAxis : categoryAxis,
2405
2899
  yAxis: isHorizontal ? categoryAxis : valueAxis,
2406
2900
  series,
@@ -2416,8 +2910,15 @@ const ECHART_EXPORT_HEIGHT = 800;
2416
2910
 
2417
2911
  // Standard chart types handled by buildSimpleChartOption (via parseChart)
2418
2912
  const STANDARD_CHART_TYPES = new Set([
2419
- 'bar', 'line', 'multi-line', 'area', 'pie', 'doughnut',
2420
- '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',
2421
2922
  ]);
2422
2923
 
2423
2924
  /**
@@ -2452,13 +2953,18 @@ export async function renderExtendedChartForExport(
2452
2953
  if (!chartType) return '';
2453
2954
 
2454
2955
  let option: EChartsOption;
2455
- let legendGroups: LegendGroupData[] = [];
2956
+ let legendGroups: LegendGroupData[] = []; // eslint-disable-line no-useless-assignment
2456
2957
  const colors = getSeriesColors(effectivePalette);
2457
2958
 
2458
2959
  if (STANDARD_CHART_TYPES.has(chartType)) {
2459
2960
  const parsed = parseChart(content, effectivePalette);
2460
2961
  if (parsed.error) return '';
2461
- option = buildSimpleChartOption(parsed, effectivePalette, isDark, ECHART_EXPORT_WIDTH);
2962
+ option = buildSimpleChartOption(
2963
+ parsed,
2964
+ effectivePalette,
2965
+ isDark,
2966
+ ECHART_EXPORT_WIDTH
2967
+ );
2462
2968
  legendGroups = getSimpleChartLegendGroups(parsed, colors);
2463
2969
  } else {
2464
2970
  const parsed = parseExtendedChart(content, effectivePalette);
@@ -2487,7 +2993,8 @@ export async function renderExtendedChartForExport(
2487
2993
 
2488
2994
  // The SSR output already includes xmlns, width, height, and viewBox.
2489
2995
  // Inject font-family and background on the root <svg> element.
2490
- const bgStyle = theme !== 'transparent' ? `background: ${effectivePalette.bg}; ` : '';
2996
+ const bgStyle =
2997
+ theme !== 'transparent' ? `background: ${effectivePalette.bg}; ` : '';
2491
2998
  let result = svgString.replace(
2492
2999
  /^<svg /,
2493
3000
  `<svg style="${bgStyle}font-family: ${FONT_FAMILY}" `
@@ -2495,13 +3002,18 @@ export async function renderExtendedChartForExport(
2495
3002
 
2496
3003
  // Inject custom legend SVG when present
2497
3004
  if (legendGroups.length > 0) {
2498
- 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;
2499
3007
  const legendY = 8 + titleHeight;
2500
3008
  // In static export, expand the first group so entries are visible
2501
3009
  // Extract grid offsets for plot-area-centered legend
2502
3010
  const grid = option.grid as Record<string, unknown> | undefined;
2503
- const gridLeftPct = grid?.left ? parseFloat(String(grid.left)) : undefined;
2504
- 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;
2505
3017
  const { svg: legendSvgStr } = renderLegendSvg(legendGroups, {
2506
3018
  palette: effectivePalette,
2507
3019
  isDark,
@@ -2514,12 +3026,13 @@ export async function renderExtendedChartForExport(
2514
3026
  // Insert legend group right after the opening <svg ...> tag
2515
3027
  result = result.replace(
2516
3028
  /(<svg[^>]*>)/,
2517
- `$1<g transform="translate(0,${legendY})">${legendSvgStr}</g>`,
3029
+ `$1<g transform="translate(0,${legendY})">${legendSvgStr}</g>`
2518
3030
  );
2519
3031
  }
2520
3032
 
2521
3033
  if (options?.branding !== false) {
2522
- const brandColor = theme === 'transparent' ? '#888' : effectivePalette.textMuted;
3034
+ const brandColor =
3035
+ theme === 'transparent' ? '#888' : effectivePalette.textMuted;
2523
3036
  result = injectBranding(result, brandColor);
2524
3037
  }
2525
3038