@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.
@@ -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
  }