diff --git a/electron/index.html b/electron/index.html index 4989ca4..3db0b7b 100644 --- a/electron/index.html +++ b/electron/index.html @@ -42,7 +42,7 @@ @@ -295,6 +295,7 @@
+
@@ -325,6 +326,72 @@
+ + @@ -546,6 +613,7 @@ + diff --git a/electron/package-lock.json b/electron/package-lock.json index c183aa9..f9ee86d 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -13,6 +13,7 @@ "adm-zip": "^0.5.14", "fs-extra": "^11.0.0", "js-yaml": "^4.1.0", + "plotly.js-dist": "^2.26.0", "python-shell": "^5.0.0" }, "devDependencies": { @@ -3057,6 +3058,12 @@ "node": ">=10.4.0" } }, + "node_modules/plotly.js-dist": { + "version": "2.35.3", + "resolved": "https://registry.npmjs.org/plotly.js-dist/-/plotly.js-dist-2.35.3.tgz", + "integrity": "sha512-dqB9+FUyBFZN04xWnZoYwaeeF4Jj9T/m0CHYmoozmPC3R4Dy0TRJsHgbRVLPxgYQqodzniVUj17+2wmJuGaZAg==", + "license": "MIT" + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/electron/package.json b/electron/package.json index 92e43c2..6102616 100644 --- a/electron/package.json +++ b/electron/package.json @@ -31,7 +31,8 @@ "js-yaml": "^4.1.0", "adm-zip": "^0.5.14", "@electron/remote": "^2.0.0", - "python-shell": "^5.0.0" + "python-shell": "^5.0.0", + "plotly.js-dist": "^2.26.0" }, "build": { "appId": "com.fourdst.bundlemanager", diff --git a/electron/renderer-refactored.js b/electron/renderer-refactored.js index 3ce3d6a..3c685d8 100644 --- a/electron/renderer-refactored.js +++ b/electron/renderer-refactored.js @@ -12,6 +12,7 @@ const uiComponents = require('./renderer/ui-components'); const eventHandlers = require('./renderer/event-handlers'); const opatHandler = require('./renderer/opat-handler'); const fillWorkflow = require('./renderer/fill-workflow'); +const opatPlotting = require('./renderer/opat-plotting'); // Initialize all modules with their dependencies function initializeModules() { @@ -23,7 +24,8 @@ function initializeModules() { uiComponents, eventHandlers, opatHandler, - fillWorkflow + fillWorkflow, + opatPlotting }; // Initialize each module with its dependencies @@ -32,6 +34,7 @@ function initializeModules() { eventHandlers.initializeDependencies(deps); opatHandler.initializeDependencies(deps); fillWorkflow.initializeDependencies(deps); + opatPlotting.initializePlottingDependencies(deps); console.log('[RENDERER] All modules initialized with dependencies'); } @@ -84,5 +87,6 @@ window.uiComponents = uiComponents; window.eventHandlers = eventHandlers; window.opatHandler = opatHandler; window.fillWorkflow = fillWorkflow; +window.opatPlotting = opatPlotting; // === REGENERATED CODE END === diff --git a/electron/renderer/opat-handler.js b/electron/renderer/opat-handler.js index 3ba910f..af443c6 100644 --- a/electron/renderer/opat-handler.js +++ b/electron/renderer/opat-handler.js @@ -2,7 +2,7 @@ // Extracted from renderer.js to centralize OPAT file parsing and display logic // Import dependencies (these will be injected when integrated) -let stateManager, domManager; +let stateManager, domManager, opatPlotting; // OPAT File Inspector variables let opatFileInput, opatBrowseBtn, opatView, opatCloseBtn; @@ -29,6 +29,11 @@ function initializeOPATElements() { // Initialize OPAT tab navigation initializeOPATTabs(); + // Initialize plotting elements if module is available + if (opatPlotting) { + opatPlotting.initializePlottingElements(); + } + // Add window resize listener to update table heights window.updateTableHeights = function() { const newHeight = Math.max(300, window.innerHeight - 450); @@ -82,6 +87,11 @@ function resetOPATViewerState() { if (opatIndexSelector) opatIndexSelector.innerHTML = ''; if (opatTablesDisplay) opatTablesDisplay.innerHTML = ''; if (opatTableDataContent) opatTableDataContent.innerHTML = ''; + + // Reset plotting state if module is available + if (opatPlotting) { + opatPlotting.resetPlottingState(); + } } // Handle OPAT file selection @@ -100,21 +110,24 @@ async function handleOPATFileSelection(event) { stateManager.setOPATFile(currentOPATFile); displayOPATFileInfo(); + displayAllTableTags(); populateIndexSelector(); - // Show OPAT view and hide other views + // Populate plotting selectors if module is available + if (opatPlotting) { + opatPlotting.populatePlotIndexSelector(); + } + + // Show OPAT view hideAllViews(); opatView.classList.remove('hidden'); - // Update title with filename - document.getElementById('opat-title').textContent = `OPAT File Inspector - ${file.name}`; - domManager.hideSpinner(); - + console.log('OPAT file loaded successfully'); } catch (error) { - console.error('Error parsing OPAT file:', error); + console.error('Error loading OPAT file:', error); domManager.hideSpinner(); - domManager.showModal('Error', `Failed to parse OPAT file: ${error.message}`); + alert('Error loading OPAT file: ' + error.message); } } @@ -376,6 +389,7 @@ function showCategoryHomeScreen(category) { function initializeDependencies(deps) { stateManager = deps.stateManager; domManager = deps.domManager; + opatPlotting = deps.opatPlotting; } module.exports = { diff --git a/electron/renderer/opat-plotting.js b/electron/renderer/opat-plotting.js new file mode 100644 index 0000000..cf84138 --- /dev/null +++ b/electron/renderer/opat-plotting.js @@ -0,0 +1,833 @@ +// OPAT plotting module for the 4DSTAR Bundle Manager +// Handles interactive plotting functionality using Plotly.js + +// Import dependencies (these will be injected when integrated) +let stateManager, domManager; + +// Plotting UI elements +let plotIndexSelector, plotTableSelector, plotXAxis, plotYAxis, plotType, plotTitle; +let createPlotBtn, savePlotBtn, clearPlotBtn; +let plotContainer, plotPlaceholder, plotDisplay; + +// Current plot state +let currentPlotData = null; +let currentPlotLayout = null; + +// Initialize plotting UI elements +function initializePlottingElements() { + plotIndexSelector = document.getElementById('plot-index-selector'); + plotTableSelector = document.getElementById('plot-table-selector'); + plotXAxis = document.getElementById('plot-x-axis'); + plotYAxis = document.getElementById('plot-y-axis'); + plotType = document.getElementById('plot-type'); + plotTitle = document.getElementById('plot-title'); + + createPlotBtn = document.getElementById('create-plot-btn'); + savePlotBtn = document.getElementById('save-plot-btn'); + clearPlotBtn = document.getElementById('clear-plot-btn'); + + plotContainer = document.getElementById('plot-container'); + plotPlaceholder = document.getElementById('plot-placeholder'); + plotDisplay = document.getElementById('plot-display'); + + // Check if elements were found + const elements = { + plotIndexSelector, plotTableSelector, plotXAxis, plotYAxis, plotType, plotTitle, + createPlotBtn, savePlotBtn, clearPlotBtn, plotContainer, plotPlaceholder, plotDisplay + }; + + const missingElements = Object.entries(elements) + .filter(([name, element]) => !element) + .map(([name]) => name); + + if (missingElements.length > 0) { + console.warn('[OPAT_PLOTTING] Missing elements:', missingElements); + console.warn('[OPAT_PLOTTING] Will retry initialization when needed'); + return; // Don't add event listeners if elements are missing + } + + // Event listeners (only add if all elements are present) + plotIndexSelector.addEventListener('change', handlePlotIndexChange); + plotTableSelector.addEventListener('change', handlePlotTableChange); + createPlotBtn.addEventListener('click', handleCreatePlot); + savePlotBtn.addEventListener('click', handleSavePlot); + clearPlotBtn.addEventListener('click', handleClearPlot); + + // Set up a backup event listener setup function for later use + setupEventListenersIfNeeded(); + + console.log('[OPAT_PLOTTING] All plotting elements found and initialized successfully'); +} + +// Setup event listeners if they weren't set up during initial initialization +function setupEventListenersIfNeeded() { + // This function can be called later to ensure event listeners are properly set up + // even if elements weren't available during initial setup + + const selectElements = { + 'plot-index-selector': handlePlotIndexChange, + 'plot-table-selector': handlePlotTableChange + }; + + const buttonElements = { + 'create-plot-btn': handleCreatePlot, + 'save-plot-btn': handleSavePlot, + 'clear-plot-btn': handleClearPlot + }; + + // Add change listeners to select elements + for (const [elementId, handler] of Object.entries(selectElements)) { + const element = document.getElementById(elementId); + if (element && !element.hasAttribute('data-plotting-change-listener')) { + element.addEventListener('change', handler); + element.setAttribute('data-plotting-change-listener', 'true'); + console.log(`[OPAT_PLOTTING] Added change listener to ${elementId}`); + } + } + + // Add click listeners to button elements + for (const [elementId, handler] of Object.entries(buttonElements)) { + const element = document.getElementById(elementId); + if (element && !element.hasAttribute('data-plotting-click-listener')) { + element.addEventListener('click', handler); + element.setAttribute('data-plotting-click-listener', 'true'); + console.log(`[OPAT_PLOTTING] Added click listener to ${elementId}`); + } + } +} + +// Populate plot index selector when OPAT file is loaded +function populatePlotIndexSelector() { + console.log('[OPAT_PLOTTING] populatePlotIndexSelector called'); + + // Always try to find the element fresh, in case it wasn't available during initial setup + const selector = document.getElementById('plot-index-selector'); + if (!selector) { + console.error('[OPAT_PLOTTING] Could not find plot-index-selector element in DOM'); + return; + } + + // Update our reference + plotIndexSelector = selector; + + const opatFile = stateManager.getOPATFile(); + if (!opatFile) { + console.warn('[OPAT_PLOTTING] No OPAT file available'); + plotIndexSelector.innerHTML = ''; + return; + } + + console.log('[OPAT_PLOTTING] OPAT file found with', opatFile.cards.size, 'cards'); + plotIndexSelector.innerHTML = ''; + + let optionCount = 0; + for (const [indexName, card] of opatFile.cards) { + const option = document.createElement('option'); + option.value = indexName; + option.textContent = `${indexName} (${card.tableData.size} tables)`; + plotIndexSelector.appendChild(option); + optionCount++; + console.log('[OPAT_PLOTTING] Added option:', indexName); + } + + console.log(`[OPAT_PLOTTING] Successfully populated ${optionCount} index vectors`); + + // Also initialize other selectors if they weren't found during initial setup + if (!plotTableSelector) { + plotTableSelector = document.getElementById('plot-table-selector'); + } + if (!plotXAxis) { + plotXAxis = document.getElementById('plot-x-axis'); + } + if (!plotYAxis) { + plotYAxis = document.getElementById('plot-y-axis'); + } + + // Ensure event listeners are set up + ensureEventListenersAreSetup(); +} + +// Ensure event listeners are properly set up (called when needed) +function ensureEventListenersAreSetup() { + console.log('[OPAT_PLOTTING] Ensuring event listeners are set up'); + + // Check and set up index selector listener + const indexSelector = document.getElementById('plot-index-selector'); + if (indexSelector && !indexSelector.hasAttribute('data-plotting-change-listener')) { + indexSelector.addEventListener('change', handlePlotIndexChange); + indexSelector.setAttribute('data-plotting-change-listener', 'true'); + console.log('[OPAT_PLOTTING] Added change listener to plot-index-selector'); + } + + // Check and set up table selector listener + const tableSelector = document.getElementById('plot-table-selector'); + if (tableSelector && !tableSelector.hasAttribute('data-plotting-change-listener')) { + tableSelector.addEventListener('change', handlePlotTableChange); + tableSelector.setAttribute('data-plotting-change-listener', 'true'); + console.log('[OPAT_PLOTTING] Added change listener to plot-table-selector'); + } + + // Check and set up create plot button listener + const createPlotBtn = document.getElementById('create-plot-btn'); + if (createPlotBtn && !createPlotBtn.hasAttribute('data-plotting-click-listener')) { + createPlotBtn.addEventListener('click', handleCreatePlot); + createPlotBtn.setAttribute('data-plotting-click-listener', 'true'); + console.log('[OPAT_PLOTTING] Added click listener to create-plot-btn'); + } + + // Check and set up save plot button listener + const savePlotBtn = document.getElementById('save-plot-btn'); + if (savePlotBtn && !savePlotBtn.hasAttribute('data-plotting-click-listener')) { + savePlotBtn.addEventListener('click', handleSavePlot); + savePlotBtn.setAttribute('data-plotting-click-listener', 'true'); + console.log('[OPAT_PLOTTING] Added click listener to save-plot-btn'); + } + + // Update our references + if (indexSelector) plotIndexSelector = indexSelector; + if (tableSelector) plotTableSelector = tableSelector; + if (createPlotBtn) createPlotButton = createPlotBtn; + if (savePlotBtn) savePlotButton = savePlotBtn; +} + +// Handle index vector selection change +function handlePlotIndexChange() { + console.log('[OPAT_PLOTTING] handlePlotIndexChange called'); + + // Ensure we have references to the elements + if (!plotIndexSelector) plotIndexSelector = document.getElementById('plot-index-selector'); + if (!plotTableSelector) plotTableSelector = document.getElementById('plot-table-selector'); + if (!plotXAxis) plotXAxis = document.getElementById('plot-x-axis'); + if (!plotYAxis) plotYAxis = document.getElementById('plot-y-axis'); + + if (!plotIndexSelector || !plotTableSelector || !plotXAxis || !plotYAxis) { + console.error('[OPAT_PLOTTING] Missing required elements for index change'); + return; + } + + const selectedIndex = plotIndexSelector.value; + console.log('[OPAT_PLOTTING] Selected index:', selectedIndex); + + plotTableSelector.innerHTML = ''; + plotXAxis.innerHTML = ''; + plotYAxis.innerHTML = ''; + + if (!selectedIndex) { + console.log('[OPAT_PLOTTING] No index selected, returning'); + return; + } + + const opatFile = stateManager.getOPATFile(); + if (!opatFile) return; + + const card = opatFile.cards.get(selectedIndex); + if (!card) return; + + // Populate table selector + for (const [tableName, tableData] of card.tableData) { + const option = document.createElement('option'); + option.value = tableName; + option.textContent = `${tableName} (${tableData.N_R}×${tableData.N_C})`; + plotTableSelector.appendChild(option); + } + + console.log(`[OPAT_PLOTTING] Populated ${card.tableData.size} tables for index ${selectedIndex}`); +} + +// Handle table selection change +function handlePlotTableChange() { + console.log('[OPAT_PLOTTING] handlePlotTableChange called'); + + // Ensure we have references to the elements + if (!plotIndexSelector) plotIndexSelector = document.getElementById('plot-index-selector'); + if (!plotTableSelector) plotTableSelector = document.getElementById('plot-table-selector'); + if (!plotXAxis) plotXAxis = document.getElementById('plot-x-axis'); + if (!plotYAxis) plotYAxis = document.getElementById('plot-y-axis'); + + if (!plotIndexSelector || !plotTableSelector || !plotXAxis || !plotYAxis) { + console.error('[OPAT_PLOTTING] Missing required elements for table change'); + return; + } + + const selectedIndex = plotIndexSelector.value; + const selectedTable = plotTableSelector.value; + + console.log('[OPAT_PLOTTING] Selected index:', selectedIndex, 'Selected table:', selectedTable); + + plotXAxis.innerHTML = ''; + plotYAxis.innerHTML = ''; + + if (!selectedIndex || !selectedTable) { + console.log('[OPAT_PLOTTING] No index or table selected, returning'); + return; + } + + const opatFile = stateManager.getOPATFile(); + if (!opatFile) { + console.error('[OPAT_PLOTTING] No OPAT file available'); + return; + } + + const card = opatFile.cards.get(selectedIndex); + if (!card) { + console.error('[OPAT_PLOTTING] Card not found for index:', selectedIndex); + return; + } + + const tableData = card.tableData.get(selectedTable); + if (!tableData) { + console.error('[OPAT_PLOTTING] Table data not found for table:', selectedTable); + return; + } + + // Get the table index entry for names + const tableIndexEntry = card.tableIndex.get(selectedTable); + if (!tableIndexEntry) { + console.error('[OPAT_PLOTTING] Table index entry not found for table:', selectedTable); + return; + } + + console.log('[OPAT_PLOTTING] Table info:', { + rowName: tableIndexEntry.rowName, + columnName: tableIndexEntry.columnName, + numRows: tableData.N_R, + numColumns: tableData.N_C, + rowValuesLength: tableData.rowValues?.length, + columnValuesLength: tableData.columnValues?.length + }); + + // Populate axis selectors with row and column values + const variables = []; + + // Add row values as the primary option (typically the independent variable) + if (tableData.rowValues && tableData.rowValues.length > 0) { + const rowName = tableIndexEntry.rowName.trim() || 'Row Values'; + variables.push({ + name: rowName, + value: '__row_values__', + isDefault: true, + type: 'row' + }); + } + + // Add column values as the secondary option + if (tableData.columnValues && tableData.columnValues.length > 0) { + const columnName = tableIndexEntry.columnName.trim() || 'Column Values'; + variables.push({ + name: columnName, + value: '__column_values__', + isDefault: true, + type: 'column' + }); + } + + // Add individual row options with their actual values + if (tableData.rowValues && tableData.rowValues.length > 0) { + const rowName = tableIndexEntry.rowName.trim() || 'Row'; + for (let i = 0; i < Math.min(tableData.rowValues.length, 20); i++) { // Limit to 20 for UI performance + const value = tableData.rowValues[i]; + const displayValue = typeof value === 'number' ? value.toPrecision(4) : value; + variables.push({ + name: `Data Row ${i + 1} (${rowName} = ${displayValue})`, + value: `row_${i}`, + isDefault: false, + type: 'row_data', + index: i + }); + } + } + + // Add individual column options with their actual values + if (tableData.columnValues && tableData.columnValues.length > 0) { + const columnName = tableIndexEntry.columnName.trim() || 'Column'; + for (let i = 0; i < Math.min(tableData.columnValues.length, 20); i++) { // Limit to 20 for UI performance + const value = tableData.columnValues[i]; + const displayValue = typeof value === 'number' ? value.toPrecision(4) : value; + variables.push({ + name: `Data Column ${i + 1} (${columnName} = ${displayValue})`, + value: `col_${i}`, + isDefault: false, + type: 'column_data', + index: i + }); + } + } + + // Add data columns as additional options (for accessing the actual table data) + for (let i = 0; i < Math.min(tableData.N_C, 10); i++) { // Limit for UI performance + variables.push({ + name: `Table Data Column ${i + 1}`, + value: `data_col_${i}`, + isDefault: false, + type: 'data' + }); + } + + // Populate the select elements + variables.forEach((variable, index) => { + const xOption = document.createElement('option'); + xOption.value = variable.value; + xOption.textContent = variable.name; + plotXAxis.appendChild(xOption); + + const yOption = document.createElement('option'); + yOption.value = variable.value; + yOption.textContent = variable.name; + plotYAxis.appendChild(yOption); + }); + + // Set default selections: row values for X-axis, column values for Y-axis + const rowVariable = variables.find(v => v.type === 'row'); + const columnVariable = variables.find(v => v.type === 'column'); + + if (rowVariable) { + plotXAxis.value = rowVariable.value; + console.log('[OPAT_PLOTTING] Set default X-axis to:', rowVariable.name); + } + + if (columnVariable) { + plotYAxis.value = columnVariable.value; + console.log('[OPAT_PLOTTING] Set default Y-axis to:', columnVariable.name); + } + + console.log(`[OPAT_PLOTTING] Populated ${variables.length} variables for table ${selectedTable}`); +} + +// Handle create plot button click +async function handleCreatePlot() { + console.log('[OPAT_PLOTTING] handleCreatePlot called'); + + // Ensure we have references to all elements + if (!plotIndexSelector) plotIndexSelector = document.getElementById('plot-index-selector'); + if (!plotTableSelector) plotTableSelector = document.getElementById('plot-table-selector'); + if (!plotXAxis) plotXAxis = document.getElementById('plot-x-axis'); + if (!plotYAxis) plotYAxis = document.getElementById('plot-y-axis'); + if (!plotType) plotType = document.getElementById('plot-type'); + if (!plotTitle) plotTitle = document.getElementById('plot-title'); + + if (!plotIndexSelector || !plotTableSelector || !plotXAxis || !plotYAxis || !plotType || !plotTitle) { + console.error('[OPAT_PLOTTING] Missing required elements for plot creation'); + return; + } + + const selectedIndex = plotIndexSelector.value; + const selectedTable = plotTableSelector.value; + const xVariable = plotXAxis.value; + const yVariable = plotYAxis.value; + const plotTypeValue = plotType.value; + const titleText = plotTitle.value || 'OPAT Data Plot'; + + console.log('[OPAT_PLOTTING] Plot parameters:', { + selectedIndex, selectedTable, xVariable, yVariable, plotTypeValue, titleText + }); + + if (!selectedIndex || !selectedTable || !xVariable || !yVariable) { + console.warn('[OPAT_PLOTTING] Missing required fields'); + alert('Please select all required fields (Index Vector, Table, X-Axis, Y-Axis)'); + return; + } + + try { + domManager.showSpinner(); + + // Get button references + const createBtn = document.getElementById('create-plot-btn'); + const saveBtn = document.getElementById('save-plot-btn'); + const clearBtn = document.getElementById('clear-plot-btn'); + + if (createBtn) createBtn.disabled = true; + + const plotData = await generatePlotData(selectedIndex, selectedTable, xVariable, yVariable, plotTypeValue); + const layout = generatePlotLayout(titleText, xVariable, yVariable); + + await createPlotlyPlot(plotData, layout); + + // Enable save and clear buttons + if (saveBtn) saveBtn.disabled = false; + if (clearBtn) clearBtn.disabled = false; + + console.log('[OPAT_PLOTTING] Plot created successfully'); + } catch (error) { + console.error('[OPAT_PLOTTING] Error creating plot:', error); + alert('Error creating plot: ' + error.message); + } finally { + domManager.hideSpinner(); + const createBtn = document.getElementById('create-plot-btn'); + if (createBtn) createBtn.disabled = false; + } +} + +// Generate plot data from OPAT table +async function generatePlotData(indexName, tableName, xVariable, yVariable, plotTypeValue) { + const opatFile = stateManager.getOPATFile(); + const card = opatFile.cards.get(indexName); + const tableData = card.tableData.get(tableName); + + const xData = extractVariableData(tableData, xVariable); + const yData = extractVariableData(tableData, yVariable); + + let trace; + + switch (plotTypeValue) { + case 'scatter': + trace = { + x: xData, + y: yData, + mode: 'markers', + type: 'scatter', + marker: { + color: '#3b82f6', + size: 6, + opacity: 0.7 + }, + name: `${tableName}` + }; + break; + + case 'line': + trace = { + x: xData, + y: yData, + mode: 'lines+markers', + type: 'scatter', + line: { + color: '#3b82f6', + width: 2 + }, + marker: { + color: '#3b82f6', + size: 4 + }, + name: `${tableName}` + }; + break; + + case 'heatmap': + // For heatmap, we need 2D data + const zData = reshapeDataForHeatmap(tableData); + trace = { + z: zData, + type: 'heatmap', + colorscale: 'Viridis', + name: `${tableName}` + }; + break; + + case 'contour': + // For contour, we need 2D data + const contourData = reshapeDataForHeatmap(tableData); + trace = { + z: contourData, + type: 'contour', + colorscale: 'Viridis', + name: `${tableName}` + }; + break; + + default: + throw new Error(`Unsupported plot type: ${plotTypeValue}`); + } + + return [trace]; +} + +// Extract variable data from table +function extractVariableData(tableData, variable) { + console.log('[OPAT_PLOTTING] Extracting variable data for:', variable); + + // Extract row values (the actual row axis values from the OPAT table) + if (variable === '__row_values__') { + if (tableData.rowValues && tableData.rowValues.length > 0) { + console.log('[OPAT_PLOTTING] Using row values, length:', tableData.rowValues.length); + return Array.from(tableData.rowValues); + } else { + console.warn('[OPAT_PLOTTING] No row values available, using indices'); + return Array.from({ length: tableData.N_R }, (_, i) => i); + } + } + + // Extract column values (the actual column axis values from the OPAT table) + if (variable === '__column_values__') { + if (tableData.columnValues && tableData.columnValues.length > 0) { + console.log('[OPAT_PLOTTING] Using column values, length:', tableData.columnValues.length); + return Array.from(tableData.columnValues); + } else { + console.warn('[OPAT_PLOTTING] No column values available, using indices'); + return Array.from({ length: tableData.N_C }, (_, i) => i); + } + } + + // Legacy support for row/column indices + if (variable === '__row_index__') { + return Array.from({ length: tableData.N_R }, (_, i) => i); + } + + if (variable === '__col_index__') { + return Array.from({ length: tableData.N_C }, (_, i) => i); + } + + // Extract data from a specific row (all columns for that row) + if (variable.startsWith('row_')) { + const rowIndex = parseInt(variable.split('_')[1]); + console.log('[OPAT_PLOTTING] Extracting data from row:', rowIndex); + const data = []; + + for (let col = 0; col < tableData.N_C; col++) { + try { + const value = tableData.getValue(rowIndex, col, 0); // Use getValue method like OPAT explorer + data.push(typeof value === 'number' ? value : parseFloat(value) || 0); + } catch (error) { + console.warn('[OPAT_PLOTTING] Error extracting data at row', rowIndex, 'col', col, ':', error); + data.push(0); + } + } + + return data; + } + + // Extract data from a specific column (all rows for that column) + if (variable.startsWith('col_')) { + const colIndex = parseInt(variable.split('_')[1]); + console.log('[OPAT_PLOTTING] Extracting data from column:', colIndex); + const data = []; + + for (let row = 0; row < tableData.N_R; row++) { + try { + const value = tableData.getValue(row, colIndex, 0); // Use getValue method like OPAT explorer + data.push(typeof value === 'number' ? value : parseFloat(value) || 0); + } catch (error) { + console.warn('[OPAT_PLOTTING] Error extracting data at row', row, 'col', colIndex, ':', error); + data.push(0); + } + } + + return data; + } + + // Extract data from specific data columns (legacy table data access) + if (variable.startsWith('data_col_')) { + const colIndex = parseInt(variable.split('_')[2]); + console.log('[OPAT_PLOTTING] Extracting data column:', colIndex); + const data = []; + + for (let row = 0; row < tableData.N_R; row++) { + try { + const value = tableData.getValue(row, colIndex, 0); // Use getValue method like OPAT explorer + data.push(typeof value === 'number' ? value : parseFloat(value) || 0); + } catch (error) { + console.warn('[OPAT_PLOTTING] Error extracting data at row', row, 'col', colIndex, ':', error); + data.push(0); + } + } + + return data; + } + + throw new Error(`Unknown variable: ${variable}`); +} + +// Reshape data for heatmap/contour plots +function reshapeDataForHeatmap(tableData) { + const data = []; + for (let row = 0; row < Math.min(tableData.N_R, 50); row++) { // Limit for performance + const rowData = []; + for (let col = 0; col < Math.min(tableData.N_C, 50); col++) { + try { + const value = tableData.getValue(row, col, 0); // Use getValue method like OPAT explorer + rowData.push(typeof value === 'number' ? value : parseFloat(value) || 0); + } catch (error) { + rowData.push(0); + } + } + data.push(rowData); + } + return data; +} + +// Generate plot layout +function generatePlotLayout(title, xVariable, yVariable) { + const isDarkMode = document.body.classList.contains('dark-mode'); + + return { + title: { + text: title, + font: { + color: isDarkMode ? '#f3f4f6' : '#1f2937', + size: 16 + } + }, + xaxis: { + title: { + text: formatVariableName(xVariable), + font: { + color: isDarkMode ? '#d1d5db' : '#374151' + } + }, + tickfont: { + color: isDarkMode ? '#9ca3af' : '#6b7280' + }, + gridcolor: isDarkMode ? '#4b5563' : '#e5e7eb' + }, + yaxis: { + title: { + text: formatVariableName(yVariable), + font: { + color: isDarkMode ? '#d1d5db' : '#374151' + } + }, + tickfont: { + color: isDarkMode ? '#9ca3af' : '#6b7280' + }, + gridcolor: isDarkMode ? '#4b5563' : '#e5e7eb' + }, + plot_bgcolor: isDarkMode ? '#374151' : 'white', + paper_bgcolor: isDarkMode ? '#374151' : 'white', + font: { + color: isDarkMode ? '#f3f4f6' : '#1f2937' + }, + margin: { t: 50, r: 50, b: 50, l: 50 } + }; +} + +// Format variable name for display +function formatVariableName(variable) { + if (variable === '__row_index__') return 'Row Index'; + if (variable === '__col_index__') return 'Column Index'; + if (variable.startsWith('col_')) { + const colIndex = parseInt(variable.split('_')[1]); + return `Column ${colIndex + 1}`; + } + return variable; +} + +// Create Plotly plot +async function createPlotlyPlot(data, layout) { + console.log('[OPAT_PLOTTING] createPlotlyPlot called'); + + currentPlotData = data; + currentPlotLayout = layout; + + // Get DOM elements + const placeholder = document.getElementById('plot-placeholder'); + const display = document.getElementById('plot-display'); + + if (!placeholder || !display) { + console.error('[OPAT_PLOTTING] Missing plot container elements'); + throw new Error('Plot container elements not found'); + } + + // Hide placeholder and show plot display + placeholder.classList.add('hidden'); + display.classList.remove('hidden'); + + // Create the plot + await Plotly.newPlot(display, data, layout, { + responsive: true, + displayModeBar: true, + modeBarButtonsToAdd: ['downloadImage'], + toImageButtonOptions: { + format: 'png', + filename: 'opat_plot', + height: 500, + width: 700, + scale: 2 + } + }); +} + +// Handle save plot button click +async function handleSavePlot() { + if (!currentPlotData || !currentPlotLayout) { + alert('No plot to save'); + return; + } + + try { + // Use Plotly's built-in download functionality + const filename = plotTitle.value ? + plotTitle.value.replace(/[^a-z0-9]/gi, '_').toLowerCase() : + 'opat_plot'; + + await Plotly.downloadImage(plotDisplay, { + format: 'png', + width: 1200, + height: 800, + filename: filename + }); + + console.log('[OPAT_PLOTTING] Plot saved as PNG'); + } catch (error) { + console.error('[OPAT_PLOTTING] Error saving plot:', error); + alert('Error saving plot: ' + error.message); + } +} + +// Handle clear plot button click +function handleClearPlot() { + if (plotDisplay && currentPlotData) { + Plotly.purge(plotDisplay); + } + + currentPlotData = null; + currentPlotLayout = null; + + // Show placeholder and hide plot display (with null checks) + if (plotDisplay) { + plotDisplay.classList.add('hidden'); + } + if (plotPlaceholder) { + plotPlaceholder.classList.remove('hidden'); + } + + // Disable save and clear buttons (with null checks) + if (savePlotBtn) { + savePlotBtn.disabled = true; + } + if (clearPlotBtn) { + clearPlotBtn.disabled = true; + } + + console.log('[OPAT_PLOTTING] Plot cleared'); +} + +// Reset plotting state when OPAT file changes +function resetPlottingState() { + handleClearPlot(); + + // Reset selectors with null checks + if (plotIndexSelector) { + plotIndexSelector.innerHTML = ''; + } + if (plotTableSelector) { + plotTableSelector.innerHTML = ''; + } + if (plotXAxis) { + plotXAxis.innerHTML = ''; + } + if (plotYAxis) { + plotYAxis.innerHTML = ''; + } + if (plotTitle) { + plotTitle.value = ''; + } + + console.log('[OPAT_PLOTTING] Plotting state reset'); +} + +// Initialize dependencies (called when module is loaded) +function initializePlottingDependencies(deps) { + stateManager = deps.stateManager; + domManager = deps.domManager; +} + +module.exports = { + initializePlottingDependencies, + initializePlottingElements, + populatePlotIndexSelector, + handlePlotIndexChange, + handlePlotTableChange, + handleCreatePlot, + handleSavePlot, + handleClearPlot, + resetPlottingState +}; diff --git a/electron/styles.css b/electron/styles.css index eec52ba..37ce023 100644 --- a/electron/styles.css +++ b/electron/styles.css @@ -1931,6 +1931,145 @@ body.dark-mode .info-tab-pane a { flex-direction: column; } +/* OPAT Plotting Tab Styling */ +.plot-controls { + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 20px; +} + +.plot-control-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} + +@media (max-width: 768px) { + .plot-control-row { + grid-template-columns: 1fr; + gap: 12px; + } +} + +.plot-control-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.opat-input { + padding: 8px 12px; + background: white; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 0.9rem; + color: #374151; + transition: border-color 0.2s ease; +} + +.opat-input:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); +} + +body.dark-mode .opat-input { + background: #4b5563; + border: 1px solid #6b7280; + color: #f3f4f6; +} + +body.dark-mode .opat-input:focus { + border-color: #3b82f6; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); +} + +.plot-actions { + display: flex; + gap: 12px; + justify-content: flex-start; + align-items: center; + padding-top: 16px; + border-top: 1px solid #e5e7eb; +} + +body.dark-mode .plot-actions { + border-top: 1px solid #4b5563; +} + +.plot-container { + min-height: 500px; + background: white; + border: 1px solid #e5e7eb; + border-radius: 8px; + overflow: hidden; + position: relative; +} + +body.dark-mode .plot-container { + background: #374151; + border: 1px solid #4b5563; +} + +.plot-placeholder { + display: flex; + align-items: center; + justify-content: center; + height: 500px; + color: #9ca3af; + font-style: italic; + text-align: center; + padding: 20px; +} + +body.dark-mode .plot-placeholder { + color: #6b7280; +} + +.plot-display { + width: 100%; + height: 500px; +} + +/* Plotly.js theme integration */ +.plot-display .plotly { + background-color: transparent !important; +} + +body.dark-mode .plot-display .plotly { + background-color: transparent !important; +} + +/* Custom plotly toolbar styling */ +.plot-display .modebar { + background: rgba(255, 255, 255, 0.9) !important; + border-radius: 6px !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important; + border: 1px solid #e5e7eb !important; +} + +body.dark-mode .plot-display .modebar { + background: rgba(55, 65, 81, 0.9) !important; + border: 1px solid #4b5563 !important; +} + +.plot-display .modebar-btn { + color: #374151 !important; +} + +body.dark-mode .plot-display .modebar-btn { + color: #f3f4f6 !important; +} + +.plot-display .modebar-btn:hover { + background: rgba(59, 130, 246, 0.1) !important; +} + +body.dark-mode .plot-display .modebar-btn:hover { + background: rgba(59, 130, 246, 0.2) !important; +} + .opat-table-container { flex: 1; display: flex;