From 09fe8d4503f0e642fa7b4c7f6a0f3ee289c27451 Mon Sep 17 00:00:00 2001 From: Benedikt Seeger <benedikt.seeger@ptb.de> Date: Thu, 27 Feb 2025 13:21:05 +0100 Subject: [PATCH] fixed sirealListXMLList uncer parsing --- package-lock.json | 6 + package.json | 5 +- src/dccQuantity.js | 124 ++++++++++++++++ src/renderers/MeasurementRenderer.js | 212 +++++++-------------------- 4 files changed, 186 insertions(+), 161 deletions(-) create mode 100644 src/dccQuantity.js diff --git a/package-lock.json b/package-lock.json index d7d8d7b..86247cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "dcc-viewer", "version": "1.0.0", "dependencies": { + "dsiunits-js": "^0.9.1", "events": "^3.3.0", "jsoneditor": "^9.5.6", "plotly.js-dist": "^2.18.2", @@ -419,6 +420,11 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/dsiunits-js": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/dsiunits-js/-/dsiunits-js-0.9.1.tgz", + "integrity": "sha512-4xrSwUks86EteWgQkm38Kb9iC2BlslMxp8tUld304B7GnFoQOBNV7qtaSSKWDR0vXramHNpueq00YO155LXttQ==" + }, "node_modules/esbuild": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", diff --git a/package.json b/package.json index b57ad8b..1a58818 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,11 @@ "preview": "vite preview" }, "dependencies": { - "xml2js": "^0.4.23", + "dsiunits-js": "^0.9.1", + "events": "^3.3.0", "jsoneditor": "^9.5.6", "plotly.js-dist": "^2.18.2", - "events": "^3.3.0" + "xml2js": "^0.4.23" }, "devDependencies": { "vite": "^4.0.0" diff --git a/src/dccQuantity.js b/src/dccQuantity.js new file mode 100644 index 0000000..23068d2 --- /dev/null +++ b/src/dccQuantity.js @@ -0,0 +1,124 @@ +// src/dccQuantity.js +import { DSIUnit } from "dsiunits-js/src/dsiUnit.js"; +// Import the unit input component if needed: +// import "dsi-units-js-lib/src/dsiUnitInput.js"; + +/** + * Base class for a DCC quantity. + * Holds a pointer to the original JSON data. + */ +export class DCCQuantity { + constructor(jsonData) { + this.jsonData = jsonData; + } + + /** + * Returns the refType attribute of the quantity. + */ + getRefType() { + return (this.jsonData.$ && this.jsonData.$.refType) ? this.jsonData.$.refType : ''; + } + + /** + * Returns the quantity name in the specified language. + * @param {string} language - e.g., 'en', 'de' + */ + getName(language) { + const nameData = this.jsonData['dcc:name']; + if (!nameData) return ''; + const content = nameData['dcc:content']; + if (Array.isArray(content)) { + const match = content.find(item => item.$ && item.$.lang === language) || content[0]; + return match._ || match; + } else { + return content._ || content; + } + } +} + +/** + * Class for quantities represented by <si:realListXMLList>. + */ +export class DCCRealListQuantity extends DCCQuantity { + constructor(jsonData) { + super(jsonData); + } + + /** + * Extracts and returns an array of numeric values. + */ + getValues() { + const realList = this.jsonData['si:realListXMLList']; + if (realList && realList['si:valueXMLList']) { + return realList['si:valueXMLList'] + .trim() + .split(/\s+/) + .map(v => parseFloat(v)); + } + return []; + } + + /** + * Returns the unit string rendered as HTML using the dsi-units-js library. + */ + getUnit() { + const realList = this.jsonData['si:realListXMLList']; + if (realList && realList['si:unitXMLList']) { + const rawUnit = realList['si:unitXMLList'].trim(); + const unit = new DSIUnit(rawUnit); + // Render the unit in a one-line format + return unit.toHTML({ oneLine: true }); + } + return ''; + } + + /** + * Extracts and returns an array of uncertainty values. + */ + getUncertainty() { + const realList = this.jsonData['si:realListXMLList']; + if ( + realList['si:measurementUncertaintyUnivariateXMLList'] && + realList['si:measurementUncertaintyUnivariateXMLList']['si:expandedMUXMLList'] && + realList['si:measurementUncertaintyUnivariateXMLList']['si:expandedMUXMLList']['si:valueExpandedMUXMLList'] + ) { + const errStr = realList['si:measurementUncertaintyUnivariateXMLList']['si:expandedMUXMLList']['si:valueExpandedMUXMLList']; + return errStr.trim().split(/\s+/).map(v => parseFloat(v)); + } + return []; + } +} + +/** + * Class for quantities represented by a single <si:real>. + * (This class is provided for completeness and may be expanded as needed.) + */ +export class DCCRealQuantity extends DCCQuantity { + constructor(jsonData) { + super(jsonData); + } + + /** + * Returns the numeric value from the <si:real> element. + */ + getValue() { + const realData = this.jsonData['si:real']; + if (realData && realData['si:value']) { + return parseFloat(realData['si:value']); + } + return null; + } + + /** + * Returns the unit rendered as HTML. + */ + getUnit() { + const realData = this.jsonData['si:real']; + if (realData && realData['si:unit']) { + const rawUnit = realData['si:unit'].trim(); + const unit = new DSIUnit(rawUnit); + return unit.toHTML({ oneLine: true }); + } + return ''; + } +} \ No newline at end of file diff --git a/src/renderers/MeasurementRenderer.js b/src/renderers/MeasurementRenderer.js index 5c44671..8befef2 100644 --- a/src/renderers/MeasurementRenderer.js +++ b/src/renderers/MeasurementRenderer.js @@ -1,11 +1,6 @@ import Plotly from 'plotly.js-dist'; +import { DCCRealListQuantity } from '../dccQuantity.js'; -// Dummy unit converter: currently returns the raw unit (to be enhanced later) -function convertUnit(rawUnit) { - return rawUnit; -} - -// Define Tab10 palette and lighter shades for table backgrounds const palette = [ '#1f77b4', '#ff7f0e', @@ -44,12 +39,10 @@ export function renderMeasurementResults(measurementResults, language) { let results = measurementResults['dcc:measurementResult']; console.debug('Raw dcc:measurementResult:', results); - if (!results) { console.error("Missing 'dcc:measurementResult' in measurementResults"); return; } - if (!Array.isArray(results)) { results = [results]; } @@ -58,7 +51,6 @@ export function renderMeasurementResults(measurementResults, language) { const measurementResult = results[0]; console.debug('Using measurementResult:', measurementResult); - // Extract the result object from dcc:results -> dcc:result let resultObj = measurementResult['dcc:results'] && measurementResult['dcc:results']['dcc:result']; if (!resultObj) { console.error("Missing 'dcc:results' or 'dcc:result' in measurementResult:", measurementResult); @@ -69,7 +61,6 @@ export function renderMeasurementResults(measurementResults, language) { } console.debug('Using result object:', resultObj); - // 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']; @@ -81,7 +72,6 @@ export function renderMeasurementResults(measurementResults, language) { } } console.debug('Result name:', resultName); - const tabTitle = document.createElement('h2'); tabTitle.textContent = resultName; container.appendChild(tabTitle); @@ -93,14 +83,12 @@ export function renderMeasurementResults(measurementResults, language) { const listData = resultObj['dcc:data']['dcc:list']; console.debug('List data:', listData); - // Flatten quantities from the list - let quantities = []; + let quantityJSONs = []; if (listData['dcc:quantity']) { - quantities = Array.isArray(listData['dcc:quantity']) ? listData['dcc:quantity'] : [listData['dcc:quantity']]; + quantityJSONs = Array.isArray(listData['dcc:quantity']) + ? listData['dcc:quantity'] + : [listData['dcc:quantity']]; } - console.debug('Quantities from list:', quantities); - - // Also add quantities from measurementMetaData if available if (listData['dcc:measurementMetaData'] && listData['dcc:measurementMetaData']['dcc:metaData']) { let metaData = listData['dcc:measurementMetaData']['dcc:metaData']; if (!Array.isArray(metaData)) metaData = [metaData]; @@ -108,30 +96,22 @@ export function renderMeasurementResults(measurementResults, language) { if (md['dcc:data'] && md['dcc:data']['dcc:quantity']) { let qs = md['dcc:data']['dcc:quantity']; if (!Array.isArray(qs)) qs = [qs]; - quantities = quantities.concat(qs); + quantityJSONs = quantityJSONs.concat(qs); } }); } - console.debug('Combined quantities:', quantities); + console.debug('Combined quantity JSONs:', quantityJSONs); - // Separate index quantities and data quantities; extract extra info (uncertainty, comment, conformity) const indexQuantities = []; const dataQuantities = []; const extraInfo = []; - quantities.forEach(q => { + quantityJSONs.forEach(q => { if (q.$ && q.$.refType && q.$.refType.match(/basic_tableIndex/)) { - indexQuantities.push(q); + indexQuantities.push(new DCCRealListQuantity(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)); - } + dataQuantities.push(new DCCRealListQuantity(q)); + let uncertainty = (new DCCRealListQuantity(q)).getUncertainty(); let comment = ''; - let validArray = []; let conformity = null; if (q['dcc:measurementMetaData'] && q['dcc:measurementMetaData']['dcc:metaData']) { let md = q['dcc:measurementMetaData']['dcc:metaData']; @@ -147,9 +127,6 @@ export function renderMeasurementResults(measurementResults, language) { comment = desc._ || desc; } } - if (item['dcc:validXMLList']) { - validArray = item['dcc:validXMLList'].trim().split(/\s+/); - } } if (item.$ && item.$.refType && item.$.refType.includes('basic_conformity')) { if (item['dcc:conformityXMLList']) { @@ -158,27 +135,17 @@ export function renderMeasurementResults(measurementResults, language) { } }); } - extraInfo.push({ uncertainty, comment, valid: validArray, conformity }); + extraInfo.push({ uncertainty, comment, conformity }); } }); - console.debug('Index Quantities:', indexQuantities); - console.debug('Data Quantities:', dataQuantities); + console.debug('Index Quantities (objects):', indexQuantities); + console.debug('Data Quantities (objects):', dataQuantities); console.debug('Extra info for data quantities:', extraInfo); - // Create radio buttons for X-axis selection const xAxisContainer = document.createElement('div'); xAxisContainer.innerHTML = '<strong>Select X-Axis:</strong> '; indexQuantities.forEach((q, idx) => { - let nameStr = 'Index ' + idx; - if (q['dcc:name'] && q['dcc:name']['dcc:content']) { - let content = q['dcc:name']['dcc:content']; - if (Array.isArray(content)) { - const match = content.find(item => item.$ && item.$.lang === language) || content[0]; - nameStr = match._ || match; - } else { - nameStr = content._ || content; - } - } + let nameStr = q.getName(language) || ('Index ' + idx); const radio = document.createElement('input'); radio.type = 'radio'; radio.name = 'xAxisSelect'; @@ -192,7 +159,6 @@ export function renderMeasurementResults(measurementResults, language) { }); container.appendChild(xAxisContainer); - // Tolerance toggle (placeholder) const toleranceToggle = document.createElement('input'); toleranceToggle.type = 'checkbox'; toleranceToggle.id = 'toleranceToggle'; @@ -204,17 +170,13 @@ export function renderMeasurementResults(measurementResults, language) { tolContainer.appendChild(tolLabel); container.appendChild(tolContainer); - // 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 visualization function updateVisualization() { const selectedRadio = document.querySelector('input[name="xAxisSelect"]:checked'); if (!selectedRadio) { @@ -223,22 +185,13 @@ export function renderMeasurementResults(measurementResults, language) { } const selectedIndex = selectedRadio.value; const xQuantity = indexQuantities[selectedIndex]; - let xValues = []; - 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(); - } - } + const xValues = xQuantity.getValues(); + const xUnit = xQuantity.getUnit(); console.debug('Selected X-Axis values:', xValues); console.debug('X-Axis unit:', xUnit); - // Build table headers and arrays for values, comments, conformity, uncertainties const headers = []; - let xHeader = 'X-Axis (selected) (' + convertUnit(xUnit) + ')'; + const xHeader = 'X-Axis (selected) (' + xUnit + ')'; headers.push(xHeader); const dataValues = []; @@ -246,54 +199,24 @@ export function renderMeasurementResults(measurementResults, language) { 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)) { - const match = content.find(item => item.$ && item.$.lang === language) || content[0]; - header = match._ || match; - } else { - header = content._ || content; - } - } - if (q['si:realListXMLList'] && q['si:realListXMLList']['si:unitXMLList']) { - unit = q['si:realListXMLList']['si:unitXMLList'].trim(); + let header = q.getName(language); + let unit = q.getUnit(); + if (!header.toLowerCase().includes(" in ")) { + header = header + " in " + unit; } - headers.push(header + ' in ' + convertUnit(unit)); + headers.push(header); headers.push('Comments'); - // Add conformity header only if data exists - if (extraInfo[idx] && extraInfo[idx].conformity) { - headers.push('Conformity'); - } else { - headers.push(''); - } - - let values = []; - if (q['si:realListXMLList'] && q['si:realListXMLList']['si:valueXMLList']) { - values = q['si:realListXMLList']['si:valueXMLList'].trim().split(/\s+/).map(v => parseFloat(v)); - } + headers.push(extraInfo[idx] && extraInfo[idx].conformity ? 'Conformity' : ''); + let values = q.getValues(); if (values.length === 1 && xValues.length > 1) { 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 : ''; - commentsArray.push(comment); - - let conformity = []; - if (extraInfo[idx] && extraInfo[idx].conformity) { - conformity = extraInfo[idx].conformity; - } - conformityArray.push(conformity); + uncertaintiesArray.push(extraInfo[idx] ? extraInfo[idx].uncertainty : null); + commentsArray.push(extraInfo[idx] ? extraInfo[idx].comment : ''); + conformityArray.push(extraInfo[idx] ? extraInfo[idx].conformity : []); }); - + console.debug('Table data arrays:', { dataValues, commentsArray, conformityArray, uncertaintiesArray }); const tableData = [headers]; for (let i = 0; i < xValues.length; i++) { const row = []; @@ -305,61 +228,42 @@ export function renderMeasurementResults(measurementResults, language) { } row.push(cellValue); row.push(commentsArray[idx] || ''); - if (conformityArray[idx] && conformityArray[idx][i]) { - row.push(conformityArray[idx][i]); - } else { - row.push(''); - } + row.push(conformityArray[idx] && conformityArray[idx][i] ? conformityArray[idx][i] : ''); }); tableData.push(row); } - console.debug('Table data:', tableData); + console.debug('Final table data:', tableData); renderTable(tableData); - // Group data quantities by unit for plotting and assign colors const unitGroups = {}; dataQuantities.forEach((q, idx) => { - let unit = ''; - if (q['si:realListXMLList'] && q['si:realListXMLList']['si:unitXMLList']) { - unit = q['si:realListXMLList']['si:unitXMLList'].trim(); - } + const unit = q.getUnit(); if (!unitGroups[unit]) { unitGroups[unit] = []; } 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)); - } + let values = q.getValues(); if (values.length === 1 && xValues.length > 1) { values = new Array(xValues.length).fill(values[0]); } - let uncertainty = uncertaintiesArray[idx]; - let conformity = []; - if (conformityArray[idx]) { - conformity = conformityArray[idx]; - } - // Use Tab10 palette; assign a color for this data quantity - let traceColor = palette[idx % palette.length]; - unitGroups[unit].push({ name: header, y: values, uncertainty: uncertainty, conformity: conformity, color: traceColor, index: idx }); + const uncertainty = uncertaintiesArray[idx]; + const conformity = conformityArray[idx]; + const traceColor = palette[idx % palette.length]; + unitGroups[unit].push({ name: header, y: values, uncertainty, conformity, color: traceColor, index: idx }); }); console.debug('Unit groups for plots:', unitGroups); - // Build plots: one subplot per unit group, sharing the same X-axis - const plotsContainer = document.getElementById('subplotsContainer'); - plotsContainer.innerHTML = ''; + subplotsContainer.innerHTML = ''; const unitKeys = Object.keys(unitGroups); unitKeys.forEach((unit, groupIdx) => { const group = unitGroups[unit]; const graphDiv = document.createElement('div'); graphDiv.style.width = '100%'; graphDiv.style.height = '300px'; - plotsContainer.appendChild(graphDiv); + subplotsContainer.appendChild(graphDiv); - // Build traces for this group const groupTraces = group.map(trace => { - // Prepare tooltip with short format - const hovertemplate = 'X: %{x} ' + convertUnit(xUnit) + ' | ' + trace.name + ': %{y} ± %{error_y.array} ' + convertUnit(unit) + ' | Conformity: %{customdata}<extra></extra>'; + const hovertemplate = 'X: %{x} ' + xUnit + ' | ' + trace.name + ': %{y} ± %{customdata_unc} ' + unit + ' | Conformity: %{customdata}<extra></extra>'; return { x: xValues, y: trace.y, @@ -369,38 +273,27 @@ export function renderMeasurementResults(measurementResults, language) { name: trace.name, marker: { color: trace.color }, hovertemplate: hovertemplate, - customdata: (trace.conformity && trace.conformity.length === xValues.length) ? trace.conformity : new Array(xValues.length).fill('') + customdata: (trace.conformity && trace.conformity.length === xValues.length) ? trace.conformity : new Array(xValues.length).fill(''), + customdata_unc: (trace.uncertainty && trace.uncertainty.length === xValues.length) ? trace.uncertainty : new Array(xValues.length).fill('0') }; }); - // Only the last subplot gets an X-axis label let xaxisTitle = ''; if (groupIdx === unitKeys.length - 1) { - let xLabel = 'X: '; - 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]; - xLabel += match._ || match; - } else { - xLabel += content._ || content; - } - } - xaxisTitle = xLabel + ' in ' + convertUnit(xUnit); + let xLabel = 'X: ' + xQuantity.getName(language); + xaxisTitle = xLabel + ' in ' + xUnit; } - const layout = { xaxis: { title: { text: xaxisTitle, font: { size: 18, family: 'Arial', color: 'black' } }, tickfont: { family: 'Arial', size: 14, color: 'black' } }, - yaxis: { title: { text: convertUnit(unit), font: { size: 18, family: 'Arial', color: 'black' } }, tickfont: { family: 'Arial', size: 14, color: 'black' } }, + yaxis: { title: { text: unit, font: { size: 18, family: 'Arial', color: 'black' } }, tickfont: { family: 'Arial', size: 14, color: 'black' } }, hovermode: 'closest', margin: { t: 20, b: 40 } }; Plotly.newPlot(graphDiv, groupTraces, layout).then(() => { console.debug('Plot rendered for unit:', unit); - // Bold caption above the plot: "QuantityName in Unit"; remove duplicate unit if any const caption = document.createElement('div'); - caption.innerHTML = '<b>' + group[0].name + ' in ' + convertUnit(unit) + '</b>'; + caption.innerHTML = '<b>' + group[0].name +'</b>'; caption.style.textAlign = 'center'; caption.style.marginBottom = '5px'; graphDiv.parentNode.insertBefore(caption, graphDiv); @@ -408,7 +301,7 @@ export function renderMeasurementResults(measurementResults, language) { 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); } }); @@ -418,7 +311,6 @@ export function renderMeasurementResults(measurementResults, language) { }); } - // Render table from a 2D array with colored backgrounds for data columns function renderTable(tableData) { const tableContainer = document.getElementById('tableContainer'); tableContainer.innerHTML = ''; @@ -427,7 +319,12 @@ export function renderMeasurementResults(measurementResults, language) { const tr = document.createElement('tr'); rowData.forEach((cellData, cellIndex) => { const cell = document.createElement(rowIndex === 0 ? 'th' : 'td'); - cell.textContent = cellData; + // For headers, allow HTML rendering (dsiUnits output) by setting innerHTML. + if (rowIndex === 0) { + cell.innerHTML = cellData; + } else { + cell.textContent = cellData; + } cell.style.padding = '4px'; cell.style.border = '1px solid #ccc'; if (rowIndex === 0 && cellIndex > 0) { @@ -456,14 +353,11 @@ export function renderMeasurementResults(measurementResults, language) { } updateVisualization(); - const radios = document.querySelectorAll('input[name="xAxisSelect"]'); radios.forEach(radio => { radio.addEventListener('change', updateVisualization); }); - toleranceToggle.addEventListener('change', () => { console.log('Tolerance toggle:', toleranceToggle.checked); - // Future: update plot/table for tolerance markings and color coding }); } -- GitLab