@teachinglab/omd 0.7.31 → 0.7.33
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/package.json +1 -1
- package/src/json-schemas.md +13 -4
- package/src/omdCoordinatePlane.js +388 -18
package/package.json
CHANGED
package/src/json-schemas.md
CHANGED
|
@@ -785,20 +785,29 @@ This document provides schemas and examples for the `loadFromJSON` method used i
|
|
|
785
785
|
"backgroundColor": "string",
|
|
786
786
|
"backgroundCornerRadius": "number",
|
|
787
787
|
"backgroundOpacity": "number",
|
|
788
|
-
"showBackground": "boolean"
|
|
788
|
+
"showBackground": "boolean",
|
|
789
|
+
"interactive": "boolean (optional) - when true, render sliders below the graph",
|
|
790
|
+
"variables": [
|
|
791
|
+
"array of strings or objects: { name, label, value, min, max, step, color, graphIndex }"
|
|
792
|
+
]
|
|
789
793
|
}
|
|
790
794
|
```
|
|
791
795
|
|
|
792
796
|
### Example
|
|
793
797
|
```json
|
|
794
798
|
{
|
|
799
|
+
"interactive": true,
|
|
800
|
+
"variables": [
|
|
801
|
+
{ "name": "a", "label": "a", "value": 2, "min": -5, "max": 5, "step": 0.1 },
|
|
802
|
+
{ "name": "b", "label": "b", "value": 1, "min": -5, "max": 5, "step": 0.1 }
|
|
803
|
+
],
|
|
795
804
|
"graphEquations": [
|
|
796
805
|
{
|
|
797
|
-
"equation": "y = x
|
|
798
|
-
"color": "
|
|
806
|
+
"equation": "y = a*x + b",
|
|
807
|
+
"color": "#0f766e",
|
|
799
808
|
"strokeWidth": 2,
|
|
800
809
|
"domain": { "min": -5, "max": 5 },
|
|
801
|
-
"label": "
|
|
810
|
+
"label": "y = a*x + b",
|
|
802
811
|
"labelAtX": 2,
|
|
803
812
|
"labelPosition": "above"
|
|
804
813
|
}
|
|
@@ -51,6 +51,13 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
51
51
|
this.backgroundOpacity = 1.0;
|
|
52
52
|
this.showBackground = true;
|
|
53
53
|
|
|
54
|
+
this.interactive = false;
|
|
55
|
+
this.variables = [];
|
|
56
|
+
this.basePaddingBottom = 30;
|
|
57
|
+
this.interactivePaddingBottom = 0;
|
|
58
|
+
this.renderedFunctionEntries = [];
|
|
59
|
+
this.sliderControlRows = [];
|
|
60
|
+
|
|
54
61
|
this.calculatePadding();
|
|
55
62
|
this.updateLayout();
|
|
56
63
|
}
|
|
@@ -85,6 +92,10 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
85
92
|
this.backgroundCornerRadius = (data.backgroundCornerRadius !== undefined) ? data.backgroundCornerRadius : this.backgroundCornerRadius;
|
|
86
93
|
this.backgroundOpacity = (data.backgroundOpacity !== undefined) ? data.backgroundOpacity : this.backgroundOpacity;
|
|
87
94
|
this.showBackground = (data.showBackground !== undefined) ? data.showBackground : this.showBackground;
|
|
95
|
+
this.interactive = data.interactive === true || data.interactive === "true";
|
|
96
|
+
this.variables = this.normalizeInteractiveVariables(
|
|
97
|
+
data.variables || data.interactiveVariables || []
|
|
98
|
+
);
|
|
88
99
|
|
|
89
100
|
this.calculatePadding();
|
|
90
101
|
this.updateLayout();
|
|
@@ -153,6 +164,11 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
153
164
|
const shapesHolder = new jsvgGroup();
|
|
154
165
|
graphClipMask.addChild(shapesHolder);
|
|
155
166
|
this.addShapes(shapesHolder);
|
|
167
|
+
|
|
168
|
+
this.sliderControlRows = [];
|
|
169
|
+
if (this.shouldShowInteractiveControls()) {
|
|
170
|
+
this.createInteractiveControls(this.svgObject);
|
|
171
|
+
}
|
|
156
172
|
}
|
|
157
173
|
|
|
158
174
|
createAxisGrid(gridHolder, isXAxis) {
|
|
@@ -280,7 +296,11 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
280
296
|
|
|
281
297
|
calculatePadding() {
|
|
282
298
|
this.paddingLeft = this.yLabel ? 50 : 30;
|
|
283
|
-
this.
|
|
299
|
+
this.basePaddingBottom = this.xLabel ? 50 : 30;
|
|
300
|
+
this.interactivePaddingBottom = this.shouldShowInteractiveControls()
|
|
301
|
+
? this.getInteractiveControlHeight()
|
|
302
|
+
: 0;
|
|
303
|
+
this.paddingBottom = this.basePaddingBottom + this.interactivePaddingBottom;
|
|
284
304
|
this.paddingTop = 25;
|
|
285
305
|
this.paddingRight = 25;
|
|
286
306
|
}
|
|
@@ -374,17 +394,29 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
374
394
|
}
|
|
375
395
|
|
|
376
396
|
graphMultipleFunctions(holder) {
|
|
397
|
+
this.renderedFunctionEntries = [];
|
|
398
|
+
|
|
377
399
|
for (const functionConfig of this.graphEquations) {
|
|
378
400
|
try {
|
|
401
|
+
const compiledExpression = this.compileFunctionExpression(functionConfig.equation);
|
|
379
402
|
const path = new jsvgPath();
|
|
380
403
|
path.setStrokeColor(this.getAllowedColor(functionConfig.color));
|
|
381
404
|
path.setStrokeWidth(functionConfig.strokeWidth);
|
|
382
|
-
this.graphFunctionWithDomain(path, functionConfig.
|
|
405
|
+
this.graphFunctionWithDomain(path, functionConfig.domain, compiledExpression);
|
|
383
406
|
holder.addChild(path);
|
|
384
407
|
|
|
408
|
+
const entry = {
|
|
409
|
+
functionConfig,
|
|
410
|
+
compiledExpression,
|
|
411
|
+
path,
|
|
412
|
+
label: null
|
|
413
|
+
};
|
|
414
|
+
|
|
385
415
|
if (functionConfig.label) {
|
|
386
|
-
this.addFunctionLabel(holder, functionConfig);
|
|
416
|
+
entry.label = this.addFunctionLabel(holder, functionConfig, compiledExpression);
|
|
387
417
|
}
|
|
418
|
+
|
|
419
|
+
this.renderedFunctionEntries.push(entry);
|
|
388
420
|
} catch (e) {
|
|
389
421
|
console.warn(`Failed to graph equation: ${functionConfig.equation}`, e);
|
|
390
422
|
}
|
|
@@ -399,25 +431,28 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
399
431
|
return expression;
|
|
400
432
|
}
|
|
401
433
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const expression = this._extractExpression(functionString);
|
|
434
|
+
compileFunctionExpression(functionString) {
|
|
435
|
+
return math.compile(this._extractExpression(functionString));
|
|
436
|
+
}
|
|
406
437
|
|
|
407
|
-
|
|
438
|
+
graphFunctionWithDomain(pathObject, domain, compiledExpression) {
|
|
439
|
+
pathObject.clearPoints();
|
|
408
440
|
|
|
409
441
|
const leftLimit = (domain && typeof domain.min === 'number') ? domain.min : this.xMin;
|
|
410
442
|
const rightLimit = (domain && typeof domain.max === 'number') ? domain.max : this.xMax;
|
|
411
443
|
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
444
|
+
const pointCount = this.shouldShowInteractiveControls() ? 400 : 1000;
|
|
445
|
+
const span = rightLimit - leftLimit;
|
|
446
|
+
for (let i = 0; i <= pointCount; i++) {
|
|
447
|
+
const x = leftLimit + (span * i) / pointCount;
|
|
448
|
+
const y = compiledExpression.evaluate(this.getEvaluationScope({ x }));
|
|
449
|
+
if (!Number.isFinite(y)) continue;
|
|
415
450
|
pathObject.addPoint(this.toGraphPixelX(x), this.toGraphPixelY(y));
|
|
416
451
|
}
|
|
417
452
|
pathObject.updatePath();
|
|
418
453
|
}
|
|
419
454
|
|
|
420
|
-
addFunctionLabel(holder, functionConfig) {
|
|
455
|
+
addFunctionLabel(holder, functionConfig, compiledExpression) {
|
|
421
456
|
const labelText = String(functionConfig.label);
|
|
422
457
|
const fontSize = 12;
|
|
423
458
|
const padding = 6;
|
|
@@ -433,15 +468,18 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
433
468
|
label.setVerticalCentering();
|
|
434
469
|
label.setFontWeight(500);
|
|
435
470
|
|
|
471
|
+
this.updateFunctionLabelPosition(label, functionConfig, compiledExpression);
|
|
472
|
+
holder.addChild(label);
|
|
473
|
+
return label;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
updateFunctionLabelPosition(label, functionConfig, compiledExpression) {
|
|
436
477
|
const anchorX = (typeof functionConfig.labelAtX === 'number')
|
|
437
478
|
? functionConfig.labelAtX
|
|
438
479
|
: this.xMin + (this.xMax - this.xMin) * 0.1;
|
|
439
480
|
|
|
440
481
|
let anchorY = this.yMin + (this.yMax - this.yMin) * 0.1;
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
const compiled = math.compile(equationBody);
|
|
444
|
-
const yVal = compiled.evaluate({ x: anchorX });
|
|
482
|
+
const yVal = compiledExpression.evaluate(this.getEvaluationScope({ x: anchorX }));
|
|
445
483
|
anchorY = yVal;
|
|
446
484
|
|
|
447
485
|
const xPx = this.toGraphPixelX(anchorX);
|
|
@@ -469,7 +507,6 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
469
507
|
}
|
|
470
508
|
|
|
471
509
|
label.setPosition(finalX, finalY);
|
|
472
|
-
holder.addChild(label);
|
|
473
510
|
}
|
|
474
511
|
|
|
475
512
|
drawLineSegments(holder) {
|
|
@@ -530,6 +567,339 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
530
567
|
}
|
|
531
568
|
}
|
|
532
569
|
|
|
570
|
+
shouldShowInteractiveControls() {
|
|
571
|
+
return this.interactive === true && this.variables.length > 0;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
getInteractiveControlHeight() {
|
|
575
|
+
return 18 + this.variables.length * 38;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
normalizeInteractiveVariables(variables) {
|
|
579
|
+
if (!Array.isArray(variables)) return [];
|
|
580
|
+
|
|
581
|
+
return variables
|
|
582
|
+
.map((variable, index) => this.normalizeInteractiveVariable(variable, index))
|
|
583
|
+
.filter(Boolean);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
normalizeInteractiveVariable(variable, index) {
|
|
587
|
+
const source = (typeof variable === "string")
|
|
588
|
+
? { name: variable }
|
|
589
|
+
: (variable && typeof variable === "object" ? variable : null);
|
|
590
|
+
|
|
591
|
+
if (!source) return null;
|
|
592
|
+
|
|
593
|
+
const name = String(
|
|
594
|
+
source.name ??
|
|
595
|
+
source.variable ??
|
|
596
|
+
source.id ??
|
|
597
|
+
""
|
|
598
|
+
).trim();
|
|
599
|
+
|
|
600
|
+
if (!name) return null;
|
|
601
|
+
|
|
602
|
+
let min = Number.isFinite(source.min) ? source.min : -10;
|
|
603
|
+
let max = Number.isFinite(source.max) ? source.max : 10;
|
|
604
|
+
if (max < min) {
|
|
605
|
+
const tmp = min;
|
|
606
|
+
min = max;
|
|
607
|
+
max = tmp;
|
|
608
|
+
}
|
|
609
|
+
if (max === min) {
|
|
610
|
+
max = min + 1;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const step = (Number.isFinite(source.step) && source.step > 0) ? source.step : 0.1;
|
|
614
|
+
const fallbackValue = (min <= 0 && max >= 0) ? 0 : min;
|
|
615
|
+
const initialValue = this.clampInteractiveValue(
|
|
616
|
+
Number.isFinite(source.value) ? source.value : fallbackValue,
|
|
617
|
+
min,
|
|
618
|
+
max
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
name,
|
|
623
|
+
label: String(source.label ?? name),
|
|
624
|
+
min,
|
|
625
|
+
max,
|
|
626
|
+
step,
|
|
627
|
+
value: initialValue,
|
|
628
|
+
color: source.color ?? null,
|
|
629
|
+
graphIndex: Number.isInteger(source.graphIndex)
|
|
630
|
+
? source.graphIndex
|
|
631
|
+
: (Number.isInteger(source.equationIndex) ? source.equationIndex : null),
|
|
632
|
+
order: index
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
clampInteractiveValue(value, min, max) {
|
|
637
|
+
return Math.max(min, Math.min(max, value));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
snapInteractiveValue(value, variable) {
|
|
641
|
+
const step = variable.step || 0.1;
|
|
642
|
+
const snapped = variable.min + Math.round((value - variable.min) / step) * step;
|
|
643
|
+
const decimals = this.getStepPrecision(step);
|
|
644
|
+
const rounded = Number(snapped.toFixed(decimals));
|
|
645
|
+
return this.clampInteractiveValue(rounded, variable.min, variable.max);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
getStepPrecision(step) {
|
|
649
|
+
const stepText = String(step);
|
|
650
|
+
if (!stepText.includes(".")) return 0;
|
|
651
|
+
return stepText.split(".")[1].length;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
getInteractiveVariableValue(variable) {
|
|
655
|
+
if (!variable) return 0;
|
|
656
|
+
if (Number.isFinite(variable.currentValue)) return variable.currentValue;
|
|
657
|
+
if (Number.isFinite(variable.value)) return variable.value;
|
|
658
|
+
return 0;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
getEvaluationScope(extraScope = {}) {
|
|
662
|
+
const scope = { ...extraScope };
|
|
663
|
+
for (const variable of this.variables) {
|
|
664
|
+
scope[variable.name] = this.getInteractiveVariableValue(variable);
|
|
665
|
+
}
|
|
666
|
+
return scope;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
createInteractiveControls(parentSvg) {
|
|
670
|
+
const controlsGroup = this.createSvgElement("g", {
|
|
671
|
+
"data-omd-interactive-controls": "true"
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
const graphBottom = this.paddingTop + this.graphHeight;
|
|
675
|
+
const startY = graphBottom + this.basePaddingBottom + 8;
|
|
676
|
+
const labelX = this.paddingLeft;
|
|
677
|
+
const valueX = this.width - this.paddingRight;
|
|
678
|
+
const trackStart = this.paddingLeft + 36;
|
|
679
|
+
const trackEnd = this.width - this.paddingRight - 56;
|
|
680
|
+
const trackWidth = Math.max(40, trackEnd - trackStart);
|
|
681
|
+
|
|
682
|
+
this.sliderControlRows = this.variables.map((variable, index) => {
|
|
683
|
+
const color = this.resolveInteractiveVariableColor(variable);
|
|
684
|
+
const rowY = startY + index * 38;
|
|
685
|
+
const rowGroup = this.createSvgElement("g", {
|
|
686
|
+
transform: `translate(0, ${rowY})`,
|
|
687
|
+
"data-omd-variable": variable.name
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const label = this.createSvgElement("text", {
|
|
691
|
+
x: labelX,
|
|
692
|
+
y: 4,
|
|
693
|
+
fill: color,
|
|
694
|
+
"font-size": 13,
|
|
695
|
+
"font-weight": 700,
|
|
696
|
+
"dominant-baseline": "middle"
|
|
697
|
+
});
|
|
698
|
+
label.textContent = variable.label;
|
|
699
|
+
|
|
700
|
+
const track = this.createSvgElement("line", {
|
|
701
|
+
x1: trackStart,
|
|
702
|
+
y1: 0,
|
|
703
|
+
x2: trackStart + trackWidth,
|
|
704
|
+
y2: 0,
|
|
705
|
+
stroke: "#d1d5db",
|
|
706
|
+
"stroke-width": 6,
|
|
707
|
+
"stroke-linecap": "round"
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
const activeTrack = this.createSvgElement("line", {
|
|
711
|
+
x1: trackStart,
|
|
712
|
+
y1: 0,
|
|
713
|
+
x2: trackStart,
|
|
714
|
+
y2: 0,
|
|
715
|
+
stroke: color,
|
|
716
|
+
"stroke-width": 6,
|
|
717
|
+
"stroke-linecap": "round"
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
const thumb = this.createSvgElement("circle", {
|
|
721
|
+
cx: trackStart,
|
|
722
|
+
cy: 0,
|
|
723
|
+
r: 8,
|
|
724
|
+
fill: color,
|
|
725
|
+
stroke: "#ffffff",
|
|
726
|
+
"stroke-width": 2
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
const valueText = this.createSvgElement("text", {
|
|
730
|
+
x: valueX,
|
|
731
|
+
y: 4,
|
|
732
|
+
fill: color,
|
|
733
|
+
"font-size": 12,
|
|
734
|
+
"font-weight": 600,
|
|
735
|
+
"dominant-baseline": "middle",
|
|
736
|
+
"text-anchor": "end"
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const hitArea = this.createSvgElement("rect", {
|
|
740
|
+
x: trackStart - 10,
|
|
741
|
+
y: -14,
|
|
742
|
+
width: trackWidth + 20,
|
|
743
|
+
height: 28,
|
|
744
|
+
fill: "transparent",
|
|
745
|
+
"pointer-events": "all",
|
|
746
|
+
rx: 14
|
|
747
|
+
});
|
|
748
|
+
hitArea.style.cursor = "pointer";
|
|
749
|
+
|
|
750
|
+
rowGroup.appendChild(label);
|
|
751
|
+
rowGroup.appendChild(track);
|
|
752
|
+
rowGroup.appendChild(activeTrack);
|
|
753
|
+
rowGroup.appendChild(thumb);
|
|
754
|
+
rowGroup.appendChild(valueText);
|
|
755
|
+
rowGroup.appendChild(hitArea);
|
|
756
|
+
controlsGroup.appendChild(rowGroup);
|
|
757
|
+
|
|
758
|
+
const row = {
|
|
759
|
+
variable,
|
|
760
|
+
color,
|
|
761
|
+
label,
|
|
762
|
+
track,
|
|
763
|
+
activeTrack,
|
|
764
|
+
thumb,
|
|
765
|
+
valueText,
|
|
766
|
+
hitArea,
|
|
767
|
+
trackStart,
|
|
768
|
+
trackEnd: trackStart + trackWidth
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
this.attachSliderInteraction(row);
|
|
772
|
+
this.updateSliderRow(row);
|
|
773
|
+
return row;
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
parentSvg.appendChild(controlsGroup);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
attachSliderInteraction(row) {
|
|
780
|
+
const updateFromPointer = (event) => {
|
|
781
|
+
const localX = this.clientToLocalSvgX(event.clientX);
|
|
782
|
+
const ratio = (localX - row.trackStart) / (row.trackEnd - row.trackStart);
|
|
783
|
+
const unclampedValue = row.variable.min + ratio * (row.variable.max - row.variable.min);
|
|
784
|
+
const nextValue = this.snapInteractiveValue(unclampedValue, row.variable);
|
|
785
|
+
row.variable.currentValue = nextValue;
|
|
786
|
+
row.variable.value = nextValue;
|
|
787
|
+
this.refreshInteractiveElements();
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
row.hitArea.addEventListener("pointerdown", (event) => {
|
|
791
|
+
event.preventDefault();
|
|
792
|
+
event.stopPropagation();
|
|
793
|
+
|
|
794
|
+
updateFromPointer(event);
|
|
795
|
+
row.hitArea.setPointerCapture?.(event.pointerId);
|
|
796
|
+
|
|
797
|
+
const handleMove = (moveEvent) => {
|
|
798
|
+
moveEvent.preventDefault();
|
|
799
|
+
moveEvent.stopPropagation();
|
|
800
|
+
updateFromPointer(moveEvent);
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
const stop = (endEvent) => {
|
|
804
|
+
endEvent?.preventDefault?.();
|
|
805
|
+
endEvent?.stopPropagation?.();
|
|
806
|
+
row.hitArea.removeEventListener("pointermove", handleMove);
|
|
807
|
+
row.hitArea.removeEventListener("pointerup", stop);
|
|
808
|
+
row.hitArea.removeEventListener("pointercancel", stop);
|
|
809
|
+
try {
|
|
810
|
+
row.hitArea.releasePointerCapture?.(event.pointerId);
|
|
811
|
+
} catch {}
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
row.hitArea.addEventListener("pointermove", handleMove);
|
|
815
|
+
row.hitArea.addEventListener("pointerup", stop);
|
|
816
|
+
row.hitArea.addEventListener("pointercancel", stop);
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
refreshInteractiveElements() {
|
|
821
|
+
for (const entry of this.renderedFunctionEntries) {
|
|
822
|
+
try {
|
|
823
|
+
this.graphFunctionWithDomain(entry.path, entry.functionConfig.domain, entry.compiledExpression);
|
|
824
|
+
if (entry.label) {
|
|
825
|
+
this.updateFunctionLabelPosition(entry.label, entry.functionConfig, entry.compiledExpression);
|
|
826
|
+
}
|
|
827
|
+
} catch (error) {
|
|
828
|
+
console.warn(`Failed to refresh equation: ${entry.functionConfig?.equation}`, error);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
for (const row of this.sliderControlRows) {
|
|
833
|
+
this.updateSliderRow(row);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
updateSliderRow(row) {
|
|
838
|
+
const currentValue = this.getInteractiveVariableValue(row.variable);
|
|
839
|
+
const ratio = (currentValue - row.variable.min) / (row.variable.max - row.variable.min);
|
|
840
|
+
const clampedRatio = Math.max(0, Math.min(1, ratio));
|
|
841
|
+
const thumbX = row.trackStart + (row.trackEnd - row.trackStart) * clampedRatio;
|
|
842
|
+
|
|
843
|
+
row.activeTrack.setAttribute("x2", String(thumbX));
|
|
844
|
+
row.thumb.setAttribute("cx", String(thumbX));
|
|
845
|
+
row.valueText.textContent = this.formatInteractiveValue(currentValue, row.variable.step);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
formatInteractiveValue(value, step) {
|
|
849
|
+
const decimals = Math.min(4, Math.max(0, this.getStepPrecision(step)));
|
|
850
|
+
const rounded = Number(value.toFixed(decimals));
|
|
851
|
+
return `${rounded}`;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
resolveInteractiveVariableColor(variable) {
|
|
855
|
+
if (variable.color) {
|
|
856
|
+
return this.getAllowedColor(variable.color);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const graphIndex = variable.graphIndex;
|
|
860
|
+
if (Number.isInteger(graphIndex) && this.graphEquations[graphIndex]?.color) {
|
|
861
|
+
return this.getAllowedColor(this.graphEquations[graphIndex].color);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const matchedEquation = this.graphEquations.find((graphConfig) =>
|
|
865
|
+
this.equationContainsVariable(graphConfig?.equation, variable.name)
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
if (matchedEquation?.color) {
|
|
869
|
+
return this.getAllowedColor(matchedEquation.color);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return this.getAllowedColor("#2563eb");
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
equationContainsVariable(equation, variableName) {
|
|
876
|
+
if (!equation || !variableName) return false;
|
|
877
|
+
const escapedName = variableName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
878
|
+
const variableRegex = new RegExp(`(^|[^A-Za-z0-9_])${escapedName}([^A-Za-z0-9_]|$)`);
|
|
879
|
+
return variableRegex.test(String(equation));
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
createSvgElement(tagName, attributes = {}) {
|
|
883
|
+
const element = document.createElementNS("http://www.w3.org/2000/svg", tagName);
|
|
884
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
885
|
+
if (value !== undefined && value !== null) {
|
|
886
|
+
element.setAttribute(key, String(value));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return element;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
clientToLocalSvgX(clientX) {
|
|
893
|
+
const rect = this.svgObject.getBoundingClientRect();
|
|
894
|
+
const viewBox = (this.svgObject.getAttribute("viewBox") || "0 0 0 0")
|
|
895
|
+
.split(/\s+/)
|
|
896
|
+
.map(Number);
|
|
897
|
+
const [viewBoxX = 0, , viewBoxWidth = 0] = viewBox;
|
|
898
|
+
if (!rect.width || !viewBoxWidth) return 0;
|
|
899
|
+
|
|
900
|
+
return viewBoxX + ((clientX - rect.left) / rect.width) * viewBoxWidth;
|
|
901
|
+
}
|
|
902
|
+
|
|
533
903
|
// Background customization methods
|
|
534
904
|
setBackgroundColor(color) {
|
|
535
905
|
this.backgroundColor = color;
|
|
@@ -558,4 +928,4 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
558
928
|
if (options.show !== undefined) this.showBackground = options.show;
|
|
559
929
|
this.updateLayout();
|
|
560
930
|
}
|
|
561
|
-
}
|
|
931
|
+
}
|