@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.
- 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 +189 -194
- 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 +3699 -1564
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -6
- package/dist/index.d.ts +7 -6
- package/dist/index.js +3699 -1564
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +822 -1060
- 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 +113 -62
- package/src/chart.ts +149 -64
- package/src/class/parser.ts +84 -28
- package/src/class/renderer.ts +2 -2
- package/src/cli.ts +179 -77
- package/src/completion.ts +381 -182
- package/src/d3.ts +1026 -428
- package/src/dgmo-mermaid.ts +16 -13
- package/src/dgmo-router.ts +70 -24
- package/src/echarts.ts +682 -169
- 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 +55 -29
- package/src/er/renderer.ts +112 -53
- package/src/gantt/calculator.ts +91 -29
- package/src/gantt/parser.ts +291 -97
- package/src/gantt/renderer.ts +1120 -350
- package/src/graph/flowchart-parser.ts +48 -75
- package/src/graph/state-parser.ts +54 -27
- package/src/infra/parser.ts +161 -177
- package/src/infra/renderer.ts +723 -271
- package/src/infra/types.ts +0 -1
- package/src/initiative-status/parser.ts +144 -56
- package/src/kanban/parser.ts +27 -19
- package/src/org/layout.ts +111 -44
- package/src/org/parser.ts +71 -27
- package/src/org/resolver.ts +3 -3
- package/src/palettes/index.ts +3 -2
- package/src/render.ts +1 -2
- package/src/sequence/parser.ts +209 -100
- package/src/sitemap/parser.ts +73 -44
- 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 +82 -72
- package/src/utils/tag-groups.ts +4 -41
- 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 {
|
|
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: {
|
|
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',
|
|
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',
|
|
130
|
-
'
|
|
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(
|
|
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
|
|
198
|
-
|
|
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 =
|
|
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 (
|
|
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]
|
|
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(
|
|
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(
|
|
264
|
-
|
|
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
|
|
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(
|
|
296
|
-
|
|
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]
|
|
301
|
-
|
|
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({
|
|
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 =
|
|
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(
|
|
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 = (
|
|
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))
|
|
448
|
+
if (parsed.nameColors.some(Boolean))
|
|
449
|
+
result.seriesNameColors = parsed.nameColors;
|
|
372
450
|
continue;
|
|
373
451
|
}
|
|
374
452
|
|
|
375
|
-
if (firstToken === '
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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.
|
|
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.
|
|
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))
|
|
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(
|
|
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(
|
|
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({
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
553
|
-
|
|
554
|
-
|
|
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 {
|
|
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: {
|
|
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(
|
|
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
|
-
?
|
|
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 && {
|
|
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
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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 = [
|
|
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
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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 {
|
|
1046
|
-
|
|
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
|
|
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(
|
|
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 (
|
|
1118
|
-
|
|
1119
|
-
|
|
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 = {
|
|
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 =
|
|
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 ??
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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] ??
|
|
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,
|
|
1425
|
-
|
|
1426
|
-
|
|
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,
|
|
1435
|
-
|
|
1436
|
-
|
|
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 =
|
|
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 =
|
|
1458
|
-
|
|
1459
|
-
|
|
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 =
|
|
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 (
|
|
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: {
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
1886
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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: {
|
|
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: {
|
|
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 =
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
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
|
+
);
|
|
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 =
|
|
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 =
|
|
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(
|
|
2043
|
-
|
|
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({
|
|
2112
|
-
|
|
2113
|
-
|
|
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 =
|
|
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(
|
|
2149
|
-
|
|
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(
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
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: {
|
|
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
|
|
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 =
|
|
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: {
|
|
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
|
|
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: {
|
|
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 =
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
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(
|
|
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({
|
|
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',
|
|
2420
|
-
'
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
2504
|
-
|
|
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 =
|
|
3034
|
+
const brandColor =
|
|
3035
|
+
theme === 'transparent' ? '#888' : effectivePalette.textMuted;
|
|
2523
3036
|
result = injectBranding(result, brandColor);
|
|
2524
3037
|
}
|
|
2525
3038
|
|