Skip to content
Snippets Groups Projects
MeasurementRenderer.js 20.67 KiB
import Plotly from 'plotly.js-dist';
import { DCCRealListQuantity, DCCConformity } from '../dccQuantity.js';
import JSONEditor from 'jsoneditor';
import 'jsoneditor/dist/jsoneditor.css';
import { UsedMethodsRenderer } from './UsedMethodsRenderer.js';
import { InfluenceConditionsRenderer } from './InfluenceConditionsRenderer.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 = '';

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

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

    // Render Used Methods directly (always visible but collapsed)
    const usedMethodsContainer = document.createElement('div');
    const usedMethodsRenderer = new UsedMethodsRenderer(result['dcc:usedMethods'], language);
    usedMethodsContainer.appendChild(usedMethodsRenderer.render());
    tabPanel.appendChild(usedMethodsContainer);

    // Render Influence Conditions using the new renderer (collapsible by default)
    const influenceRenderer = new InfluenceConditionsRenderer(result['dcc:influenceConditions'], language);
    const influenceDetails = document.createElement('div');
    influenceDetails.open = false;
    const influenceSummary = document.createElement('summary');
    influenceDetails.appendChild(influenceSummary);
    influenceDetails.appendChild(influenceRenderer.render());
    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.';
  }
  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 (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;
    }
  }
  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);
    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 = [];
  const extraInfo = [];
  quantityJSONs.forEach(q => {
    if (q.$ && q.$.refType && q.$.refType.match(/basic_tableIndex/)) {
      indexQuantities.push(new DCCRealListQuantity(q));
    } else {
      const quantity = new DCCRealListQuantity(q);
      dataQuantities.push(quantity);
      let uncertainty = quantity.getUncertainty();
      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]; }
        const confMeta = md.find(item => item.$ && item.$.refType && item.$.refType.includes('basic_conformity'));
        if (confMeta) {
          conformity = new DCCConformity(confMeta, language);
        }
      }
      extraInfo.push({ uncertainty, conformity });
    }
  });

  const dataHeaders = dataQuantities.map((q, idx) => {
    let headerText = q.getName(language);
    let unit = q.getUnit();
    if (!headerText.toLowerCase().includes(" in ")) {
      headerText = headerText + " in " + unit;
    }
    return headerText;
  });

  // Create scaling toggles and X-axis selector controls
  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);
  tabPanel.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);
  });
  tabPanel.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);
  tabPanel.appendChild(tolContainer);

  // Create containers for plots and table.
  const subplotsContainer = document.createElement('div');
  subplotsContainer.id = 'subplotsContainer';
  tabPanel.appendChild(subplotsContainer);
  const tableContainer = document.createElement('div');
  tableContainer.id = 'tableContainer';
  tabPanel.appendChild(tableContainer);

  function updateVisualization() {
    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];
    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]);
      if (info.conformity) { headers.push('Conformity'); }
      dataValues.push(dataQuantities[idx].getValues());
      uncertaintiesArray.push(info.uncertainty || []);
      conformityArray.push(info.conformity ? info.conformity.getConformityValues() : []);
    });
    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(conformityArray[idx][i] || '');
        }
      });
      row.push(extraInfo.map(info => info.comment || '').filter(c => c).join(' ; '));
      tableData.push(row);
    }
    renderTable(tableData, computeConformityMapping());

    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 conformityColors.fail;
        }
        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('')
        };
      });

      const toleranceTraces = [];
      if (tabPanel.querySelector('#toleranceToggle').checked) {
        group.forEach(trace => {
          const confObj = extraInfo[trace.index].conformity;
          if (confObj) {
            const lower = confObj.getLowerLimit(language);
            const upper = confObj.getUpperLimit(language);
            if (lower && typeof lower.value === 'number') {
              toleranceTraces.push({
                x: [Math.min(...xValues), Math.max(...xValues)],
                y: [lower.value, lower.value],
                mode: 'lines',
                line: { dash: 'dash', color: '#d62728', width: 2 },
                name: lower.name,
                hoverinfo: 'none',
                showlegend: true
              });
            }
            if (upper && typeof upper.value === 'number') {
              toleranceTraces.push({
                x: [Math.min(...xValues), Math.max(...xValues)],
                y: [upper.value, upper.value],
                mode: 'lines',
                line: { dash: 'dot', color: '#d62728', width: 2 },
                name: upper.name,
                hoverinfo: 'none',
                showlegend: true
              });
            }
          }
        });
      }
      const allTraces = groupTraces.concat(toleranceTraces);

      let xaxisTitle = '';
      if (groupIdx === unitKeys.length - 1) {
        let xLabel = 'X: ' + xQuantity.getName(language);
        xaxisTitle = '<b>' + xLabel + ' in ' + xUnit + '</b>';
      }
      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' } },
          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'
        },
        legend: {
          orientation: 'h',
          x: 0.5,
          y: 0.98,
          xanchor: 'center',
          yanchor: 'top'
        },
        hovermode: 'closest',
        margin: { t: 20, b: 40 }
      };
      Plotly.newPlot(graphDiv, allTraces, layout).then(() => {
        // Force a resize to ensure full width
        setTimeout(() => {
          if (graphDiv.offsetWidth > 0 && graphDiv.offsetHeight > 0) {
            Plotly.Plots.resize(graphDiv);
          } else {
            console.warn('Plot div is not visible; skipping resize.');
          }
        }, 100);
        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, conformityMapping) {
    const tableContainer = tabPanel.querySelector('#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 && conformityMapping[cellIndex] !== undefined) {
            cell.style.backgroundColor = conformityMapping[cellIndex];
          }
        } else {
          cell.textContent = cellData;
          if (tableData[0][cellIndex] && tableData[0][cellIndex].toLowerCase().includes('conformity')) {
            const confKey = cellData.trim().toLowerCase();
            if (conformityColors[confKey]) {
              cell.style.backgroundColor = conformityColors[confKey];
            }
          }
        }
        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 computeConformityMapping() {
    let mapping = {};
    let col = 1; // Column 0 is X-Axis.
    for (let i = 0; i < dataHeaders.length; i++) {
      mapping[col] = palette[i % palette.length]; // Color the data header column
      col++;
      if (extraInfo[i] && extraInfo[i].conformity) {
        mapping[col] = palette[i % palette.length]; // Color the conformity header column
        col++;
      }
    }
    return mapping;
  }

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

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

  updateVisualization();
  const radios = tabPanel.querySelectorAll('input[name="xAxisSelect"]');
  radios.forEach(radio => { radio.addEventListener('change', 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();
  });
}