@slxu/graphsx 0.1.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/src/plot.js ADDED
@@ -0,0 +1,842 @@
1
+ import { GraphDslError } from "./errors.js";
2
+ import { parseMarkup } from "./markup.js";
3
+ import {
4
+ applyPointMaps,
5
+ assertFiniteMathValue,
6
+ evaluateMathExpression,
7
+ isMathSource,
8
+ mathValue,
9
+ realNumberValue
10
+ } from "./plot-math.js";
11
+
12
+ const STYLE_TAGS = new Set(["Style"]);
13
+ const DATA_TAGS = new Set(["Data", "Dataset"]);
14
+ const AXIS_TAGS = new Set(["Axis", "XAxis", "YAxis"]);
15
+ const TICK_TAGS = new Set(["Ticks", "ticks"]);
16
+ const CURVE_TAGS = new Set(["Curve", "Series"]);
17
+ const LINE_TAGS = new Set(["Line"]);
18
+ const POINT_TAGS = new Set(["Point", "Mark", "Scatter"]);
19
+ const TEXT_TAGS = new Set(["Text", "Label"]);
20
+ const LEGEND_TAGS = new Set(["Legend", "legend"]);
21
+ const ANNOTATION_NODE_TAGS = new Set(["Rect", "rect", "Circle", "circle", "Anchor", "anchor"]);
22
+ const ANNOTATION_LINK_TAGS = new Set(["Link"]);
23
+ const ANNOTATION_PATH_TAGS = new Set(["Path", "path"]);
24
+ const PORT_TAGS = new Set(["Port"]);
25
+ const SIDE_ATTRS = ["left", "right", "top", "bottom"];
26
+ const SIDE_ANGLES = {
27
+ left: 180,
28
+ right: 0,
29
+ top: -90,
30
+ bottom: 90
31
+ };
32
+ const STYLE_ATTRS = new Set([
33
+ "fill",
34
+ "fillOpacity",
35
+ "opacity",
36
+ "stroke",
37
+ "strokeDasharray",
38
+ "strokeLinecap",
39
+ "strokeLinejoin",
40
+ "strokeOpacity",
41
+ "strokeWidth",
42
+ "fontFamily",
43
+ "fontSize",
44
+ "fontStyle",
45
+ "fontWeight"
46
+ ]);
47
+ const numberValue = realNumberValue;
48
+
49
+ export function parsePlots(source) {
50
+ const roots = parseMarkup(source).filter((node) => node.type === "element");
51
+ const plots = roots.filter((node) => node.name === "Plot");
52
+
53
+ if (plots.length !== roots.length) {
54
+ throw new GraphDslError("Top-level elements must be <Plot>");
55
+ }
56
+
57
+ return plots.map(buildPlotModel);
58
+ }
59
+
60
+ export function parsePlot(source) {
61
+ const plots = parsePlots(source);
62
+
63
+ if (plots.length !== 1) {
64
+ throw new GraphDslError(`Expected exactly one <Plot>, found ${plots.length}`);
65
+ }
66
+
67
+ return plots[0];
68
+ }
69
+
70
+ export function buildPlotModel(plotElement) {
71
+ assertElement(plotElement, "Plot");
72
+ assertKnownPlotChildren(plotElement);
73
+
74
+ const styles = buildStyles(plotElement.children.filter((child) => STYLE_TAGS.has(child.name)));
75
+ const dataRecords = buildData(plotElement.children.filter((child) => DATA_TAGS.has(child.name)));
76
+ const axes = plotElement.children.filter((child) => AXIS_TAGS.has(child.name)).map((child) => buildAxis(child, styles));
77
+ const curves = plotElement.children.filter((child) => CURVE_TAGS.has(child.name)).map((child) => buildCurve(child, styles, dataRecords));
78
+ const lines = plotElement.children.filter((child) => LINE_TAGS.has(child.name)).map((child) => buildLine(child, styles, dataRecords));
79
+ const marks = plotElement.children.filter((child) => POINT_TAGS.has(child.name)).map((child) => buildMark(child, styles, dataRecords));
80
+ const labels = plotElement.children.filter((child) => TEXT_TAGS.has(child.name)).map((child) => buildText(child, styles));
81
+ const legends = plotElement.children.filter((child) => LEGEND_TAGS.has(child.name)).map((child) => buildLegend(child, styles));
82
+ const annotations = buildAnnotations(plotElement, styles);
83
+
84
+ return {
85
+ type: "plot",
86
+ attrs: normalizePlotAttrs(plotElement.attrs),
87
+ styles: Object.fromEntries(styles),
88
+ data: Object.fromEntries([...dataRecords].map(([id, record]) => [id, record.points])),
89
+ dataSources: Object.fromEntries(dataRecords),
90
+ axes,
91
+ curves,
92
+ lines,
93
+ marks,
94
+ labels,
95
+ legends,
96
+ annotations
97
+ };
98
+ }
99
+
100
+ function normalizePlotAttrs(attrs) {
101
+ const normalized = { ...attrs };
102
+ if (Array.isArray(normalized.xDomain)) {
103
+ normalized.xDomain = numericPair(normalized.xDomain, "<Plot> xDomain");
104
+ }
105
+ if (Array.isArray(normalized.xdomain)) {
106
+ normalized.xdomain = numericPair(normalized.xdomain, "<Plot> xDomain");
107
+ }
108
+ if (Array.isArray(normalized.yDomain)) {
109
+ normalized.yDomain = numericPair(normalized.yDomain, "<Plot> yDomain");
110
+ }
111
+ if (Array.isArray(normalized.ydomain)) {
112
+ normalized.ydomain = numericPair(normalized.ydomain, "<Plot> yDomain");
113
+ }
114
+ return normalized;
115
+ }
116
+
117
+ function buildAxis(axisElement, styles) {
118
+ assertKnownAxisChildren(axisElement);
119
+ const dim = axisElement.name === "XAxis" || axisElement.attrs.x ? "x"
120
+ : axisElement.name === "YAxis" || axisElement.attrs.y ? "y"
121
+ : axisElement.attrs.dim ?? axisElement.attrs.axis ?? "x";
122
+ if (dim !== "x" && dim !== "y") {
123
+ throw new GraphDslError(`Axis dim must be "x" or "y"`);
124
+ }
125
+ return {
126
+ dim,
127
+ attrs: normalizeTickAttrs({ ...axisElement.attrs, dim }, axisElement.name),
128
+ ticks: axisElement.children.filter((child) => TICK_TAGS.has(child.name)).map((child) => buildTicks(child, styles))
129
+ };
130
+ }
131
+
132
+ function buildTicks(tickElement, styles) {
133
+ return {
134
+ attrs: normalizeTickAttrs(resolveStyledAttrs(tickElement.attrs, styles), tickElement.name)
135
+ };
136
+ }
137
+
138
+ function normalizeTickAttrs(attrs, elementName) {
139
+ const normalized = { ...attrs };
140
+ if (Array.isArray(normalized.values)) {
141
+ normalized.values = normalized.values.map((value) => numberValue(value, `<${elementName}> value`));
142
+ }
143
+ if (Array.isArray(normalized.ticks)) {
144
+ normalized.ticks = normalized.ticks.map((value) => numberValue(value, `<${elementName}> tick`));
145
+ }
146
+ return normalized;
147
+ }
148
+
149
+ function buildCurve(curveElement, styles, data) {
150
+ const points = resolvePoints(curveElement, data);
151
+ const dataId = dataIdAttr(curveElement);
152
+ validateSeriesAnimation(curveElement, dataId, data);
153
+ const attrs = resolveStyledAttrs(curveElement.attrs, styles);
154
+ return {
155
+ id: curveElement.attrs.id,
156
+ dataId,
157
+ points,
158
+ attrs: normalizeSeriesAttrs(attrs, curveElement.name)
159
+ };
160
+ }
161
+
162
+ function buildLine(lineElement, styles, data) {
163
+ if (hasPoints(lineElement) || lineElement.attrs.data) {
164
+ const dataId = dataIdAttr(lineElement);
165
+ validateSeriesAnimation(lineElement, dataId, data);
166
+ const attrs = resolveStyledAttrs(lineElement.attrs, styles);
167
+ return {
168
+ id: lineElement.attrs.id,
169
+ dataId,
170
+ points: resolvePoints(lineElement, data),
171
+ attrs: normalizeSeriesAttrs(attrs, lineElement.name)
172
+ };
173
+ }
174
+
175
+ const attrs = resolveStyledAttrs(lineElement.attrs, styles);
176
+ return {
177
+ id: lineElement.attrs.id,
178
+ from: pointAttr(lineElement, "from"),
179
+ to: pointAttr(lineElement, "to"),
180
+ attrs: normalizeSeriesAttrs(attrs, lineElement.name)
181
+ };
182
+ }
183
+
184
+ function buildMark(markElement, styles, data) {
185
+ if (hasPoints(markElement) || markElement.attrs.data) {
186
+ const dataId = dataIdAttr(markElement);
187
+ validateSeriesAnimation(markElement, dataId, data);
188
+ const attrs = resolveStyledAttrs(markElement.attrs, styles);
189
+ return {
190
+ id: markElement.attrs.id,
191
+ dataId,
192
+ points: resolvePoints(markElement, data),
193
+ attrs: normalizeSeriesAttrs(attrs, markElement.name)
194
+ };
195
+ }
196
+
197
+ const attrs = resolveStyledAttrs(markElement.attrs, styles);
198
+ return {
199
+ id: markElement.attrs.id,
200
+ at: pointAttr(markElement, "at"),
201
+ attrs: normalizeSeriesAttrs(attrs, markElement.name)
202
+ };
203
+ }
204
+
205
+ function hasPoints(element) {
206
+ return Array.isArray(element.attrs.points) || (Array.isArray(element.attrs.x) && Array.isArray(element.attrs.y));
207
+ }
208
+
209
+ function buildText(textElement, styles) {
210
+ return {
211
+ id: textElement.attrs.id,
212
+ at: pointAttr(textElement, "at"),
213
+ text: textElement.attrs.label ?? textElement.attrs.text ?? "",
214
+ attrs: resolveStyledAttrs(textElement.attrs, styles)
215
+ };
216
+ }
217
+
218
+ function buildLegend(legendElement, styles) {
219
+ return {
220
+ id: legendElement.attrs.id,
221
+ attrs: resolveStyledAttrs(legendElement.attrs, styles)
222
+ };
223
+ }
224
+
225
+ function buildAnnotations(plotElement, styles) {
226
+ const nodes = plotElement.children
227
+ .filter((child) => ANNOTATION_NODE_TAGS.has(child.name))
228
+ .map((child) => buildAnnotationNode(child, styles));
229
+ assertUniqueAnnotationIds(nodes);
230
+ const ports = new Set(nodes.flatMap((node) => Object.keys(node.ports).map((portId) => `${node.id}.${portId}`)));
231
+ const links = plotElement.children
232
+ .filter((child) => ANNOTATION_LINK_TAGS.has(child.name))
233
+ .map((child) => buildAnnotationLink(child, styles, ports));
234
+ const paths = plotElement.children
235
+ .filter((child) => ANNOTATION_PATH_TAGS.has(child.name))
236
+ .map((child) => buildAnnotationPath(child, styles));
237
+ return { nodes, links, paths };
238
+ }
239
+
240
+ function buildAnnotationNode(nodeElement, styles) {
241
+ assertKnownAnnotationChildren(nodeElement);
242
+ const id = requiredAttr(nodeElement, "id");
243
+ const shape = annotationShape(nodeElement.name);
244
+ const attrs = normalizeAnnotationNodeAttrs(resolveStyledAttrs(nodeElement.attrs, styles), nodeElement.name);
245
+ const node = {
246
+ id,
247
+ shape,
248
+ at: annotationPointAttr(nodeElement, "at", [0, 0]),
249
+ atUnit: coordinateUnit(attrs.atUnit ?? attrs.atunit ?? attrs.unit),
250
+ attrs,
251
+ ports: {}
252
+ };
253
+
254
+ for (const portElement of nodeElement.children.filter((child) => PORT_TAGS.has(child.name))) {
255
+ const port = buildAnnotationPort(portElement, node, styles);
256
+ if (node.ports[port.id]) {
257
+ throw new GraphDslError(`Duplicate port id "${port.id}" on "${id}"`);
258
+ }
259
+ node.ports[port.id] = port;
260
+ }
261
+ addDefaultAnnotationPorts(node);
262
+ return node;
263
+ }
264
+
265
+ function annotationShape(name) {
266
+ if (name === "Circle" || name === "circle") return "circle";
267
+ if (name === "Anchor" || name === "anchor") return "anchor";
268
+ return "rect";
269
+ }
270
+
271
+ function normalizeAnnotationNodeAttrs(attrs, elementName) {
272
+ const normalized = { ...attrs };
273
+ if (Array.isArray(normalized.size)) {
274
+ normalized.w = numberValue(normalized.size[0], `<${elementName}> size width`);
275
+ normalized.h = numberValue(normalized.size[1], `<${elementName}> size height`);
276
+ }
277
+ if (normalized.w != null) normalized.w = numberValue(normalized.w, `<${elementName}> w`);
278
+ if (normalized.h != null) normalized.h = numberValue(normalized.h, `<${elementName}> h`);
279
+ if (normalized.r != null) normalized.r = numberValue(normalized.r, `<${elementName}> r`);
280
+ if (normalized.corner != null) normalized.corner = numberValue(normalized.corner, `<${elementName}> corner`);
281
+ return normalized;
282
+ }
283
+
284
+ function buildAnnotationPort(portElement, node, styles) {
285
+ const id = requiredAttr(portElement, "id");
286
+ const attrs = resolveStyledAttrs(portElement.attrs, styles);
287
+ const side = resolvePortSide(attrs);
288
+ const position = Array.isArray(attrs.at)
289
+ ? annotationPoint(attrs.at, `<Port> at`)
290
+ : defaultAnnotationPortPosition(node, side);
291
+ return {
292
+ id,
293
+ side,
294
+ angle: attrs.angle == null ? (SIDE_ANGLES[side] ?? 0) : numberValue(attrs.angle, `<Port> angle`),
295
+ x: position.x,
296
+ y: position.y,
297
+ attrs
298
+ };
299
+ }
300
+
301
+ function addDefaultAnnotationPorts(node) {
302
+ if (node.shape === "anchor") {
303
+ if (!node.ports.center) {
304
+ node.ports.center = {
305
+ id: "center",
306
+ side: null,
307
+ angle: 0,
308
+ x: 0,
309
+ y: 0,
310
+ auto: true,
311
+ attrs: { id: "center" }
312
+ };
313
+ }
314
+ return;
315
+ }
316
+
317
+ for (const side of SIDE_ATTRS) {
318
+ if (node.ports[side]) continue;
319
+ const point = defaultAnnotationPortPosition(node, side);
320
+ node.ports[side] = {
321
+ id: side,
322
+ side,
323
+ angle: SIDE_ANGLES[side],
324
+ x: point.x,
325
+ y: point.y,
326
+ auto: true,
327
+ attrs: { id: side, [side]: true }
328
+ };
329
+ }
330
+ }
331
+
332
+ function defaultAnnotationPortPosition(node, side) {
333
+ if (node.shape === "circle") {
334
+ const r = Number(node.attrs.r ?? 5);
335
+ const positions = {
336
+ left: { x: -r, y: 0 },
337
+ right: { x: r, y: 0 },
338
+ top: { x: 0, y: -r },
339
+ bottom: { x: 0, y: r }
340
+ };
341
+ return positions[side] ?? { x: 0, y: 0 };
342
+ }
343
+
344
+ const w = Number(node.attrs.w ?? 80);
345
+ const h = Number(node.attrs.h ?? 28);
346
+ const positions = {
347
+ left: { x: 0, y: h / 2 },
348
+ right: { x: w, y: h / 2 },
349
+ top: { x: w / 2, y: 0 },
350
+ bottom: { x: w / 2, y: h }
351
+ };
352
+ return positions[side] ?? { x: w / 2, y: h / 2 };
353
+ }
354
+
355
+ function resolvePortSide(attrs) {
356
+ return SIDE_ATTRS.find((side) => attrs[side]) ?? null;
357
+ }
358
+
359
+ function buildAnnotationLink(linkElement, styles, ports) {
360
+ const from = annotationEndpointAttr(linkElement, "from");
361
+ const to = annotationEndpointAttr(linkElement, "to");
362
+ if (!ports.has(from)) throw new GraphDslError(`Unknown annotation port "${from}"`);
363
+ if (!ports.has(to)) throw new GraphDslError(`Unknown annotation port "${to}"`);
364
+ return {
365
+ id: linkElement.attrs.id,
366
+ from,
367
+ to,
368
+ attrs: resolveStyledAttrs(linkElement.attrs, styles)
369
+ };
370
+ }
371
+
372
+ function buildAnnotationPath(pathElement, styles) {
373
+ const attrs = resolveStyledAttrs(pathElement.attrs, styles);
374
+ return {
375
+ id: attrs.id ?? null,
376
+ points: Array.isArray(attrs.points) ? attrs.points.map((point) => annotationPoint(point, `<Path> points`)) : null,
377
+ atUnit: coordinateUnit(attrs.atUnit ?? attrs.atunit ?? attrs.unit),
378
+ attrs
379
+ };
380
+ }
381
+
382
+ function annotationPointAttr(element, name, fallback = null) {
383
+ const value = element.attrs[name];
384
+ if (value == null && fallback) return annotationPoint(fallback, `<${element.name}> ${name}`);
385
+ if (!Array.isArray(value)) {
386
+ throw new GraphDslError(`<${element.name}> requires ${name}={[x, y]}`);
387
+ }
388
+ return annotationPoint(value, `<${element.name}> ${name}`);
389
+ }
390
+
391
+ function annotationPoint(value, label) {
392
+ if (!Array.isArray(value) || value.length < 2) {
393
+ throw new GraphDslError(`${label} must be [x, y]`);
394
+ }
395
+ return {
396
+ x: numberValue(value[0], `${label} x`),
397
+ y: numberValue(value[1], `${label} y`)
398
+ };
399
+ }
400
+
401
+ function coordinateUnit(value) {
402
+ const unit = String(value ?? "data").toLowerCase();
403
+ if (unit !== "data" && unit !== "screen") {
404
+ throw new GraphDslError(`Plot annotation unit must be "data" or "screen"`);
405
+ }
406
+ return unit;
407
+ }
408
+
409
+ function annotationEndpointAttr(element, name) {
410
+ const value = element.attrs[name];
411
+ if (typeof value === "string") return value;
412
+ throw new GraphDslError(`<${element.name}> "${name}" must be a quoted port address like "note.left"`);
413
+ }
414
+
415
+ function resolvePoints(element, data = new Map()) {
416
+ if (element.attrs.data) {
417
+ const record = data.get(element.attrs.data);
418
+ if (!record) {
419
+ throw new GraphDslError(`Unknown data "${element.attrs.data}"`);
420
+ }
421
+ return applyPointMaps(clonePoints(record.points), element.attrs, `<${element.name}>`);
422
+ }
423
+
424
+ if (Array.isArray(element.attrs.points)) {
425
+ return applyPointMaps(element.attrs.points.map((point) => normalizePoint(point, "points")), element.attrs, `<${element.name}>`);
426
+ }
427
+
428
+ if (Array.isArray(element.attrs.x) && Array.isArray(element.attrs.y)) {
429
+ if (element.attrs.x.length !== element.attrs.y.length) {
430
+ throw new GraphDslError(`<${element.name}> x and y arrays must have the same length`);
431
+ }
432
+ return applyPointMaps(element.attrs.x.map((x, index) => normalizePoint([x, element.attrs.y[index]], "x/y")), element.attrs, `<${element.name}>`);
433
+ }
434
+
435
+ if (Array.isArray(element.attrs.x) && isMathSource(element.attrs.y)) {
436
+ return generatePointsFromX(element);
437
+ }
438
+
439
+ if (isMathSource(element.attrs.x) && isMathSource(element.attrs.y)) {
440
+ return generateParametricPointsFromDomain(element);
441
+ }
442
+
443
+ if (isMathSource(element.attrs.y)) {
444
+ return generatePointsFromDomain(element);
445
+ }
446
+
447
+ throw new GraphDslError(`<${element.name}> requires points={[[x, y], ...]} or x/y arrays`);
448
+ }
449
+
450
+ function pointAttr(element, name) {
451
+ if (!Array.isArray(element.attrs[name])) {
452
+ throw new GraphDslError(`<${element.name}> requires ${name}={[x, y]}`);
453
+ }
454
+ return normalizePoint(element.attrs[name], name);
455
+ }
456
+
457
+ function normalizePoint(point, propName) {
458
+ if (!Array.isArray(point) || point.length < 2) {
459
+ throw new GraphDslError(`Plot ${propName} values must be [x, y] pairs`);
460
+ }
461
+ const x = mathValue(point[0], `${propName} x`);
462
+ const y = mathValue(point[1], `${propName} y`);
463
+ assertFiniteMathValue(x, `${propName} x`);
464
+ assertFiniteMathValue(y, `${propName} y`);
465
+ return { x, y };
466
+ }
467
+
468
+ function generatePointsFromX(element) {
469
+ const variable = dataVariable(element, "x");
470
+ const expression = String(element.attrs.y);
471
+ const params = paramsScope(element);
472
+
473
+ return element.attrs.x.map((rawX) => {
474
+ const x = mathValue(rawX, `${element.name} x`);
475
+ return {
476
+ x,
477
+ y: evaluateMathExpression(expression, new Map([...params, [variable, x]]), `<${element.name}> y`)
478
+ };
479
+ });
480
+ }
481
+
482
+ function generatePointsFromDomain(element) {
483
+ const variable = dataVariable(element, "x");
484
+ const expression = String(element.attrs.y);
485
+ const domain = numericPair(element.attrs.domain, `<${element.name}> domain`);
486
+ const samples = Math.max(2, Math.floor(Number(element.attrs.samples ?? 100)));
487
+ if (!Number.isFinite(samples)) {
488
+ throw new GraphDslError(`<${element.name}> samples must be a finite number`);
489
+ }
490
+ const params = paramsScope(element);
491
+ const [min, max] = domain;
492
+ const step = samples === 1 ? 0 : (max - min) / (samples - 1);
493
+
494
+ return Array.from({ length: samples }, (_unused, index) => {
495
+ const x = min + step * index;
496
+ return {
497
+ x,
498
+ y: evaluateMathExpression(expression, new Map([...params, [variable, x]]), `<${element.name}> y`)
499
+ };
500
+ });
501
+ }
502
+
503
+ function generateParametricPointsFromDomain(element) {
504
+ const variable = dataVariable(element, "t");
505
+ const xExpression = String(element.attrs.x);
506
+ const yExpression = String(element.attrs.y);
507
+ const domain = numericPair(element.attrs.domain, `<${element.name}> domain`);
508
+ const samples = Math.max(2, Math.floor(Number(element.attrs.samples ?? 100)));
509
+ if (!Number.isFinite(samples)) {
510
+ throw new GraphDslError(`<${element.name}> samples must be a finite number`);
511
+ }
512
+ const params = paramsScope(element);
513
+ const [min, max] = domain;
514
+ const step = samples === 1 ? 0 : (max - min) / (samples - 1);
515
+
516
+ return Array.from({ length: samples }, (_unused, index) => {
517
+ const value = min + step * index;
518
+ const scope = new Map([...params, [variable, value]]);
519
+ return {
520
+ x: evaluateMathExpression(xExpression, scope, `<${element.name}> x`),
521
+ y: evaluateMathExpression(yExpression, scope, `<${element.name}> y`)
522
+ };
523
+ });
524
+ }
525
+
526
+ export function regeneratePlotData(source, overrides = {}) {
527
+ if (!source?.generated) {
528
+ throw new GraphDslError(`Only generated <Data> can be animated`);
529
+ }
530
+ const params = new Map(Object.entries(source.params ?? {}));
531
+ for (const [key, value] of Object.entries(overrides ?? {})) {
532
+ if (!params.has(key)) {
533
+ throw new GraphDslError(`Animation variable "${key}" is not declared in <Data id="${source.id}"> params`);
534
+ }
535
+ params.set(key, realNumberValue(value, `animation param "${key}"`));
536
+ }
537
+
538
+ if (source.kind === "parametric") {
539
+ const [min, max] = source.domain;
540
+ const step = source.samples === 1 ? 0 : (max - min) / (source.samples - 1);
541
+ return Array.from({ length: source.samples }, (_unused, index) => {
542
+ const value = min + step * index;
543
+ const scope = new Map([...params, [source.variable, value]]);
544
+ return {
545
+ x: evaluateMathExpression(source.xExpression, scope, `<Data id="${source.id}"> x`),
546
+ y: evaluateMathExpression(source.yExpression, scope, `<Data id="${source.id}"> y`)
547
+ };
548
+ });
549
+ }
550
+
551
+ if (Array.isArray(source.x)) {
552
+ return source.x.map((rawX) => {
553
+ const x = mathValue(rawX, `${source.id} x`);
554
+ return {
555
+ x,
556
+ y: evaluateMathExpression(source.expression, new Map([...params, [source.variable, x]]), `<Data id="${source.id}"> y`)
557
+ };
558
+ });
559
+ }
560
+
561
+ const [min, max] = source.domain;
562
+ const step = source.samples === 1 ? 0 : (max - min) / (source.samples - 1);
563
+ return Array.from({ length: source.samples }, (_unused, index) => {
564
+ const x = min + step * index;
565
+ return {
566
+ x,
567
+ y: evaluateMathExpression(source.expression, new Map([...params, [source.variable, x]]), `<Data id="${source.id}"> y`)
568
+ };
569
+ });
570
+ }
571
+
572
+ function dataVariable(element, fallback) {
573
+ return String(element.attrs.variable ?? element.attrs.var ?? fallback);
574
+ }
575
+
576
+ function paramsScope(element) {
577
+ const params = new Map();
578
+ const source = element.attrs.params;
579
+ if (source == null) return params;
580
+ if (!source || typeof source !== "object" || Array.isArray(source)) {
581
+ throw new GraphDslError(`<${element.name}> params must be an object`);
582
+ }
583
+ for (const [key, value] of Object.entries(source)) {
584
+ params.set(key, mathValue(value, `param "${key}"`));
585
+ }
586
+ return params;
587
+ }
588
+
589
+ function numericPair(value, propName) {
590
+ if (!Array.isArray(value) || value.length < 2) {
591
+ throw new GraphDslError(`${propName} must be [min, max]`);
592
+ }
593
+ const min = realNumberValue(value[0], `${propName} min`);
594
+ const max = realNumberValue(value[1], `${propName} max`);
595
+ if (!Number.isFinite(min) || !Number.isFinite(max)) {
596
+ throw new GraphDslError(`${propName} values must be finite numbers`);
597
+ }
598
+ return [min, max];
599
+ }
600
+
601
+ function buildStyles(styleElements) {
602
+ const styles = new Map();
603
+ for (const element of styleElements) {
604
+ const id = requiredAttr(element, "id");
605
+ if (styles.has(id)) {
606
+ throw new GraphDslError(`Duplicate style id "${id}"`);
607
+ }
608
+ styles.set(id, styleAttrs(element.attrs));
609
+ }
610
+ return styles;
611
+ }
612
+
613
+ function buildData(dataElements) {
614
+ const data = new Map();
615
+ for (const element of dataElements) {
616
+ const id = requiredAttr(element, "id");
617
+ if (data.has(id)) {
618
+ throw new GraphDslError(`Duplicate data id "${id}"`);
619
+ }
620
+ data.set(id, buildDataRecord(element, id));
621
+ }
622
+ return data;
623
+ }
624
+
625
+ function buildDataRecord(element, id) {
626
+ if (Array.isArray(element.attrs.x) && isMathSource(element.attrs.y)) {
627
+ const params = Object.fromEntries(paramsScope(element));
628
+ return {
629
+ id,
630
+ generated: true,
631
+ kind: "function-x",
632
+ variable: dataVariable(element, "x"),
633
+ expression: String(element.attrs.y),
634
+ params,
635
+ x: element.attrs.x.slice(),
636
+ points: generatePointsFromX(element)
637
+ };
638
+ }
639
+
640
+ if (isMathSource(element.attrs.x) && isMathSource(element.attrs.y)) {
641
+ const domain = numericPair(element.attrs.domain, `<${element.name}> domain`);
642
+ const samples = Math.max(2, Math.floor(Number(element.attrs.samples ?? 100)));
643
+ if (!Number.isFinite(samples)) {
644
+ throw new GraphDslError(`<${element.name}> samples must be a finite number`);
645
+ }
646
+ const params = Object.fromEntries(paramsScope(element));
647
+ return {
648
+ id,
649
+ generated: true,
650
+ kind: "parametric",
651
+ variable: dataVariable(element, "t"),
652
+ xExpression: String(element.attrs.x),
653
+ yExpression: String(element.attrs.y),
654
+ params,
655
+ domain,
656
+ samples,
657
+ points: generateParametricPointsFromDomain(element)
658
+ };
659
+ }
660
+
661
+ if (isMathSource(element.attrs.y) && !Array.isArray(element.attrs.y)) {
662
+ const domain = numericPair(element.attrs.domain, `<${element.name}> domain`);
663
+ const samples = Math.max(2, Math.floor(Number(element.attrs.samples ?? 100)));
664
+ if (!Number.isFinite(samples)) {
665
+ throw new GraphDslError(`<${element.name}> samples must be a finite number`);
666
+ }
667
+ const params = Object.fromEntries(paramsScope(element));
668
+ return {
669
+ id,
670
+ generated: true,
671
+ kind: "function-domain",
672
+ variable: dataVariable(element, "x"),
673
+ expression: String(element.attrs.y),
674
+ params,
675
+ domain,
676
+ samples,
677
+ points: generatePointsFromDomain(element)
678
+ };
679
+ }
680
+
681
+ return {
682
+ id,
683
+ generated: false,
684
+ points: resolvePoints(element)
685
+ };
686
+ }
687
+
688
+ function validateSeriesAnimation(element, dataId, data) {
689
+ const animate = element.attrs.animate;
690
+ if (animate == null) return;
691
+ if (!animate || typeof animate !== "object" || Array.isArray(animate)) {
692
+ throw new GraphDslError(`<${element.name}> animate must be an object`);
693
+ }
694
+ if (!dataId) {
695
+ throw new GraphDslError(`<${element.name}> animate requires data="..."`);
696
+ }
697
+ const record = data.get(dataId);
698
+ if (!record?.generated) {
699
+ throw new GraphDslError(`<${element.name}> animate requires generated <Data>`);
700
+ }
701
+ for (const key of Object.keys(animationVariableRanges(animate))) {
702
+ if (!Object.hasOwn(record.params ?? {}, key)) {
703
+ throw new GraphDslError(`Animation variable "${key}" is not declared in <Data id="${dataId}"> params`);
704
+ }
705
+ }
706
+ }
707
+
708
+ function animationVariableRanges(animate) {
709
+ return Object.fromEntries(Object.entries(animate).filter(([key, value]) => (
710
+ !ANIMATION_SETTING_KEYS.has(key) && Array.isArray(value)
711
+ )));
712
+ }
713
+
714
+ const ANIMATION_SETTING_KEYS = new Set(["duration", "loop"]);
715
+
716
+ function dataIdAttr(element) {
717
+ return element.attrs.data == null ? null : String(element.attrs.data);
718
+ }
719
+
720
+ function clonePoints(points) {
721
+ return points.map((point) => ({ ...point }));
722
+ }
723
+
724
+ function resolveStyledAttrs(attrs, styles) {
725
+ const resolved = { ...attrs };
726
+ const direct = directStyleAttrs(attrs);
727
+ if (attrs.useStyle) {
728
+ const named = styles.get(attrs.useStyle);
729
+ if (!named) {
730
+ throw new GraphDslError(`Unknown style "${attrs.useStyle}"`);
731
+ }
732
+ resolved.style = { ...named, ...direct, ...(attrs.style ?? {}) };
733
+ } else if (Object.keys(direct).length > 0 || attrs.style) {
734
+ resolved.style = { ...direct, ...(attrs.style ?? {}) };
735
+ }
736
+ return resolved;
737
+ }
738
+
739
+ function normalizeSeriesAttrs(attrs, elementName) {
740
+ if (attrs.animate == null) return attrs;
741
+ return {
742
+ ...attrs,
743
+ animate: normalizeAnimateAttrs(attrs.animate, elementName)
744
+ };
745
+ }
746
+
747
+ function normalizeAnimateAttrs(animate, elementName) {
748
+ if (!animate || typeof animate !== "object" || Array.isArray(animate)) {
749
+ throw new GraphDslError(`<${elementName}> animate must be an object`);
750
+ }
751
+ const normalized = { ...animate };
752
+ if (normalized.duration != null) {
753
+ normalized.duration = numberValue(normalized.duration, `<${elementName}> animate duration`);
754
+ }
755
+ for (const [key, value] of Object.entries(normalized)) {
756
+ if (ANIMATION_SETTING_KEYS.has(key)) continue;
757
+ if (!Array.isArray(value)) continue;
758
+ normalized[key] = numericPair(value, `<${elementName}> animate ${key}`);
759
+ }
760
+ return normalized;
761
+ }
762
+
763
+ function directStyleAttrs(attrs) {
764
+ return Object.fromEntries(Object.entries(attrs).filter(([key]) => STYLE_ATTRS.has(key)));
765
+ }
766
+
767
+ function styleAttrs(attrs) {
768
+ const { id, ...style } = attrs;
769
+ return style;
770
+ }
771
+
772
+ function assertKnownPlotChildren(plotElement) {
773
+ for (const child of plotElement.children.filter((node) => node.type === "element")) {
774
+ if (
775
+ STYLE_TAGS.has(child.name)
776
+ || DATA_TAGS.has(child.name)
777
+ || AXIS_TAGS.has(child.name)
778
+ || CURVE_TAGS.has(child.name)
779
+ || LINE_TAGS.has(child.name)
780
+ || POINT_TAGS.has(child.name)
781
+ || TEXT_TAGS.has(child.name)
782
+ || LEGEND_TAGS.has(child.name)
783
+ || ANNOTATION_NODE_TAGS.has(child.name)
784
+ || ANNOTATION_LINK_TAGS.has(child.name)
785
+ || ANNOTATION_PATH_TAGS.has(child.name)
786
+ ) {
787
+ continue;
788
+ }
789
+ throw new GraphDslError(`Unknown tag <${child.name}> in <Plot>`);
790
+ }
791
+ }
792
+
793
+ function assertKnownAnnotationChildren(nodeElement) {
794
+ for (const child of nodeElement.children.filter((node) => node.type === "element")) {
795
+ if (PORT_TAGS.has(child.name)) continue;
796
+ throw new GraphDslError(`Unknown tag <${child.name}> in <${nodeElement.name}>`);
797
+ }
798
+ }
799
+
800
+ function assertKnownAxisChildren(axisElement) {
801
+ for (const child of axisElement.children.filter((node) => node.type === "element")) {
802
+ if (TICK_TAGS.has(child.name)) {
803
+ continue;
804
+ }
805
+ throw new GraphDslError(`Unknown tag <${child.name}> in <${axisElement.name}>`);
806
+ }
807
+ }
808
+
809
+ function assertElement(element, name) {
810
+ if (!element || element.type !== "element" || element.name !== name) {
811
+ throw new GraphDslError(`Expected <${name}>`);
812
+ }
813
+ }
814
+
815
+ function isDigit(char) {
816
+ return char != null && /[0-9]/.test(char);
817
+ }
818
+
819
+ function isIdentifierStart(char) {
820
+ return char != null && /[A-Za-z_]/.test(char);
821
+ }
822
+
823
+ function isIdentifierPart(char) {
824
+ return char != null && /[A-Za-z0-9_]/.test(char);
825
+ }
826
+
827
+ function requiredAttr(element, name) {
828
+ if (element.attrs[name] == null || element.attrs[name] === "") {
829
+ throw new GraphDslError(`<${element.name}> requires ${name}`);
830
+ }
831
+ return element.attrs[name];
832
+ }
833
+
834
+ function assertUniqueAnnotationIds(nodes) {
835
+ const seen = new Set();
836
+ for (const node of nodes) {
837
+ if (seen.has(node.id)) {
838
+ throw new GraphDslError(`Duplicate annotation id "${node.id}"`);
839
+ }
840
+ seen.add(node.id);
841
+ }
842
+ }