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