@diagrammo/dgmo 0.47.0 → 0.48.0
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/dist/auto.js +1 -1
- package/dist/auto.mjs +1 -1
- package/dist/element.js +1 -1
- package/dist/element.mjs +1 -1
- package/package.json +19 -70
- package/dist/advanced.cjs +0 -71648
- package/dist/advanced.d.cts +0 -5719
- package/dist/auto.cjs +0 -70648
- package/dist/auto.d.cts +0 -54
- package/dist/block.cjs +0 -70208
- package/dist/block.d.cts +0 -115
- package/dist/editor.cjs +0 -526
- package/dist/editor.d.cts +0 -26
- package/dist/element.cjs +0 -70530
- package/dist/element.d.cts +0 -62
- package/dist/highlight.cjs +0 -852
- package/dist/highlight.d.cts +0 -49
- package/dist/index.cjs +0 -70491
- package/dist/index.d.cts +0 -160
- package/dist/pert.cjs +0 -325
- package/dist/pert.d.cts +0 -559
- package/dist/themes-1-CuKpeH.d.cts +0 -908
- package/src/advanced.ts +0 -873
- package/src/arc/parser.ts +0 -15
- package/src/arc/renderer.ts +0 -633
- package/src/auto/index.ts +0 -621
- package/src/auto/shared.ts +0 -370
- package/src/auto/styles.ts +0 -73
- package/src/block/layout.ts +0 -198
- package/src/block/parser.ts +0 -427
- package/src/block/renderer.ts +0 -484
- package/src/block/types.ts +0 -67
- package/src/boxes-and-lines/collapse.ts +0 -96
- package/src/boxes-and-lines/focus.ts +0 -257
- package/src/boxes-and-lines/label-placement.ts +0 -339
- package/src/boxes-and-lines/layout-grouped.ts +0 -912
- package/src/boxes-and-lines/layout-layered.ts +0 -722
- package/src/boxes-and-lines/layout-search.ts +0 -1678
- package/src/boxes-and-lines/layout.ts +0 -398
- package/src/boxes-and-lines/parser.ts +0 -1181
- package/src/boxes-and-lines/renderer.ts +0 -1465
- package/src/boxes-and-lines/types.ts +0 -66
- package/src/c4/layout.ts +0 -2441
- package/src/c4/parser.ts +0 -1158
- package/src/c4/renderer.ts +0 -2008
- package/src/c4/types.ts +0 -89
- package/src/chart-type-registry.ts +0 -529
- package/src/chart-types.ts +0 -225
- package/src/chart.ts +0 -685
- package/src/charts-d3/bar.ts +0 -338
- package/src/charts-d3/chord.ts +0 -140
- package/src/charts-d3/function.ts +0 -147
- package/src/charts-d3/funnel.ts +0 -104
- package/src/charts-d3/heatmap.ts +0 -145
- package/src/charts-d3/index.ts +0 -337
- package/src/charts-d3/interactions.ts +0 -477
- package/src/charts-d3/line.ts +0 -344
- package/src/charts-d3/mount.ts +0 -151
- package/src/charts-d3/pie.ts +0 -117
- package/src/charts-d3/polar.ts +0 -89
- package/src/charts-d3/radar.ts +0 -0
- package/src/charts-d3/sankey.ts +0 -0
- package/src/charts-d3/scatter.ts +0 -185
- package/src/charts-d3/shared.ts +0 -208
- package/src/class/layout.ts +0 -326
- package/src/class/parser.ts +0 -635
- package/src/class/renderer.ts +0 -793
- package/src/class/types.ts +0 -58
- package/src/cli-banner.ts +0 -106
- package/src/cli.ts +0 -1080
- package/src/colors-diagnostics.ts +0 -35
- package/src/colors.ts +0 -379
- package/src/completion-types.ts +0 -27
- package/src/completion.ts +0 -2668
- package/src/cycle/layout.ts +0 -1026
- package/src/cycle/parser.ts +0 -387
- package/src/cycle/renderer.ts +0 -653
- package/src/cycle/types.ts +0 -101
- package/src/d3.ts +0 -1434
- package/src/data-chart-parser.ts +0 -1007
- package/src/dgmo-router.ts +0 -296
- package/src/diagnostics-registry.ts +0 -108
- package/src/diagnostics.ts +0 -0
- package/src/dimensions.ts +0 -20
- package/src/directives-registry.ts +0 -396
- package/src/editor/dgmo.grammar +0 -76
- package/src/editor/dgmo.grammar.d.ts +0 -2
- package/src/editor/dgmo.grammar.js +0 -18
- package/src/editor/dgmo.grammar.terms.d.ts +0 -5
- package/src/editor/dgmo.grammar.terms.js +0 -35
- package/src/editor/highlight-api.ts +0 -596
- package/src/editor/highlight.ts +0 -36
- package/src/editor/index.ts +0 -27
- package/src/editor/keywords.ts +0 -128
- package/src/editor/tokens.ts +0 -30
- package/src/element/index.ts +0 -432
- package/src/embed/css.ts +0 -312
- package/src/embed/escape.ts +0 -15
- package/src/embed/index.ts +0 -280
- package/src/er/classify.ts +0 -228
- package/src/er/layout.ts +0 -530
- package/src/er/parser.ts +0 -750
- package/src/er/renderer.ts +0 -725
- package/src/er/types.ts +0 -52
- package/src/error-card.ts +0 -269
- package/src/event-line/diagnostics.ts +0 -117
- package/src/event-line/parser.ts +0 -643
- package/src/event-line/renderer.ts +0 -1742
- package/src/event-line/types.ts +0 -74
- package/src/fonts.ts +0 -3
- package/src/gantt/calculator.ts +0 -979
- package/src/gantt/parser.ts +0 -1641
- package/src/gantt/renderer.ts +0 -3787
- package/src/gantt/resolver.ts +0 -172
- package/src/gantt/types.ts +0 -206
- package/src/graph/diagnostics.ts +0 -64
- package/src/graph/edge-spline.ts +0 -29
- package/src/graph/flowchart-parser.ts +0 -759
- package/src/graph/flowchart-renderer.ts +0 -817
- package/src/graph/layout.ts +0 -472
- package/src/graph/notes.ts +0 -21
- package/src/graph/state-collapse.ts +0 -78
- package/src/graph/state-parser.ts +0 -574
- package/src/graph/state-renderer.ts +0 -653
- package/src/graph/types.ts +0 -65
- package/src/index.ts +0 -261
- package/src/infra/compute.ts +0 -1385
- package/src/infra/layout.ts +0 -888
- package/src/infra/parser.ts +0 -1234
- package/src/infra/renderer.ts +0 -2534
- package/src/infra/roles.ts +0 -75
- package/src/infra/types.ts +0 -216
- package/src/infra/validation.ts +0 -201
- package/src/journey-map/layout.ts +0 -490
- package/src/journey-map/parser.ts +0 -557
- package/src/journey-map/renderer.ts +0 -1580
- package/src/journey-map/types.ts +0 -47
- package/src/kanban/mutations.ts +0 -175
- package/src/kanban/parser.ts +0 -509
- package/src/kanban/renderer.ts +0 -1449
- package/src/kanban/types.ts +0 -39
- package/src/label-layout.ts +0 -368
- package/src/map/colorize.ts +0 -54
- package/src/map/completion.ts +0 -316
- package/src/map/context-labels.ts +0 -609
- package/src/map/data/PROVENANCE.json +0 -1
- package/src/map/data/README.md +0 -40
- package/src/map/data/airport-collisions.json +0 -1
- package/src/map/data/airports.json +0 -1
- package/src/map/data/gazetteer.json +0 -1
- package/src/map/data/lakes.json +0 -1
- package/src/map/data/mountain-ranges.json +0 -1
- package/src/map/data/na-lakes.json +0 -1
- package/src/map/data/na-land.json +0 -1
- package/src/map/data/region-names.json +0 -1
- package/src/map/data/rivers.json +0 -1
- package/src/map/data/types.ts +0 -121
- package/src/map/data/us-states.json +0 -1
- package/src/map/data/water-bodies.json +0 -1
- package/src/map/data/world-coarse.json +0 -1
- package/src/map/data/world-detail.json +0 -1
- package/src/map/diagnostics.ts +0 -134
- package/src/map/dimensions.ts +0 -150
- package/src/map/geo-query.ts +0 -295
- package/src/map/geo.ts +0 -438
- package/src/map/invert.ts +0 -111
- package/src/map/layout.ts +0 -5069
- package/src/map/legend-band.ts +0 -127
- package/src/map/load-data.ts +0 -186
- package/src/map/parser.ts +0 -783
- package/src/map/renderer.ts +0 -1445
- package/src/map/resolved-types.ts +0 -160
- package/src/map/resolver.ts +0 -1146
- package/src/map/types.ts +0 -194
- package/src/mindmap/collapse.ts +0 -67
- package/src/mindmap/layout.ts +0 -649
- package/src/mindmap/parser.ts +0 -430
- package/src/mindmap/renderer.ts +0 -637
- package/src/mindmap/text-wrap.ts +0 -218
- package/src/mindmap/types.ts +0 -56
- package/src/org/collapse.ts +0 -151
- package/src/org/layout.ts +0 -1262
- package/src/org/parser.ts +0 -519
- package/src/org/renderer.ts +0 -866
- package/src/org/resolver.ts +0 -636
- package/src/palettes/atlas.ts +0 -77
- package/src/palettes/blueprint.ts +0 -73
- package/src/palettes/catppuccin.ts +0 -84
- package/src/palettes/color-utils.ts +0 -443
- package/src/palettes/index.ts +0 -67
- package/src/palettes/nord.ts +0 -67
- package/src/palettes/registry.ts +0 -114
- package/src/palettes/slate.ts +0 -73
- package/src/palettes/tidewater.ts +0 -73
- package/src/palettes/tokyo-night.ts +0 -86
- package/src/palettes/types.ts +0 -88
- package/src/pert/analyzer.ts +0 -1183
- package/src/pert/diagnostics.ts +0 -110
- package/src/pert/index.ts +0 -43
- package/src/pert/internal.ts +0 -252
- package/src/pert/layout.ts +0 -971
- package/src/pert/monte-carlo.ts +0 -446
- package/src/pert/parser.ts +0 -1606
- package/src/pert/renderer.ts +0 -4009
- package/src/pert/share-normalize.ts +0 -62
- package/src/pert/types.ts +0 -604
- package/src/pyramid/parser.ts +0 -185
- package/src/pyramid/renderer.ts +0 -768
- package/src/pyramid/types.ts +0 -28
- package/src/quadrant/parser.ts +0 -18
- package/src/quadrant/renderer.ts +0 -638
- package/src/raci/diagnostics.ts +0 -213
- package/src/raci/index.ts +0 -23
- package/src/raci/mutations.ts +0 -321
- package/src/raci/parser.ts +0 -699
- package/src/raci/renderer.ts +0 -1627
- package/src/raci/types.ts +0 -82
- package/src/raci/variants.ts +0 -320
- package/src/render.ts +0 -268
- package/src/ring/parser.ts +0 -212
- package/src/ring/renderer.ts +0 -490
- package/src/ring/types.ts +0 -27
- package/src/sequence/collapse.ts +0 -174
- package/src/sequence/parser.ts +0 -1596
- package/src/sequence/participant-inference.ts +0 -152
- package/src/sequence/renderer.ts +0 -3168
- package/src/sequence/tag-resolution.ts +0 -164
- package/src/sharing.ts +0 -211
- package/src/sitemap/collapse.ts +0 -100
- package/src/sitemap/layout.ts +0 -925
- package/src/sitemap/parser.ts +0 -755
- package/src/sitemap/renderer.ts +0 -828
- package/src/sitemap/types.ts +0 -42
- package/src/slope/parser.ts +0 -18
- package/src/slope/renderer.ts +0 -378
- package/src/swimlane/diagnostics.ts +0 -94
- package/src/swimlane/layout.ts +0 -597
- package/src/swimlane/parser.ts +0 -613
- package/src/swimlane/renderer.ts +0 -681
- package/src/swimlane/types.ts +0 -151
- package/src/tech-radar/interactive.ts +0 -1129
- package/src/tech-radar/layout.ts +0 -197
- package/src/tech-radar/parser.ts +0 -436
- package/src/tech-radar/renderer.ts +0 -1239
- package/src/tech-radar/shared.ts +0 -151
- package/src/tech-radar/types.ts +0 -86
- package/src/themes.ts +0 -22
- package/src/timeline/parser.ts +0 -472
- package/src/timeline/renderer.ts +0 -3024
- package/src/timeline/types.ts +0 -33
- package/src/timeline/viz-parser.ts +0 -21
- package/src/treemap/diagnostics.ts +0 -62
- package/src/treemap/layout-radial.ts +0 -233
- package/src/treemap/layout.ts +0 -189
- package/src/treemap/parser.ts +0 -463
- package/src/treemap/renderer-radial.ts +0 -438
- package/src/treemap/renderer.ts +0 -572
- package/src/treemap/treemap-shared.ts +0 -309
- package/src/treemap/types.ts +0 -60
- package/src/utils/arrow-markers.ts +0 -51
- package/src/utils/arrows-diagnostics.ts +0 -62
- package/src/utils/arrows.ts +0 -227
- package/src/utils/brand.ts +0 -61
- package/src/utils/card.ts +0 -183
- package/src/utils/collapse-engine/tree-edges.ts +0 -115
- package/src/utils/collapse-engine/tree.ts +0 -117
- package/src/utils/d3-helpers.ts +0 -219
- package/src/utils/d3-types.ts +0 -9
- package/src/utils/description-helpers.ts +0 -38
- package/src/utils/duration.ts +0 -312
- package/src/utils/export-container.ts +0 -41
- package/src/utils/expr-eval.ts +0 -27
- package/src/utils/fit-canvas.ts +0 -64
- package/src/utils/hover-styles.ts +0 -673
- package/src/utils/inline-markdown.ts +0 -96
- package/src/utils/legend-constants.ts +0 -48
- package/src/utils/legend-d3.ts +0 -618
- package/src/utils/legend-integration.ts +0 -107
- package/src/utils/legend-layout.ts +0 -776
- package/src/utils/legend-svg.ts +0 -214
- package/src/utils/legend-types.ts +0 -272
- package/src/utils/name-normalize.ts +0 -163
- package/src/utils/note-box/constants.ts +0 -25
- package/src/utils/note-box/index.ts +0 -11
- package/src/utils/note-box/metrics.ts +0 -90
- package/src/utils/note-box/svg.ts +0 -331
- package/src/utils/notes/bounds.ts +0 -30
- package/src/utils/notes/build.ts +0 -131
- package/src/utils/notes/index.ts +0 -18
- package/src/utils/notes/model.ts +0 -19
- package/src/utils/notes/parse.ts +0 -131
- package/src/utils/notes/place.ts +0 -177
- package/src/utils/notes/resolve.ts +0 -88
- package/src/utils/number-format.ts +0 -36
- package/src/utils/parsing.ts +0 -953
- package/src/utils/reserved-key-registry.ts +0 -239
- package/src/utils/safe-href.ts +0 -38
- package/src/utils/scaling.ts +0 -97
- package/src/utils/svg-embed.ts +0 -251
- package/src/utils/tag-groups.ts +0 -607
- package/src/utils/text-measure.ts +0 -122
- package/src/utils/time-ticks.ts +0 -253
- package/src/utils/title-constants.ts +0 -24
- package/src/utils/visual-conventions.ts +0 -61
- package/src/utils/wrapped-desc.ts +0 -87
- package/src/venn/parser.ts +0 -18
- package/src/venn/renderer.ts +0 -1047
- package/src/version-control/diagnostics.ts +0 -67
- package/src/version-control/parser.ts +0 -544
- package/src/version-control/renderer.ts +0 -724
- package/src/version-control/types.ts +0 -100
- package/src/visualizations/parse.ts +0 -1560
- package/src/visualizations/types.ts +0 -262
- package/src/wireframe/layout.ts +0 -479
- package/src/wireframe/parser.ts +0 -1114
- package/src/wireframe/renderer.ts +0 -1318
- package/src/wireframe/types.ts +0 -110
- package/src/wordcloud/parser.ts +0 -18
- package/src/wordcloud/renderer.ts +0 -357
package/src/chart.ts
DELETED
|
@@ -1,685 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Types
|
|
3
|
-
// ============================================================
|
|
4
|
-
|
|
5
|
-
export type ChartType = 'bar' | 'line' | 'pie' | 'polar-area' | 'radar';
|
|
6
|
-
|
|
7
|
-
export interface ChartDataPoint {
|
|
8
|
-
label: string;
|
|
9
|
-
value: number;
|
|
10
|
-
extraValues?: number[];
|
|
11
|
-
color?: string;
|
|
12
|
-
lineNumber: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ChartEra {
|
|
16
|
-
start: string; // exact category label, e.g. "'77"
|
|
17
|
-
end: string; // exact category label, e.g. "'81"
|
|
18
|
-
label: string; // display name, e.g. "Carter"
|
|
19
|
-
color: string | null; // resolved CSS color, or null → palette default
|
|
20
|
-
lineNumber: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
import type { DgmoError } from './diagnostics';
|
|
24
|
-
|
|
25
|
-
export interface ParsedChart {
|
|
26
|
-
type: ChartType;
|
|
27
|
-
title?: string;
|
|
28
|
-
titleLineNumber?: number;
|
|
29
|
-
series?: string;
|
|
30
|
-
seriesLineNumber?: number;
|
|
31
|
-
xlabel?: string;
|
|
32
|
-
xlabelLineNumber?: number;
|
|
33
|
-
ylabel?: string;
|
|
34
|
-
ylabelLineNumber?: number;
|
|
35
|
-
/** Right (secondary) y-axis label — set by a `y-right-label` option or a
|
|
36
|
-
* grouped-series axis header (§15.1 dual-axis line charts). */
|
|
37
|
-
yrlabel?: string;
|
|
38
|
-
yrlabelLineNumber?: number;
|
|
39
|
-
seriesNames?: string[];
|
|
40
|
-
seriesNameLineNumbers?: number[];
|
|
41
|
-
seriesNameColors?: (string | undefined)[];
|
|
42
|
-
/** Per-series axis assignment, parallel to seriesNames. Present only when the
|
|
43
|
-
* series block uses the grouped (dual-axis) form; absent ⇒ all left. */
|
|
44
|
-
seriesAxes?: ('left' | 'right')[];
|
|
45
|
-
/** Bar multi-series layout, set by a `stack` or `group` block header
|
|
46
|
-
* (consolidation #24). Absent ⇒ single-series bar. Drives stacked vs
|
|
47
|
-
* clustered rendering in `charts-d3/bar.ts`. */
|
|
48
|
-
barLayout?: 'stack' | 'group';
|
|
49
|
-
/** Pie hole inner-radius ratio (0–0.9), set by a `hole` directive
|
|
50
|
-
* (bare ⇒ default). Absent ⇒ solid pie. (#23) */
|
|
51
|
-
hole?: number;
|
|
52
|
-
/** Suppress the pie center total (bare `no-center-total`). The total
|
|
53
|
-
* shows by default whenever a hole is present. (#23) */
|
|
54
|
-
noCenterTotal?: boolean;
|
|
55
|
-
/** Render a line chart filled, i.e. as an area (bare `fill`). (#25) */
|
|
56
|
-
fill?: boolean;
|
|
57
|
-
orientation?: 'horizontal' | 'vertical';
|
|
58
|
-
color?: string;
|
|
59
|
-
label?: string;
|
|
60
|
-
noName?: boolean;
|
|
61
|
-
noValue?: boolean;
|
|
62
|
-
noPercent?: boolean;
|
|
63
|
-
/** Render with full intent saturation instead of the canonical 25% tint. */
|
|
64
|
-
solidFill?: boolean;
|
|
65
|
-
/** Cross-chart-type: when true, the renderer suppresses the chart title. */
|
|
66
|
-
noTitle?: boolean;
|
|
67
|
-
data: ChartDataPoint[];
|
|
68
|
-
eras?: ChartEra[];
|
|
69
|
-
diagnostics: DgmoError[];
|
|
70
|
-
error: string | null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ============================================================
|
|
74
|
-
// Colors
|
|
75
|
-
// ============================================================
|
|
76
|
-
|
|
77
|
-
import { resolveColorWithDiagnostic, RECOGNIZED_COLOR_NAMES } from './colors';
|
|
78
|
-
import type { PaletteColors } from './palettes';
|
|
79
|
-
import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
|
|
80
|
-
import {
|
|
81
|
-
extractColor,
|
|
82
|
-
normalizeNumericToken,
|
|
83
|
-
parseFirstLine,
|
|
84
|
-
parseSeriesNames,
|
|
85
|
-
} from './utils/parsing';
|
|
86
|
-
|
|
87
|
-
// ============================================================
|
|
88
|
-
// Parser
|
|
89
|
-
// ============================================================
|
|
90
|
-
|
|
91
|
-
const VALID_TYPES = new Set<ChartType>([
|
|
92
|
-
'bar',
|
|
93
|
-
'line',
|
|
94
|
-
'pie',
|
|
95
|
-
'polar-area',
|
|
96
|
-
'radar',
|
|
97
|
-
]);
|
|
98
|
-
|
|
99
|
-
/** Known option keywords for the simple chart parser. */
|
|
100
|
-
const KNOWN_OPTIONS = new Set([
|
|
101
|
-
'chart',
|
|
102
|
-
'title',
|
|
103
|
-
'series',
|
|
104
|
-
'stack',
|
|
105
|
-
'group',
|
|
106
|
-
'hole',
|
|
107
|
-
'x-label',
|
|
108
|
-
'y-label',
|
|
109
|
-
'y-right-label',
|
|
110
|
-
'label',
|
|
111
|
-
'no-name',
|
|
112
|
-
'no-value',
|
|
113
|
-
'no-percent',
|
|
114
|
-
'color',
|
|
115
|
-
]);
|
|
116
|
-
|
|
117
|
-
/** Default doughnut-hole inner-radius ratio when `hole` is given with no value. */
|
|
118
|
-
const DEFAULT_HOLE = 0.6;
|
|
119
|
-
|
|
120
|
-
/** Known boolean options for the simple chart parser. */
|
|
121
|
-
const KNOWN_BOOLEANS = new Set([
|
|
122
|
-
'orientation-horizontal',
|
|
123
|
-
'solid-fill',
|
|
124
|
-
'no-title',
|
|
125
|
-
'fill',
|
|
126
|
-
'hole',
|
|
127
|
-
]);
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Gate a `series`/`stack`/`group` block header against the chart type.
|
|
131
|
-
* Returns true when the header must be REJECTED (skip collection):
|
|
132
|
-
* - `series` on a `bar` chart is a hard error → use `stack`/`group` (#24).
|
|
133
|
-
* Emits a warning (but proceeds) for `stack`/`group` on a non-bar chart.
|
|
134
|
-
*/
|
|
135
|
-
function rejectSeriesHeader(
|
|
136
|
-
result: ParsedChart,
|
|
137
|
-
keyword: string,
|
|
138
|
-
lineNumber: number
|
|
139
|
-
): boolean {
|
|
140
|
-
if (result.type === 'bar' && keyword === 'series') {
|
|
141
|
-
const diag = makeDgmoError(
|
|
142
|
-
lineNumber,
|
|
143
|
-
"On bar charts, declare multiple series with a 'stack' or 'group' block header, not 'series'."
|
|
144
|
-
);
|
|
145
|
-
result.diagnostics.push(diag);
|
|
146
|
-
result.error = formatDgmoError(diag);
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
if ((keyword === 'stack' || keyword === 'group') && result.type !== 'bar') {
|
|
150
|
-
result.diagnostics.push(
|
|
151
|
-
makeDgmoError(
|
|
152
|
-
lineNumber,
|
|
153
|
-
`'${keyword}' is a bar layout header; ${result.type} charts use a 'series' block.`,
|
|
154
|
-
'warning'
|
|
155
|
-
)
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Apply the grouped-series (dual-axis) results from parseSeriesNames onto the
|
|
163
|
-
* ParsedChart. The axis headers double as the left/right y-axis labels, and
|
|
164
|
-
* `seriesAxes` is only recorded when a right-axis series actually exists — so a
|
|
165
|
-
* flat series block (or a grouped block with only `y-label` headers) leaves the
|
|
166
|
-
* chart shape identical to single-axis charts.
|
|
167
|
-
*/
|
|
168
|
-
function applySeriesAxes(
|
|
169
|
-
result: ParsedChart,
|
|
170
|
-
parsed: ReturnType<typeof parseSeriesNames>,
|
|
171
|
-
lineNumber: number
|
|
172
|
-
): void {
|
|
173
|
-
if (parsed.leftLabel !== undefined) {
|
|
174
|
-
result.ylabel = parsed.leftLabel;
|
|
175
|
-
result.ylabelLineNumber = lineNumber;
|
|
176
|
-
}
|
|
177
|
-
if (parsed.rightLabel !== undefined) {
|
|
178
|
-
result.yrlabel = parsed.rightLabel;
|
|
179
|
-
result.yrlabelLineNumber = lineNumber;
|
|
180
|
-
}
|
|
181
|
-
if (parsed.axes?.includes('right')) {
|
|
182
|
-
result.seriesAxes = parsed.axes;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Parses the simple chart text format into a structured object.
|
|
188
|
-
*
|
|
189
|
-
* Format (colon-free):
|
|
190
|
-
* ```
|
|
191
|
-
* bar My Chart
|
|
192
|
-
* series Revenue
|
|
193
|
-
*
|
|
194
|
-
* Jan 120
|
|
195
|
-
* Feb 200
|
|
196
|
-
* Mar 150
|
|
197
|
-
* ```
|
|
198
|
-
*/
|
|
199
|
-
export function parseChart(
|
|
200
|
-
content: string,
|
|
201
|
-
palette?: PaletteColors
|
|
202
|
-
): ParsedChart {
|
|
203
|
-
const lines = content.split('\n');
|
|
204
|
-
const parsedEras: ChartEra[] = [];
|
|
205
|
-
const rawEras: {
|
|
206
|
-
start: string;
|
|
207
|
-
afterArrow: string;
|
|
208
|
-
color: string | null;
|
|
209
|
-
lineNumber: number;
|
|
210
|
-
}[] = [];
|
|
211
|
-
const result: ParsedChart = {
|
|
212
|
-
type: 'bar',
|
|
213
|
-
data: [],
|
|
214
|
-
eras: parsedEras,
|
|
215
|
-
diagnostics: [],
|
|
216
|
-
error: null,
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
const fail = (line: number, message: string): ParsedChart => {
|
|
220
|
-
const diag = makeDgmoError(line, message);
|
|
221
|
-
result.diagnostics.push(diag);
|
|
222
|
-
result.error = formatDgmoError(diag);
|
|
223
|
-
return result;
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
let firstLineParsed = false;
|
|
227
|
-
|
|
228
|
-
for (let i = 0; i < lines.length; i++) {
|
|
229
|
-
// In-bounds by loop guard.
|
|
230
|
-
const trimmed = lines[i]!.trim();
|
|
231
|
-
const lineNumber = i + 1;
|
|
232
|
-
|
|
233
|
-
// Skip empty lines
|
|
234
|
-
if (!trimmed) continue;
|
|
235
|
-
|
|
236
|
-
// Reject legacy ## section headers
|
|
237
|
-
if (/^#{2,}\s+/.test(trimmed)) {
|
|
238
|
-
result.diagnostics.push(
|
|
239
|
-
makeDgmoError(
|
|
240
|
-
lineNumber,
|
|
241
|
-
`'${trimmed}' removed — use a bracket container '[${trimmed.replace(/^#+\s*/, '')}]' instead (## headers are no longer supported).`
|
|
242
|
-
)
|
|
243
|
-
);
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Skip comments
|
|
248
|
-
if (trimmed.startsWith('//')) continue;
|
|
249
|
-
|
|
250
|
-
// First non-empty, non-comment line: chart type + optional title
|
|
251
|
-
if (!firstLineParsed) {
|
|
252
|
-
firstLineParsed = true;
|
|
253
|
-
const firstLine = parseFirstLine(trimmed);
|
|
254
|
-
if (firstLine) {
|
|
255
|
-
const raw = firstLine.chartType.toLowerCase();
|
|
256
|
-
const chartType = raw as ChartType;
|
|
257
|
-
if (VALID_TYPES.has(chartType)) {
|
|
258
|
-
result.type = chartType;
|
|
259
|
-
if (firstLine.title) {
|
|
260
|
-
result.title = firstLine.title;
|
|
261
|
-
result.titleLineNumber = lineNumber;
|
|
262
|
-
}
|
|
263
|
-
continue;
|
|
264
|
-
} else {
|
|
265
|
-
let msg = `Unsupported chart type: ${firstLine.chartType}. Supported types: ${[...VALID_TYPES].join(', ')}.`;
|
|
266
|
-
const hint = suggest(raw, [...VALID_TYPES]);
|
|
267
|
-
if (hint) msg += ` ${hint}`;
|
|
268
|
-
return fail(lineNumber, msg);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
// If the first line is a single word (no spaces, no colon, no numbers),
|
|
272
|
-
// treat it as an unrecognized chart type rather than falling through
|
|
273
|
-
if (
|
|
274
|
-
!trimmed.includes(' ') &&
|
|
275
|
-
!trimmed.includes(':') &&
|
|
276
|
-
!/\d/.test(trimmed)
|
|
277
|
-
) {
|
|
278
|
-
let msg = `Unsupported chart type: ${trimmed}. Supported types: ${[...VALID_TYPES].join(', ')}.`;
|
|
279
|
-
const hint = suggest(trimmed.toLowerCase(), [...VALID_TYPES]);
|
|
280
|
-
if (hint) msg += ` ${hint}`;
|
|
281
|
-
return fail(lineNumber, msg);
|
|
282
|
-
}
|
|
283
|
-
// Fall through — first line might be a data row or option
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Era line (§1.5 trailing-token):
|
|
287
|
-
// `era Day 1 -> Day 3 Rough Seas` (no color)
|
|
288
|
-
// `era Day 1 -> Day 3 Rough Seas blue` (trailing color word)
|
|
289
|
-
// Color (if any) is the last whitespace-delimited token of the label.
|
|
290
|
-
const eraMatch = trimmed.match(/^era\s+(.+?)\s*->\s*(.+?)\s*$/);
|
|
291
|
-
if (eraMatch) {
|
|
292
|
-
// Capture group 2 guaranteed present after regex match.
|
|
293
|
-
const afterArrow = eraMatch[2]!.trim();
|
|
294
|
-
const spaceIdx = afterArrow.indexOf(' ');
|
|
295
|
-
if (spaceIdx >= 0) {
|
|
296
|
-
// Peel trailing-token color off the after-arrow label region.
|
|
297
|
-
const lastSpaceIdx = afterArrow.lastIndexOf(' ');
|
|
298
|
-
const trailing = afterArrow.substring(lastSpaceIdx + 1);
|
|
299
|
-
const hasColor = RECOGNIZED_COLOR_NAMES.includes(
|
|
300
|
-
trailing as (typeof RECOGNIZED_COLOR_NAMES)[number]
|
|
301
|
-
);
|
|
302
|
-
const labelPart = hasColor
|
|
303
|
-
? afterArrow.substring(0, lastSpaceIdx).trimEnd()
|
|
304
|
-
: afterArrow;
|
|
305
|
-
rawEras.push({
|
|
306
|
-
// Capture group 1 guaranteed present after regex match.
|
|
307
|
-
start: eraMatch[1]!.trim(),
|
|
308
|
-
afterArrow: labelPart,
|
|
309
|
-
color: hasColor
|
|
310
|
-
? (resolveColorWithDiagnostic(
|
|
311
|
-
trailing,
|
|
312
|
-
lineNumber,
|
|
313
|
-
result.diagnostics,
|
|
314
|
-
palette
|
|
315
|
-
) ?? null)
|
|
316
|
-
: null,
|
|
317
|
-
lineNumber,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Extract first token to check for known options
|
|
324
|
-
const spaceIdx = trimmed.indexOf(' ');
|
|
325
|
-
const firstToken = (
|
|
326
|
-
spaceIdx >= 0 ? trimmed.substring(0, spaceIdx) : trimmed
|
|
327
|
-
).toLowerCase();
|
|
328
|
-
|
|
329
|
-
// Bare boolean options (e.g. orientation-horizontal)
|
|
330
|
-
if (KNOWN_BOOLEANS.has(firstToken) && spaceIdx < 0) {
|
|
331
|
-
if (firstToken === 'orientation-horizontal') {
|
|
332
|
-
result.orientation = 'horizontal';
|
|
333
|
-
} else if (firstToken === 'solid-fill') {
|
|
334
|
-
result.solidFill = true;
|
|
335
|
-
} else if (firstToken === 'no-title') {
|
|
336
|
-
result.noTitle = true;
|
|
337
|
-
} else if (firstToken === 'fill') {
|
|
338
|
-
result.fill = true;
|
|
339
|
-
} else if (firstToken === 'hole') {
|
|
340
|
-
result.hole = DEFAULT_HOLE;
|
|
341
|
-
}
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Known option with a value
|
|
346
|
-
if (KNOWN_OPTIONS.has(firstToken) && spaceIdx >= 0) {
|
|
347
|
-
const value = trimmed.substring(spaceIdx + 1).trim();
|
|
348
|
-
|
|
349
|
-
if (firstToken === 'chart') {
|
|
350
|
-
const raw = value.toLowerCase();
|
|
351
|
-
const chartType = raw as ChartType;
|
|
352
|
-
if (VALID_TYPES.has(chartType)) {
|
|
353
|
-
result.type = chartType;
|
|
354
|
-
} else {
|
|
355
|
-
let msg = `Unsupported chart type: ${value}. Supported types: ${[...VALID_TYPES].join(', ')}.`;
|
|
356
|
-
const hint = suggest(raw, [...VALID_TYPES]);
|
|
357
|
-
if (hint) msg += ` ${hint}`;
|
|
358
|
-
return fail(lineNumber, msg);
|
|
359
|
-
}
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (firstToken === 'title') {
|
|
364
|
-
result.title = value;
|
|
365
|
-
result.titleLineNumber = lineNumber;
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (firstToken === 'x-label') {
|
|
370
|
-
result.xlabel = value;
|
|
371
|
-
result.xlabelLineNumber = lineNumber;
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (firstToken === 'y-label') {
|
|
376
|
-
result.ylabel = value;
|
|
377
|
-
result.ylabelLineNumber = lineNumber;
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (firstToken === 'y-right-label') {
|
|
382
|
-
result.yrlabel = value;
|
|
383
|
-
result.yrlabelLineNumber = lineNumber;
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (firstToken === 'label') {
|
|
388
|
-
result.label = value;
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (firstToken === 'hole') {
|
|
393
|
-
const ratio = parseFloat(value);
|
|
394
|
-
if (Number.isFinite(ratio) && ratio >= 0 && ratio < 1) {
|
|
395
|
-
result.hole = ratio;
|
|
396
|
-
} else {
|
|
397
|
-
result.diagnostics.push(
|
|
398
|
-
makeDgmoError(
|
|
399
|
-
lineNumber,
|
|
400
|
-
'hole must be a ratio between 0 and 0.9 (e.g. "hole 0.6"). Using the default.',
|
|
401
|
-
'warning'
|
|
402
|
-
)
|
|
403
|
-
);
|
|
404
|
-
result.hole = DEFAULT_HOLE;
|
|
405
|
-
}
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (firstToken === 'color') {
|
|
410
|
-
const resolvedColor = resolveColorWithDiagnostic(
|
|
411
|
-
value.trim(),
|
|
412
|
-
lineNumber,
|
|
413
|
-
result.diagnostics,
|
|
414
|
-
palette
|
|
415
|
-
);
|
|
416
|
-
if (resolvedColor !== undefined) result.color = resolvedColor;
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (
|
|
421
|
-
firstToken === 'series' ||
|
|
422
|
-
firstToken === 'stack' ||
|
|
423
|
-
firstToken === 'group'
|
|
424
|
-
) {
|
|
425
|
-
const rejected = rejectSeriesHeader(result, firstToken, lineNumber);
|
|
426
|
-
const parsed = parseSeriesNames(
|
|
427
|
-
value,
|
|
428
|
-
lines,
|
|
429
|
-
i,
|
|
430
|
-
palette,
|
|
431
|
-
rejected ? undefined : result.diagnostics
|
|
432
|
-
);
|
|
433
|
-
i = parsed.newIndex; // consume the indented block even when rejected
|
|
434
|
-
if (rejected) continue;
|
|
435
|
-
result.series = parsed.series;
|
|
436
|
-
result.seriesLineNumber = lineNumber;
|
|
437
|
-
if (parsed.names.length > 1) {
|
|
438
|
-
result.seriesNames = parsed.names;
|
|
439
|
-
result.seriesNameLineNumbers = parsed.nameLineNumbers;
|
|
440
|
-
}
|
|
441
|
-
if (parsed.nameColors.some(Boolean))
|
|
442
|
-
result.seriesNameColors = parsed.nameColors;
|
|
443
|
-
applySeriesAxes(result, parsed, lineNumber);
|
|
444
|
-
if (firstToken === 'stack' || firstToken === 'group')
|
|
445
|
-
result.barLayout = firstToken;
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Bare boolean options: no-name, no-value, no-percent
|
|
451
|
-
if (firstToken === 'no-name') {
|
|
452
|
-
result.noName = true;
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
455
|
-
if (firstToken === 'no-value') {
|
|
456
|
-
result.noValue = true;
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
if (firstToken === 'no-percent') {
|
|
460
|
-
result.noPercent = true;
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
if (firstToken === 'no-center-total') {
|
|
464
|
-
result.noCenterTotal = true;
|
|
465
|
-
continue;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Silent-ignore unrecognized no-* flags (typos, future flags).
|
|
469
|
-
// Per-chart honoring is handled at the renderer; edit-time discovery
|
|
470
|
-
// happens via autocomplete + docs, not parse-time errors.
|
|
471
|
-
if (firstToken.startsWith('no-') && spaceIdx < 0) {
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Bare "series" / "stack" / "group" header — collect indented names.
|
|
476
|
-
if (
|
|
477
|
-
(firstToken === 'series' ||
|
|
478
|
-
firstToken === 'stack' ||
|
|
479
|
-
firstToken === 'group') &&
|
|
480
|
-
spaceIdx === -1
|
|
481
|
-
) {
|
|
482
|
-
const rejected = rejectSeriesHeader(result, firstToken, lineNumber);
|
|
483
|
-
const parsed = parseSeriesNames('', lines, i, palette);
|
|
484
|
-
i = parsed.newIndex; // consume the indented block even when rejected
|
|
485
|
-
if (rejected) continue;
|
|
486
|
-
result.series = parsed.series;
|
|
487
|
-
result.seriesLineNumber = lineNumber;
|
|
488
|
-
if (parsed.names.length > 1) {
|
|
489
|
-
result.seriesNames = parsed.names;
|
|
490
|
-
result.seriesNameLineNumbers = parsed.nameLineNumbers;
|
|
491
|
-
}
|
|
492
|
-
if (parsed.nameColors.some(Boolean))
|
|
493
|
-
result.seriesNameColors = parsed.nameColors;
|
|
494
|
-
applySeriesAxes(result, parsed, lineNumber);
|
|
495
|
-
if (firstToken === 'stack' || firstToken === 'group')
|
|
496
|
-
result.barLayout = firstToken;
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Data row: parse from the right — rightmost numeric token(s) = value(s), everything left = label
|
|
501
|
-
// Supports space-separated multi-values when series are defined: "Jan 100 200 300"
|
|
502
|
-
const seriesCount = result.seriesNames?.length ?? 0;
|
|
503
|
-
const multiValue = seriesCount >= 2;
|
|
504
|
-
const dataValues = parseDataRowValues(trimmed, {
|
|
505
|
-
multiValue,
|
|
506
|
-
...(multiValue && { expectedValues: seriesCount }),
|
|
507
|
-
});
|
|
508
|
-
if (dataValues) {
|
|
509
|
-
const { label: rawLabel, color: pointColor } = extractColor(
|
|
510
|
-
dataValues.label,
|
|
511
|
-
palette,
|
|
512
|
-
result.diagnostics,
|
|
513
|
-
lineNumber
|
|
514
|
-
);
|
|
515
|
-
const [first, ...rest] = dataValues.values;
|
|
516
|
-
result.data.push({
|
|
517
|
-
label: rawLabel,
|
|
518
|
-
// parseDataRowValues guarantees values.length >= 1.
|
|
519
|
-
value: first!,
|
|
520
|
-
...(rest.length > 0 && { extraValues: rest }),
|
|
521
|
-
...(pointColor && { color: pointColor }),
|
|
522
|
-
lineNumber,
|
|
523
|
-
});
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// Catch-all: nothing matched this line
|
|
528
|
-
let msg = `Unexpected line: '${trimmed}'.`;
|
|
529
|
-
const hint = suggest(firstToken, [...KNOWN_OPTIONS, ...KNOWN_BOOLEANS]);
|
|
530
|
-
if (hint) msg += ` ${hint}`;
|
|
531
|
-
result.diagnostics.push(makeDgmoError(lineNumber, msg, 'warning'));
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Resolve raw eras against known data labels (longest-prefix match for multi-word labels)
|
|
535
|
-
const knownLabels = new Set(result.data.map((d) => d.label));
|
|
536
|
-
for (const raw of rawEras) {
|
|
537
|
-
// Find the longest prefix of afterArrow that matches a known label
|
|
538
|
-
const words = raw.afterArrow.split(' ');
|
|
539
|
-
let end = '';
|
|
540
|
-
let label = '';
|
|
541
|
-
let matched = false;
|
|
542
|
-
for (let w = words.length - 1; w >= 1; w--) {
|
|
543
|
-
const candidateEnd = words.slice(0, w).join(' ');
|
|
544
|
-
if (knownLabels.has(candidateEnd)) {
|
|
545
|
-
end = candidateEnd;
|
|
546
|
-
label = words.slice(w).join(' ');
|
|
547
|
-
matched = true;
|
|
548
|
-
break;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
if (!matched) {
|
|
552
|
-
// Fallback: first token = end, rest = label
|
|
553
|
-
// String.split always returns at least one element.
|
|
554
|
-
end = words[0]!;
|
|
555
|
-
label = words.slice(1).join(' ');
|
|
556
|
-
}
|
|
557
|
-
parsedEras.push({
|
|
558
|
-
start: raw.start,
|
|
559
|
-
end,
|
|
560
|
-
label,
|
|
561
|
-
color: raw.color,
|
|
562
|
-
lineNumber: raw.lineNumber,
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// Eras are only valid for line charts (incl. filled/area and multi-series).
|
|
567
|
-
if (result.type !== 'line') {
|
|
568
|
-
result.eras = [];
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// Validation
|
|
572
|
-
const warn = (line: number, message: string): void => {
|
|
573
|
-
result.diagnostics.push(makeDgmoError(line, message, 'warning'));
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
if (!result.error && result.data.length === 0) {
|
|
577
|
-
warn(1, 'No data points found. Add data in format: Label 123');
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
if (!result.error && result.seriesNames) {
|
|
581
|
-
const expectedCount = result.seriesNames.length;
|
|
582
|
-
for (const dp of result.data) {
|
|
583
|
-
const actualCount = 1 + (dp.extraValues?.length ?? 0);
|
|
584
|
-
if (actualCount !== expectedCount) {
|
|
585
|
-
warn(
|
|
586
|
-
dp.lineNumber,
|
|
587
|
-
`Data point "${dp.label}" has ${actualCount} value(s), but ${expectedCount} series defined. Each row must have ${expectedCount} values.`
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
// Filter out mismatched data points so renderers get clean data
|
|
592
|
-
result.data = result.data.filter((dp) => {
|
|
593
|
-
const actualCount = 1 + (dp.extraValues?.length ?? 0);
|
|
594
|
-
return actualCount === expectedCount;
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Dual-axis (§15.1) is only honored by line / multi-line charts. Drop the
|
|
599
|
-
// axis assignment elsewhere so other renderers see plain single-axis data.
|
|
600
|
-
if (!result.error && result.seriesAxes && result.type !== 'line') {
|
|
601
|
-
warn(
|
|
602
|
-
result.seriesLineNumber ?? 1,
|
|
603
|
-
`Dual y-axes (y-right-label) are only supported on line / multi-line charts; ignoring the secondary axis for "${result.type}".`
|
|
604
|
-
);
|
|
605
|
-
delete result.seriesAxes;
|
|
606
|
-
delete result.yrlabel;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// A right-axis label with nothing assigned to it renders nothing — flag it so
|
|
610
|
-
// the author wraps the intended series in a `y-right-label` group.
|
|
611
|
-
if (
|
|
612
|
-
!result.error &&
|
|
613
|
-
result.yrlabel &&
|
|
614
|
-
!result.seriesAxes?.includes('right')
|
|
615
|
-
) {
|
|
616
|
-
warn(
|
|
617
|
-
result.yrlabelLineNumber ?? 1,
|
|
618
|
-
`y-right-label is set but no series is assigned to the right axis. Group the series under a "y-right-label" header (see §15.1).`
|
|
619
|
-
);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
return result;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// ============================================================
|
|
626
|
-
// Data Row Parser
|
|
627
|
-
// ============================================================
|
|
628
|
-
|
|
629
|
-
/**
|
|
630
|
-
* Parse a data row line: everything before the last numeric token(s) is the label,
|
|
631
|
-
* numeric tokens at the end are the values. Values are space-separated.
|
|
632
|
-
*
|
|
633
|
-
* Examples:
|
|
634
|
-
* "Jan 120" → { label: "Jan", values: [120] }
|
|
635
|
-
* "North America 250" → { label: "North America", values: [250] }
|
|
636
|
-
* "Q1 10 20 30" → { label: "Q1", values: [10, 20, 30] }
|
|
637
|
-
* "Revenue 1_000" → { label: "Revenue", values: [1000] }
|
|
638
|
-
*
|
|
639
|
-
* Returns null if the line has no numeric value at the end.
|
|
640
|
-
*/
|
|
641
|
-
export function parseDataRowValues(
|
|
642
|
-
line: string,
|
|
643
|
-
options?: {
|
|
644
|
-
multiValue?: boolean;
|
|
645
|
-
expectedValues?: number;
|
|
646
|
-
}
|
|
647
|
-
): { label: string; values: number[] } | null {
|
|
648
|
-
// Values are space-separated.
|
|
649
|
-
// When multiValue is enabled, walk backward collecting consecutive numeric tokens.
|
|
650
|
-
// Otherwise (default), take only the last token — preserving labels that contain
|
|
651
|
-
// numbers (e.g., "Region 5 300" → label "Region 5", value 300).
|
|
652
|
-
const tokens = line.split(/\s+/);
|
|
653
|
-
if (tokens.length < 2) return null;
|
|
654
|
-
|
|
655
|
-
if (options?.multiValue) {
|
|
656
|
-
const limit = options.expectedValues ?? Infinity;
|
|
657
|
-
const values: number[] = [];
|
|
658
|
-
let idx = tokens.length - 1;
|
|
659
|
-
while (idx >= 1 && values.length < limit) {
|
|
660
|
-
// In-bounds by loop guard (idx >= 1 and idx <= tokens.length - 1).
|
|
661
|
-
const tok = tokens[idx]!;
|
|
662
|
-
const normTok = normalizeNumericToken(tok) ?? tok;
|
|
663
|
-
const num = parseFloat(normTok);
|
|
664
|
-
if (isNaN(num) || !isFinite(Number(normTok))) break;
|
|
665
|
-
values.unshift(num);
|
|
666
|
-
idx--;
|
|
667
|
-
}
|
|
668
|
-
if (values.length === 0) return null;
|
|
669
|
-
const label = tokens.slice(0, idx + 1).join(' ');
|
|
670
|
-
if (!label) return null;
|
|
671
|
-
return { label, values };
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// Single-value mode: only the last space-separated token
|
|
675
|
-
// tokens.length >= 2 by the earlier length check.
|
|
676
|
-
const lastToken = tokens[tokens.length - 1]!;
|
|
677
|
-
const normalizedLast = normalizeNumericToken(lastToken) ?? lastToken;
|
|
678
|
-
const num = parseFloat(normalizedLast);
|
|
679
|
-
if (isNaN(num) || !isFinite(Number(normalizedLast))) return null;
|
|
680
|
-
|
|
681
|
-
const label = tokens.slice(0, -1).join(' ');
|
|
682
|
-
if (!label) return null;
|
|
683
|
-
|
|
684
|
-
return { label, values: [num] };
|
|
685
|
-
}
|