diff --git a/src/renderers/MeasurementRenderer.js b/src/renderers/MeasurementRenderer.js index 306213b1dca4a9d0a86333732faa5f304519877f..edaaa3dff9ce8e5220d13787fb52a66eba3f22d1 100644 --- a/src/renderers/MeasurementRenderer.js +++ b/src/renderers/MeasurementRenderer.js @@ -1,5 +1,7 @@ import Plotly from 'plotly.js-dist'; import { DCCRealListQuantity, DCCConformity } from '../dccQuantity.js'; +import JSONEditor from 'jsoneditor'; +import 'jsoneditor/dist/jsoneditor.css'; const palette = [ '#1f77b4', @@ -36,15 +38,17 @@ const conformityColors = { nofail: '#9e9e9e' }; +// Render all measurement results in a tabbed layout export function renderMeasurementResults(measurementResults, language) { console.debug('renderMeasurementResults called with:', measurementResults); const container = document.getElementById('measurementResults'); container.innerHTML = ''; - if (!measurementResults) { - console.error('No measurementResults provided.'); - return; - } + // Create tab headers and content container + const tabHeader = document.createElement('ul'); + tabHeader.className = 'tab-header'; + const tabContent = document.createElement('div'); + tabContent.className = 'tab-content'; let results = measurementResults['dcc:measurementResult']; if (!results) { @@ -52,26 +56,131 @@ export function renderMeasurementResults(measurementResults, language) { return; } if (!Array.isArray(results)) { results = [results]; } - const measurementResult = results[0]; - let resultObj = measurementResult['dcc:results'] && measurementResult['dcc:results']['dcc:result']; - if (!resultObj) { - console.error("Missing 'dcc:results' or 'dcc:result' in measurementResult:", measurementResult); - return; + results.forEach((result, index) => { + // Determine tab title from dcc:name + let resultName = 'Measurement Result ' + (index + 1); + if (result['dcc:name'] && result['dcc:name']['dcc:content']) { + let content = result['dcc:name']['dcc:content']; + if (Array.isArray(content)) { + const match = content.find(item => item.$ && item.$.lang === language) || content[0]; + resultName = match._ || match; + } else { + resultName = content._ || content; + } + } + // Create tab header item + const tabItem = document.createElement('li'); + tabItem.textContent = resultName; + if (index === 0) tabItem.classList.add('active'); + + // Create tab panel for this measurement result + const tabPanel = document.createElement('div'); + tabPanel.className = 'tab-panel'; + tabPanel.style.display = (index === 0) ? 'block' : 'none'; + + tabItem.addEventListener('click', () => { + document.querySelectorAll('.tab-header li').forEach(li => li.classList.remove('active')); + document.querySelectorAll('.tab-content > div').forEach(div => div.style.display = 'none'); + tabItem.classList.add('active'); + tabPanel.style.display = 'block'; + // Force a resize event for Plotly to recalc dimensions + window.dispatchEvent(new Event('resize')); + }); + tabHeader.appendChild(tabItem); + + // Extract the single dcc:result from this measurement result and render it + let resultObj = result['dcc:results'] && result['dcc:results']['dcc:result']; + if (!resultObj) { + console.error("Missing 'dcc:results' or 'dcc:result' in measurementResult:", result); + } else { + let singleResult = Array.isArray(resultObj) ? resultObj[0] : resultObj; + renderSingleMeasurementResult(singleResult, language, tabPanel); + } + + // Add expandable section for Used Methods using JSONEditor + const methodsDetails = document.createElement('details'); + methodsDetails.open = false; + const methodsSummary = document.createElement('summary'); + methodsSummary.textContent = 'Used Methods'; + methodsDetails.appendChild(methodsSummary); + methodsDetails.appendChild(renderUsedMethods(result, language)); + tabPanel.appendChild(methodsDetails); + + // Add expandable section for Influence Conditions using JSONEditor + const influenceDetails = document.createElement('details'); + influenceDetails.open = false; + const influenceSummary = document.createElement('summary'); + influenceSummary.textContent = 'Influence Conditions'; + influenceDetails.appendChild(influenceSummary); + influenceDetails.appendChild(renderInfluenceConditions(result, language)); + tabPanel.appendChild(influenceDetails); + + tabContent.appendChild(tabPanel); + }); + + container.appendChild(tabHeader); + container.appendChild(tabContent); +} + +// Helper to render used methods as JSON tree via JSONEditor +function renderUsedMethods(measurementResult, language) { + const container = document.createElement('div'); + container.style.maxHeight = '200px'; + container.style.overflowY = 'auto'; + const options = { + mode: 'view', + mainMenuBar: false, + navigationBar: false, + statusBar: false + }; + const editor = new JSONEditor(container, options); + if (measurementResult['dcc:usedMethods']) { + editor.set(measurementResult['dcc:usedMethods']); + } else { + container.textContent = 'No used methods data available.'; + } + return container; +} + +// Helper to render influence conditions as JSON tree via JSONEditor +function renderInfluenceConditions(measurementResult, language) { + const container = document.createElement('div'); + container.style.maxHeight = '200px'; + container.style.overflowY = 'auto'; + const options = { + mode: 'view', + mainMenuBar: false, + navigationBar: false, + statusBar: false + }; + const editor = new JSONEditor(container, options); + if (measurementResult['dcc:influenceConditions']) { + editor.set(measurementResult['dcc:influenceConditions']); + } else { + container.textContent = 'No influence conditions data available.'; } - if (Array.isArray(resultObj)) { resultObj = resultObj[0]; } + return container; +} + +// Helper to render a single measurement result (charts, tables, etc.) +export function renderSingleMeasurementResult(resultObj, language, tabPanel) { + console.debug('renderSingleMeasurementResult called with:', resultObj); + tabPanel.innerHTML = ''; let resultName = 'Measurement Result'; - if (measurementResult['dcc:name'] && measurementResult['dcc:name']['dcc:content']) { - let content = measurementResult['dcc:name']['dcc:content']; + if (resultObj['dcc:name'] && resultObj['dcc:name']['dcc:content']) { + let content = resultObj['dcc:name']['dcc:content']; if (Array.isArray(content)) { const match = content.find(item => item.$ && item.$.lang === language) || content[0]; resultName = match._ || match; - } else { resultName = content._ || content; } + } else { + resultName = content._ || content; + } } - const tabTitle = document.createElement('h2'); - tabTitle.textContent = resultName; - container.appendChild(tabTitle); + const header = document.createElement('h2'); + header.textContent = resultName; + tabPanel.appendChild(header); if (!resultObj['dcc:data'] || !resultObj['dcc:data']['dcc:list']) { console.error("Missing 'dcc:data' or 'dcc:list' in result object:", resultObj); @@ -99,7 +208,6 @@ export function renderMeasurementResults(measurementResults, language) { const indexQuantities = []; const dataQuantities = []; - // extraInfo now stores { uncertainty, conformity } for each data quantity. const extraInfo = []; quantityJSONs.forEach(q => { if (q.$ && q.$.refType && q.$.refType.match(/basic_tableIndex/)) { @@ -122,15 +230,15 @@ export function renderMeasurementResults(measurementResults, language) { }); const dataHeaders = dataQuantities.map((q, idx) => { - let header = q.getName(language); + let headerText = q.getName(language); let unit = q.getUnit(); - if (!header.toLowerCase().includes(" in ")) { - header = header + " in " + unit; + if (!headerText.toLowerCase().includes(" in ")) { + headerText = headerText + " in " + unit; } - return header; + return headerText; }); - // Create scaling toggles for log axes. + // Create scaling toggles and X-axis selector controls const scalingContainer = document.createElement('div'); scalingContainer.innerHTML = '<strong>Scaling:</strong> '; const logXToggle = document.createElement('input'); @@ -149,7 +257,7 @@ export function renderMeasurementResults(measurementResults, language) { logYLabel.textContent = 'Log Y'; scalingContainer.appendChild(logYToggle); scalingContainer.appendChild(logYLabel); - container.appendChild(scalingContainer); + tabPanel.appendChild(scalingContainer); const xAxisContainer = document.createElement('div'); xAxisContainer.innerHTML = '<strong>Select X-Axis:</strong> '; @@ -166,7 +274,7 @@ export function renderMeasurementResults(measurementResults, language) { xAxisContainer.appendChild(radio); xAxisContainer.appendChild(label); }); - container.appendChild(xAxisContainer); + tabPanel.appendChild(xAxisContainer); const toleranceToggle = document.createElement('input'); toleranceToggle.type = 'checkbox'; @@ -177,17 +285,18 @@ export function renderMeasurementResults(measurementResults, language) { const tolContainer = document.createElement('div'); tolContainer.appendChild(toleranceToggle); tolContainer.appendChild(tolLabel); - container.appendChild(tolContainer); + tabPanel.appendChild(tolContainer); + // Create containers for plots and table. const subplotsContainer = document.createElement('div'); subplotsContainer.id = 'subplotsContainer'; - container.appendChild(subplotsContainer); + tabPanel.appendChild(subplotsContainer); const tableContainer = document.createElement('div'); tableContainer.id = 'tableContainer'; - container.appendChild(tableContainer); + tabPanel.appendChild(tableContainer); function updateVisualization() { - const selectedRadio = document.querySelector('input[name="xAxisSelect"]:checked'); + const selectedRadio = tabPanel.querySelector('input[name="xAxisSelect"]:checked'); if (!selectedRadio) { console.error('No X-Axis selection found.'); return; } const selectedIndex = selectedRadio.value; const xQuantity = indexQuantities[selectedIndex]; @@ -228,7 +337,7 @@ export function renderMeasurementResults(measurementResults, language) { row.push(extraInfo.map(info => info.comment || '').filter(c => c).join(' ; ')); tableData.push(row); } - renderTable(tableData); + renderTable(tableData, computeConformityMapping()); const unitGroups = {}; dataQuantities.forEach((q, idx) => { @@ -258,9 +367,7 @@ export function renderMeasurementResults(measurementResults, language) { graphDiv.style.height = '300px'; subplotsContainer.appendChild(graphDiv); plotDivs.push(graphDiv); - - // Build main traces for this unit group. - let groupTraces = group.map(trace => { + const groupTraces = group.map(trace => { let tooltip = 'X: %{x} ' + xUnit + ' | ' + trace.name + ': %{y}'; if (trace.conformity && trace.conformity.length > 0) { tooltip += ' | Conformity: %{customdata}'; @@ -289,9 +396,8 @@ export function renderMeasurementResults(measurementResults, language) { }; }); - // If tolerance markings are enabled and conformity data exists, add tolerance traces. const toleranceTraces = []; - if (document.getElementById('toleranceToggle').checked) { + if (tabPanel.querySelector('#toleranceToggle').checked) { group.forEach(trace => { const confObj = extraInfo[trace.index].conformity; if (confObj) { @@ -322,7 +428,6 @@ export function renderMeasurementResults(measurementResults, language) { } }); } - // Combine main traces with tolerance traces. const allTraces = groupTraces.concat(toleranceTraces); let xaxisTitle = ''; @@ -330,8 +435,8 @@ export function renderMeasurementResults(measurementResults, language) { let xLabel = 'X: ' + xQuantity.getName(language); xaxisTitle = '<b>' + xLabel + ' in ' + xUnit + '</b>'; } - const logX = document.getElementById('logXToggle').checked; - const logY = document.getElementById('logYToggle').checked; + const logX = tabPanel.querySelector('#logXToggle').checked; + const logY = tabPanel.querySelector('#logYToggle').checked; const layout = { xaxis: { title: { text: xaxisTitle, font: { size: 36, family: 'Arial', color: 'black' } }, @@ -343,7 +448,6 @@ export function renderMeasurementResults(measurementResults, language) { tickfont: { family: 'Arial', size: 14, color: 'black' }, type: logY ? 'log' : 'linear' }, - // Place legend on top over the plot without shifting x-axis scaling. legend: { orientation: 'h', x: 0.5, @@ -355,6 +459,8 @@ export function renderMeasurementResults(measurementResults, language) { margin: { t: 20, b: 40 } }; Plotly.newPlot(graphDiv, allTraces, layout).then(() => { + // Force a resize after a short delay to ensure full width + setTimeout(() => { Plotly.Plots.resize(graphDiv); }, 100); const caption = document.createElement('div'); caption.innerHTML = '<b>' + group[0].name + '</b>'; caption.style.textAlign = 'center'; @@ -380,8 +486,8 @@ export function renderMeasurementResults(measurementResults, language) { }); } - function renderTable(tableData) { - const tableContainer = document.getElementById('tableContainer'); + function renderTable(tableData, conformityMapping) { + const tableContainer = tabPanel.querySelector('#tableContainer'); tableContainer.innerHTML = ''; const table = document.createElement('table'); tableData.forEach((rowData, rowIndex) => { @@ -390,17 +496,13 @@ export function renderMeasurementResults(measurementResults, language) { const cell = document.createElement(rowIndex === 0 ? 'th' : 'td'); if (rowIndex === 0) { cell.innerHTML = cellData; - if (cellData !== 'Comments' && cellIndex > 0) { - const qtyIndex = Math.floor((cellIndex - 1) / 2); - cell.style.backgroundColor = lightPalette[qtyIndex % lightPalette.length]; + if (cellData !== 'Comments' && cellIndex > 0 && conformityMapping[cellIndex] !== undefined) { + cell.style.backgroundColor = conformityMapping[cellIndex]; } } else { cell.textContent = cellData; - if (tableData[0][cellIndex] && tableData[0][cellIndex].toLowerCase().includes('conformity')) { - const confVal = cellData.toLowerCase(); - if (confVal in conformityColors) { - cell.style.backgroundColor = conformityColors[confVal]; - } + if (tableData[0][cellIndex] && tableData[0][cellIndex].toLowerCase().includes('conformity') && conformityMapping[cellIndex] !== undefined) { + cell.style.backgroundColor = conformityMapping[cellIndex]; } } cell.style.padding = '4px'; @@ -414,20 +516,36 @@ export function renderMeasurementResults(measurementResults, language) { tableContainer.appendChild(table); } + function computeConformityMapping() { + let mapping = {}; + let col = 1; // Column 0 is X-Axis. + for (let i = 0; i < dataHeaders.length; i++) { + col++; // value column. + if (extraInfo[i] && extraInfo[i].conformity) { + mapping[col] = palette[i % palette.length]; + col++; + } + } + return mapping; + } + function highlightTableRow(rowIndex) { - const rows = document.getElementById('tableContainer').querySelectorAll('tr'); + const rows = tabPanel.querySelector('#tableContainer').querySelectorAll('tr'); if (rows[rowIndex]) { rows[rowIndex].style.backgroundColor = '#fee'; } } function clearTableRowHighlights() { - const rows = document.getElementById('tableContainer').querySelectorAll('tr'); + const rows = tabPanel.querySelector('#tableContainer').querySelectorAll('tr'); rows.forEach(row => row.style.backgroundColor = ''); } updateVisualization(); - const radios = document.querySelectorAll('input[name="xAxisSelect"]'); + const radios = tabPanel.querySelectorAll('input[name="xAxisSelect"]'); radios.forEach(radio => { radio.addEventListener('change', updateVisualization); }); - document.getElementById('logXToggle').addEventListener('change', updateVisualization); - document.getElementById('logYToggle').addEventListener('change', updateVisualization); - toleranceToggle.addEventListener('change', () => { console.log('Tolerance toggle:', toleranceToggle.checked); updateVisualization(); }); + tabPanel.querySelector('#logXToggle').addEventListener('change', updateVisualization); + tabPanel.querySelector('#logYToggle').addEventListener('change', updateVisualization); + tabPanel.querySelector('#toleranceToggle').addEventListener('change', () => { + console.log('Tolerance toggle:', tabPanel.querySelector('#toleranceToggle').checked); + updateVisualization(); + }); }