@teachinglab/omd
Version:
omd
319 lines (263 loc) • 10.8 kB
JavaScript
import { omdColor } from "./omdColor.js";
import { jsvgGroup, jsvgRect, jsvgTextBox, jsvgLine} from "@teachinglab/jsvg";
export class omdTapeLabel extends jsvgGroup
{
constructor()
{
// initialization
super();
this.startValue = 1;
this.endValue = 3;
this.startIndex = 0;
this.endIndex = 1;
this.label = "label";
this.showBelow = false;
this.unitWidth = 30;
this.indexPositions = [];
this.startPos = 0;
this.endPos = 0;
this.updateLayout();
}
setIndexPositions( xPosArray )
{
this.indexPositions = xPosArray
}
loadFromJSON( data )
{
if (!data || typeof data !== "object") { return; }
const { startIndex, endIndex, label, showBelow } = data;
if (startIndex !== undefined) {
this.startIndex = startIndex;
if (startIndex >= 0 && startIndex <= this.indexPositions.length) {
this.startPos = this.indexPositions[startIndex];
}
}
if (endIndex !== undefined) {
this.endIndex = endIndex;
if (endIndex >= 0 && endIndex <= this.indexPositions.length) {
this.endPos = this.indexPositions[endIndex];
}
}
// startValue/endValue no longer supported; use startIndex/endIndex only
if (label !== undefined) { this.label = label; }
if (showBelow !== undefined) { this.showBelow = showBelow; }
this.updateLayout();
}
updateLayout()
{
this.removeAllChildren();
// make line
var startX = this.startPos;
var endX = this.endPos;
var W = endX - startX;
this.line = new jsvgLine();
this.line.setEndpoints( startX, 0, endX, 0 );
this.addChild( this.line );
// make ticks
var tick = new jsvgLine();
tick.setEndpoints( startX, -3, startX, 3 );
this.addChild( tick );
tick = new jsvgLine();
tick.setEndpoints( endX, -3, endX, 3 );
this.addChild( tick );
// make label text
var labelText = new jsvgTextBox();
labelText.setWidthAndHeight( W,30 );
labelText.setText ( this.name );
labelText.setFontFamily( "Albert Sans" );
labelText.setFontColor( "black" );
labelText.setFontSize( 14 );
labelText.setAlignment("center");
labelText.setText( this.label );
if ( this.showBelow )
labelText.setPosition( startX, 5 );
else
labelText.setPosition( startX, -20 );
this.addChild( labelText );
// Calculate dimensions for this label
var labelWidth = Math.max(W, endX);
var labelHeight = this.showBelow ? 35 : 30; // Account for text position
// Set proper bounds and viewBox for the label
this.setWidthAndHeight( labelWidth, labelHeight );
this.svgObject.setAttribute("viewBox", `0 ${this.showBelow ? -5 : -25} ${labelWidth} ${labelHeight}`);
}
}
export class omdTapeDiagram extends jsvgGroup
{
constructor()
{
// initialization
super();
this.type = "omdTapeDiagram";
this.title = "";
this.values = [];
this.labelSet = [];
this.totalWidth = 300;
this.updateLayout();
}
loadFromJSON( data )
{
if ( typeof data.title !== "undefined" )
this.title = data.title;
if ( typeof data.values !== "undefined" )
this.values = data.values;
if ( typeof data.labelSet !== "undefined" )
this.labelSet = data.labelSet;
if ( typeof data.totalWidth !== "undefined" )
this.totalWidth = data.totalWidth;
this.updateLayout();
}
setValues( newValues )
{
this.values = newValues;
this.updateLayout();
}
updateLayout()
{
this.removeAllChildren();
const leftPadding = this.title ? 80 : 20;
const rightPadding = 20;
const titleWidth = 70;
// Add title if present
if (this.title) {
const titleText = new jsvgTextBox();
titleText.setWidthAndHeight(titleWidth, 30);
titleText.setFontFamily("Albert Sans");
titleText.setFontColor("black");
titleText.setFontSize(12);
titleText.setAlignment("left");
titleText.setText(this.title);
titleText.setPosition(5, 5);
this.addChild(titleText);
}
// Parse values and calculate proportional widths
const parsedValues = [];
let totalNumericValue = 0;
for (const valueData of this.values) {
let value = "";
let showLabel = true;
let color = omdColor.lightGray;
let numericValue = 1; // default for non-numeric
// Handle both old format (simple values) and new format (objects)
if (typeof valueData === "object" && valueData !== null) {
value = valueData.value || "";
showLabel = valueData.showLabel !== undefined ? valueData.showLabel : true;
color = valueData.color || omdColor.lightGray;
} else {
value = valueData.toString();
}
// Parse numeric value from string (e.g., "3", "2x", "5y")
// Extract coefficient from expressions like "2x", "3", "0.5y"
const match = value.match(/^([0-9.]+)?([a-zA-Z]*)$/);
if (match) {
const coefficient = match[1] ? parseFloat(match[1]) : (match[2] ? 1 : 1);
const variable = match[2] || "";
numericValue = coefficient;
}
parsedValues.push({ value, showLabel, color, numericValue });
totalNumericValue += numericValue;
}
// Calculate width for each segment based on proportion
var pX = leftPadding;
var indexPositions = [];
for (const parsed of parsedValues) {
indexPositions.push(pX);
// Calculate proportional width
const proportion = totalNumericValue > 0 ? parsed.numericValue / totalNumericValue : 1 / parsedValues.length;
const segmentWidth = this.totalWidth * proportion;
// Make box
var box = new jsvgRect();
box.setWidthAndHeight(segmentWidth, 30);
box.setCornerRadius(5);
box.setStrokeColor("white");
box.setStrokeWidth(1);
box.setFillColor(parsed.color);
box.setPosition(pX, 0);
this.addChild(box);
// Make box text (if showLabel is true)
if (parsed.showLabel) {
var boxText = new jsvgTextBox();
boxText.setWidthAndHeight(segmentWidth, 30);
boxText.setFontFamily("Albert Sans");
boxText.setFontColor("black");
boxText.setFontSize(18);
boxText.setAlignment("center");
boxText.setVerticalCentering();
boxText.setText(parsed.value);
boxText.setPosition(pX, 0);
this.addChild(boxText);
}
pX += segmentWidth;
}
indexPositions.push(pX);
// Calculate dimensions
var contentWidth = pX - leftPadding;
var contentHeight = 30;
var topLabelSpace = 0;
var bottomLabelSpace = 0;
// Sort labels by span length (number of segments they cover)
const sortedLabels = this.labelSet.slice().map((labelData, index) => ({
data: labelData,
span: (labelData.endIndex || 0) - (labelData.startIndex || 0)
})).sort((a, b) => a.span - b.span); // Shortest first
// Track occupied label layers to prevent overlap
const topLayers = [];
const bottomLayers = [];
// Make label text
for ( const item of sortedLabels )
{
const labelData = item.data;
const T = new omdTapeLabel();
T.setIndexPositions( indexPositions );
T.loadFromJSON( labelData );
if ( T.showBelow ) {
// Find the lowest available layer for this label
let layer = 0;
const start = labelData.startIndex || 0;
const end = labelData.endIndex || 0;
while (layer < bottomLayers.length) {
const conflicts = bottomLayers[layer].some(occupied =>
!(end <= occupied.start || start >= occupied.end)
);
if (!conflicts) break;
layer++;
}
if (layer === bottomLayers.length) {
bottomLayers.push([]);
}
bottomLayers[layer].push({ start, end });
const yPos = 40 + (layer * 35); // Stack labels with 35px spacing
T.setPosition(0, yPos);
bottomLabelSpace = Math.max(bottomLabelSpace, yPos + 30);
} else {
// Find the highest available layer for this label
let layer = 0;
const start = labelData.startIndex || 0;
const end = labelData.endIndex || 0;
while (layer < topLayers.length) {
const conflicts = topLayers[layer].some(occupied =>
!(end <= occupied.start || start >= occupied.end)
);
if (!conflicts) break;
layer++;
}
if (layer === topLayers.length) {
topLayers.push([]);
}
topLayers[layer].push({ start, end });
const yPos = -10 - (layer * 35); // Stack labels upward with 35px spacing
T.setPosition(0, yPos);
topLabelSpace = Math.max(topLabelSpace, (layer + 1) * 35);
}
this.addChild( T );
}
// Set proper dimensions including space for labels above and below
this.width = leftPadding + contentWidth + rightPadding;
this.height = topLabelSpace + contentHeight + bottomLabelSpace;
// Adjust viewBox to show everything including title and labels
const viewBoxY = -topLabelSpace;
const viewBoxHeight = this.height;
this.svgObject.setAttribute("viewBox", `0 ${viewBoxY} ${this.width} ${viewBoxHeight}`);
this.svgObject.setAttribute("viewBox", `0 ${-topLabelSpace} ${this.width} ${this.height}`);
}
}