UNPKG

@teachinglab/omd

Version:

omd

499 lines (426 loc) 19.8 kB
import { omdColor } from "./omdColor.js"; import { jsvgGroup, jsvgRect, jsvgTextBox, jsvgClipMask } 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; // Background customization properties this.backgroundColor = omdColor.lightGray; this.backgroundCornerRadius = 15; this.backgroundOpacity = 1.0; this.showBackground = true; // Alternating row color properties this.alternatingRowColors = [omdColor.mediumGray, omdColor.lightGray]; // Should be an array of colors or null this.headerBackgroundColor = omdColor.lightGray; this.cellBackgroundColor = "white"; // Legacy properties for backward compatibility this.evenRowColor = "rgba(255,255,255,0.5)"; this.oddRowColor = "transparent"; this.alternatingRowOpacity = 1.0; 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; // Load background customization properties if ( typeof data.backgroundColor != "undefined" ) this.backgroundColor = data.backgroundColor; if ( typeof data.backgroundCornerRadius != "undefined" ) this.backgroundCornerRadius = data.backgroundCornerRadius; if ( typeof data.backgroundOpacity != "undefined" ) this.backgroundOpacity = data.backgroundOpacity; if ( typeof data.showBackground != "undefined" ) this.showBackground = data.showBackground; // Load alternating row color properties if ( typeof data.alternatingRowColors != "undefined" ) { this.alternatingRowColors = data.alternatingRowColors; console.log('LoadFromJSON - Setting alternatingRowColors to:', this.alternatingRowColors); } if ( typeof data.evenRowColor != "undefined" ) this.evenRowColor = data.evenRowColor; if ( typeof data.oddRowColor != "undefined" ) this.oddRowColor = data.oddRowColor; if ( typeof data.alternatingRowOpacity != "undefined" ) this.alternatingRowOpacity = data.alternatingRowOpacity; 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() { console.log('updateLayout called - alternatingRowColors:', this.alternatingRowColors); console.log('alternatingRowColors type:', typeof this.alternatingRowColors); console.log('alternatingRowColors isArray:', Array.isArray(this.alternatingRowColors)); 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); const contentOffsetX = Math.max(0, (displayWidth - this.width) / 2); // Create a clipped group for rounded corners if corner radius is specified let tableContentGroup; if (this.backgroundCornerRadius > 0) { // Create clip mask with rounded corners const clipMask = new jsvgClipMask(this.width, bodyHeight, this.backgroundCornerRadius); clipMask.setPosition(contentOffsetX, titleOffset); this.addChild(clipMask); // Table content will be added to the clip mask tableContentGroup = clipMask; // Create table background inside the clip mask (so it gets rounded corners) if (this.showBackground) { const tableBg = new jsvgRect(); tableBg.setWidthAndHeight(this.width, bodyHeight); tableBg.setFillColor(this.backgroundColor); if (this.backgroundOpacity < 1.0) { tableBg.setOpacity(this.backgroundOpacity); } tableBg.setStrokeWidth(0); tableBg.setPosition(0, 0); // Relative to clip mask tableContentGroup.addChild(tableBg); } } else { // No clipping needed, create background and use the main group if (this.showBackground) { const tableBg = new jsvgRect(); tableBg.setWidthAndHeight(this.width, bodyHeight); tableBg.setFillColor(this.backgroundColor); if (this.backgroundOpacity < 1.0) { tableBg.setOpacity(this.backgroundOpacity); } tableBg.setStrokeWidth(0); tableBg.setPosition(contentOffsetX, titleOffset); this.addChild(tableBg); } tableContentGroup = this; } // Remove the separate footer rectangle since the main background now covers everything // 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 let currentX = 0; const headerY = 0; // Relative to the clipped content group // First create a full-width header background if using alternating colors if (this.alternatingRowColors && Array.isArray(this.alternatingRowColors) && this.alternatingRowColors.length > 0) { console.log('Creating header background with color:', this.alternatingRowColors[0]); const headerBg = new jsvgRect(); headerBg.setWidthAndHeight(this.width, this.headerHeight); headerBg.setFillColor(this.alternatingRowColors[0]); // Headers use first color headerBg.setCornerRadius(0); headerBg.setStrokeWidth(0); headerBg.setPosition(0, headerY); tableContentGroup.addChild(headerBg); } else { console.log('NOT creating header background - alternatingRowColors:', this.alternatingRowColors); } for (let col = 0; col < numCols; col++) { const cellWidth = cellWidths[col]; // Only create individual header background if NOT using alternating colors if (!this.alternatingRowColors || !Array.isArray(this.alternatingRowColors) || this.alternatingRowColors.length === 0) { var headerRect = new jsvgRect(); headerRect.setWidthAndHeight(cellWidth, this.headerHeight); headerRect.setFillColor(this.headerBackgroundColor || omdColor.lightGray); headerRect.setCornerRadius(0); headerRect.setStrokeWidth(0); headerRect.setPosition(currentX, headerY); tableContentGroup.addChild(headerRect); } const headerText = this.createHeaderTextBox( cellWidth, this.headerHeight, this.headers[col] || `Col ${col + 1}` ); headerText.setPosition(currentX, headerY); tableContentGroup.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; const rowY = this.headerHeight + row * this.cellHeight; // Relative to clipped content group // Create row background with alternating colors if enabled if (this.alternatingRowColors && Array.isArray(this.alternatingRowColors) && this.alternatingRowColors.length > 0) { const colorIndex = (row + 1) % this.alternatingRowColors.length; // +1 to account for header const rowColor = this.alternatingRowColors[colorIndex]; console.log(`Creating row ${row} background with color:`, rowColor, 'at position:', 0, rowY); var barRect = new jsvgRect(); barRect.setWidthAndHeight(this.width, this.cellHeight); barRect.setFillColor(rowColor); if (this.backgroundOpacity < 1.0) { barRect.setOpacity(this.backgroundOpacity); } // No corner radius on individual row backgrounds - clip mask handles the rounding barRect.setCornerRadius(0); barRect.setStrokeWidth(0); barRect.setPosition(0, rowY); tableContentGroup.addChild(barRect); } else { console.log(`Row ${row}: No alternating colors - alternatingRowColors:`, this.alternatingRowColors); } 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, rowY); tableContentGroup.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, 0); // Relative to the clipped content group tableContentGroup.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(); } // Background customization methods setBackgroundColor(color) { this.backgroundColor = color; this.updateLayout(); } setBackgroundCornerRadius(radius) { this.backgroundCornerRadius = radius; this.updateLayout(); } setBackgroundOpacity(opacity) { this.backgroundOpacity = Math.max(0, Math.min(1, opacity)); this.updateLayout(); } setShowBackground(show) { this.showBackground = show; this.updateLayout(); } setBackgroundStyle(options = {}) { if (options.backgroundColor !== undefined) this.backgroundColor = options.backgroundColor; if (options.cornerRadius !== undefined) this.backgroundCornerRadius = options.cornerRadius; if (options.opacity !== undefined) this.backgroundOpacity = Math.max(0, Math.min(1, options.opacity)); if (options.show !== undefined) this.showBackground = options.show; this.updateLayout(); } // Alternating row colors methods setAlternatingRowColors(colors) { console.log('setAlternatingRowColors called with:', colors); this.alternatingRowColors = colors; console.log('alternatingRowColors set to:', this.alternatingRowColors); this.updateLayout(); } setHeaderBackgroundColor(color) { this.headerBackgroundColor = color; this.updateLayout(); } setCellBackgroundColor(color) { this.cellBackgroundColor = color; this.updateLayout(); } }