@teachinglab/omd 0.6.5 → 0.6.7

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/index.js CHANGED
@@ -11,9 +11,6 @@
11
11
  * const { omdTable } = await import('@teachinglab/omd')
12
12
  */
13
13
 
14
- // Ensure math.js is available globally before loading modules that rely on window/global math
15
- import './omd/utils/registerMathGlobal.js';
16
-
17
14
  // Import everything first to ensure proper loading order
18
15
  import * as omdCore from './omd/core/index.js';
19
16
  import * as omdCanvas from './canvas/index.js';
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@teachinglab/omd",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "omd",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
7
7
  "type": "module",
8
8
  "exports": {
9
9
  ".": "./index.js",
10
+ "./register-math": "./omd/utils/registerMathGlobal.js",
10
11
  "./package.json": "./package.json"
11
12
  },
12
13
  "files": [
@@ -56,4 +57,3 @@
56
57
  "vite": "^5.4.0"
57
58
  }
58
59
  }
59
-
package/src/index.js CHANGED
@@ -3,11 +3,13 @@ export { omdTable } from './omdTable.js';
3
3
  export { omdBalanceHanger } from './omdBalanceHanger.js';
4
4
  export { omdCoordinatePlane } from './omdCoordinatePlane.js';
5
5
  export { omdTapeDiagram } from './omdTapeDiagram.js';
6
+ export { omdDoubleTapeDiagram } from './omdDoubleTapeDiagram.js';
6
7
  export { omdTileEquation } from './omdTileEquation.js';
7
8
 
8
9
  // OMD Charts and Diagrams
9
10
  export { omdRatioChart } from './omdRatioChart.js';
10
11
  export { omdNumberLine } from './omdNumberLine.js';
12
+ export { omdDoubleNumberLine } from './omdDoubleNumberLine.js';
11
13
  export { omdNumberTile } from './omdNumberTile.js';
12
14
 
13
15
  // OMD Mathematical Components
@@ -52,7 +54,8 @@ export default {
52
54
  omdTapeDiagram: () => import('./omdTapeDiagram.js').then(m => m.omdTapeDiagram),
53
55
  omdTileEquation: () => import('./omdTileEquation.js').then(m => m.omdTileEquation),
54
56
  omdRatioChart: () => import('./omdRatioChart.js').then(m => m.omdRatioChart),
55
- omdNumberLine: () => import('./omdNumberLine.js').then(m => m.omdNumberLine)
57
+ omdNumberLine: () => import('./omdNumberLine.js').then(m => m.omdNumberLine),
58
+ omdDoubleNumberLine: () => import('./omdDoubleNumberLine.js').then(m => m.omdDoubleNumberLine)
56
59
  },
57
60
 
58
61
  // Mathematical expressions
@@ -105,24 +105,85 @@ This document provides schemas and examples for the `loadFromJSON` method used i
105
105
  ### Schema
106
106
  ```json
107
107
  {
108
- "values": ["array"],
109
- "showValues": "boolean",
110
- "colors": ["array"],
111
- "labelSet": ["array"],
112
- "unitWidth": "number"
108
+ "title": "string (optional)",
109
+ "values": ["array (required) - can be strings or objects with {value, showLabel, color}"],
110
+ "labelSet": ["array (optional) - objects with {startIndex, endIndex, label, showBelow}"],
111
+ "totalWidth": "number (optional, default: 300)"
113
112
  }
114
113
  ```
115
114
 
116
- ### Example
115
+ ### Example with Simple Values
117
116
  ```json
118
117
  {
119
- "values": [1, 2, 3],
120
- "showValues": true,
121
- "colors": ["#FF0000", "#00FF00", "#0000FF"],
118
+ "title": "Distance",
119
+ "totalWidth": 300,
120
+ "values": ["2x", "3", "x"],
122
121
  "labelSet": [
123
- { "startIndex": 0, "endIndex": 2, "label": "Example Label", "showBelow": true }
122
+ { "startIndex": 0, "endIndex": 3, "label": "Total: 3x + 3", "showBelow": true },
123
+ { "startIndex": 0, "endIndex": 1, "label": "2x", "showBelow": false }
124
+ ]
125
+ }
126
+ ```
127
+
128
+ ### Example with Value Objects
129
+ ```json
130
+ {
131
+ "title": "Pencils",
132
+ "totalWidth": 320,
133
+ "values": [
134
+ { "value": "5", "showLabel": true, "color": "#93c5fd" },
135
+ { "value": "5", "showLabel": true, "color": "#93c5fd" },
136
+ { "value": "5", "showLabel": false, "color": "#fca5a5" }
124
137
  ],
125
- "unitWidth": 30
138
+ "labelSet": [
139
+ { "startIndex": 0, "endIndex": 3, "label": "15 total", "showBelow": true }
140
+ ]
141
+ }
142
+ ```
143
+
144
+ ---
145
+
146
+ ## 4b. `omdDoubleTapeDiagram`
147
+
148
+ `omdDoubleTapeDiagram` represents two tape diagrams aligned by their start points.
149
+
150
+ ### Schema
151
+ ```json
152
+ {
153
+ "topTapeDiagram": "object (omdTapeDiagram)",
154
+ "bottomTapeDiagram": "object (omdTapeDiagram)",
155
+ "spacing": "number (optional, default: 10)"
156
+ }
157
+ ```
158
+
159
+ ### Example
160
+ ```json
161
+ {
162
+ "topTapeDiagram": {
163
+ "title": "Pencils",
164
+ "totalWidth": 320,
165
+ "values": [
166
+ { "value": "5", "color": "#93c5fd" },
167
+ { "value": "5", "color": "#93c5fd" },
168
+ { "value": "5", "color": "#93c5fd" }
169
+ ],
170
+ "labelSet": [
171
+ { "startIndex": 0, "endIndex": 3, "label": "15 total", "showBelow": true }
172
+ ]
173
+ },
174
+ "bottomTapeDiagram": {
175
+ "title": "Cost",
176
+ "totalWidth": 320,
177
+ "values": [
178
+ { "value": "$2", "color": "#fca5a5" },
179
+ { "value": "$2", "color": "#fca5a5" },
180
+ { "value": "$2", "color": "#fca5a5" }
181
+ ],
182
+ "labelSet": [
183
+ { "startIndex": 0, "endIndex": 3, "label": "$6 total", "showBelow": true }
184
+ ]
185
+ },
186
+ "spacing": 80
126
187
  }
127
188
  ```
128
189
 
@@ -398,25 +459,85 @@ This document provides schemas and examples for the `loadFromJSON` method used i
398
459
 
399
460
  ## 15. `omdNumberLine`
400
461
 
401
- `omdNumberLine` represents a number line with labeled ticks and optional dots.
462
+ `omdNumberLine` represents a number line with labeled ticks, optional title, custom increments, units, arrows, and special numbers.
402
463
 
403
464
  ### Schema
404
465
  ```json
405
466
  {
406
- "min": "number",
407
- "max": "number",
408
- "dotValues": ["array"],
409
- "label": "string"
467
+ "title": "string (optional)",
468
+ "min": "number (required)",
469
+ "max": "number (required)",
470
+ "increment": "number (optional, default: 1)",
471
+ "showLeftArrow": "boolean (optional, default: false)",
472
+ "showRightArrow": "boolean (optional, default: false)",
473
+ "units": "string (optional)",
474
+ "hideDefaultNumbers": "boolean (optional, default: false)",
475
+ "specialNumbers": ["array (optional)"],
476
+ "totalWidth": "number (optional, default: 320)",
477
+ "dotValues": ["array (optional)"]
410
478
  }
411
479
  ```
412
480
 
413
481
  ### Example
414
482
  ```json
415
483
  {
484
+ "title": "Distance",
416
485
  "min": 0,
417
486
  "max": 10,
418
- "dotValues": [1, 5, 7],
419
- "label": "Number Line"
487
+ "increment": 1,
488
+ "units": " cm",
489
+ "dotValues": [1, 5, 7]
490
+ }
491
+ ```
492
+
493
+ ### Example with Special Numbers
494
+ ```json
495
+ {
496
+ "title": "Height",
497
+ "min": 0,
498
+ "max": 100,
499
+ "increment": 10,
500
+ "specialNumbers": [25, 75],
501
+ "units": " m",
502
+ "showRightArrow": true,
503
+ "hideDefaultNumbers": false,
504
+ "totalWidth": 400
505
+ }
506
+ ```
507
+
508
+ ---
509
+
510
+ ## 15b. `omdDoubleNumberLine`
511
+
512
+ `omdDoubleNumberLine` represents two number lines aligned by their start points, useful for showing proportional relationships.
513
+
514
+ ### Schema
515
+ ```json
516
+ {
517
+ "topNumberLine": "object (omdNumberLine)",
518
+ "bottomNumberLine": "object (omdNumberLine)",
519
+ "spacing": "number (optional, default: 10)"
520
+ }
521
+ ```
522
+
523
+ ### Example
524
+ ```json
525
+ {
526
+ "topNumberLine": {
527
+ "title": "Hours",
528
+ "min": 0,
529
+ "max": 10,
530
+ "showRightArrow": true,
531
+ "increment": 1
532
+ },
533
+ "bottomNumberLine": {
534
+ "showRightArrow": true,
535
+ "title": "Miles",
536
+ "min": 0,
537
+ "max": 50,
538
+ "increment": 10
539
+ },
540
+ "spacing": 15
420
541
  }
421
542
  ```
422
543
 
package/src/omd.js CHANGED
@@ -12,7 +12,9 @@ import { omdEquation } from "./omdEquation.js";
12
12
  import { omdEquationNode } from "../omd/nodes/omdEquationNode.js";
13
13
  import { omdFunction } from "./omdFunction.js";
14
14
  import { omdNumberLine } from "./omdNumberLine.js";
15
+ import { omdDoubleNumberLine } from "./omdDoubleNumberLine.js";
15
16
  import { omdTapeDiagram } from "./omdTapeDiagram.js";
17
+ import { omdDoubleTapeDiagram } from "./omdDoubleTapeDiagram.js";
16
18
  import { omdBalanceHanger } from "./omdBalanceHanger.js";
17
19
  import { omdNumberTile } from "./omdNumberTile.js";
18
20
  import { omdRatioChart } from "./omdRatioChart.js";
@@ -122,12 +124,18 @@ export class omd extends jsvgContainer
122
124
  case "numberLine":
123
125
  N = new omdNumberLine();
124
126
  break;
127
+ case "doubleNumberLine":
128
+ N = new omdDoubleNumberLine();
129
+ break;
125
130
  case "balanceHanger":
126
131
  N = new omdBalanceHanger();
127
132
  break;
128
133
  case "tapeDiagram":
129
134
  N = new omdTapeDiagram();
130
135
  break;
136
+ case "doubleTapeDiagram":
137
+ N = new omdDoubleTapeDiagram();
138
+ break;
131
139
  case "numberTile":
132
140
  N = new omdNumberTile();
133
141
  break;
@@ -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/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,
@@ -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.min != "undefined" )
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 != "undefined" )
37
+ if ( typeof data.max !== "undefined" )
27
38
  this.max = data.max;
28
39
 
29
- if ( typeof data.dotValues != "undefined" )
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 != "undefined" )
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 = 360;
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
- // make line
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
- // this.line.setStrokeColor( "black" );
63
- // this.line.setStrokeWidth( 1 );
64
- // this.line.setEndpoints( 0,0, 300, 0 );
65
- this.line.setWidthAndHeight(320,5);
66
- this.line.setPosition( 20, 22.5 );
67
- this.line.setFillColor( omdColor.mediumGray );
68
- this.line.setCornerRadius( 2.5 );
69
- this.addChild( this.line );
70
-
71
- // make ticks with text
72
- for( var i=this.min; i<=this.max; i++ )
73
- {
74
- var N = i - this.min;
75
- var dX = 300 / (this.max - this.min);
76
-
77
- var pX = 30 + N*dX;
78
-
79
- var tick = new jsvgLine();
80
- tick.setStrokeColor( "black" );
81
- tick.setStrokeWidth( 1 );
82
- tick.setEndpoints( pX, 20, pX, 30 );
83
- this.addChild( tick );
84
-
85
- var tickText = new jsvgTextBox();
86
- tickText.setWidthAndHeight( 30,30 );
87
- tickText.setText ( this.name );
88
- tickText.setFontFamily( "Albert Sans" );
89
- tickText.setFontColor( "black" );
90
- tickText.setFontSize( 10 );
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
- tickText.setText( i.toString() );
93
- tickText.setPosition( pX-15, 32 );
94
- this.addChild( tickText );
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
- // make dots
98
- for( var i=0; i<this.dotValues.length; i++ )
99
- {
100
- var V = this.dotValues[i];
101
-
102
- var N = V - this.min;
103
- var dX = 300 / (this.max - this.min);
104
- var pX = 30 + N*dX;
105
-
106
- var dot = new jsvgEllipse();
107
- dot.setFillColor( "black" );
108
- dot.setStrokeWidth( 0 );
109
- dot.setWidthAndHeight( 9,9 );
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
 
@@ -111,30 +111,26 @@ export class omdTapeDiagram extends jsvgGroup
111
111
 
112
112
  this.type = "omdTapeDiagram";
113
113
 
114
+ this.title = "";
114
115
  this.values = [];
115
- this.showValues = true;
116
- this.colors = [];
117
116
  this.labelSet = [];
118
- this.unitWidth = 30;
117
+ this.totalWidth = 300;
119
118
  this.updateLayout();
120
119
  }
121
120
 
122
121
  loadFromJSON( data )
123
122
  {
124
- if ( typeof data.values != "undefined" )
125
- this.values = data.values;
123
+ if ( typeof data.title !== "undefined" )
124
+ this.title = data.title;
126
125
 
127
- if ( typeof data.showValues != "undefined" )
128
- this.showValues = data.showValues;
129
-
130
- if ( typeof data.colors != "undefined" )
131
- this.colors = data.colors;
126
+ if ( typeof data.values !== "undefined" )
127
+ this.values = data.values;
132
128
 
133
- if ( typeof data.labelSet != "undefined" )
129
+ if ( typeof data.labelSet !== "undefined" )
134
130
  this.labelSet = data.labelSet;
135
131
 
136
- if ( typeof data.unitWidth != "undefined" )
137
- this.unitWidth = data.unitWidth;
132
+ if ( typeof data.totalWidth !== "undefined" )
133
+ this.totalWidth = data.totalWidth;
138
134
 
139
135
  this.updateLayout();
140
136
  }
@@ -142,104 +138,182 @@ export class omdTapeDiagram extends jsvgGroup
142
138
  setValues( newValues )
143
139
  {
144
140
  this.values = newValues;
141
+ this.updateLayout();
145
142
  }
146
143
 
147
144
  updateLayout()
148
145
  {
149
146
  this.removeAllChildren();
150
147
 
151
- // console.log( this.values );
148
+ const leftPadding = this.title ? 80 : 20;
149
+ const rightPadding = 20;
150
+ const titleWidth = 70;
151
+
152
+ // Add title if present
153
+ if (this.title) {
154
+ const titleText = new jsvgTextBox();
155
+ titleText.setWidthAndHeight(titleWidth, 30);
156
+ titleText.setFontFamily("Albert Sans");
157
+ titleText.setFontColor("black");
158
+ titleText.setFontSize(12);
159
+ titleText.setAlignment("left");
160
+ titleText.setText(this.title);
161
+ titleText.setPosition(5, 5);
162
+ this.addChild(titleText);
163
+ }
152
164
 
153
- // make box with text
154
- var pX = 0;
165
+ // Parse values and calculate proportional widths
166
+ const parsedValues = [];
167
+ let totalNumericValue = 0;
168
+
169
+ for (const valueData of this.values) {
170
+ let value = "";
171
+ let showLabel = true;
172
+ let color = omdColor.lightGray;
173
+ let numericValue = 1; // default for non-numeric
174
+
175
+ // Handle both old format (simple values) and new format (objects)
176
+ if (typeof valueData === "object" && valueData !== null) {
177
+ value = valueData.value || "";
178
+ showLabel = valueData.showLabel !== undefined ? valueData.showLabel : true;
179
+ color = valueData.color || omdColor.lightGray;
180
+ } else {
181
+ value = valueData.toString();
182
+ }
183
+
184
+ // Parse numeric value from string (e.g., "3", "2x", "5y")
185
+ // Extract coefficient from expressions like "2x", "3", "0.5y"
186
+ const match = value.match(/^([0-9.]+)?([a-zA-Z]*)$/);
187
+ if (match) {
188
+ const coefficient = match[1] ? parseFloat(match[1]) : (match[2] ? 1 : 1);
189
+ const variable = match[2] || "";
190
+ numericValue = coefficient;
191
+ }
192
+
193
+ parsedValues.push({ value, showLabel, color, numericValue });
194
+ totalNumericValue += numericValue;
195
+ }
196
+
197
+ // Calculate width for each segment based on proportion
198
+ var pX = leftPadding;
155
199
  var indexPositions = [];
156
- for( var i=0; i<this.values.length; i++ )
157
- {
200
+
201
+ for (const parsed of parsedValues) {
158
202
  indexPositions.push(pX);
159
203
 
160
- var value = this.values[i];
161
- var W = 30;
162
- if ( typeof value == "string" )
163
- {
164
- W = 20 + value.length*10;
165
- }
166
- else
167
- {
168
- W = value * this.unitWidth;
169
- }
170
-
204
+ // Calculate proportional width
205
+ const proportion = totalNumericValue > 0 ? parsed.numericValue / totalNumericValue : 1 / parsedValues.length;
206
+ const segmentWidth = this.totalWidth * proportion;
171
207
 
172
- // make box
208
+ // Make box
173
209
  var box = new jsvgRect();
174
- box.setWidthAndHeight( W, 30 );
210
+ box.setWidthAndHeight(segmentWidth, 30);
175
211
  box.setCornerRadius(5);
176
- box.setStrokeColor( "white" );
177
- box.setStrokeWidth( 1 );
178
-
179
- // Use custom color if available, otherwise default to light gray
180
- var boxColor = omdColor.lightGray;
181
- if ( this.colors && this.colors.length > i && this.colors[i] )
182
- {
183
- boxColor = this.colors[i];
212
+ box.setStrokeColor("white");
213
+ box.setStrokeWidth(1);
214
+ box.setFillColor(parsed.color);
215
+ box.setPosition(pX, 0);
216
+ this.addChild(box);
217
+
218
+ // Make box text (if showLabel is true)
219
+ if (parsed.showLabel) {
220
+ var boxText = new jsvgTextBox();
221
+ boxText.setWidthAndHeight(segmentWidth, 30);
222
+ boxText.setFontFamily("Albert Sans");
223
+ boxText.setFontColor("black");
224
+ boxText.setFontSize(18);
225
+ boxText.setAlignment("center");
226
+ boxText.setVerticalCentering();
227
+ boxText.setText(parsed.value);
228
+ boxText.setPosition(pX, 0);
229
+ this.addChild(boxText);
184
230
  }
185
- box.setFillColor( boxColor );
186
-
187
- box.setPosition( pX, 0 );
188
- this.addChild( box );
189
-
190
- // make box text
191
- var boxText = new jsvgTextBox();
192
- boxText.setWidthAndHeight( W,30 );
193
- boxText.setText ( this.name );
194
- boxText.setFontFamily( "Albert Sans" );
195
- boxText.setFontColor( "black" );
196
- boxText.setFontSize( 18 );
197
- boxText.setAlignment("center");
198
- boxText.setVerticalCentering();
199
- boxText.setText( value.toString() );
200
- boxText.setPosition( pX, 0 );
201
- this.addChild( boxText );
202
-
203
- pX += W;
231
+
232
+ pX += segmentWidth;
204
233
  }
205
234
 
206
235
  indexPositions.push(pX);
207
236
 
208
- // Calculate actual content dimensions
209
- var contentWidth = pX; // Total width of all boxes
210
- var contentHeight = 30; // Height of the tape
211
- var labelHeight = 0;
237
+ // Calculate dimensions
238
+ var contentWidth = pX - leftPadding;
239
+ var contentHeight = 30;
240
+ var topLabelSpace = 0;
241
+ var bottomLabelSpace = 0;
212
242
 
213
- // Check if labels extend the height
214
- for ( var labelData of this.labelSet )
215
- {
216
- if ( labelData.showBelow )
217
- labelHeight = Math.max(labelHeight, 70); // 40 offset + 30 text height
218
- else
219
- labelHeight = Math.max(labelHeight, 30); // 20 offset above + 10 buffer
220
- }
221
-
222
- if ( labelHeight > 0 )
223
- contentHeight += labelHeight;
243
+ // Sort labels by span length (number of segments they cover)
244
+ const sortedLabels = this.labelSet.slice().map((labelData, index) => ({
245
+ data: labelData,
246
+ span: (labelData.endIndex || 0) - (labelData.startIndex || 0)
247
+ })).sort((a, b) => a.span - b.span); // Shortest first
224
248
 
225
- // make label text
226
- for ( var labelData of this.labelSet )
249
+ // Track occupied label layers to prevent overlap
250
+ const topLayers = [];
251
+ const bottomLayers = [];
252
+
253
+ // Make label text
254
+ for ( const item of sortedLabels )
227
255
  {
228
- var T = new omdTapeLabel();
229
- T.unitWidth = this.unitWidth;
256
+ const labelData = item.data;
257
+ const T = new omdTapeLabel();
230
258
  T.setIndexPositions( indexPositions );
231
259
  T.loadFromJSON( labelData );
232
- if ( T.showBelow )
233
- T.setPosition( 0, 40 );
234
- else
235
- T.setPosition( 0, -10 );
236
- this.addChild( T )
260
+
261
+ if ( T.showBelow ) {
262
+ // Find the lowest available layer for this label
263
+ let layer = 0;
264
+ const start = labelData.startIndex || 0;
265
+ const end = labelData.endIndex || 0;
266
+
267
+ while (layer < bottomLayers.length) {
268
+ const conflicts = bottomLayers[layer].some(occupied =>
269
+ !(end <= occupied.start || start >= occupied.end)
270
+ );
271
+ if (!conflicts) break;
272
+ layer++;
273
+ }
274
+
275
+ if (layer === bottomLayers.length) {
276
+ bottomLayers.push([]);
277
+ }
278
+ bottomLayers[layer].push({ start, end });
279
+
280
+ const yPos = 40 + (layer * 35); // Stack labels with 35px spacing
281
+ T.setPosition(0, yPos);
282
+ bottomLabelSpace = Math.max(bottomLabelSpace, yPos + 30);
283
+ } else {
284
+ // Find the highest available layer for this label
285
+ let layer = 0;
286
+ const start = labelData.startIndex || 0;
287
+ const end = labelData.endIndex || 0;
288
+
289
+ while (layer < topLayers.length) {
290
+ const conflicts = topLayers[layer].some(occupied =>
291
+ !(end <= occupied.start || start >= occupied.end)
292
+ );
293
+ if (!conflicts) break;
294
+ layer++;
295
+ }
296
+
297
+ if (layer === topLayers.length) {
298
+ topLayers.push([]);
299
+ }
300
+ topLayers[layer].push({ start, end });
301
+
302
+ const yPos = -10 - (layer * 35); // Stack labels upward with 35px spacing
303
+ T.setPosition(0, yPos);
304
+ topLabelSpace = Math.max(topLabelSpace, (layer + 1) * 35);
305
+ }
306
+
307
+ this.addChild( T );
237
308
  }
238
-
239
- // Set proper bounds to hug the content
240
- this.setWidthAndHeight( contentWidth, contentHeight );
241
- // Fix the viewBox to match our actual content dimensions (no padding)
242
- this.svgObject.setAttribute("viewBox", `0 0 ${contentWidth} ${contentHeight}`);
309
+ // Set proper dimensions including space for labels above and below
310
+ this.width = leftPadding + contentWidth + rightPadding;
311
+ this.height = topLabelSpace + contentHeight + bottomLabelSpace;
312
+ // Adjust viewBox to show everything including title and labels
313
+ const viewBoxY = -topLabelSpace;
314
+ const viewBoxHeight = this.height;
315
+ this.svgObject.setAttribute("viewBox", `0 ${viewBoxY} ${this.width} ${viewBoxHeight}`);
316
+ this.svgObject.setAttribute("viewBox", `0 ${-topLabelSpace} ${this.width} ${this.height}`);
243
317
  }
244
318
 
245
319
  }