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