@teachinglab/omd 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.
Files changed (144) hide show
  1. package/README.md +138 -0
  2. package/canvas/core/canvasConfig.js +203 -0
  3. package/canvas/core/omdCanvas.js +475 -0
  4. package/canvas/drawing/segment.js +168 -0
  5. package/canvas/drawing/stroke.js +386 -0
  6. package/canvas/events/eventManager.js +435 -0
  7. package/canvas/events/pointerEventHandler.js +263 -0
  8. package/canvas/features/focusFrameManager.js +287 -0
  9. package/canvas/index.js +49 -0
  10. package/canvas/tools/eraserTool.js +322 -0
  11. package/canvas/tools/pencilTool.js +319 -0
  12. package/canvas/tools/selectTool.js +457 -0
  13. package/canvas/tools/tool.js +223 -0
  14. package/canvas/tools/toolManager.js +394 -0
  15. package/canvas/ui/cursor.js +438 -0
  16. package/canvas/ui/toolbar.js +304 -0
  17. package/canvas/utils/boundingBox.js +378 -0
  18. package/canvas/utils/mathUtils.js +259 -0
  19. package/docs/api/configuration-options.md +104 -0
  20. package/docs/api/eventManager.md +68 -0
  21. package/docs/api/focusFrameManager.md +150 -0
  22. package/docs/api/index.md +91 -0
  23. package/docs/api/main.md +58 -0
  24. package/docs/api/omdBinaryExpressionNode.md +227 -0
  25. package/docs/api/omdCanvas.md +142 -0
  26. package/docs/api/omdConfigManager.md +192 -0
  27. package/docs/api/omdConstantNode.md +117 -0
  28. package/docs/api/omdDisplay.md +121 -0
  29. package/docs/api/omdEquationNode.md +161 -0
  30. package/docs/api/omdEquationSequenceNode.md +301 -0
  31. package/docs/api/omdEquationStack.md +139 -0
  32. package/docs/api/omdFunctionNode.md +141 -0
  33. package/docs/api/omdGroupNode.md +182 -0
  34. package/docs/api/omdHelpers.md +96 -0
  35. package/docs/api/omdLeafNode.md +163 -0
  36. package/docs/api/omdNode.md +101 -0
  37. package/docs/api/omdOperationDisplayNode.md +139 -0
  38. package/docs/api/omdOperatorNode.md +127 -0
  39. package/docs/api/omdParenthesisNode.md +122 -0
  40. package/docs/api/omdPopup.md +117 -0
  41. package/docs/api/omdPowerNode.md +127 -0
  42. package/docs/api/omdRationalNode.md +128 -0
  43. package/docs/api/omdSequenceNode.md +128 -0
  44. package/docs/api/omdSimplification.md +110 -0
  45. package/docs/api/omdSqrtNode.md +79 -0
  46. package/docs/api/omdStepVisualizer.md +115 -0
  47. package/docs/api/omdStepVisualizerHighlighting.md +61 -0
  48. package/docs/api/omdStepVisualizerInteractiveSteps.md +129 -0
  49. package/docs/api/omdStepVisualizerLayout.md +60 -0
  50. package/docs/api/omdStepVisualizerNodeUtils.md +140 -0
  51. package/docs/api/omdStepVisualizerTextBoxes.md +68 -0
  52. package/docs/api/omdToolbar.md +102 -0
  53. package/docs/api/omdTranscriptionService.md +76 -0
  54. package/docs/api/omdTreeDiff.md +134 -0
  55. package/docs/api/omdUnaryExpressionNode.md +174 -0
  56. package/docs/api/omdUtilities.md +70 -0
  57. package/docs/api/omdVariableNode.md +148 -0
  58. package/docs/api/selectTool.md +74 -0
  59. package/docs/api/simplificationEngine.md +98 -0
  60. package/docs/api/simplificationRules.md +77 -0
  61. package/docs/api/simplificationUtils.md +64 -0
  62. package/docs/api/transcribe.md +43 -0
  63. package/docs/api-reference.md +85 -0
  64. package/docs/index.html +454 -0
  65. package/docs/user-guide.md +9 -0
  66. package/index.js +67 -0
  67. package/omd/config/omdConfigManager.js +267 -0
  68. package/omd/core/index.js +150 -0
  69. package/omd/core/omdEquationStack.js +347 -0
  70. package/omd/core/omdUtilities.js +115 -0
  71. package/omd/display/omdDisplay.js +443 -0
  72. package/omd/display/omdToolbar.js +502 -0
  73. package/omd/nodes/omdBinaryExpressionNode.js +460 -0
  74. package/omd/nodes/omdConstantNode.js +142 -0
  75. package/omd/nodes/omdEquationNode.js +1223 -0
  76. package/omd/nodes/omdEquationSequenceNode.js +1273 -0
  77. package/omd/nodes/omdFunctionNode.js +352 -0
  78. package/omd/nodes/omdGroupNode.js +68 -0
  79. package/omd/nodes/omdLeafNode.js +77 -0
  80. package/omd/nodes/omdNode.js +557 -0
  81. package/omd/nodes/omdOperationDisplayNode.js +322 -0
  82. package/omd/nodes/omdOperatorNode.js +109 -0
  83. package/omd/nodes/omdParenthesisNode.js +293 -0
  84. package/omd/nodes/omdPowerNode.js +236 -0
  85. package/omd/nodes/omdRationalNode.js +295 -0
  86. package/omd/nodes/omdSqrtNode.js +308 -0
  87. package/omd/nodes/omdUnaryExpressionNode.js +178 -0
  88. package/omd/nodes/omdVariableNode.js +123 -0
  89. package/omd/simplification/omdSimplification.js +171 -0
  90. package/omd/simplification/omdSimplificationEngine.js +886 -0
  91. package/omd/simplification/package.json +6 -0
  92. package/omd/simplification/rules/binaryRules.js +1037 -0
  93. package/omd/simplification/rules/functionRules.js +111 -0
  94. package/omd/simplification/rules/index.js +48 -0
  95. package/omd/simplification/rules/parenthesisRules.js +19 -0
  96. package/omd/simplification/rules/powerRules.js +143 -0
  97. package/omd/simplification/rules/rationalRules.js +475 -0
  98. package/omd/simplification/rules/sqrtRules.js +48 -0
  99. package/omd/simplification/rules/unaryRules.js +37 -0
  100. package/omd/simplification/simplificationRules.js +32 -0
  101. package/omd/simplification/simplificationUtils.js +1056 -0
  102. package/omd/step-visualizer/omdStepVisualizer.js +597 -0
  103. package/omd/step-visualizer/omdStepVisualizerHighlighting.js +206 -0
  104. package/omd/step-visualizer/omdStepVisualizerLayout.js +245 -0
  105. package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +163 -0
  106. package/omd/utils/omdNodeOverlay.js +638 -0
  107. package/omd/utils/omdPopup.js +1084 -0
  108. package/omd/utils/omdStepVisualizerInteractiveSteps.js +491 -0
  109. package/omd/utils/omdStepVisualizerNodeUtils.js +268 -0
  110. package/omd/utils/omdTranscriptionService.js +125 -0
  111. package/omd/utils/omdTreeDiff.js +734 -0
  112. package/package.json +46 -0
  113. package/src/index.js +62 -0
  114. package/src/json-schemas.md +109 -0
  115. package/src/omd-json-samples.js +115 -0
  116. package/src/omd.js +109 -0
  117. package/src/omdApp.js +391 -0
  118. package/src/omdAppCanvas.js +336 -0
  119. package/src/omdBalanceHanger.js +172 -0
  120. package/src/omdColor.js +13 -0
  121. package/src/omdCoordinatePlane.js +467 -0
  122. package/src/omdEquation.js +125 -0
  123. package/src/omdExpression.js +104 -0
  124. package/src/omdFunction.js +113 -0
  125. package/src/omdMetaExpression.js +287 -0
  126. package/src/omdNaturalExpression.js +564 -0
  127. package/src/omdNode.js +384 -0
  128. package/src/omdNumber.js +53 -0
  129. package/src/omdNumberLine.js +107 -0
  130. package/src/omdNumberTile.js +119 -0
  131. package/src/omdOperator.js +73 -0
  132. package/src/omdPowerExpression.js +92 -0
  133. package/src/omdProblem.js +55 -0
  134. package/src/omdRatioChart.js +232 -0
  135. package/src/omdRationalExpression.js +115 -0
  136. package/src/omdSampleData.js +215 -0
  137. package/src/omdShapes.js +476 -0
  138. package/src/omdSpinner.js +148 -0
  139. package/src/omdString.js +39 -0
  140. package/src/omdTable.js +369 -0
  141. package/src/omdTapeDiagram.js +245 -0
  142. package/src/omdTerm.js +92 -0
  143. package/src/omdTileEquation.js +349 -0
  144. package/src/omdVariable.js +51 -0
@@ -0,0 +1,148 @@
1
+ import { omdColor } from "./omdColor.js";
2
+ import { jsvgGroup, jsvgEllipse, jsvgPath, jsvgLine, jsvgTextBox } from "@teachinglab/jsvg";
3
+
4
+ export class omdSpinner extends jsvgGroup
5
+ {
6
+ constructor()
7
+ {
8
+ // initialization
9
+ super();
10
+
11
+ this.type = "omdNumberLine";
12
+ this.size = "medium";
13
+
14
+ this.divisions = 5;
15
+ this.arrowPosition = 1;
16
+ this.updateLayout();
17
+ }
18
+
19
+ loadFromJSON( data )
20
+ {
21
+ if ( typeof data.divisions != "undefined" )
22
+ this.divisions = data.divisions;
23
+
24
+ if ( typeof data.arrowPosition != "undefined" )
25
+ this.arrowPosition = data.arrowPosition;
26
+
27
+ if ( typeof data.size != "undefined" )
28
+ this.size = data.size;
29
+
30
+ this.updateLayout();
31
+ }
32
+
33
+ setDivisions( D )
34
+ {
35
+ this.divisions = D;
36
+ this.updateLayout();
37
+ }
38
+
39
+ setRenderType( R )
40
+ {
41
+ this.renderType = R;
42
+ this.updateLayout();
43
+ }
44
+
45
+ setSize( S )
46
+ {
47
+ this.size = S;
48
+ this.updateLayout();
49
+ }
50
+
51
+ setArrowPosition( index )
52
+ {
53
+ this.arrowPosition = index;
54
+ this.updateLayout();
55
+ }
56
+
57
+ updateLayout()
58
+ {
59
+ this.removeAllChildren();
60
+
61
+ // holder group
62
+ var G = new jsvgGroup();
63
+ this.addChild( G );
64
+
65
+ var circleSize = 120;
66
+ var textSize = 14;
67
+ if ( this.size == "large" )
68
+ {
69
+ circleSize = 120;
70
+ textSize = 14;
71
+ }
72
+ if ( this.size == "medium" )
73
+ {
74
+ circleSize = 80;
75
+ textSize = 12;
76
+ }
77
+ if ( this.size == "small" )
78
+ {
79
+ circleSize = 40;
80
+ textSize = 10;
81
+ }
82
+
83
+ // make circle
84
+ var C = new jsvgEllipse();
85
+ C.setFillColor( omdColor.mediumGray );
86
+ C.setWidthAndHeight( circleSize, circleSize );
87
+ G.addChild( C );
88
+
89
+ // offset circle position
90
+ G.setPosition( circleSize/2, circleSize/2 );
91
+
92
+ // make division lines
93
+ var total = this.divisions;
94
+ var dA = Math.PI*2.0 / total;
95
+ for( var i=0; i<total; i++ )
96
+ {
97
+ var A = i * dA - Math.PI/2.0;
98
+ var pX = Math.cos(A) * circleSize/2;
99
+ var pY = Math.sin(A) * circleSize/2;
100
+ var L = new jsvgLine();
101
+ L.setStrokeColor("white");
102
+ L.setEndpoints( 0, 0, pX, pY );
103
+ G.addChild( L );
104
+
105
+ // make tick numbers
106
+ var A = (i+0.5) * dA - Math.PI/2.0;
107
+ var pX = Math.cos(A) * circleSize*0.40;
108
+ var pY = Math.sin(A) * circleSize*0.40;
109
+ var tickText = new jsvgTextBox();
110
+ tickText.setWidthAndHeight( 30,30 );
111
+ tickText.setText ( (i+1).toString() );
112
+ tickText.setFontFamily( "Albert Sans" );
113
+ tickText.setFontColor( "black" );
114
+ tickText.setFontSize( textSize );
115
+ tickText.setAlignment("center");
116
+ tickText.setPosition( pX-15, pY-7 );
117
+ G.addChild( tickText );
118
+ }
119
+
120
+
121
+
122
+ // make arrow
123
+ var D = circleSize*0.40;
124
+ this.arrow = new jsvgPath();
125
+ this.arrow.addPoint( 0,0 );
126
+ this.arrow.addPoint( D*0.8,D*0.1 );
127
+ this.arrow.addPoint( D,0 );
128
+ this.arrow.addPoint( D*0.8,D*-0.1 );
129
+ this.arrow.addPoint( 0,0 );
130
+ this.arrow.updatePath();
131
+ this.arrow.setFillColor( "black" );
132
+ this.arrow.setOpacity( 0.80 );
133
+ G.addChild( this.arrow );
134
+
135
+ // set arrow position
136
+ var A = -90 + 360.0 / this.divisions * (this.arrowPosition-0.5);
137
+ this.arrow.setRotation( A );
138
+
139
+
140
+
141
+ // var L = new jsvgLine();
142
+ // var lineLength = circleSize*0.4;
143
+ // L.setEndpoints( 0,0, lineLength,0 );
144
+ // L.setStrokeColor("black");
145
+ // L.setStrokeWidth(2);
146
+ // L.setRotation( -22.5 );
147
+ }
148
+ }
@@ -0,0 +1,39 @@
1
+
2
+ import { omdColor } from "./omdColor.js";
3
+ import { omdMetaExpression } from "./omdMetaExpression.js"
4
+
5
+ export class omdString extends omdMetaExpression
6
+ {
7
+ constructor( V = 'string' )
8
+ {
9
+ // initialization
10
+ super();
11
+
12
+ this.type = "omdVariable";
13
+
14
+ this.numText = new jsvgTextBox();
15
+ this.numText.setWidthAndHeight( 30,30 );
16
+ this.numText.setText ( this.name );
17
+ this.numText.setFontFamily( "Albert Sans" );
18
+ this.numText.setFontColor( "black" );
19
+ this.numText.setFontSize( 18 );
20
+ this.numText.setVerticalCentering();
21
+ this.numText.setAlignment("center");
22
+ // this.numText.div.style.border = "1px solid black";
23
+ this.addChild( this.numText );
24
+
25
+ this.setName( V );
26
+ }
27
+
28
+ setName( newName )
29
+ {
30
+ this.name = newName;
31
+
32
+ var W = 15 + this.name.length*10;
33
+ this.backRect.setWidthAndHeight( W, 30 );
34
+ this.numText.setWidthAndHeight( W, 30 );
35
+ this.numText.setText ( this.name );
36
+
37
+ this.setWidthAndHeight( this.backRect.width, this.backRect.height );
38
+ }
39
+ }
@@ -0,0 +1,369 @@
1
+ import { omdColor } from "./omdColor.js";
2
+ import { jsvgGroup, jsvgRect, jsvgTextBox } from "@teachinglab/jsvg";
3
+
4
+ export class omdTable extends jsvgGroup
5
+ {
6
+ constructor()
7
+ {
8
+ // initialization
9
+ super();
10
+
11
+ this.type = "omdTable";
12
+
13
+ this.equation = "";
14
+ this.data = [];
15
+ this.headers = ['x', 'y'];
16
+ this.xMin = -5;
17
+ this.xMax = 5;
18
+ this.stepSize = 1;
19
+ this.title = "";
20
+ this.fontSize = 14;
21
+ this.headerFontSize = 16;
22
+ this.fontFamily = "Albert Sans";
23
+ this.headerFontFamily = "Albert Sans";
24
+ this.cellHeight = 35;
25
+ this.headerHeight = 40;
26
+ this.minCellWidth = 80;
27
+ this.maxCellWidth = 300;
28
+ this.padding = 10;
29
+
30
+ this.updateLayout();
31
+ }
32
+
33
+ // Estimate title width in pixels based on font size and text length
34
+ estimateTitleWidth()
35
+ {
36
+ if (!this.title || this.title.length === 0) return 0;
37
+ const titleFontSize = this.headerFontSize + 2;
38
+ const padding = 40; // side padding inside the title text box
39
+ const minWidth = 200;
40
+ const estimated = Math.round(this.title.length * (titleFontSize * 0.6)) + padding;
41
+ return Math.max(minWidth, estimated);
42
+ }
43
+
44
+ loadFromJSON( data )
45
+ {
46
+ if ( typeof data.equation != "undefined" )
47
+ this.equation = data.equation;
48
+
49
+ if ( typeof data.data != "undefined" )
50
+ this.data = data.data;
51
+
52
+ if ( typeof data.headers != "undefined" )
53
+ this.headers = data.headers;
54
+
55
+ if ( typeof data.xMin != "undefined" )
56
+ this.xMin = data.xMin;
57
+
58
+ if ( typeof data.xMax != "undefined" )
59
+ this.xMax = data.xMax;
60
+
61
+ if ( typeof data.stepSize != "undefined" )
62
+ this.stepSize = data.stepSize;
63
+
64
+ if ( typeof data.title != "undefined" )
65
+ this.title = data.title;
66
+
67
+
68
+ if ( typeof data.fontSize != "undefined" )
69
+ this.fontSize = data.fontSize;
70
+
71
+ if ( typeof data.headerFontSize != "undefined" )
72
+ this.headerFontSize = data.headerFontSize;
73
+
74
+ if ( typeof data.fontFamily != "undefined" )
75
+ this.fontFamily = data.fontFamily;
76
+
77
+ if ( typeof data.headerFontFamily != "undefined" )
78
+ this.headerFontFamily = data.headerFontFamily;
79
+
80
+ if ( typeof data.cellHeight != "undefined" )
81
+ this.cellHeight = data.cellHeight;
82
+
83
+ if ( typeof data.headerHeight != "undefined" )
84
+ this.headerHeight = data.headerHeight;
85
+
86
+ if ( typeof data.minCellWidth != "undefined" )
87
+ this.minCellWidth = data.minCellWidth;
88
+
89
+ if ( typeof data.maxCellWidth != "undefined" )
90
+ this.maxCellWidth = data.maxCellWidth;
91
+
92
+ if ( typeof data.padding != "undefined" )
93
+ this.padding = data.padding;
94
+
95
+ this.updateLayout();
96
+ }
97
+
98
+ setEquation( equation )
99
+ {
100
+ this.equation = equation;
101
+ this.updateLayout();
102
+ }
103
+
104
+ setData( data, headers )
105
+ {
106
+ this.data = data;
107
+ if ( headers )
108
+ this.headers = headers;
109
+ this.updateLayout();
110
+ }
111
+
112
+ calculateOptimalCellWidth(columnIndex)
113
+ {
114
+ let maxLength = (this.headers[columnIndex] ?? '').toString().length;
115
+
116
+ // Assume rows are arrays aligned with headers
117
+ for (let row of this.data) {
118
+ const cellValue = row[columnIndex];
119
+ if (cellValue !== null && cellValue !== undefined) {
120
+ maxLength = Math.max(maxLength, cellValue.toString().length);
121
+ }
122
+ }
123
+
124
+ // Estimate width based on character count (approximate 8 pixels per character)
125
+ const estimatedWidth = Math.max(maxLength * 8 + this.padding * 2, this.minCellWidth);
126
+ return Math.min(estimatedWidth, this.maxCellWidth);
127
+ }
128
+
129
+ generateDataFromEquation()
130
+ {
131
+ if (!this.equation || this.equation.trim().length === 0) return;
132
+
133
+ // Clear existing data and set headers
134
+ this.data = [];
135
+ this.headers = ['x', 'y'];
136
+
137
+ // Basic normalization for inline math
138
+ let expression = this.equation;
139
+ if (expression.toLowerCase().startsWith('y=')) {
140
+ expression = expression.substring(2).trim();
141
+ }
142
+ expression = expression
143
+ .replace(/(\d)([a-z])/gi, '$1*$2')
144
+ .replace(/([a-z])(\d)/gi, '$1*$2')
145
+ .replace(/\^/g, '**');
146
+
147
+ const evaluateExpression = new Function('x', `return ${expression};`);
148
+
149
+ for (let x = this.xMin; x <= this.xMax; x += this.stepSize) {
150
+ let y = evaluateExpression(x);
151
+ y = Math.round(y * 100) / 100;
152
+ this.data.push([x, y]);
153
+ }
154
+ }
155
+
156
+ updateLayout()
157
+ {
158
+ this.removeAllChildren();
159
+
160
+ // If an equation is provided, generate data before measuring/layout
161
+ if (this.equation && this.equation.length > 0) {
162
+ this.generateDataFromEquation();
163
+ }
164
+
165
+ // Calculate table dimensions
166
+ const numCols = this.headers.length;
167
+ const numRows = this.data.length;
168
+ let cellWidths = [];
169
+ let totalWidth = 0;
170
+ for (let col = 0; col < numCols; col++) {
171
+ const width = this.calculateOptimalCellWidth(col);
172
+ cellWidths.push(width);
173
+ totalWidth += width;
174
+ }
175
+ this.width = totalWidth;
176
+ const titleOffset = (this.title && this.title.length > 0) ? 30 : 0;
177
+ const bodyHeight = this.headerHeight + numRows * this.cellHeight;
178
+ const totalHeight = titleOffset + bodyHeight;
179
+ this.height = totalHeight;
180
+
181
+ // Compute a display width that ensures the title is not clipped,
182
+ // without changing column widths or table background.
183
+ const titleBoxWidth = this.estimateTitleWidth();
184
+ const displayWidth = Math.max(this.width, titleBoxWidth);
185
+
186
+ // Table background with corner radius (all four corners, covers full height)
187
+ const tableBg = new jsvgRect();
188
+ tableBg.setWidthAndHeight(this.width, bodyHeight);
189
+ tableBg.setFillColor(omdColor.lightGray);
190
+ tableBg.setCornerRadius(15);
191
+ tableBg.setStrokeWidth(0);
192
+ const contentOffsetX = Math.max(0, (displayWidth - this.width) / 2);
193
+ tableBg.setPosition(contentOffsetX, titleOffset);
194
+ this.addChild(tableBg);
195
+
196
+ // Draw a rounded footer rectangle under the last row to provide bottom rounded corners
197
+ if (numRows > 0) {
198
+ const footer = new jsvgRect();
199
+ footer.setWidthAndHeight(this.width, this.cellHeight);
200
+ footer.setFillColor(omdColor.lightGray);
201
+ footer.setCornerRadius(15);
202
+ footer.setStrokeWidth(0);
203
+ const footerY = titleOffset + this.headerHeight + (numRows - 1) * this.cellHeight;
204
+ footer.setPosition(contentOffsetX, footerY);
205
+ this.addChild(footer);
206
+ }
207
+
208
+ // Generate data from equation if provided; otherwise assume valid data/headers
209
+ if (this.equation && this.equation.length > 0) {
210
+ this.generateDataFromEquation();
211
+ }
212
+
213
+ let currentY = 0;
214
+ // Add title if provided
215
+ if (this.title && this.title.length > 0) {
216
+ var titleText = new jsvgTextBox();
217
+ // Use an expanded text box width (only for title) to avoid clipping
218
+ titleText.setWidthAndHeight(titleBoxWidth, 25);
219
+ titleText.setText(this.title);
220
+ titleText.setFontFamily(this.headerFontFamily);
221
+ titleText.setFontColor("black");
222
+ titleText.setFontSize(this.headerFontSize + 2);
223
+ titleText.setAlignment("center");
224
+ titleText.setVerticalCentering();
225
+ // Center the title within the display width (table is centered within display)
226
+ const titleX = Math.max(0, (displayWidth - titleBoxWidth) / 2);
227
+ titleText.setPosition(titleX, currentY);
228
+ titleText.setFontWeight(600);
229
+ this.addChild(titleText);
230
+ currentY += 30;
231
+ }
232
+
233
+ // Create header row (lightGray, no border, no rounded corners for cells)
234
+ let currentX = 0;
235
+ for (let col = 0; col < numCols; col++) {
236
+ const cellWidth = cellWidths[col];
237
+ var headerRect = new jsvgRect();
238
+ headerRect.setWidthAndHeight(cellWidth, this.headerHeight);
239
+ headerRect.setFillColor(omdColor.lightGray);
240
+ // Use rx/ry for top corners only on first and last header cells
241
+ if (col === 0 && numCols === 1) {
242
+ headerRect.setCornerRadius(15); // single column, round all corners
243
+ } else if (col === 0) {
244
+ headerRect.setCornerRadius(15); // round top-left
245
+ } else if (col === numCols - 1) {
246
+ headerRect.setCornerRadius(15); // round top-right
247
+ } else {
248
+ headerRect.setCornerRadius(0);
249
+ }
250
+ headerRect.setStrokeWidth(0);
251
+ headerRect.setPosition(currentX + contentOffsetX, currentY);
252
+ this.addChild(headerRect);
253
+ const headerText = this.createHeaderTextBox(
254
+ cellWidth,
255
+ this.headerHeight,
256
+ this.headers[col] || `Col ${col + 1}`
257
+ );
258
+ headerText.setPosition(currentX + contentOffsetX, currentY);
259
+ this.addChild(headerText);
260
+ currentX += cellWidth;
261
+ }
262
+ currentY += this.headerHeight;
263
+
264
+ // Create data rows with alternating colors
265
+ for (let row = 0; row < numRows; row++) {
266
+ const rowData = this.data[row];
267
+ let currentX = 0;
268
+ // Alternating bar: odd rows white 50% opacity, even rows transparent
269
+ if (row % 2 === 0) {
270
+ var barRect = new jsvgRect();
271
+ barRect.setWidthAndHeight(this.width, this.cellHeight);
272
+ barRect.setFillColor("rgba(255,255,255,0.5)");
273
+ // Round the bottom corners on the last row to respect the table background rounding
274
+ if (row === numRows - 1) {
275
+ barRect.setCornerRadius(15);
276
+ } else {
277
+ barRect.setCornerRadius(0);
278
+ }
279
+ barRect.setStrokeWidth(0);
280
+ barRect.setPosition(contentOffsetX, currentY);
281
+ this.addChild(barRect);
282
+ }
283
+ for (let col = 0; col < numCols; col++) {
284
+ const cellWidth = cellWidths[col];
285
+ const cellText = this.createBodyTextBox(cellWidth, this.cellHeight, "");
286
+ const cellValue = rowData[col];
287
+ cellText.setText((cellValue ?? '').toString());
288
+ cellText.setPosition(currentX + contentOffsetX, currentY);
289
+ this.addChild(cellText);
290
+ currentX += cellWidth;
291
+ }
292
+ currentY += this.cellHeight;
293
+ }
294
+ // Draw vertical dividing lines at each column boundary (except the far right edge)
295
+ if (numCols > 1) {
296
+ let x = 0;
297
+ for (let col = 0; col < numCols - 1; col++) {
298
+ x += cellWidths[col];
299
+ const vline = new jsvgRect();
300
+ vline.setWidthAndHeight(1, Math.max(0, bodyHeight - 1));
301
+ vline.setFillColor("black");
302
+ vline.setCornerRadius(0);
303
+ vline.setOpacity(0.5);
304
+ vline.setStrokeWidth(0);
305
+ vline.setPosition(x + contentOffsetX, titleOffset);
306
+ this.addChild(vline);
307
+ }
308
+ }
309
+ // Use displayWidth for the viewBox so the title never clips,
310
+ // but keep the table background at the original table width
311
+ this.setWidthAndHeight(displayWidth, totalHeight);
312
+ this.svgObject.setAttribute("viewBox", `0 0 ${displayWidth} ${totalHeight}`);
313
+ }
314
+
315
+ // ===== Helpers for consistent styling (match other components) =====
316
+ createHeaderTextBox(width, height, text) {
317
+ const tb = new jsvgTextBox();
318
+ tb.setWidthAndHeight(width, height);
319
+ tb.setText(text);
320
+ tb.setFontFamily(this.headerFontFamily);
321
+ tb.setFontColor("black");
322
+ tb.setFontSize(this.headerFontSize);
323
+ tb.setAlignment("center");
324
+ tb.setVerticalCentering();
325
+ tb.setFontWeight(600);
326
+ return tb;
327
+ }
328
+
329
+ createBodyTextBox(width, height, text) {
330
+ const tb = new jsvgTextBox();
331
+ tb.setWidthAndHeight(width, height);
332
+ tb.setText(text);
333
+ tb.setFontFamily(this.fontFamily);
334
+ tb.setFontColor("black");
335
+ tb.setFontSize(this.fontSize);
336
+ tb.setAlignment("center");
337
+ tb.setVerticalCentering();
338
+ tb.setFontWeight(400);
339
+ return tb;
340
+ }
341
+
342
+ addRow( rowData )
343
+ {
344
+ this.data.push( rowData );
345
+ this.updateLayout();
346
+ }
347
+
348
+ setHeaders( headers )
349
+ {
350
+ this.headers = headers;
351
+ this.updateLayout();
352
+ }
353
+
354
+ setFont( fontFamily, headerFontFamily )
355
+ {
356
+ this.fontFamily = fontFamily;
357
+ if ( headerFontFamily )
358
+ this.headerFontFamily = headerFontFamily;
359
+ else
360
+ this.headerFontFamily = fontFamily;
361
+ this.updateLayout();
362
+ }
363
+
364
+ clearData()
365
+ {
366
+ this.data = [];
367
+ this.updateLayout();
368
+ }
369
+ }