UNPKG

@teachinglab/omd

Version:

omd

369 lines (320 loc) 13.7 kB
import { omdColor } from "./omdColor.js"; import { jsvgGroup, jsvgRect, jsvgTextBox } from "@teachinglab/jsvg"; export class omdTable extends jsvgGroup { constructor() { // initialization super(); this.type = "omdTable"; this.equation = ""; this.data = []; this.headers = ['x', 'y']; this.xMin = -5; this.xMax = 5; this.stepSize = 1; this.title = ""; this.fontSize = 14; this.headerFontSize = 16; this.fontFamily = "Albert Sans"; this.headerFontFamily = "Albert Sans"; this.cellHeight = 35; this.headerHeight = 40; this.minCellWidth = 80; this.maxCellWidth = 300; this.padding = 10; this.updateLayout(); } // Estimate title width in pixels based on font size and text length estimateTitleWidth() { if (!this.title || this.title.length === 0) return 0; const titleFontSize = this.headerFontSize + 2; const padding = 40; // side padding inside the title text box const minWidth = 200; const estimated = Math.round(this.title.length * (titleFontSize * 0.6)) + padding; return Math.max(minWidth, estimated); } loadFromJSON( data ) { if ( typeof data.equation != "undefined" ) this.equation = data.equation; if ( typeof data.data != "undefined" ) this.data = data.data; if ( typeof data.headers != "undefined" ) this.headers = data.headers; if ( typeof data.xMin != "undefined" ) this.xMin = data.xMin; if ( typeof data.xMax != "undefined" ) this.xMax = data.xMax; if ( typeof data.stepSize != "undefined" ) this.stepSize = data.stepSize; if ( typeof data.title != "undefined" ) this.title = data.title; if ( typeof data.fontSize != "undefined" ) this.fontSize = data.fontSize; if ( typeof data.headerFontSize != "undefined" ) this.headerFontSize = data.headerFontSize; if ( typeof data.fontFamily != "undefined" ) this.fontFamily = data.fontFamily; if ( typeof data.headerFontFamily != "undefined" ) this.headerFontFamily = data.headerFontFamily; if ( typeof data.cellHeight != "undefined" ) this.cellHeight = data.cellHeight; if ( typeof data.headerHeight != "undefined" ) this.headerHeight = data.headerHeight; if ( typeof data.minCellWidth != "undefined" ) this.minCellWidth = data.minCellWidth; if ( typeof data.maxCellWidth != "undefined" ) this.maxCellWidth = data.maxCellWidth; if ( typeof data.padding != "undefined" ) this.padding = data.padding; this.updateLayout(); } setEquation( equation ) { this.equation = equation; this.updateLayout(); } setData( data, headers ) { this.data = data; if ( headers ) this.headers = headers; this.updateLayout(); } calculateOptimalCellWidth(columnIndex) { let maxLength = (this.headers[columnIndex] ?? '').toString().length; // Assume rows are arrays aligned with headers for (let row of this.data) { const cellValue = row[columnIndex]; if (cellValue !== null && cellValue !== undefined) { maxLength = Math.max(maxLength, cellValue.toString().length); } } // Estimate width based on character count (approximate 8 pixels per character) const estimatedWidth = Math.max(maxLength * 8 + this.padding * 2, this.minCellWidth); return Math.min(estimatedWidth, this.maxCellWidth); } generateDataFromEquation() { if (!this.equation || this.equation.trim().length === 0) return; // Clear existing data and set headers this.data = []; this.headers = ['x', 'y']; // Basic normalization for inline math let expression = this.equation; if (expression.toLowerCase().startsWith('y=')) { expression = expression.substring(2).trim(); } expression = expression .replace(/(\d)([a-z])/gi, '$1*$2') .replace(/([a-z])(\d)/gi, '$1*$2') .replace(/\^/g, '**'); const evaluateExpression = new Function('x', `return ${expression};`); for (let x = this.xMin; x <= this.xMax; x += this.stepSize) { let y = evaluateExpression(x); y = Math.round(y * 100) / 100; this.data.push([x, y]); } } updateLayout() { this.removeAllChildren(); // If an equation is provided, generate data before measuring/layout if (this.equation && this.equation.length > 0) { this.generateDataFromEquation(); } // Calculate table dimensions const numCols = this.headers.length; const numRows = this.data.length; let cellWidths = []; let totalWidth = 0; for (let col = 0; col < numCols; col++) { const width = this.calculateOptimalCellWidth(col); cellWidths.push(width); totalWidth += width; } this.width = totalWidth; const titleOffset = (this.title && this.title.length > 0) ? 30 : 0; const bodyHeight = this.headerHeight + numRows * this.cellHeight; const totalHeight = titleOffset + bodyHeight; this.height = totalHeight; // Compute a display width that ensures the title is not clipped, // without changing column widths or table background. const titleBoxWidth = this.estimateTitleWidth(); const displayWidth = Math.max(this.width, titleBoxWidth); // Table background with corner radius (all four corners, covers full height) const tableBg = new jsvgRect(); tableBg.setWidthAndHeight(this.width, bodyHeight); tableBg.setFillColor(omdColor.lightGray); tableBg.setCornerRadius(15); tableBg.setStrokeWidth(0); const contentOffsetX = Math.max(0, (displayWidth - this.width) / 2); tableBg.setPosition(contentOffsetX, titleOffset); this.addChild(tableBg); // Draw a rounded footer rectangle under the last row to provide bottom rounded corners if (numRows > 0) { const footer = new jsvgRect(); footer.setWidthAndHeight(this.width, this.cellHeight); footer.setFillColor(omdColor.lightGray); footer.setCornerRadius(15); footer.setStrokeWidth(0); const footerY = titleOffset + this.headerHeight + (numRows - 1) * this.cellHeight; footer.setPosition(contentOffsetX, footerY); this.addChild(footer); } // Generate data from equation if provided; otherwise assume valid data/headers if (this.equation && this.equation.length > 0) { this.generateDataFromEquation(); } let currentY = 0; // Add title if provided if (this.title && this.title.length > 0) { var titleText = new jsvgTextBox(); // Use an expanded text box width (only for title) to avoid clipping titleText.setWidthAndHeight(titleBoxWidth, 25); titleText.setText(this.title); titleText.setFontFamily(this.headerFontFamily); titleText.setFontColor("black"); titleText.setFontSize(this.headerFontSize + 2); titleText.setAlignment("center"); titleText.setVerticalCentering(); // Center the title within the display width (table is centered within display) const titleX = Math.max(0, (displayWidth - titleBoxWidth) / 2); titleText.setPosition(titleX, currentY); titleText.setFontWeight(600); this.addChild(titleText); currentY += 30; } // Create header row (lightGray, no border, no rounded corners for cells) let currentX = 0; for (let col = 0; col < numCols; col++) { const cellWidth = cellWidths[col]; var headerRect = new jsvgRect(); headerRect.setWidthAndHeight(cellWidth, this.headerHeight); headerRect.setFillColor(omdColor.lightGray); // Use rx/ry for top corners only on first and last header cells if (col === 0 && numCols === 1) { headerRect.setCornerRadius(15); // single column, round all corners } else if (col === 0) { headerRect.setCornerRadius(15); // round top-left } else if (col === numCols - 1) { headerRect.setCornerRadius(15); // round top-right } else { headerRect.setCornerRadius(0); } headerRect.setStrokeWidth(0); headerRect.setPosition(currentX + contentOffsetX, currentY); this.addChild(headerRect); const headerText = this.createHeaderTextBox( cellWidth, this.headerHeight, this.headers[col] || `Col ${col + 1}` ); headerText.setPosition(currentX + contentOffsetX, currentY); this.addChild(headerText); currentX += cellWidth; } currentY += this.headerHeight; // Create data rows with alternating colors for (let row = 0; row < numRows; row++) { const rowData = this.data[row]; let currentX = 0; // Alternating bar: odd rows white 50% opacity, even rows transparent if (row % 2 === 0) { var barRect = new jsvgRect(); barRect.setWidthAndHeight(this.width, this.cellHeight); barRect.setFillColor("rgba(255,255,255,0.5)"); // Round the bottom corners on the last row to respect the table background rounding if (row === numRows - 1) { barRect.setCornerRadius(15); } else { barRect.setCornerRadius(0); } barRect.setStrokeWidth(0); barRect.setPosition(contentOffsetX, currentY); this.addChild(barRect); } for (let col = 0; col < numCols; col++) { const cellWidth = cellWidths[col]; const cellText = this.createBodyTextBox(cellWidth, this.cellHeight, ""); const cellValue = rowData[col]; cellText.setText((cellValue ?? '').toString()); cellText.setPosition(currentX + contentOffsetX, currentY); this.addChild(cellText); currentX += cellWidth; } currentY += this.cellHeight; } // Draw vertical dividing lines at each column boundary (except the far right edge) if (numCols > 1) { let x = 0; for (let col = 0; col < numCols - 1; col++) { x += cellWidths[col]; const vline = new jsvgRect(); vline.setWidthAndHeight(1, Math.max(0, bodyHeight - 1)); vline.setFillColor("black"); vline.setCornerRadius(0); vline.setOpacity(0.5); vline.setStrokeWidth(0); vline.setPosition(x + contentOffsetX, titleOffset); this.addChild(vline); } } // Use displayWidth for the viewBox so the title never clips, // but keep the table background at the original table width this.setWidthAndHeight(displayWidth, totalHeight); this.svgObject.setAttribute("viewBox", `0 0 ${displayWidth} ${totalHeight}`); } // ===== Helpers for consistent styling (match other components) ===== createHeaderTextBox(width, height, text) { const tb = new jsvgTextBox(); tb.setWidthAndHeight(width, height); tb.setText(text); tb.setFontFamily(this.headerFontFamily); tb.setFontColor("black"); tb.setFontSize(this.headerFontSize); tb.setAlignment("center"); tb.setVerticalCentering(); tb.setFontWeight(600); return tb; } createBodyTextBox(width, height, text) { const tb = new jsvgTextBox(); tb.setWidthAndHeight(width, height); tb.setText(text); tb.setFontFamily(this.fontFamily); tb.setFontColor("black"); tb.setFontSize(this.fontSize); tb.setAlignment("center"); tb.setVerticalCentering(); tb.setFontWeight(400); return tb; } addRow( rowData ) { this.data.push( rowData ); this.updateLayout(); } setHeaders( headers ) { this.headers = headers; this.updateLayout(); } setFont( fontFamily, headerFontFamily ) { this.fontFamily = fontFamily; if ( headerFontFamily ) this.headerFontFamily = headerFontFamily; else this.headerFontFamily = fontFamily; this.updateLayout(); } clearData() { this.data = []; this.updateLayout(); } }