From 4403465fb91dc831de2f63296abb8185f1dfd386 Mon Sep 17 00:00:00 2001 From: Benedikt Seeger <benedikt.seeger@ptb.de> Date: Tue, 25 Feb 2025 10:27:57 +0100 Subject: [PATCH] added color coding --- src/renderers/MeasurementRenderer.js | 225 +++++++++++++++++++++------ 1 file changed, 174 insertions(+), 51 deletions(-) diff --git a/src/renderers/MeasurementRenderer.js b/src/renderers/MeasurementRenderer.js index 4cf1abc..8e119c8 100644 --- a/src/renderers/MeasurementRenderer.js +++ b/src/renderers/MeasurementRenderer.js @@ -1,5 +1,32 @@ import Plotly from 'plotly.js-dist'; +// Define Tab10 palette and lighter shades for table backgrounds +const palette = [ + '#1f77b4', + '#ff7f0e', + '#2ca02c', + '#d62728', + '#9467bd', + '#8c564b', + '#e377c2', + '#7f7f7f', + '#bcbd22', + '#17becf' +]; + +const lightPalette = [ + '#c6e2ff', + '#ffddbf', + '#c9e6c8', + '#f4c2c2', + '#dcd0ff', + '#e0cda9', + '#ffccff', + '#d3d3d3', + '#e6e6a9', + '#b3ffff' +]; + export function renderMeasurementResults(measurementResults, language) { console.debug('renderMeasurementResults called with:', measurementResults); const container = document.getElementById('measurementResults'); @@ -23,7 +50,6 @@ export function renderMeasurementResults(measurementResults, language) { } console.debug('Processed measurement result array:', results); - // Use the first measurementResult object const measurementResult = results[0]; console.debug('Using measurementResult:', measurementResult); @@ -38,7 +64,7 @@ export function renderMeasurementResults(measurementResults, language) { } console.debug('Using result object:', resultObj); - // Get the measurement result name from dcc:name (from measurementResult) + // Get measurement result name without language tag let resultName = 'Measurement Result'; if (measurementResult['dcc:name'] && measurementResult['dcc:name']['dcc:content']) { let content = measurementResult['dcc:name']['dcc:content']; @@ -52,10 +78,9 @@ export function renderMeasurementResults(measurementResults, language) { console.debug('Result name:', resultName); const tabTitle = document.createElement('h2'); - tabTitle.textContent = resultName + ' (' + language + ')'; + tabTitle.textContent = resultName; container.appendChild(tabTitle); - // Now extract dcc:data -> dcc:list from the resultObj if (!resultObj['dcc:data'] || !resultObj['dcc:data']['dcc:list']) { console.error("Missing 'dcc:data' or 'dcc:list' in result object:", resultObj); return; @@ -84,20 +109,64 @@ export function renderMeasurementResults(measurementResults, language) { } console.debug('Combined quantities:', quantities); - // Separate index quantities and data quantities + // Separate index quantities and data quantities; also extract extra info (uncertainty, comment, conformity) const indexQuantities = []; const dataQuantities = []; + const extraInfo = []; quantities.forEach(q => { if (q.$ && q.$.refType && q.$.refType.match(/basic_tableIndex/)) { indexQuantities.push(q); } else { dataQuantities.push(q); + let uncertainty = null; + if (q['si:measurementUncertaintyUnivariateXMLList'] && + q['si:measurementUncertaintyUnivariateXMLList']['si:expandedMUXMLList'] && + q['si:measurementUncertaintyUnivariateXMLList']['si:expandedMUXMLList']['si:valueExpandedMUXMLList']) { + const errStr = q['si:measurementUncertaintyUnivariateXMLList']['si:expandedMUXMLList']['si:valueExpandedMUXMLList']; + uncertainty = errStr.trim().split(/\s+/).map(v => parseFloat(v)); + } + let comment = ''; + let conformity = ''; + if (q['dcc:measurementMetaData'] && q['dcc:measurementMetaData']['dcc:metaData']) { + let md = q['dcc:measurementMetaData']['dcc:metaData']; + if (Array.isArray(md)) { + md.forEach(item => { + if (item.$ && item.$.refType && item.$.refType.includes('basic_tableRowComment')) { + if (item['dcc:description'] && item['dcc:description']['dcc:content']) { + comment = Array.isArray(item['dcc:description']['dcc:content']) ? + (item['dcc:description']['dcc:content'][0]._ || item['dcc:description']['dcc:content'][0]) : + (item['dcc:description']['dcc:content']._ || item['dcc:description']['dcc:content']); + } + } + if (item.$ && item.$.refType && item.$.refType.includes('basic_conformity')) { + if (item['dcc:conformityXMLList']) { + conformity = item['dcc:conformityXMLList'].trim(); + } + } + }); + } else { + if (md.$ && md.$.refType && md.$.refType.includes('basic_tableRowComment')) { + if (md['dcc:description'] && md['dcc:description']['dcc:content']) { + comment = Array.isArray(md['dcc:description']['dcc:content']) ? + (md['dcc:description']['dcc:content'][0]._ || md['dcc:description']['dcc:content'][0]) : + (md['dcc:description']['dcc:content']._ || md['dcc:description']['dcc:content']); + } + } + if (md.$ && md.$.refType && md.$.refType.includes('basic_conformity')) { + if (md['dcc:conformityXMLList']) { + conformity = md['dcc:conformityXMLList'].trim(); + } + } + } + } + extraInfo.push({ uncertainty, comment, conformity }); } }); console.debug('Index Quantities:', indexQuantities); console.debug('Data Quantities:', dataQuantities); + console.debug('Extra info for data quantities:', extraInfo); - // Create radio buttons for X-axis selection (from indexQuantities) + // Create radio buttons for X-axis selection const xAxisContainer = document.createElement('div'); xAxisContainer.innerHTML = '<strong>Select X-Axis:</strong> '; indexQuantities.forEach((q, idx) => { @@ -136,15 +205,17 @@ export function renderMeasurementResults(measurementResults, language) { tolContainer.appendChild(tolLabel); container.appendChild(tolContainer); - // Create containers for plots and table - const plotsContainer = document.createElement('div'); - plotsContainer.id = 'plotsContainer'; - container.appendChild(plotsContainer); + // Create container for subplots + const subplotsContainer = document.createElement('div'); + subplotsContainer.id = 'subplotsContainer'; + container.appendChild(subplotsContainer); + + // Create container for table const tableContainer = document.createElement('div'); tableContainer.id = 'tableContainer'; container.appendChild(tableContainer); - // Function to update the visualization + // Function to update visualization function updateVisualization() { const selectedRadio = document.querySelector('input[name="xAxisSelect"]:checked'); if (!selectedRadio) { @@ -154,28 +225,30 @@ export function renderMeasurementResults(measurementResults, language) { const selectedIndex = selectedRadio.value; const xQuantity = indexQuantities[selectedIndex]; let xValues = []; - if (xQuantity && xQuantity['si:realListXMLList'] && xQuantity['si:realListXMLList']['si:valueXMLList']) { - xValues = xQuantity['si:realListXMLList']['si:valueXMLList'].trim().split(/\s+/).map(v => parseFloat(v)); + let xUnit = ''; + if (xQuantity && xQuantity['si:realListXMLList']) { + if (xQuantity['si:realListXMLList']['si:valueXMLList']) { + xValues = xQuantity['si:realListXMLList']['si:valueXMLList'].trim().split(/\s+/).map(v => parseFloat(v)); + } + if (xQuantity['si:realListXMLList']['si:unitXMLList']) { + xUnit = xQuantity['si:realListXMLList']['si:unitXMLList'].trim(); + } } console.debug('Selected X-Axis values:', xValues); + console.debug('X-Axis unit:', xUnit); - // Build table headers and values + // Build table headers and arrays for values, comments, conformity, uncertainties const headers = []; - let xHeader = 'X-Axis'; - if (xQuantity['dcc:name'] && xQuantity['dcc:name']['dcc:content']) { - let content = xQuantity['dcc:name']['dcc:content']; - if (Array.isArray(content)) { - const match = content.find(item => item.$ && item.$.lang === language) || content[0]; - xHeader = match._ || match; - } else { - xHeader = content._ || content; - } - } + let xHeader = 'X-Axis (' + xUnit + ')'; headers.push(xHeader); const dataValues = []; - dataQuantities.forEach(q => { + const commentsArray = []; + const conformityArray = []; + const uncertaintiesArray = []; + dataQuantities.forEach((q, idx) => { let header = 'Data'; + let unit = ''; if (q['dcc:name'] && q['dcc:name']['dcc:content']) { let content = q['dcc:name']['dcc:content']; if (Array.isArray(content)) { @@ -185,7 +258,13 @@ export function renderMeasurementResults(measurementResults, language) { header = content._ || content; } } - headers.push(header); + if (q['si:realListXMLList'] && q['si:realListXMLList']['si:unitXMLList']) { + unit = q['si:realListXMLList']['si:unitXMLList'].trim(); + } + headers.push(header + ' (' + unit + ')'); + headers.push('Comments'); + headers.push('Conformity'); + let values = []; if (q['si:realListXMLList'] && q['si:realListXMLList']['si:valueXMLList']) { values = q['si:realListXMLList']['si:valueXMLList'].trim().split(/\s+/).map(v => parseFloat(v)); @@ -194,22 +273,38 @@ export function renderMeasurementResults(measurementResults, language) { values = new Array(xValues.length).fill(values[0]); } dataValues.push(values); + + let uncertainty = null; + if (extraInfo[idx] && extraInfo[idx].uncertainty) { + uncertainty = extraInfo[idx].uncertainty; + } + uncertaintiesArray.push(uncertainty); + + let comment = extraInfo[idx] ? extraInfo[idx].comment : ''; + let conformity = extraInfo[idx] ? extraInfo[idx].conformity : ''; + commentsArray.push(comment); + conformityArray.push(conformity); }); - // Build table rows const tableData = [headers]; for (let i = 0; i < xValues.length; i++) { const row = []; row.push(xValues[i]); - dataValues.forEach(values => { - row.push(values[i] !== undefined ? values[i] : ''); + dataValues.forEach((values, idx) => { + let cellValue = values[i] !== undefined ? values[i] : ''; + if (uncertaintiesArray[idx] && uncertaintiesArray[idx][i] !== undefined) { + cellValue = cellValue + ' ± ' + uncertaintiesArray[idx][i]; + } + row.push(cellValue); + row.push(commentsArray[idx] || ''); + row.push(conformityArray[idx] || ''); }); tableData.push(row); } console.debug('Table data:', tableData); renderTable(tableData); - // Group data quantities by unit for plotting + // Group data quantities by unit for plotting and assign colors const unitGroups = {}; dataQuantities.forEach((q, idx) => { let unit = ''; @@ -219,7 +314,7 @@ export function renderMeasurementResults(measurementResults, language) { if (!unitGroups[unit]) { unitGroups[unit] = []; } - let header = headers[idx + 1]; + let header = headers[idx * 3 + 1]; let values = []; if (q['si:realListXMLList'] && q['si:realListXMLList']['si:valueXMLList']) { values = q['si:realListXMLList']['si:valueXMLList'].trim().split(/\s+/).map(v => parseFloat(v)); @@ -227,41 +322,66 @@ export function renderMeasurementResults(measurementResults, language) { if (values.length === 1 && xValues.length > 1) { values = new Array(xValues.length).fill(values[0]); } - unitGroups[unit].push({ name: header, y: values }); + let uncertainty = uncertaintiesArray[idx]; + let conformity = conformityArray[idx]; + // Assign color: use palette; override if conformity indicates pass/fail + let traceColor = palette[idx % palette.length]; + if (conformity.toLowerCase().includes('pass')) { + traceColor = '#2ca02c'; + } else if (conformity.toLowerCase().includes('fail')) { + traceColor = '#d62728'; + } + unitGroups[unit].push({ name: header, y: values, uncertainty: uncertainty, conformity: conformity, color: traceColor }); }); console.debug('Unit groups for plots:', unitGroups); - // Clear and render plots + // Build a single Plotly figure with subplots (here, one plot per unit group) + const plotsContainer = document.getElementById('subplotsContainer'); plotsContainer.innerHTML = ''; - Object.keys(unitGroups).forEach(unit => { + Object.keys(unitGroups).forEach((unit, idx) => { + const group = unitGroups[unit]; const graphDiv = document.createElement('div'); graphDiv.style.width = '100%'; graphDiv.style.height = '300px'; plotsContainer.appendChild(graphDiv); - const traces = unitGroups[unit].map(trace => { + // Build traces for this unit group + const groupTraces = group.map(trace => { + const hovertemplate = 'X-Axis: %{x} ' + xUnit + ' | ' + trace.name + ': %{y} ± %{error_y.array} (' + unit + ') | Conformity: <b style="color:' + (trace.conformity.toLowerCase().includes('pass') ? '#2ca02c' : (trace.conformity.toLowerCase().includes('fail') ? '#d62728' : '#000')) + '">' + trace.conformity + '</b><extra></extra>'; return { x: xValues, y: trace.y, + error_y: { type: 'data', array: trace.uncertainty || new Array(xValues.length).fill(0), visible: true, color: trace.color }, type: 'scatter', mode: 'lines+markers', - name: trace.name + name: trace.name, + marker: { color: trace.color }, + hovertemplate: hovertemplate, + customdata: trace.conformity }; }); + const layout = { - title: 'Plot (' + unit + ')', - xaxis: { title: xHeader }, - yaxis: { title: unit }, - hovermode: 'closest' + xaxis: { title: { text: xHeader, font: { size: 16, weight: 'bold' } } }, + yaxis: { title: { text: unit, font: { size: 16, weight: 'bold' } } }, + hovermode: 'closest', + margin: { t: 20, b: 40 } }; - Plotly.newPlot(graphDiv, traces, layout).then(() => { + + Plotly.newPlot(graphDiv, groupTraces, layout).then(() => { console.debug('Plot rendered for unit:', unit); + // Add caption below the plot: italic text "QuantityName in Unit" + const caption = document.createElement('div'); + // Use the name of the first trace as QuantityName + caption.innerHTML = '<i>' + group[0].name + ' in ' + unit + '</i>'; + caption.style.textAlign = 'center'; + caption.style.fontStyle = 'italic'; + graphDiv.parentNode.insertBefore(caption, graphDiv.nextSibling); }); - // Coupled mouseover events for table row highlighting graphDiv.on('plotly_hover', function(data) { if (data.points && data.points.length > 0) { - const pointIndex = data.points[0].pointIndex + 1; // offset for header row + const pointIndex = data.points[0].pointIndex + 1; highlightTableRow(pointIndex); } }); @@ -271,17 +391,23 @@ export function renderMeasurementResults(measurementResults, language) { }); } - // Render table from a 2D array + // Render table from a 2D array with colored backgrounds for data columns function renderTable(tableData) { + const tableContainer = document.getElementById('tableContainer'); tableContainer.innerHTML = ''; const table = document.createElement('table'); tableData.forEach((rowData, rowIndex) => { const tr = document.createElement('tr'); - rowData.forEach(cellData => { + rowData.forEach((cellData, cellIndex) => { const cell = document.createElement(rowIndex === 0 ? 'th' : 'td'); cell.textContent = cellData; cell.style.padding = '4px'; cell.style.border = '1px solid #ccc'; + if (rowIndex === 0 && cellIndex > 0) { + const dataIndex = Math.floor((cellIndex - 1) / 3); + const color = palette[dataIndex % palette.length]; + cell.style.backgroundColor = lightPalette[dataIndex % lightPalette.length]; + } tr.appendChild(cell); }); tr.addEventListener('mouseover', () => { tr.style.backgroundColor = '#eef'; }); @@ -291,28 +417,25 @@ export function renderMeasurementResults(measurementResults, language) { tableContainer.appendChild(table); } - // Functions for coupled table row highlights function highlightTableRow(rowIndex) { - const rows = tableContainer.querySelectorAll('tr'); + const rows = document.getElementById('tableContainer').querySelectorAll('tr'); if (rows[rowIndex]) { rows[rowIndex].style.backgroundColor = '#fee'; } } + function clearTableRowHighlights() { - const rows = tableContainer.querySelectorAll('tr'); + const rows = document.getElementById('tableContainer').querySelectorAll('tr'); rows.forEach(row => row.style.backgroundColor = ''); } - // Initial update updateVisualization(); - // Update visualization when X-axis selection changes const radios = document.querySelectorAll('input[name="xAxisSelect"]'); radios.forEach(radio => { radio.addEventListener('change', updateVisualization); }); - // Tolerance toggle event (placeholder) toleranceToggle.addEventListener('change', () => { console.log('Tolerance toggle:', toleranceToggle.checked); // Future: update plot/table for tolerance markings and color coding -- GitLab