Skip to content
Snippets Groups Projects
MeasurementRenderer.js 15.43 KiB
import Plotly from 'plotly.js-dist';
import { DCCRealListQuantity } from '../dccQuantity.js';

const palette = [
  '#1f77b4',
  '#ff7f0e',
  '#2ca02c',
  '#d62728',
  '#9467bd',
  '#8c564b',
  '#e377c2',
  '#7f7f7f',
  '#bcbd22',
  '#17becf'
];

const lightPalette = [
  '#c6e2ff',
  '#ffddbf',
  '#c9e6c8',
  '#f4c2c2',
  '#dcd0ff',
  '#e0cda9',
  '#ffccff',
  '#d3d3d3',
  '#e6e6a9',
  '#b3ffff'
];

const conformityColors = {
  pass: '#2ca02c',
  fail: '#d62728',
  conditionalpass: '#8bc34a',
  conditionalfail: '#ff9800',
  nopass: '#9e9e9e',
  nofail: '#9e9e9e'
};

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;
  }

  let results = measurementResults['dcc:measurementResult'];
  if (!results) {
    console.error("Missing 'dcc:measurementResult' in measurementResults");
    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;
  }
  if (Array.isArray(resultObj)) { resultObj = resultObj[0]; }

  let resultName = 'Measurement Result';
  if (measurementResult['dcc:name'] && measurementResult['dcc:name']['dcc:content']) {
    let content = measurementResult['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;
    }
  }
  const tabTitle = document.createElement('h2');
  tabTitle.textContent = resultName;
  container.appendChild(tabTitle);

  if (!resultObj['dcc:data'] || !resultObj['dcc:data']['dcc:list']) {
    console.error("Missing 'dcc:data' or 'dcc:list' in result object:", resultObj);
    return;
  }
  const listData = resultObj['dcc:data']['dcc:list'];

  let quantityJSONs = [];
  if (listData['dcc:quantity']) {
    quantityJSONs = Array.isArray(listData['dcc:quantity'])
        ? listData['dcc:quantity']
        : [listData['dcc:quantity']];
  }
  if (listData['dcc:measurementMetaData'] && listData['dcc:measurementMetaData']['dcc:metaData']) {
    let metaData = listData['dcc:measurementMetaData']['dcc:metaData'];
    if (!Array.isArray(metaData)) { metaData = [metaData]; }
    metaData.forEach(md => {
      if (md['dcc:data'] && md['dcc:data']['dcc:quantity']) {
        let qs = md['dcc:data']['dcc:quantity'];
        if (!Array.isArray(qs)) { qs = [qs]; }
        quantityJSONs = quantityJSONs.concat(qs);
      }
    });
  }

  const indexQuantities = [];
  const dataQuantities = [];
  // extraInfo: for each data quantity, store { uncertainty, comments: [{ comment, validArray }], conformity }
  const extraInfo = [];
  quantityJSONs.forEach(q => {
    if (q.$ && q.$.refType && q.$.refType.match(/basic_tableIndex/)) {
      indexQuantities.push(new DCCRealListQuantity(q));
    } else {
      dataQuantities.push(new DCCRealListQuantity(q));
      const uncertainty = (new DCCRealListQuantity(q)).getUncertainty();
      let comments = [];
      let conformity = null;
      if (q['dcc:measurementMetaData'] && q['dcc:measurementMetaData']['dcc:metaData']) {
        let md = q['dcc:measurementMetaData']['dcc:metaData'];
        if (!Array.isArray(md)) { md = [md]; }
        md.forEach(item => {
          if (item.$ && item.$.refType && item.$.refType.includes('basic_tableRowComment')) {
            let commentText = '';
            if (item['dcc:description'] && item['dcc:description']['dcc:content']) {
              let desc = item['dcc:description']['dcc:content'];
              if (Array.isArray(desc)) {
                const match = desc.find(d => d.$ && d.$.lang === language) || desc[0];
                commentText = match._ || match;
              } else {
                commentText = desc._ || desc;
              }
            }
            let validArray = [];
            if (item['dcc:validXMLList']) {
              validArray = item['dcc:validXMLList'].trim().split(/\s+/);
            }
            comments.push({ comment: commentText, valid: validArray });
          }
          if (item.$ && item.$.refType && item.$.refType.includes('basic_conformity')) {
            if (item['dcc:conformityXMLList']) {
              conformity = item['dcc:conformityXMLList'].trim().split(/\s+/);
            }
          }
        });
      }
      extraInfo.push({ uncertainty, comments, conformity });
    }
  });

  // Build data headers array for each data quantity
  const dataHeaders = dataQuantities.map((q, idx) => {
    let header = q.getName(language);
    let unit = q.getUnit();
    if (!header.toLowerCase().includes(" in ")) {
      header = header + " in " + unit;
    }
    return header;
  });

  // Create scaling toggles for log axes
  const scalingContainer = document.createElement('div');
  scalingContainer.innerHTML = '<strong>Scaling:</strong> ';
  const logXToggle = document.createElement('input');
  logXToggle.type = 'checkbox';
  logXToggle.id = 'logXToggle';
  const logXLabel = document.createElement('label');
  logXLabel.htmlFor = 'logXToggle';
  logXLabel.textContent = 'Log X';
  scalingContainer.appendChild(logXToggle);
  scalingContainer.appendChild(logXLabel);
  const logYToggle = document.createElement('input');
  logYToggle.type = 'checkbox';
  logYToggle.id = 'logYToggle';
  const logYLabel = document.createElement('label');
  logYLabel.htmlFor = 'logYToggle';
  logYLabel.textContent = 'Log Y';
  scalingContainer.appendChild(logYToggle);
  scalingContainer.appendChild(logYLabel);
  container.appendChild(scalingContainer);

  const xAxisContainer = document.createElement('div');
  xAxisContainer.innerHTML = '<strong>Select X-Axis:</strong> ';
  indexQuantities.forEach((q, idx) => {
    let nameStr = q.getName(language) || ('Index ' + idx);
    const radio = document.createElement('input');
    radio.type = 'radio';
    radio.name = 'xAxisSelect';
    radio.value = idx;
    if (idx === 0) { radio.checked = true; }
    const label = document.createElement('label');
    label.textContent = nameStr;
    label.style.marginRight = '10px';
    xAxisContainer.appendChild(radio);
    xAxisContainer.appendChild(label);
  });
  container.appendChild(xAxisContainer);

  const toleranceToggle = document.createElement('input');
  toleranceToggle.type = 'checkbox';
  toleranceToggle.id = 'toleranceToggle';
  const tolLabel = document.createElement('label');
  tolLabel.htmlFor = 'toleranceToggle';
  tolLabel.textContent = ' Enable tolerance markings';
  const tolContainer = document.createElement('div');
  tolContainer.appendChild(toleranceToggle);
  tolContainer.appendChild(tolLabel);
  container.appendChild(tolContainer);

  const subplotsContainer = document.createElement('div');
  subplotsContainer.id = 'subplotsContainer';
  container.appendChild(subplotsContainer);
  const tableContainer = document.createElement('div');
  tableContainer.id = 'tableContainer';
  container.appendChild(tableContainer);

  function updateVisualization() {
    const selectedRadio = document.querySelector('input[name="xAxisSelect"]:checked');
    if (!selectedRadio) { console.error('No X-Axis selection found.'); return; }
    const selectedIndex = selectedRadio.value;
    const xQuantity = indexQuantities[selectedIndex];
    const xValues = xQuantity.getValues();
    const xUnit = xQuantity.getUnit();
    console.debug('Selected X-Axis values:', xValues);
    console.debug('X-Axis unit:', xUnit);

    const headers = [];
    headers.push('X-Axis (selected) (' + xUnit + ')');

    const dataValues = [];
    const uncertaintiesArray = [];
    const conformityArray = [];
    extraInfo.forEach((info, idx) => {
      headers.push(dataHeaders[idx]);
      // Only include Conformity header if conformity exists for this quantity
      if (info.conformity) { headers.push('Conformity'); }
      dataValues.push(dataQuantities[idx].getValues());
      uncertaintiesArray.push(info.uncertainty || []);
      conformityArray.push(info.conformity || []);
    });
    headers.push('Comments');

    const tableData = [headers];
    for (let i = 0; i < xValues.length; i++) {
      const row = [];
      row.push(xValues[i]);
      extraInfo.forEach((info, idx) => {
        let cellValue = (dataValues[idx][i] !== undefined) ? dataValues[idx][i] : '';
        if (uncertaintiesArray[idx] && uncertaintiesArray[idx][i] !== undefined) {
          cellValue = cellValue + ' ± ' + uncertaintiesArray[idx][i];
        }
        row.push(cellValue);
        if (info.conformity) {
          row.push(info.conformity[i] || '');
        }
      });
      // For comments, combine all comments valid at this row from all quantities
      let rowComments = extraInfo.map(info => {
        if (info.comments) {
          // info.comments is an array of { comment, valid } objects
          return info.comments.filter(cObj => cObj.valid[i] === 'true').map(cObj => cObj.comment).join(' ; ');
        }
        return '';
      }).filter(c => c).join(' | ');
      row.push(rowComments);
      tableData.push(row);
    }
    renderTable(tableData);

    const unitGroups = {};
    dataQuantities.forEach((q, idx) => {
      const unit = q.getUnit();
      if (!unitGroups[unit]) { unitGroups[unit] = []; }
      const header = dataHeaders[idx];
      let values = q.getValues();
      const uncertainty = uncertaintiesArray[idx];
      const conformity = conformityArray[idx];
      const defaultColor = palette[idx % palette.length];
      const markerColorArray = values.map((val, i) => {
        if (conformity && conformity[i] && conformity[i].toLowerCase().includes('fail')) {
          return '#d62728';
        }
        return defaultColor;
      });
      unitGroups[unit].push({ name: header, y: values, uncertainty, conformity, defaultColor, markerColor: markerColorArray, index: idx });
    });

    subplotsContainer.innerHTML = '';
    const unitKeys = Object.keys(unitGroups);
    const plotDivs = [];
    unitKeys.forEach((unit, groupIdx) => {
      const group = unitGroups[unit];
      const graphDiv = document.createElement('div');
      graphDiv.style.width = '100%';
      graphDiv.style.height = '300px';
      subplotsContainer.appendChild(graphDiv);
      plotDivs.push(graphDiv);
      const groupTraces = group.map(trace => {
        let tooltip = 'X: %{x} ' + xUnit + ' | ' + trace.name + ': %{y}';
        if (trace.conformity && trace.conformity.length > 0) {
          tooltip += ' | Conformity: %{customdata}';
        }
        tooltip += '<extra></extra>';
        return {
          x: xValues,
          y: trace.y,
          error_y: {
            type: 'data',
            array: (trace.uncertainty && trace.uncertainty.length === xValues.length) ? trace.uncertainty : new Array(xValues.length).fill(0),
            visible: true,
            color: trace.defaultColor
          },
          type: 'scatter',
          mode: 'lines+markers',
          name: trace.name,
          marker: { color: trace.markerColor },
          line: { color: trace.defaultColor },
          hovertemplate: tooltip,
          customdata: (trace.conformity && trace.conformity.length === xValues.length) ? trace.conformity : new Array(xValues.length).fill('')
        };
      });
      let xaxisTitle = '';
      if (groupIdx === unitKeys.length - 1) {
        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 layout = {
        xaxis: {
          title: { text: xaxisTitle, font: { size: 36, family: 'Arial', color: 'black' } },
          tickfont: { family: 'Arial', size: 14, color: 'black' },
          type: logX ? 'log' : 'linear'
        },
        yaxis: {
          title: { text: unit, font: { size: 18, family: 'Arial', color: 'black' } },
          tickfont: { family: 'Arial', size: 14, color: 'black' },
          type: logY ? 'log' : 'linear'
        },
        hovermode: 'closest',
        margin: { t: 20, b: 40 }
      };
      Plotly.newPlot(graphDiv, groupTraces, layout).then(() => {
        const caption = document.createElement('div');
        caption.innerHTML = '<b>' + group[0].name + '</b>';
        caption.style.textAlign = 'center';
        caption.style.marginBottom = '5px';
        graphDiv.parentNode.insertBefore(caption, graphDiv);
      });
      graphDiv.on('plotly_hover', function(data) {
        if (data.points && data.points.length > 0) {
          const pointIndex = data.points[0].pointIndex + 1;
          highlightTableRow(pointIndex);
        }
      });
      graphDiv.on('plotly_unhover', function() {
        clearTableRowHighlights();
      });
      graphDiv.on('plotly_relayout', function(eventData) {
        plotDivs.forEach(div => {
          if (div !== graphDiv && eventData['xaxis.range[0]'] && eventData['xaxis.range[1]']) {
            Plotly.relayout(div, { 'xaxis.range': [eventData['xaxis.range[0]'], eventData['xaxis.range[1]']] });
          }
        });
      });
    });
  }

  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, cellIndex) => {
        const cell = document.createElement(rowIndex === 0 ? 'th' : 'td');
        if (rowIndex === 0) {
          cell.innerHTML = cellData;
          if (cellData !== 'Comments' && cellIndex > 0) {
            // Use correct header mapping: assume each quantity occupies either 1 (if no conformity) or 2 columns.
            // We'll compute the index based on dataHeaders length.
            const qtyIndex = cellIndex % 2 === 1 ? Math.floor(cellIndex / 2) : Math.floor((cellIndex - 1) / 2);
            cell.style.backgroundColor = lightPalette[qtyIndex % lightPalette.length];
          }
        } else {
          cell.textContent = cellData;
        }
        cell.style.padding = '4px';
        cell.style.border = '1px solid #ccc';
        tr.appendChild(cell);
      });
      tr.addEventListener('mouseover', () => { tr.style.backgroundColor = '#eef'; });
      tr.addEventListener('mouseout', () => { tr.style.backgroundColor = ''; });
      table.appendChild(tr);
    });
    tableContainer.appendChild(table);
  }

  function highlightTableRow(rowIndex) {
    const rows = document.getElementById('tableContainer').querySelectorAll('tr');
    if (rows[rowIndex]) {
      rows[rowIndex].style.backgroundColor = '#fee';
    }
  }

  function clearTableRowHighlights() {
    const rows = document.getElementById('tableContainer').querySelectorAll('tr');
    rows.forEach(row => row.style.backgroundColor = '');
  }

  updateVisualization();
  const radios = document.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); });
}