@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.
- package/.claude/commands/dgmo-diagram-this.md +60 -0
- package/.claude/commands/dgmo-document-project.md +128 -0
- package/.claude/commands/dgmo.md +185 -50
- package/.cursorrules +32 -37
- package/.github/copilot-instructions.md +35 -44
- package/.windsurfrules +32 -37
- package/README.md +4 -4
- package/dist/cli.cjs +153 -153
- package/dist/editor.cjs +336 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.d.cts +27 -0
- package/dist/editor.d.ts +27 -0
- package/dist/editor.js +305 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.cjs +3336 -1055
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3336 -1055
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +30 -29
- package/gallery/fixtures/arc.dgmo +18 -0
- package/gallery/fixtures/area.dgmo +19 -0
- package/gallery/fixtures/bar-stacked.dgmo +10 -0
- package/gallery/fixtures/bar.dgmo +10 -0
- package/gallery/fixtures/c4-full.dgmo +52 -0
- package/gallery/fixtures/c4.dgmo +17 -0
- package/gallery/fixtures/chord.dgmo +12 -0
- package/gallery/fixtures/class-basic.dgmo +14 -0
- package/gallery/fixtures/class-full.dgmo +43 -0
- package/gallery/fixtures/doughnut.dgmo +8 -0
- package/gallery/fixtures/flowchart-basic.dgmo +3 -0
- package/gallery/fixtures/flowchart-colors.dgmo +5 -0
- package/gallery/fixtures/flowchart-complex.dgmo +17 -0
- package/gallery/fixtures/flowchart-decision.dgmo +5 -0
- package/gallery/fixtures/flowchart-full.dgmo +13 -0
- package/gallery/fixtures/flowchart-groups.dgmo +10 -0
- package/gallery/fixtures/flowchart-loop.dgmo +7 -0
- package/gallery/fixtures/flowchart-nested.dgmo +7 -0
- package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
- package/gallery/fixtures/function.dgmo +8 -0
- package/gallery/fixtures/funnel.dgmo +7 -0
- package/gallery/fixtures/gantt-full.dgmo +49 -0
- package/gallery/fixtures/gantt.dgmo +42 -0
- package/gallery/fixtures/heatmap.dgmo +8 -0
- package/gallery/fixtures/infra-full.dgmo +78 -0
- package/gallery/fixtures/infra-overload.dgmo +25 -0
- package/gallery/fixtures/infra.dgmo +47 -0
- package/gallery/fixtures/initiative-status-full.dgmo +46 -0
- package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
- package/gallery/fixtures/initiative-status.dgmo +9 -0
- package/gallery/fixtures/line.dgmo +19 -0
- package/gallery/fixtures/multi-line.dgmo +11 -0
- package/gallery/fixtures/org-basic.dgmo +16 -0
- package/gallery/fixtures/org-full.dgmo +69 -0
- package/gallery/fixtures/org-teams.dgmo +25 -0
- package/gallery/fixtures/pie.dgmo +9 -0
- package/gallery/fixtures/polar-area.dgmo +8 -0
- package/gallery/fixtures/quadrant.dgmo +18 -0
- package/gallery/fixtures/radar.dgmo +8 -0
- package/gallery/fixtures/sankey.dgmo +31 -0
- package/gallery/fixtures/scatter.dgmo +21 -0
- package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
- package/gallery/fixtures/sequence-tags.dgmo +41 -0
- package/gallery/fixtures/sequence.dgmo +35 -0
- package/gallery/fixtures/sitemap-basic.dgmo +12 -0
- package/gallery/fixtures/sitemap-full.dgmo +156 -0
- package/gallery/fixtures/slope.dgmo +8 -0
- package/gallery/fixtures/spr-eras.dgmo +62 -0
- package/gallery/fixtures/state.dgmo +30 -0
- package/gallery/fixtures/timeline-intraday.dgmo +14 -0
- package/gallery/fixtures/timeline.dgmo +32 -0
- package/gallery/fixtures/venn.dgmo +10 -0
- package/gallery/fixtures/wordcloud.dgmo +24 -0
- package/package.json +51 -2
- package/src/c4/layout.ts +372 -90
- package/src/c4/parser.ts +100 -55
- package/src/chart.ts +91 -28
- package/src/class/parser.ts +41 -12
- package/src/cli.ts +168 -61
- package/src/completion.ts +378 -183
- package/src/d3.ts +887 -288
- package/src/dgmo-mermaid.ts +16 -13
- package/src/dgmo-router.ts +69 -23
- package/src/echarts.ts +646 -153
- package/src/editor/dgmo.grammar +69 -0
- package/src/editor/dgmo.grammar.d.ts +2 -0
- package/src/editor/dgmo.grammar.js +18 -0
- package/src/editor/dgmo.grammar.terms.d.ts +5 -0
- package/src/editor/dgmo.grammar.terms.js +35 -0
- package/src/editor/highlight.ts +36 -0
- package/src/editor/index.ts +28 -0
- package/src/editor/keywords.ts +220 -0
- package/src/editor/tokens.ts +30 -0
- package/src/er/parser.ts +48 -14
- package/src/er/renderer.ts +112 -53
- package/src/gantt/calculator.ts +91 -29
- package/src/gantt/parser.ts +197 -71
- package/src/gantt/renderer.ts +1120 -350
- package/src/graph/flowchart-parser.ts +46 -25
- package/src/graph/state-parser.ts +47 -17
- package/src/infra/parser.ts +157 -53
- package/src/infra/renderer.ts +723 -271
- package/src/initiative-status/parser.ts +138 -44
- package/src/kanban/parser.ts +25 -14
- package/src/org/layout.ts +111 -44
- package/src/org/parser.ts +69 -22
- package/src/palettes/index.ts +3 -2
- package/src/sequence/parser.ts +193 -61
- package/src/sitemap/parser.ts +65 -29
- package/src/utils/arrows.ts +2 -22
- package/src/utils/duration.ts +39 -21
- package/src/utils/legend-constants.ts +0 -2
- 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 {
|
|
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: {
|
|
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',
|
|
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',
|
|
131
|
-
'
|
|
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(
|
|
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
|
|
199
|
-
|
|
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 =
|
|
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 (
|
|
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]
|
|
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(
|
|
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(
|
|
265
|
-
|
|
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
|
|
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(
|
|
297
|
-
|
|
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]
|
|
302
|
-
|
|
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({
|
|
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 =
|
|
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(
|
|
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 = (
|
|
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))
|
|
448
|
+
if (parsed.nameColors.some(Boolean))
|
|
449
|
+
result.seriesNameColors = parsed.nameColors;
|
|
373
450
|
continue;
|
|
374
451
|
}
|
|
375
452
|
|
|
376
|
-
if (firstToken === '
|
|
377
|
-
|
|
378
|
-
|
|
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') {
|
|
420
|
-
|
|
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))
|
|
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(
|
|
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(
|
|
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({
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
557
|
-
|
|
558
|
-
|
|
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 {
|
|
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: {
|
|
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(
|
|
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
|
-
?
|
|
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 && {
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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 = [
|
|
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
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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 {
|
|
1056
|
-
|
|
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
|
|
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(
|
|
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 (
|
|
1128
|
-
|
|
1129
|
-
|
|
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 = {
|
|
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 =
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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] ??
|
|
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,
|
|
1435
|
-
|
|
1436
|
-
|
|
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,
|
|
1445
|
-
|
|
1446
|
-
|
|
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 =
|
|
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 =
|
|
1468
|
-
|
|
1469
|
-
|
|
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 =
|
|
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 (
|
|
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: {
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
1896
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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: {
|
|
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: {
|
|
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 =
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
const
|
|
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 =
|
|
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 =
|
|
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(
|
|
2053
|
-
|
|
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({
|
|
2122
|
-
|
|
2123
|
-
|
|
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 =
|
|
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(
|
|
2159
|
-
|
|
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: {
|
|
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 =
|
|
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: {
|
|
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: {
|
|
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 =
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
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(
|
|
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({
|
|
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',
|
|
2440
|
-
'
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
2524
|
-
|
|
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 =
|
|
3034
|
+
const brandColor =
|
|
3035
|
+
theme === 'transparent' ? '#888' : effectivePalette.textMuted;
|
|
2543
3036
|
result = injectBranding(result, brandColor);
|
|
2544
3037
|
}
|
|
2545
3038
|
|