@teachinglab/omd 0.6.6 → 0.7.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/canvas/features/resizeHandleManager.js +4 -0
- package/canvas/tools/SelectTool.js +8 -2
- package/canvas/ui/toolbar.js +40 -23
- package/index.js +2 -1
- package/jsvg/jsvgComponents.js +2 -1
- package/npm-docs/json-schemas.md +12 -27
- package/omd/display/omdDisplay.js +4 -13
- package/omd/nodes/omdFunctionNode.js +5 -1
- package/package.json +1 -1
- package/src/index.js +17 -3
- package/src/json-schemas.md +154 -27
- package/src/omd.js +8 -0
- package/src/omdDoubleNumberLine.js +72 -0
- package/src/omdDoubleTapeDiagram.js +115 -0
- package/src/omdEquation.js +64 -1
- package/src/omdFactory.js +4 -0
- package/src/omdNumberLine.js +173 -54
- package/src/omdTapeDiagram.js +161 -87
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { omdColor } from "./omdColor.js";
|
|
2
|
+
import { jsvgGroup } from "@teachinglab/jsvg";
|
|
3
|
+
import { omdNumberLine } from "./omdNumberLine.js";
|
|
4
|
+
|
|
5
|
+
export class omdDoubleNumberLine extends jsvgGroup
|
|
6
|
+
{
|
|
7
|
+
constructor()
|
|
8
|
+
{
|
|
9
|
+
// initialization
|
|
10
|
+
super();
|
|
11
|
+
|
|
12
|
+
this.type = "omdDoubleNumberLine";
|
|
13
|
+
|
|
14
|
+
this.topNumberLine = new omdNumberLine();
|
|
15
|
+
this.bottomNumberLine = new omdNumberLine();
|
|
16
|
+
|
|
17
|
+
this.spacing = 30;
|
|
18
|
+
|
|
19
|
+
this.updateLayout();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
loadFromJSON( data )
|
|
23
|
+
{
|
|
24
|
+
// Load spacing first, before updating layout
|
|
25
|
+
if ( typeof data.spacing !== "undefined" ) {
|
|
26
|
+
this.spacing = data.spacing;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if ( typeof data.topNumberLine !== "undefined" ) {
|
|
30
|
+
this.topNumberLine.loadFromJSON(data.topNumberLine);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if ( typeof data.bottomNumberLine !== "undefined" ) {
|
|
34
|
+
this.bottomNumberLine.loadFromJSON(data.bottomNumberLine);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Don't call updateLayout here - let it be called separately
|
|
38
|
+
// This prevents double-calling and ensures spacing is used
|
|
39
|
+
this.updateLayout();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
updateLayout()
|
|
43
|
+
{
|
|
44
|
+
this.removeAllChildren();
|
|
45
|
+
|
|
46
|
+
// Calculate the maximum left padding needed to align the start of the lines
|
|
47
|
+
const topLeftPadding = this.topNumberLine.title ? 80 : 20;
|
|
48
|
+
const bottomLeftPadding = this.bottomNumberLine.title ? 80 : 20;
|
|
49
|
+
const maxLeftPadding = Math.max(topLeftPadding, bottomLeftPadding);
|
|
50
|
+
|
|
51
|
+
// Position top number line
|
|
52
|
+
const topXOffset = maxLeftPadding - topLeftPadding;
|
|
53
|
+
this.topNumberLine.setPosition(topXOffset, 0);
|
|
54
|
+
this.addChild(this.topNumberLine);
|
|
55
|
+
|
|
56
|
+
// Position bottom number line - spacing controls the gap
|
|
57
|
+
// If spacing = 0, they overlap. If spacing = 10, there's a 10px gap between them
|
|
58
|
+
const bottomXOffset = maxLeftPadding - bottomLeftPadding;
|
|
59
|
+
this.bottomNumberLine.setPosition(bottomXOffset, this.spacing);
|
|
60
|
+
this.addChild(this.bottomNumberLine);
|
|
61
|
+
|
|
62
|
+
// Set overall dimensions
|
|
63
|
+
const maxWidth = Math.max(
|
|
64
|
+
this.topNumberLine.width + topXOffset,
|
|
65
|
+
this.bottomNumberLine.width + bottomXOffset
|
|
66
|
+
);
|
|
67
|
+
this.width = maxWidth;
|
|
68
|
+
this.height = 70 + this.spacing; // Top line (70px) + spacing
|
|
69
|
+
this.svgObject.setAttribute('viewBox', `0 0 ${this.width} ${this.height}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
|
|
2
|
+
import { jsvgGroup } from "@teachinglab/jsvg";
|
|
3
|
+
import { omdTapeDiagram } from "./omdTapeDiagram.js";
|
|
4
|
+
|
|
5
|
+
export class omdDoubleTapeDiagram extends jsvgGroup
|
|
6
|
+
{
|
|
7
|
+
constructor()
|
|
8
|
+
{
|
|
9
|
+
// initialization
|
|
10
|
+
super();
|
|
11
|
+
|
|
12
|
+
this.type = "omdDoubleTapeDiagram";
|
|
13
|
+
|
|
14
|
+
this.topTapeDiagram = new omdTapeDiagram();
|
|
15
|
+
this.bottomTapeDiagram = new omdTapeDiagram();
|
|
16
|
+
|
|
17
|
+
this.spacing = 30;
|
|
18
|
+
|
|
19
|
+
this.updateLayout();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
loadFromJSON( data )
|
|
23
|
+
{
|
|
24
|
+
// Load spacing first, before updating layout
|
|
25
|
+
if ( typeof data.spacing !== "undefined" ) {
|
|
26
|
+
this.spacing = data.spacing;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if ( typeof data.topTapeDiagram !== "undefined" ) {
|
|
30
|
+
this.topTapeDiagram.loadFromJSON(data.topTapeDiagram);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if ( typeof data.bottomTapeDiagram !== "undefined" ) {
|
|
34
|
+
this.bottomTapeDiagram.loadFromJSON(data.bottomTapeDiagram);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.updateLayout();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
updateLayout()
|
|
41
|
+
{
|
|
42
|
+
this.removeAllChildren();
|
|
43
|
+
|
|
44
|
+
// Calculate total numeric values for both tapes to determine unit width
|
|
45
|
+
const topTotal = this.calculateTotalValue(this.topTapeDiagram);
|
|
46
|
+
const bottomTotal = this.calculateTotalValue(this.bottomTapeDiagram);
|
|
47
|
+
|
|
48
|
+
// Find the maximum total to determine a consistent unit width
|
|
49
|
+
const maxTotal = Math.max(topTotal, bottomTotal);
|
|
50
|
+
const baseWidth = 300; // Base width for the longest tape
|
|
51
|
+
const unitWidth = maxTotal > 0 ? baseWidth / maxTotal : baseWidth;
|
|
52
|
+
|
|
53
|
+
// Set each tape's width based on its total value
|
|
54
|
+
this.topTapeDiagram.totalWidth = topTotal * unitWidth;
|
|
55
|
+
this.bottomTapeDiagram.totalWidth = bottomTotal * unitWidth;
|
|
56
|
+
|
|
57
|
+
// Force update of both tape diagrams with new widths
|
|
58
|
+
this.topTapeDiagram.updateLayout();
|
|
59
|
+
this.bottomTapeDiagram.updateLayout();
|
|
60
|
+
|
|
61
|
+
// Calculate the maximum left padding needed to align the start of the tapes
|
|
62
|
+
const topLeftPadding = this.topTapeDiagram.title ? 80 : 20;
|
|
63
|
+
const bottomLeftPadding = this.bottomTapeDiagram.title ? 80 : 20;
|
|
64
|
+
const maxLeftPadding = Math.max(topLeftPadding, bottomLeftPadding);
|
|
65
|
+
|
|
66
|
+
// Position top tape diagram
|
|
67
|
+
const topXOffset = maxLeftPadding - topLeftPadding;
|
|
68
|
+
this.topTapeDiagram.setPosition(topXOffset, 0);
|
|
69
|
+
this.addChild(this.topTapeDiagram);
|
|
70
|
+
|
|
71
|
+
// Position bottom tape diagram
|
|
72
|
+
// spacing controls the gap between the bottom of top tape and top of bottom tape
|
|
73
|
+
const bottomXOffset = maxLeftPadding - bottomLeftPadding;
|
|
74
|
+
const bottomYPosition = this.topTapeDiagram.height + this.spacing;
|
|
75
|
+
this.bottomTapeDiagram.setPosition(bottomXOffset, bottomYPosition);
|
|
76
|
+
this.addChild(this.bottomTapeDiagram);
|
|
77
|
+
|
|
78
|
+
// Set overall dimensions
|
|
79
|
+
const maxWidth = Math.max(
|
|
80
|
+
this.topTapeDiagram.width + topXOffset,
|
|
81
|
+
this.bottomTapeDiagram.width + bottomXOffset
|
|
82
|
+
);
|
|
83
|
+
this.width = maxWidth;
|
|
84
|
+
this.height = this.topTapeDiagram.height + this.spacing + this.bottomTapeDiagram.height;
|
|
85
|
+
this.svgObject.setAttribute('viewBox', `0 0 ${this.width} ${this.height}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
calculateTotalValue(tapeDiagram)
|
|
89
|
+
{
|
|
90
|
+
let total = 0;
|
|
91
|
+
|
|
92
|
+
for (const valueData of tapeDiagram.values) {
|
|
93
|
+
let value = "";
|
|
94
|
+
|
|
95
|
+
// Handle both old format (simple values) and new format (objects)
|
|
96
|
+
if (typeof valueData === "object" && valueData !== null) {
|
|
97
|
+
value = valueData.value || "";
|
|
98
|
+
} else {
|
|
99
|
+
value = valueData.toString();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Parse numeric value from string (e.g., "3", "2x", "5y")
|
|
103
|
+
const match = value.match(/^([0-9.]+)?([a-zA-Z]*)$/);
|
|
104
|
+
if (match) {
|
|
105
|
+
const coefficient = match[1] ? parseFloat(match[1]) : (match[2] ? 1 : 1);
|
|
106
|
+
total += coefficient;
|
|
107
|
+
} else {
|
|
108
|
+
total += 1; // Default for unparseable values
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return total;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
}
|
package/src/omdEquation.js
CHANGED
|
@@ -9,6 +9,7 @@ import { omdVariable } from "./omdVariable.js";
|
|
|
9
9
|
import { omdString } from "./omdString.js";
|
|
10
10
|
import { omdNumber } from "./omdNumber.js";
|
|
11
11
|
import { parseEquationString } from "./omdUtils.js";
|
|
12
|
+
import { omdEquationNode } from "../omd/nodes/omdEquationNode.js";
|
|
12
13
|
|
|
13
14
|
export class omdEquation extends omdMetaExpression
|
|
14
15
|
{
|
|
@@ -37,12 +38,24 @@ export class omdEquation extends omdMetaExpression
|
|
|
37
38
|
|
|
38
39
|
this.rightHolder = new jsvgGroup();
|
|
39
40
|
this.equationStack.addChild( this.rightHolder );
|
|
41
|
+
|
|
42
|
+
this.equationNode = null;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
// make an equation (x + 2) = (2x - 3)
|
|
43
46
|
|
|
44
47
|
loadFromJSON( data )
|
|
45
48
|
{
|
|
49
|
+
// Prefer math.js parsing into omdEquationNode for richer rendering (functions, rationals, roots)
|
|
50
|
+
if (typeof data.equation === 'string' && data.equation.trim()) {
|
|
51
|
+
try {
|
|
52
|
+
this._renderWithEquationNode(data.equation, data.fontSize);
|
|
53
|
+
return;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.warn('⚠️ omdEquation math.js render failed, falling back to legacy parsing:', e?.message || e);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
// Helper function to fix operator symbols in termsAndOpers arrays
|
|
47
60
|
function fixOperatorSymbols(expressionData) {
|
|
48
61
|
if (expressionData && expressionData.termsAndOpers && Array.isArray(expressionData.termsAndOpers)) {
|
|
@@ -136,6 +149,37 @@ export class omdEquation extends omdMetaExpression
|
|
|
136
149
|
this.updateLayout();
|
|
137
150
|
}
|
|
138
151
|
|
|
152
|
+
_renderWithEquationNode(equationString, fontSize) {
|
|
153
|
+
if (typeof math === 'undefined' || typeof math.parse !== 'function') {
|
|
154
|
+
throw new Error('math.js is required to parse equation strings');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const eqNode = omdEquationNode.fromString(equationString);
|
|
158
|
+
if (typeof fontSize === 'number' && eqNode.setFontSize) {
|
|
159
|
+
eqNode.setFontSize(fontSize);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (eqNode.hideBackgroundByDefault) eqNode.hideBackgroundByDefault();
|
|
163
|
+
if (eqNode.computeDimensions) eqNode.computeDimensions();
|
|
164
|
+
if (eqNode.updateLayout) eqNode.updateLayout();
|
|
165
|
+
|
|
166
|
+
// Clear the legacy stack and render just the parsed node
|
|
167
|
+
if (typeof this.equationStack.removeAllChildren === 'function') {
|
|
168
|
+
this.equationStack.removeAllChildren();
|
|
169
|
+
} else {
|
|
170
|
+
this.equationStack.childList = [];
|
|
171
|
+
}
|
|
172
|
+
this.equationStack.addChild(eqNode);
|
|
173
|
+
this.equationStack.setSpacer(0);
|
|
174
|
+
|
|
175
|
+
this.equationNode = eqNode;
|
|
176
|
+
this.leftExpression = null;
|
|
177
|
+
this.rightExpression = null;
|
|
178
|
+
|
|
179
|
+
this.centerEquation = false;
|
|
180
|
+
this.updateLayout();
|
|
181
|
+
}
|
|
182
|
+
|
|
139
183
|
|
|
140
184
|
setLeftAndRightExpressions( leftExp, rightExp )
|
|
141
185
|
{
|
|
@@ -156,6 +200,25 @@ export class omdEquation extends omdMetaExpression
|
|
|
156
200
|
|
|
157
201
|
updateLayout()
|
|
158
202
|
{
|
|
203
|
+
if (this.equationNode) {
|
|
204
|
+
const node = this.equationNode;
|
|
205
|
+
if (node.computeDimensions) node.computeDimensions();
|
|
206
|
+
if (node.updateLayout) node.updateLayout();
|
|
207
|
+
|
|
208
|
+
this.equationStack.doHorizontalLayout();
|
|
209
|
+
this.equationStack.setPosition(this.inset, this.inset);
|
|
210
|
+
|
|
211
|
+
const W = node.width || this.equationStack.width;
|
|
212
|
+
const H = node.height || this.equationStack.height;
|
|
213
|
+
|
|
214
|
+
this.backRect.setWidthAndHeight( W + this.inset*2, H + this.inset*2 );
|
|
215
|
+
this.setWidthAndHeight( this.backRect.width, this.backRect.height );
|
|
216
|
+
this.width = this.backRect.width;
|
|
217
|
+
this.height = this.backRect.height;
|
|
218
|
+
this.svgObject.setAttribute('viewBox', `0 0 ${this.width} ${this.height}`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
159
222
|
this.leftHolder.setWidthAndHeight( this.leftExpression.width, this.leftExpression.height );
|
|
160
223
|
this.rightHolder.setWidthAndHeight( this.rightExpression.width, this.rightExpression.height );
|
|
161
224
|
|
|
@@ -186,4 +249,4 @@ export class omdEquation extends omdMetaExpression
|
|
|
186
249
|
}
|
|
187
250
|
}
|
|
188
251
|
|
|
189
|
-
}
|
|
252
|
+
}
|
package/src/omdFactory.js
CHANGED
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
import { omdBalanceHanger } from './omdBalanceHanger.js';
|
|
19
19
|
import { omdTable } from './omdTable.js';
|
|
20
20
|
import { omdTapeDiagram } from './omdTapeDiagram.js';
|
|
21
|
+
import { omdDoubleTapeDiagram } from './omdDoubleTapeDiagram.js';
|
|
21
22
|
import { omdCoordinatePlane } from './omdCoordinatePlane.js';
|
|
22
23
|
import { omdNumberLine } from './omdNumberLine.js';
|
|
24
|
+
import { omdDoubleNumberLine } from './omdDoubleNumberLine.js';
|
|
23
25
|
import { omdNumberTile } from './omdNumberTile.js';
|
|
24
26
|
import { omdRatioChart } from './omdRatioChart.js';
|
|
25
27
|
import { omdTileEquation } from './omdTileEquation.js';
|
|
@@ -48,8 +50,10 @@ const OMD_TYPE_MAP = {
|
|
|
48
50
|
'balanceHanger': omdBalanceHanger,
|
|
49
51
|
'table': omdTable,
|
|
50
52
|
'tapeDiagram': omdTapeDiagram,
|
|
53
|
+
'doubleTapeDiagram': omdDoubleTapeDiagram,
|
|
51
54
|
'coordinatePlane': omdCoordinatePlane,
|
|
52
55
|
'numberLine': omdNumberLine,
|
|
56
|
+
'doubleNumberLine': omdDoubleNumberLine,
|
|
53
57
|
'numberTile': omdNumberTile,
|
|
54
58
|
'ratioChart': omdRatioChart,
|
|
55
59
|
'tileEquation': omdTileEquation,
|
package/src/omdNumberLine.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import { omdColor } from "./omdColor.js";
|
|
3
|
-
import { jsvgGroup, jsvgRect, jsvgLine, jsvgTextBox, jsvgEllipse } from "@teachinglab/jsvg";
|
|
3
|
+
import { jsvgGroup, jsvgRect, jsvgLine, jsvgTextBox, jsvgEllipse, jsvgPath } from "@teachinglab/jsvg";
|
|
4
4
|
|
|
5
5
|
export class omdNumberLine extends jsvgGroup
|
|
6
6
|
{
|
|
@@ -11,8 +11,16 @@ export class omdNumberLine extends jsvgGroup
|
|
|
11
11
|
|
|
12
12
|
this.type = "omdNumberLine";
|
|
13
13
|
|
|
14
|
+
this.title = "";
|
|
14
15
|
this.min = 0;
|
|
15
16
|
this.max = 10;
|
|
17
|
+
this.increment = 1;
|
|
18
|
+
this.showLeftArrow = false;
|
|
19
|
+
this.showRightArrow = false;
|
|
20
|
+
this.units = "";
|
|
21
|
+
this.hideDefaultNumbers = false;
|
|
22
|
+
this.specialNumbers = [];
|
|
23
|
+
this.totalWidth = 320;
|
|
16
24
|
this.dotValues = [];
|
|
17
25
|
this.label = "";
|
|
18
26
|
this.updateLayout();
|
|
@@ -20,16 +28,40 @@ export class omdNumberLine extends jsvgGroup
|
|
|
20
28
|
|
|
21
29
|
loadFromJSON( data )
|
|
22
30
|
{
|
|
23
|
-
if ( typeof data.
|
|
31
|
+
if ( typeof data.title !== "undefined" )
|
|
32
|
+
this.title = data.title;
|
|
33
|
+
|
|
34
|
+
if ( typeof data.min !== "undefined" )
|
|
24
35
|
this.min = data.min;
|
|
25
36
|
|
|
26
|
-
if ( typeof data.max
|
|
37
|
+
if ( typeof data.max !== "undefined" )
|
|
27
38
|
this.max = data.max;
|
|
28
39
|
|
|
29
|
-
if ( typeof data.
|
|
40
|
+
if ( typeof data.increment !== "undefined" )
|
|
41
|
+
this.increment = data.increment;
|
|
42
|
+
|
|
43
|
+
if ( typeof data.showLeftArrow !== "undefined" )
|
|
44
|
+
this.showLeftArrow = data.showLeftArrow;
|
|
45
|
+
|
|
46
|
+
if ( typeof data.showRightArrow !== "undefined" )
|
|
47
|
+
this.showRightArrow = data.showRightArrow;
|
|
48
|
+
|
|
49
|
+
if ( typeof data.units !== "undefined" )
|
|
50
|
+
this.units = data.units;
|
|
51
|
+
|
|
52
|
+
if ( typeof data.hideDefaultNumbers !== "undefined" )
|
|
53
|
+
this.hideDefaultNumbers = data.hideDefaultNumbers;
|
|
54
|
+
|
|
55
|
+
if ( typeof data.specialNumbers !== "undefined" )
|
|
56
|
+
this.specialNumbers = data.specialNumbers;
|
|
57
|
+
|
|
58
|
+
if ( typeof data.totalWidth !== "undefined" )
|
|
59
|
+
this.totalWidth = data.totalWidth;
|
|
60
|
+
|
|
61
|
+
if ( typeof data.dotValues !== "undefined" )
|
|
30
62
|
this.dotValues = data.dotValues;
|
|
31
63
|
|
|
32
|
-
if ( typeof data.label
|
|
64
|
+
if ( typeof data.label !== "undefined" )
|
|
33
65
|
this.label = data.label;
|
|
34
66
|
|
|
35
67
|
this.updateLayout();
|
|
@@ -52,63 +84,150 @@ export class omdNumberLine extends jsvgGroup
|
|
|
52
84
|
{
|
|
53
85
|
this.removeAllChildren();
|
|
54
86
|
|
|
87
|
+
const leftPadding = this.title ? 80 : 20;
|
|
88
|
+
const rightPadding = 20;
|
|
89
|
+
const arrowSize = 10;
|
|
90
|
+
const tickOverhang = 8; // How much the line extends past the end ticks
|
|
91
|
+
const lineWidth = this.totalWidth;
|
|
92
|
+
// Calculate usable width (space for ticks) - subtract space for arrows only
|
|
93
|
+
const usableLineWidth = lineWidth - (this.showLeftArrow ? arrowSize : 0) - (this.showRightArrow ? arrowSize : 0);
|
|
94
|
+
|
|
55
95
|
// Set proper dimensions and viewBox for positioning
|
|
56
|
-
this.width =
|
|
96
|
+
this.width = leftPadding + lineWidth + rightPadding;
|
|
57
97
|
this.height = 70;
|
|
58
98
|
this.svgObject.setAttribute('viewBox', `0 0 ${this.width} ${this.height}`);
|
|
59
99
|
|
|
60
|
-
//
|
|
100
|
+
// Add title if present
|
|
101
|
+
if (this.title) {
|
|
102
|
+
const titleText = new jsvgTextBox();
|
|
103
|
+
titleText.setWidthAndHeight(70, 30);
|
|
104
|
+
titleText.setFontFamily("Albert Sans");
|
|
105
|
+
titleText.setFontColor("black");
|
|
106
|
+
titleText.setFontSize(12);
|
|
107
|
+
titleText.setAlignment("left");
|
|
108
|
+
titleText.setText(this.title);
|
|
109
|
+
titleText.setPosition(5, 20);
|
|
110
|
+
this.addChild(titleText);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Calculate line position and width
|
|
114
|
+
// Line starts at: leftPadding + (arrow space if present), and extends past ticks by tickOverhang on each end
|
|
115
|
+
const lineStartX = leftPadding + (this.showLeftArrow ? arrowSize : 0);
|
|
116
|
+
const lineActualWidth = usableLineWidth; // This is the space between arrows (or edges), ticks go here with overhang
|
|
117
|
+
|
|
118
|
+
// Draw main line (extends past the first and last ticks by tickOverhang)
|
|
61
119
|
this.line = new jsvgRect();
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.line.
|
|
66
|
-
this.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
120
|
+
this.line.setWidthAndHeight(lineActualWidth, 5);
|
|
121
|
+
this.line.setPosition(lineStartX, 22.5);
|
|
122
|
+
this.line.setFillColor(omdColor.mediumGray);
|
|
123
|
+
this.line.setCornerRadius(2.5);
|
|
124
|
+
this.addChild(this.line);
|
|
125
|
+
|
|
126
|
+
// Draw left arrow if needed
|
|
127
|
+
if (this.showLeftArrow) {
|
|
128
|
+
// Cover the rounded corner with a rectangle
|
|
129
|
+
const coverRect = new jsvgRect();
|
|
130
|
+
coverRect.setWidthAndHeight(3, 5);
|
|
131
|
+
coverRect.setPosition(lineStartX, 22.5);
|
|
132
|
+
coverRect.setFillColor(omdColor.mediumGray);
|
|
133
|
+
this.addChild(coverRect);
|
|
134
|
+
|
|
135
|
+
const leftArrow = new jsvgPath();
|
|
136
|
+
const arrowY = 25;
|
|
137
|
+
const arrowX = leftPadding; // Arrow tip position
|
|
138
|
+
leftArrow.addPoint(arrowX + arrowSize, arrowY - 5);
|
|
139
|
+
leftArrow.addPoint(arrowX, arrowY);
|
|
140
|
+
leftArrow.addPoint(arrowX + arrowSize, arrowY + 5);
|
|
141
|
+
leftArrow.addPoint(arrowX + arrowSize, arrowY - 5); // Close the path
|
|
142
|
+
leftArrow.updatePath();
|
|
143
|
+
leftArrow.setFillColor(omdColor.mediumGray);
|
|
144
|
+
leftArrow.setStrokeWidth(0);
|
|
145
|
+
leftArrow.path.setAttribute("fill", omdColor.mediumGray);
|
|
146
|
+
this.addChild(leftArrow);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Draw right arrow if needed
|
|
150
|
+
if (this.showRightArrow) {
|
|
151
|
+
// Cover the rounded corner with a rectangle
|
|
152
|
+
const coverRect = new jsvgRect();
|
|
153
|
+
coverRect.setWidthAndHeight(3, 5);
|
|
154
|
+
coverRect.setPosition(lineStartX + lineActualWidth - 3, 22.5);
|
|
155
|
+
coverRect.setFillColor(omdColor.mediumGray);
|
|
156
|
+
this.addChild(coverRect);
|
|
157
|
+
|
|
158
|
+
const rightArrow = new jsvgPath();
|
|
159
|
+
const arrowY = 25;
|
|
160
|
+
const arrowX = leftPadding + lineWidth - arrowSize; // Arrow tip position
|
|
161
|
+
rightArrow.addPoint(arrowX, arrowY - 5);
|
|
162
|
+
rightArrow.addPoint(arrowX + arrowSize, arrowY);
|
|
163
|
+
rightArrow.addPoint(arrowX, arrowY + 5);
|
|
164
|
+
rightArrow.addPoint(arrowX, arrowY - 5); // Close the path
|
|
165
|
+
rightArrow.updatePath();
|
|
166
|
+
rightArrow.setFillColor(omdColor.mediumGray);
|
|
167
|
+
rightArrow.setStrokeWidth(0);
|
|
168
|
+
rightArrow.path.setAttribute("fill", omdColor.mediumGray);
|
|
169
|
+
this.addChild(rightArrow);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Collect all numbers that should be displayed
|
|
173
|
+
const numbersToShow = new Set();
|
|
174
|
+
|
|
175
|
+
// Add increment-based numbers if not hidden
|
|
176
|
+
if (!this.hideDefaultNumbers) {
|
|
177
|
+
for (let i = this.min; i <= this.max; i += this.increment) {
|
|
178
|
+
numbersToShow.add(i);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Add special numbers
|
|
183
|
+
for (const num of this.specialNumbers) {
|
|
184
|
+
if (num >= this.min && num <= this.max) {
|
|
185
|
+
numbersToShow.add(num);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Draw ticks and labels for all numbers
|
|
190
|
+
const sortedNumbers = Array.from(numbersToShow).sort((a, b) => a - b);
|
|
191
|
+
// Ticks are positioned with tickOverhang on both ends
|
|
192
|
+
const tickBaseX = lineStartX + tickOverhang;
|
|
193
|
+
const tickSpan = usableLineWidth - 2 * tickOverhang; // Space for ticks between the overhangs
|
|
194
|
+
for (const value of sortedNumbers) {
|
|
195
|
+
const normalized = (value - this.min) / (this.max - this.min);
|
|
196
|
+
const pX = tickBaseX + normalized * tickSpan;
|
|
197
|
+
|
|
198
|
+
// Draw tick
|
|
199
|
+
const tick = new jsvgLine();
|
|
200
|
+
tick.setStrokeColor("black");
|
|
201
|
+
tick.setStrokeWidth(1);
|
|
202
|
+
tick.setEndpoints(pX, 20, pX, 30);
|
|
203
|
+
this.addChild(tick);
|
|
204
|
+
|
|
205
|
+
// Draw label
|
|
206
|
+
const tickText = new jsvgTextBox();
|
|
207
|
+
tickText.setWidthAndHeight(40, 30);
|
|
208
|
+
tickText.setFontFamily("Albert Sans");
|
|
209
|
+
tickText.setFontColor("black");
|
|
210
|
+
tickText.setFontSize(10);
|
|
91
211
|
tickText.setAlignment("center");
|
|
92
|
-
|
|
93
|
-
tickText.
|
|
94
|
-
|
|
212
|
+
const labelText = this.units ? `${value}${this.units}` : value.toString();
|
|
213
|
+
tickText.setText(labelText);
|
|
214
|
+
tickText.setPosition(pX - 20, 32);
|
|
215
|
+
this.addChild(tickText);
|
|
95
216
|
}
|
|
96
217
|
|
|
97
|
-
//
|
|
98
|
-
for(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
dot.
|
|
108
|
-
dot.
|
|
109
|
-
|
|
110
|
-
dot.setPosition( pX, 25 );
|
|
111
|
-
this.addChild( dot );
|
|
218
|
+
// Draw dots
|
|
219
|
+
for (const V of this.dotValues) {
|
|
220
|
+
if (V < this.min || V > this.max) continue;
|
|
221
|
+
|
|
222
|
+
const normalized = (V - this.min) / (this.max - this.min);
|
|
223
|
+
const pX = tickBaseX + normalized * tickSpan;
|
|
224
|
+
|
|
225
|
+
const dot = new jsvgEllipse();
|
|
226
|
+
dot.setFillColor("black");
|
|
227
|
+
dot.setStrokeWidth(0);
|
|
228
|
+
dot.setWidthAndHeight(8, 8);
|
|
229
|
+
dot.setPosition(pX, 25);
|
|
230
|
+
this.addChild(dot);
|
|
112
231
|
}
|
|
113
232
|
}
|
|
114
233
|
|