@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.
@@ -0,0 +1,459 @@
1
+ import { GraphDslError } from "./errors.js";
2
+ import { EXPRESSION_LITERAL, REF_LITERAL, isExpressionLiteral, isRefLiteral } from "./literals.js";
3
+
4
+ export function applyPointMaps(points, attrs = {}, label = "series") {
5
+ const xMap = attrs.xMap ?? attrs.xmap;
6
+ const yMap = attrs.yMap ?? attrs.ymap;
7
+ if (!isMathSource(xMap) && !isMathSource(yMap)) return points;
8
+
9
+ return points.map((point) => {
10
+ const scope = new Map([
11
+ ["x", point.x],
12
+ ["y", point.y]
13
+ ]);
14
+ return {
15
+ x: isMathSource(xMap)
16
+ ? evaluateMathExpression(String(xMap), scope, `${label} xMap`)
17
+ : point.x,
18
+ y: isMathSource(yMap)
19
+ ? evaluateMathExpression(String(yMap), scope, `${label} yMap`)
20
+ : point.y
21
+ };
22
+ });
23
+ }
24
+
25
+ export function realNumberValue(value, label) {
26
+ const result = mathValue(value, label);
27
+ return finiteRealNumber(result, label);
28
+ }
29
+
30
+ export function mathValue(value, label) {
31
+ if (isExpressionLiteral(value)) {
32
+ return evaluateMathExpression(value[EXPRESSION_LITERAL], new Map(), label);
33
+ }
34
+ if (isRefLiteral(value) && MATH_CONSTANTS.has(value[REF_LITERAL])) {
35
+ return MATH_CONSTANTS.get(value[REF_LITERAL]);
36
+ }
37
+ if (typeof value === "string" && value.trim() !== "" && !isNumericString(value)) {
38
+ return evaluateMathExpression(value, new Map(), label);
39
+ }
40
+ const number = Number(value);
41
+ if (!Number.isFinite(number)) {
42
+ throw new GraphDslError(`${label} must be a finite number`);
43
+ }
44
+ return number;
45
+ }
46
+
47
+ export function assertFiniteMathValue(value, label) {
48
+ if (!isFiniteMathValue(value)) {
49
+ throw new GraphDslError(`${label} must be a finite number`);
50
+ }
51
+ }
52
+
53
+ export function isMathSource(value) {
54
+ return typeof value === "string" && value.trim() !== "";
55
+ }
56
+
57
+ export function evaluateMathExpression(source, scope, label) {
58
+ const parser = new PlotMathParser(source, scope, label);
59
+ return parser.parse();
60
+ }
61
+
62
+ function isNumericString(value) {
63
+ return /^[-+]?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?$/i.test(value.trim());
64
+ }
65
+
66
+ const MATH_CONSTANTS = new Map([
67
+ ["pi", Math.PI],
68
+ ["PI", Math.PI],
69
+ ["e", Math.E],
70
+ ["E", Math.E]
71
+ ]);
72
+
73
+ const MATH_FUNCTIONS = new Map([
74
+ ["abs", complexAbs],
75
+ ["acos", realUnary(Math.acos, "acos")],
76
+ ["asin", realUnary(Math.asin, "asin")],
77
+ ["atan", realUnary(Math.atan, "atan")],
78
+ ["atan2", Math.atan2],
79
+ ["ceil", realUnary(Math.ceil, "ceil")],
80
+ ["conj", complexConj],
81
+ ["cos", complexCos],
82
+ ["cosh", complexCosh],
83
+ ["exp", complexExp],
84
+ ["floor", realUnary(Math.floor, "floor")],
85
+ ["im", complexImag],
86
+ ["imag", complexImag],
87
+ ["log", complexLog],
88
+ ["log10", realUnary(Math.log10, "log10")],
89
+ ["max", realVariadic(Math.max, "max")],
90
+ ["min", realVariadic(Math.min, "min")],
91
+ ["phase", complexArg],
92
+ ["pow", complexPow],
93
+ ["re", complexReal],
94
+ ["real", complexReal],
95
+ ["round", realUnary(Math.round, "round")],
96
+ ["sin", complexSin],
97
+ ["sinh", complexSinh],
98
+ ["sqrt", complexSqrt],
99
+ ["tan", complexTan],
100
+ ["tanh", complexTanh],
101
+ ["arg", complexArg],
102
+ ["angle", complexArg]
103
+ ]);
104
+
105
+ const COMPLEX_EPSILON = 1e-12;
106
+
107
+ function complex(re, im = 0) {
108
+ return normalizeComplex({ re: Number(re), im: Number(im) });
109
+ }
110
+
111
+ function isComplex(value) {
112
+ return value && typeof value === "object" && Object.hasOwn(value, "re") && Object.hasOwn(value, "im");
113
+ }
114
+
115
+ function complexParts(value) {
116
+ if (isComplex(value)) return value;
117
+ return { re: Number(value), im: 0 };
118
+ }
119
+
120
+ function normalizeComplex(value) {
121
+ if (!Number.isFinite(value.re) || !Number.isFinite(value.im)) return value;
122
+ if (Math.abs(value.im) < COMPLEX_EPSILON) return value.re;
123
+ if (Math.abs(value.re) < COMPLEX_EPSILON) return { re: 0, im: value.im };
124
+ return value;
125
+ }
126
+
127
+ function isFiniteMathValue(value) {
128
+ if (isComplex(value)) return Number.isFinite(value.re) && Number.isFinite(value.im);
129
+ return Number.isFinite(value);
130
+ }
131
+
132
+ function finiteRealNumber(value, label) {
133
+ assertFiniteMathValue(value, label);
134
+ if (isComplex(value)) {
135
+ if (Math.abs(value.im) > COMPLEX_EPSILON) {
136
+ throw new GraphDslError(`${label} must be real`);
137
+ }
138
+ return value.re;
139
+ }
140
+ return value;
141
+ }
142
+
143
+ function complexReal(value) {
144
+ return complexParts(value).re;
145
+ }
146
+
147
+ function complexImag(value) {
148
+ return complexParts(value).im;
149
+ }
150
+
151
+ function complexAbs(value) {
152
+ const z = complexParts(value);
153
+ return Math.hypot(z.re, z.im);
154
+ }
155
+
156
+ function complexArg(value) {
157
+ const z = complexParts(value);
158
+ return Math.atan2(z.im, z.re);
159
+ }
160
+
161
+ function complexConj(value) {
162
+ const z = complexParts(value);
163
+ return normalizeComplex({ re: z.re, im: -z.im });
164
+ }
165
+
166
+ function complexNeg(value) {
167
+ const z = complexParts(value);
168
+ return normalizeComplex({ re: -z.re, im: -z.im });
169
+ }
170
+
171
+ function complexAdd(a, b) {
172
+ const left = complexParts(a);
173
+ const right = complexParts(b);
174
+ return normalizeComplex({ re: left.re + right.re, im: left.im + right.im });
175
+ }
176
+
177
+ function complexSub(a, b) {
178
+ const left = complexParts(a);
179
+ const right = complexParts(b);
180
+ return normalizeComplex({ re: left.re - right.re, im: left.im - right.im });
181
+ }
182
+
183
+ function complexMul(a, b) {
184
+ const left = complexParts(a);
185
+ const right = complexParts(b);
186
+ return normalizeComplex({
187
+ re: left.re * right.re - left.im * right.im,
188
+ im: left.re * right.im + left.im * right.re
189
+ });
190
+ }
191
+
192
+ function complexDiv(a, b) {
193
+ const left = complexParts(a);
194
+ const right = complexParts(b);
195
+ const denominator = right.re * right.re + right.im * right.im;
196
+ return normalizeComplex({
197
+ re: (left.re * right.re + left.im * right.im) / denominator,
198
+ im: (left.im * right.re - left.re * right.im) / denominator
199
+ });
200
+ }
201
+
202
+ function complexSqrt(value) {
203
+ const z = complexParts(value);
204
+ if (z.im === 0 && z.re >= 0) return Math.sqrt(z.re);
205
+ const radius = Math.hypot(z.re, z.im);
206
+ return normalizeComplex({
207
+ re: Math.sqrt(Math.max(0, (radius + z.re) / 2)),
208
+ im: (z.im < 0 ? -1 : 1) * Math.sqrt(Math.max(0, (radius - z.re) / 2))
209
+ });
210
+ }
211
+
212
+ function complexExp(value) {
213
+ const z = complexParts(value);
214
+ const scale = Math.exp(z.re);
215
+ return normalizeComplex({
216
+ re: scale * Math.cos(z.im),
217
+ im: scale * Math.sin(z.im)
218
+ });
219
+ }
220
+
221
+ function complexLog(value) {
222
+ const z = complexParts(value);
223
+ return normalizeComplex({
224
+ re: Math.log(Math.hypot(z.re, z.im)),
225
+ im: Math.atan2(z.im, z.re)
226
+ });
227
+ }
228
+
229
+ function complexPow(a, b) {
230
+ const exponent = complexParts(b);
231
+ if (!isComplex(a) && exponent.im === 0 && Number.isInteger(exponent.re)) {
232
+ return a ** exponent.re;
233
+ }
234
+ return complexExp(complexMul(b, complexLog(a)));
235
+ }
236
+
237
+ function complexSin(value) {
238
+ const z = complexParts(value);
239
+ return normalizeComplex({
240
+ re: Math.sin(z.re) * Math.cosh(z.im),
241
+ im: Math.cos(z.re) * Math.sinh(z.im)
242
+ });
243
+ }
244
+
245
+ function complexCos(value) {
246
+ const z = complexParts(value);
247
+ return normalizeComplex({
248
+ re: Math.cos(z.re) * Math.cosh(z.im),
249
+ im: -Math.sin(z.re) * Math.sinh(z.im)
250
+ });
251
+ }
252
+
253
+ function complexTan(value) {
254
+ return complexDiv(complexSin(value), complexCos(value));
255
+ }
256
+
257
+ function complexSinh(value) {
258
+ const z = complexParts(value);
259
+ return normalizeComplex({
260
+ re: Math.sinh(z.re) * Math.cos(z.im),
261
+ im: Math.cosh(z.re) * Math.sin(z.im)
262
+ });
263
+ }
264
+
265
+ function complexCosh(value) {
266
+ const z = complexParts(value);
267
+ return normalizeComplex({
268
+ re: Math.cosh(z.re) * Math.cos(z.im),
269
+ im: Math.sinh(z.re) * Math.sin(z.im)
270
+ });
271
+ }
272
+
273
+ function complexTanh(value) {
274
+ const z = complexParts(value);
275
+ const denominator = Math.cosh(2 * z.re) + Math.cos(2 * z.im);
276
+ return normalizeComplex({
277
+ re: Math.sinh(2 * z.re) / denominator,
278
+ im: Math.sin(2 * z.im) / denominator
279
+ });
280
+ }
281
+
282
+ function realUnary(fn, name) {
283
+ return (value) => fn(finiteRealNumber(value, `argument to ${name}`));
284
+ }
285
+
286
+ function realVariadic(fn, name) {
287
+ return (...values) => fn(...values.map((value) => finiteRealNumber(value, `argument to ${name}`)));
288
+ }
289
+
290
+ class PlotMathParser {
291
+ constructor(source, scope, label) {
292
+ this.source = String(source);
293
+ this.scope = scope;
294
+ this.label = label;
295
+ this.index = 0;
296
+ }
297
+
298
+ parse() {
299
+ const value = this.parseExpression();
300
+ this.skipWhitespace();
301
+ if (!this.isDone()) {
302
+ throw new GraphDslError(`Unsupported expression "${this.source}" in ${this.label}`);
303
+ }
304
+ if (!isFiniteMathValue(value)) {
305
+ throw new GraphDslError(`Expression "${this.source}" in ${this.label} did not evaluate to a finite number`);
306
+ }
307
+ return value;
308
+ }
309
+
310
+ parseExpression() {
311
+ let value = this.parseTerm();
312
+ while (true) {
313
+ this.skipWhitespace();
314
+ if (this.consume("+")) {
315
+ value = complexAdd(value, this.parseTerm());
316
+ } else if (this.consume("-")) {
317
+ value = complexSub(value, this.parseTerm());
318
+ } else {
319
+ return value;
320
+ }
321
+ }
322
+ }
323
+
324
+ parseTerm() {
325
+ let value = this.parsePower();
326
+ while (true) {
327
+ this.skipWhitespace();
328
+ if (this.consume("*")) {
329
+ value = complexMul(value, this.parsePower());
330
+ } else if (this.consume("/")) {
331
+ value = complexDiv(value, this.parsePower());
332
+ } else {
333
+ return value;
334
+ }
335
+ }
336
+ }
337
+
338
+ parsePower() {
339
+ let value = this.parseFactor();
340
+ this.skipWhitespace();
341
+ if (this.consume("^")) {
342
+ value = complexPow(value, this.parsePower());
343
+ }
344
+ return value;
345
+ }
346
+
347
+ parseFactor() {
348
+ this.skipWhitespace();
349
+ if (this.consume("+")) return this.parseFactor();
350
+ if (this.consume("-")) return complexNeg(this.parseFactor());
351
+ if (this.consume("(")) {
352
+ const value = this.parseExpression();
353
+ this.skipWhitespace();
354
+ if (!this.consume(")")) {
355
+ throw new GraphDslError(`Unclosed expression "${this.source}" in ${this.label}`);
356
+ }
357
+ return value;
358
+ }
359
+ if (isDigit(this.peek()) || this.peek() === ".") {
360
+ return this.parseNumber();
361
+ }
362
+ if (isIdentifierStart(this.peek())) {
363
+ return this.parseIdentifierOrCall();
364
+ }
365
+ throw new GraphDslError(`Unsupported expression "${this.source}" in ${this.label}`);
366
+ }
367
+
368
+ parseNumber() {
369
+ const start = this.index;
370
+ while (isDigit(this.peek())) this.index += 1;
371
+ if (this.peek() === ".") {
372
+ this.index += 1;
373
+ while (isDigit(this.peek())) this.index += 1;
374
+ }
375
+ if (this.peek() === "e" || this.peek() === "E") {
376
+ this.index += 1;
377
+ if (this.peek() === "+" || this.peek() === "-") this.index += 1;
378
+ while (isDigit(this.peek())) this.index += 1;
379
+ }
380
+ const raw = this.source.slice(start, this.index);
381
+ const value = Number(raw);
382
+ if (!Number.isFinite(value)) {
383
+ throw new GraphDslError(`Invalid number "${raw}" in ${this.label}`);
384
+ }
385
+ if (this.peek() === "j") {
386
+ this.index += 1;
387
+ return complex(0, value);
388
+ }
389
+ return value;
390
+ }
391
+
392
+ parseIdentifierOrCall() {
393
+ const name = this.parseIdentifier();
394
+ this.skipWhitespace();
395
+ if (this.consume("(")) {
396
+ const fn = MATH_FUNCTIONS.get(name);
397
+ if (!fn) {
398
+ throw new GraphDslError(`Unknown math function "${name}" in ${this.label}`);
399
+ }
400
+ const args = this.parseArguments();
401
+ return fn(...args);
402
+ }
403
+ if (this.scope.has(name)) return this.scope.get(name);
404
+ if (MATH_CONSTANTS.has(name)) return MATH_CONSTANTS.get(name);
405
+ throw new GraphDslError(`Unknown variable "${name}" in ${this.label}`);
406
+ }
407
+
408
+ parseIdentifier() {
409
+ const start = this.index;
410
+ this.index += 1;
411
+ while (isIdentifierPart(this.peek())) this.index += 1;
412
+ return this.source.slice(start, this.index);
413
+ }
414
+
415
+ parseArguments() {
416
+ const args = [];
417
+ this.skipWhitespace();
418
+ if (this.consume(")")) return args;
419
+ while (!this.isDone()) {
420
+ args.push(this.parseExpression());
421
+ this.skipWhitespace();
422
+ if (this.consume(")")) return args;
423
+ if (!this.consume(",")) {
424
+ throw new GraphDslError(`Expected "," in function call "${this.source}"`);
425
+ }
426
+ }
427
+ throw new GraphDslError(`Unclosed function call "${this.source}"`);
428
+ }
429
+
430
+ skipWhitespace() {
431
+ while (/\s/.test(this.peek() ?? "")) this.index += 1;
432
+ }
433
+
434
+ consume(value) {
435
+ if (!this.source.startsWith(value, this.index)) return false;
436
+ this.index += value.length;
437
+ return true;
438
+ }
439
+
440
+ peek() {
441
+ return this.source[this.index];
442
+ }
443
+
444
+ isDone() {
445
+ return this.index >= this.source.length;
446
+ }
447
+ }
448
+
449
+ function isDigit(char) {
450
+ return char != null && /[0-9]/.test(char);
451
+ }
452
+
453
+ function isIdentifierStart(char) {
454
+ return char != null && /[A-Za-z_]/.test(char);
455
+ }
456
+
457
+ function isIdentifierPart(char) {
458
+ return char != null && /[A-Za-z0-9_]/.test(char);
459
+ }