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