diff --git a/project_structure.json b/project_structure.json index 55cbd1ed5d920d4f753cbe5d28b922fb3cc161d3..5fffa51cd401c0d5f38bd14aec351c110fd33961 100644 --- a/project_structure.json +++ b/project_structure.json @@ -1,7 +1,7 @@ { "src": { "renderers": { - "MeasurementRenderer.js": "import Plotly from 'plotly.js-dist';\n\nexport function renderMeasurementResults(measurementResults, language) {\n console.debug('renderMeasurementResults called with:', measurementResults);\n const container = document.getElementById('measurementResults');\n container.innerHTML = '';\n\n if (!measurementResults) {\n console.error('No measurementResults provided.');\n return;\n }\n\n let result = measurementResults['dcc:measurementResult'];\n console.debug('Raw dcc:measurementResult:', result);\n\n if (!result) {\n console.error(\"Missing 'dcc:measurementResult' in measurementResults\");\n return;\n }\n\n if (!Array.isArray(result)) {\n result = [result];\n }\n console.debug('Processed measurement result array:', result);\n\n // Use the first measurement result for rendering\n result = result[0];\n\n // Get the measurement result name from dcc:name\n let resultName = 'Measurement Result';\n if (result['dcc:name'] && result['dcc:name']['dcc:content']) {\n let content = result['dcc:name']['dcc:content'];\n if (Array.isArray(content)) {\n const match = content.find(item => item.$ && item.$.lang === language) || content[0];\n resultName = match._ || match;\n } else {\n resultName = content._ || content;\n }\n }\n console.debug('Result name:', resultName);\n\n const tabTitle = document.createElement('h2');\n tabTitle.textContent = resultName + ' (' + language + ')';\n container.appendChild(tabTitle);\n\n if (!result['dcc:data'] || !result['dcc:data']['dcc:list']) {\n console.error(\"Missing 'dcc:data' or 'dcc:list' in measurement result:\", result);\n return;\n }\n const listData = result['dcc:data']['dcc:list'];\n console.debug('List data:', listData);\n\n // Flatten all quantities from the list\n let quantities = [];\n if (listData['dcc:quantity']) {\n quantities = Array.isArray(listData['dcc:quantity']) ? listData['dcc:quantity'] : [listData['dcc:quantity']];\n }\n console.debug('Quantities from list:', quantities);\n\n // Also add quantities from measurementMetaData\n if (listData['dcc:measurementMetaData'] && listData['dcc:measurementMetaData']['dcc:metaData']) {\n let metaData = listData['dcc:measurementMetaData']['dcc:metaData'];\n if (!Array.isArray(metaData)) metaData = [metaData];\n metaData.forEach(md => {\n if (md['dcc:data'] && md['dcc:data']['dcc:quantity']) {\n let qs = md['dcc:data']['dcc:quantity'];\n if (!Array.isArray(qs)) qs = [qs];\n quantities = quantities.concat(qs);\n }\n });\n }\n console.debug('Combined quantities:', quantities);\n\n // Separate index quantities and data quantities\n const indexQuantities = [];\n const dataQuantities = [];\n quantities.forEach(q => {\n if (q.$ && q.$.refType && q.$.refType.match(/basic_tableIndex/)) {\n indexQuantities.push(q);\n } else {\n dataQuantities.push(q);\n }\n });\n console.debug('Index Quantities:', indexQuantities);\n console.debug('Data Quantities:', dataQuantities);\n\n // Create radio buttons for X-axis selection (from indexQuantities)\n const xAxisContainer = document.createElement('div');\n xAxisContainer.innerHTML = '<strong>Select X-Axis:</strong> ';\n indexQuantities.forEach((q, idx) => {\n let nameStr = 'Index ' + idx;\n if (q['dcc:name'] && q['dcc:name']['dcc:content']) {\n let content = q['dcc:name']['dcc:content'];\n if (Array.isArray(content)) {\n const match = content.find(item => item.$ && item.$.lang === language) || content[0];\n nameStr = match._ || match;\n } else {\n nameStr = content._ || content;\n }\n }\n const radio = document.createElement('input');\n radio.type = 'radio';\n radio.name = 'xAxisSelect';\n radio.value = idx;\n if (idx === 0) radio.checked = true;\n const label = document.createElement('label');\n label.textContent = nameStr;\n label.style.marginRight = '10px';\n xAxisContainer.appendChild(radio);\n xAxisContainer.appendChild(label);\n });\n container.appendChild(xAxisContainer);\n\n // Tolerance toggle (placeholder)\n const toleranceToggle = document.createElement('input');\n toleranceToggle.type = 'checkbox';\n toleranceToggle.id = 'toleranceToggle';\n const tolLabel = document.createElement('label');\n tolLabel.htmlFor = 'toleranceToggle';\n tolLabel.textContent = ' Enable tolerance markings';\n const tolContainer = document.createElement('div');\n tolContainer.appendChild(toleranceToggle);\n tolContainer.appendChild(tolLabel);\n container.appendChild(tolContainer);\n\n // Create containers for plots and table\n const plotsContainer = document.createElement('div');\n plotsContainer.id = 'plotsContainer';\n container.appendChild(plotsContainer);\n const tableContainer = document.createElement('div');\n tableContainer.id = 'tableContainer';\n container.appendChild(tableContainer);\n\n // Function to update the visualization\n function updateVisualization() {\n const selectedRadio = document.querySelector('input[name=\"xAxisSelect\"]:checked');\n if (!selectedRadio) {\n console.error('No X-Axis selection found.');\n return;\n }\n const selectedIndex = selectedRadio.value;\n const xQuantity = indexQuantities[selectedIndex];\n let xValues = [];\n if (xQuantity && xQuantity['si:realListXMLList'] && xQuantity['si:realListXMLList']['si:valueXMLList']) {\n xValues = xQuantity['si:realListXMLList']['si:valueXMLList'].trim().split(/\\s+/).map(v => parseFloat(v));\n }\n console.debug('Selected X-Axis values:', xValues);\n\n // Build table headers and values\n const headers = [];\n let xHeader = 'X-Axis';\n if (xQuantity['dcc:name'] && xQuantity['dcc:name']['dcc:content']) {\n let content = xQuantity['dcc:name']['dcc:content'];\n if (Array.isArray(content)) {\n const match = content.find(item => item.$ && item.$.lang === language) || content[0];\n xHeader = match._ || match;\n } else {\n xHeader = content._ || content;\n }\n }\n headers.push(xHeader);\n\n const dataValues = [];\n dataQuantities.forEach(q => {\n let header = 'Data';\n if (q['dcc:name'] && q['dcc:name']['dcc:content']) {\n let content = q['dcc:name']['dcc:content'];\n if (Array.isArray(content)) {\n const match = content.find(item => item.$ && item.$.lang === language) || content[0];\n header = match._ || match;\n } else {\n header = content._ || content;\n }\n }\n headers.push(header);\n let values = [];\n if (q['si:realListXMLList'] && q['si:realListXMLList']['si:valueXMLList']) {\n values = q['si:realListXMLList']['si:valueXMLList'].trim().split(/\\s+/).map(v => parseFloat(v));\n }\n if (values.length === 1 && xValues.length > 1) {\n values = new Array(xValues.length).fill(values[0]);\n }\n dataValues.push(values);\n });\n\n // Build table rows\n const tableData = [headers];\n for (let i = 0; i < xValues.length; i++) {\n const row = [];\n row.push(xValues[i]);\n dataValues.forEach(values => {\n row.push(values[i] !== undefined ? values[i] : '');\n });\n tableData.push(row);\n }\n console.debug('Table data:', tableData);\n renderTable(tableData);\n\n // Group data quantities by unit for plotting\n const unitGroups = {};\n dataQuantities.forEach((q, idx) => {\n let unit = '';\n if (q['si:realListXMLList'] && q['si:realListXMLList']['si:unitXMLList']) {\n unit = q['si:realListXMLList']['si:unitXMLList'].trim();\n }\n if (!unitGroups[unit]) {\n unitGroups[unit] = [];\n }\n let header = headers[idx + 1];\n let values = [];\n if (q['si:realListXMLList'] && q['si:realListXMLList']['si:valueXMLList']) {\n values = q['si:realListXMLList']['si:valueXMLList'].trim().split(/\\s+/).map(v => parseFloat(v));\n }\n if (values.length === 1 && xValues.length > 1) {\n values = new Array(xValues.length).fill(values[0]);\n }\n unitGroups[unit].push({ name: header, y: values });\n });\n console.debug('Unit groups for plots:', unitGroups);\n\n // Clear and render plots\n plotsContainer.innerHTML = '';\n Object.keys(unitGroups).forEach(unit => {\n const graphDiv = document.createElement('div');\n graphDiv.style.width = '100%';\n graphDiv.style.height = '300px';\n plotsContainer.appendChild(graphDiv);\n\n const traces = unitGroups[unit].map(trace => {\n return {\n x: xValues,\n y: trace.y,\n type: 'scatter',\n mode: 'lines+markers',\n name: trace.name\n };\n });\n const layout = {\n title: 'Plot (' + unit + ')',\n xaxis: { title: xHeader },\n yaxis: { title: unit },\n hovermode: 'closest'\n };\n Plotly.newPlot(graphDiv, traces, layout).then(() => {\n console.debug('Plot rendered for unit:', unit);\n });\n\n // Coupled mouseover events\n graphDiv.on('plotly_hover', function(data) {\n if (data.points && data.points.length > 0) {\n const pointIndex = data.points[0].pointIndex + 1; // offset for header row\n highlightTableRow(pointIndex);\n }\n });\n graphDiv.on('plotly_unhover', function() {\n clearTableRowHighlights();\n });\n });\n }\n\n // Render table from a 2D array\n function renderTable(tableData) {\n tableContainer.innerHTML = '';\n const table = document.createElement('table');\n tableData.forEach((rowData, rowIndex) => {\n const tr = document.createElement('tr');\n rowData.forEach(cellData => {\n const cell = document.createElement(rowIndex === 0 ? 'th' : 'td');\n cell.textContent = cellData;\n cell.style.padding = '4px';\n cell.style.border = '1px solid #ccc';\n tr.appendChild(cell);\n });\n tr.addEventListener('mouseover', () => { tr.style.backgroundColor = '#eef'; });\n tr.addEventListener('mouseout', () => { tr.style.backgroundColor = ''; });\n table.appendChild(tr);\n });\n tableContainer.appendChild(table);\n }\n\n // Functions for coupled table row highlights\n function highlightTableRow(rowIndex) {\n const rows = tableContainer.querySelectorAll('tr');\n if (rows[rowIndex]) {\n rows[rowIndex].style.backgroundColor = '#fee';\n }\n }\n function clearTableRowHighlights() {\n const rows = tableContainer.querySelectorAll('tr');\n rows.forEach(row => row.style.backgroundColor = '');\n }\n\n // Initial update\n updateVisualization();\n\n // Update visualization when X-axis selection changes\n const radios = document.querySelectorAll('input[name=\"xAxisSelect\"]');\n radios.forEach(radio => {\n radio.addEventListener('change', updateVisualization);\n });\n\n // Tolerance toggle event (placeholder)\n toleranceToggle.addEventListener('change', () => {\n console.log('Tolerance toggle:', toleranceToggle.checked);\n // Future: update plot/table for tolerance markings and color coding\n });\n}\n" + "MeasurementRenderer.js": "import Plotly from 'plotly.js-dist';\n\n// Dummy unit converter: currently returns the raw unit (to be enhanced later)\nfunction convertUnit(rawUnit) {\n return rawUnit;\n}\n\n// Define Tab10 palette and lighter shades for table backgrounds\nconst palette = [\n '#1f77b4',\n '#ff7f0e',\n '#2ca02c',\n '#d62728',\n '#9467bd',\n '#8c564b',\n '#e377c2',\n '#7f7f7f',\n '#bcbd22',\n '#17becf'\n];\n\nconst lightPalette = [\n '#c6e2ff',\n '#ffddbf',\n '#c9e6c8',\n '#f4c2c2',\n '#dcd0ff',\n '#e0cda9',\n '#ffccff',\n '#d3d3d3',\n '#e6e6a9',\n '#b3ffff'\n];\n\nexport function renderMeasurementResults(measurementResults, language) {\n console.debug('renderMeasurementResults called with:', measurementResults);\n const container = document.getElementById('measurementResults');\n container.innerHTML = '';\n\n if (!measurementResults) {\n console.error('No measurementResults provided.');\n return;\n }\n\n let results = measurementResults['dcc:measurementResult'];\n console.debug('Raw dcc:measurementResult:', results);\n\n if (!results) {\n console.error(\"Missing 'dcc:measurementResult' in measurementResults\");\n return;\n }\n\n if (!Array.isArray(results)) {\n results = [results];\n }\n console.debug('Processed measurement result array:', results);\n\n const measurementResult = results[0];\n console.debug('Using measurementResult:', measurementResult);\n\n // Extract the result object from dcc:results -> dcc:result\n let resultObj = measurementResult['dcc:results'] && measurementResult['dcc:results']['dcc:result'];\n if (!resultObj) {\n console.error(\"Missing 'dcc:results' or 'dcc:result' in measurementResult:\", measurementResult);\n return;\n }\n if (Array.isArray(resultObj)) {\n resultObj = resultObj[0];\n }\n console.debug('Using result object:', resultObj);\n\n // Get measurement result name without language tag\n let resultName = 'Measurement Result';\n if (measurementResult['dcc:name'] && measurementResult['dcc:name']['dcc:content']) {\n let content = measurementResult['dcc:name']['dcc:content'];\n if (Array.isArray(content)) {\n const match = content.find(item => item.$ && item.$.lang === language) || content[0];\n resultName = match._ || match;\n } else {\n resultName = content._ || content;\n }\n }\n console.debug('Result name:', resultName);\n\n const tabTitle = document.createElement('h2');\n tabTitle.textContent = resultName;\n container.appendChild(tabTitle);\n\n if (!resultObj['dcc:data'] || !resultObj['dcc:data']['dcc:list']) {\n console.error(\"Missing 'dcc:data' or 'dcc:list' in result object:\", resultObj);\n return;\n }\n const listData = resultObj['dcc:data']['dcc:list'];\n console.debug('List data:', listData);\n\n // Flatten quantities from the list\n let quantities = [];\n if (listData['dcc:quantity']) {\n quantities = Array.isArray(listData['dcc:quantity']) ? listData['dcc:quantity'] : [listData['dcc:quantity']];\n }\n console.debug('Quantities from list:', quantities);\n\n // Also add quantities from measurementMetaData if available\n if (listData['dcc:measurementMetaData'] && listData['dcc:measurementMetaData']['dcc:metaData']) {\n let metaData = listData['dcc:measurementMetaData']['dcc:metaData'];\n if (!Array.isArray(metaData)) metaData = [metaData];\n metaData.forEach(md => {\n if (md['dcc:data'] && md['dcc:data']['dcc:quantity']) {\n let qs = md['dcc:data']['dcc:quantity'];\n if (!Array.isArray(qs)) qs = [qs];\n quantities = quantities.concat(qs);\n }\n });\n }\n console.debug('Combined quantities:', quantities);\n\n // Separate index quantities and data quantities; extract extra info (uncertainty, comment, conformity)\n const indexQuantities = [];\n const dataQuantities = [];\n const extraInfo = [];\n quantities.forEach(q => {\n if (q.$ && q.$.refType && q.$.refType.match(/basic_tableIndex/)) {\n indexQuantities.push(q);\n } else {\n dataQuantities.push(q);\n let uncertainty = null;\n if (q['si:measurementUncertaintyUnivariateXMLList'] &&\n q['si:measurementUncertaintyUnivariateXMLList']['si:expandedMUXMLList'] &&\n q['si:measurementUncertaintyUnivariateXMLList']['si:expandedMUXMLList']['si:valueExpandedMUXMLList']) {\n const errStr = q['si:measurementUncertaintyUnivariateXMLList']['si:expandedMUXMLList']['si:valueExpandedMUXMLList'];\n uncertainty = errStr.trim().split(/\\s+/).map(v => parseFloat(v));\n }\n let comment = '';\n let validArray = [];\n let conformity = null;\n if (q['dcc:measurementMetaData'] && q['dcc:measurementMetaData']['dcc:metaData']) {\n let md = q['dcc:measurementMetaData']['dcc:metaData'];\n if (!Array.isArray(md)) { md = [md]; }\n md.forEach(item => {\n if (item.$ && item.$.refType && item.$.refType.includes('basic_tableRowComment')) {\n if (item['dcc:description'] && item['dcc:description']['dcc:content']) {\n let desc = item['dcc:description']['dcc:content'];\n if (Array.isArray(desc)) {\n const match = desc.find(d => d.$ && d.$.lang === language) || desc[0];\n comment = match._ || match;\n } else {\n comment = desc._ || desc;\n }\n }\n if (item['dcc:validXMLList']) {\n validArray = item['dcc:validXMLList'].trim().split(/\\s+/);\n }\n }\n if (item.$ && item.$.refType && item.$.refType.includes('basic_conformity')) {\n if (item['dcc:conformityXMLList']) {\n conformity = item['dcc:conformityXMLList'].trim().split(/\\s+/);\n }\n }\n });\n }\n extraInfo.push({ uncertainty, comment, valid: validArray, conformity });\n }\n });\n console.debug('Index Quantities:', indexQuantities);\n console.debug('Data Quantities:', dataQuantities);\n console.debug('Extra info for data quantities:', extraInfo);\n\n // Create radio buttons for X-axis selection\n const xAxisContainer = document.createElement('div');\n xAxisContainer.innerHTML = '<strong>Select X-Axis:</strong> ';\n indexQuantities.forEach((q, idx) => {\n let nameStr = 'Index ' + idx;\n if (q['dcc:name'] && q['dcc:name']['dcc:content']) {\n let content = q['dcc:name']['dcc:content'];\n if (Array.isArray(content)) {\n const match = content.find(item => item.$ && item.$.lang === language) || content[0];\n nameStr = match._ || match;\n } else {\n nameStr = content._ || content;\n }\n }\n const radio = document.createElement('input');\n radio.type = 'radio';\n radio.name = 'xAxisSelect';\n radio.value = idx;\n if (idx === 0) radio.checked = true;\n const label = document.createElement('label');\n label.textContent = nameStr;\n label.style.marginRight = '10px';\n xAxisContainer.appendChild(radio);\n xAxisContainer.appendChild(label);\n });\n container.appendChild(xAxisContainer);\n\n // Tolerance toggle (placeholder)\n const toleranceToggle = document.createElement('input');\n toleranceToggle.type = 'checkbox';\n toleranceToggle.id = 'toleranceToggle';\n const tolLabel = document.createElement('label');\n tolLabel.htmlFor = 'toleranceToggle';\n tolLabel.textContent = ' Enable tolerance markings';\n const tolContainer = document.createElement('div');\n tolContainer.appendChild(toleranceToggle);\n tolContainer.appendChild(tolLabel);\n container.appendChild(tolContainer);\n\n // Create container for subplots\n const subplotsContainer = document.createElement('div');\n subplotsContainer.id = 'subplotsContainer';\n container.appendChild(subplotsContainer);\n\n // Create container for table\n const tableContainer = document.createElement('div');\n tableContainer.id = 'tableContainer';\n container.appendChild(tableContainer);\n\n // Function to update visualization\n function updateVisualization() {\n const selectedRadio = document.querySelector('input[name=\"xAxisSelect\"]:checked');\n if (!selectedRadio) {\n console.error('No X-Axis selection found.');\n return;\n }\n const selectedIndex = selectedRadio.value;\n const xQuantity = indexQuantities[selectedIndex];\n let xValues = [];\n let xUnit = '';\n if (xQuantity && xQuantity['si:realListXMLList']) {\n if (xQuantity['si:realListXMLList']['si:valueXMLList']) {\n xValues = xQuantity['si:realListXMLList']['si:valueXMLList'].trim().split(/\\s+/).map(v => parseFloat(v));\n }\n if (xQuantity['si:realListXMLList']['si:unitXMLList']) {\n xUnit = xQuantity['si:realListXMLList']['si:unitXMLList'].trim();\n }\n }\n console.debug('Selected X-Axis values:', xValues);\n console.debug('X-Axis unit:', xUnit);\n\n // Build table headers and arrays for values, comments, conformity, uncertainties\n const headers = [];\n let xHeader = 'X-Axis (selected) (' + convertUnit(xUnit) + ')';\n headers.push(xHeader);\n\n const dataValues = [];\n const commentsArray = [];\n const conformityArray = [];\n const uncertaintiesArray = [];\n dataQuantities.forEach((q, idx) => {\n let header = 'Data';\n let unit = '';\n if (q['dcc:name'] && q['dcc:name']['dcc:content']) {\n let content = q['dcc:name']['dcc:content'];\n if (Array.isArray(content)) {\n const match = content.find(item => item.$ && item.$.lang === language) || content[0];\n header = match._ || match;\n } else {\n header = content._ || content;\n }\n }\n if (q['si:realListXMLList'] && q['si:realListXMLList']['si:unitXMLList']) {\n unit = q['si:realListXMLList']['si:unitXMLList'].trim();\n }\n headers.push(header + ' in ' + convertUnit(unit));\n headers.push('Comments');\n // Add conformity header only if data exists\n if (extraInfo[idx] && extraInfo[idx].conformity) {\n headers.push('Conformity');\n } else {\n headers.push('');\n }\n\n let values = [];\n if (q['si:realListXMLList'] && q['si:realListXMLList']['si:valueXMLList']) {\n values = q['si:realListXMLList']['si:valueXMLList'].trim().split(/\\s+/).map(v => parseFloat(v));\n }\n if (values.length === 1 && xValues.length > 1) {\n values = new Array(xValues.length).fill(values[0]);\n }\n dataValues.push(values);\n\n let uncertainty = null;\n if (extraInfo[idx] && extraInfo[idx].uncertainty) {\n uncertainty = extraInfo[idx].uncertainty;\n }\n uncertaintiesArray.push(uncertainty);\n\n let comment = extraInfo[idx] ? extraInfo[idx].comment : '';\n commentsArray.push(comment);\n\n let conformity = [];\n if (extraInfo[idx] && extraInfo[idx].conformity) {\n conformity = extraInfo[idx].conformity;\n }\n conformityArray.push(conformity);\n });\n\n const tableData = [headers];\n for (let i = 0; i < xValues.length; i++) {\n const row = [];\n row.push(xValues[i]);\n dataValues.forEach((values, idx) => {\n let cellValue = values[i] !== undefined ? values[i] : '';\n if (uncertaintiesArray[idx] && uncertaintiesArray[idx][i] !== undefined) {\n cellValue = cellValue + ' ± ' + uncertaintiesArray[idx][i];\n }\n row.push(cellValue);\n row.push(commentsArray[idx] || '');\n if (conformityArray[idx] && conformityArray[idx][i]) {\n row.push(conformityArray[idx][i]);\n } else {\n row.push('');\n }\n });\n tableData.push(row);\n }\n console.debug('Table data:', tableData);\n renderTable(tableData);\n\n // Group data quantities by unit for plotting and assign colors\n const unitGroups = {};\n dataQuantities.forEach((q, idx) => {\n let unit = '';\n if (q['si:realListXMLList'] && q['si:realListXMLList']['si:unitXMLList']) {\n unit = q['si:realListXMLList']['si:unitXMLList'].trim();\n }\n if (!unitGroups[unit]) {\n unitGroups[unit] = [];\n }\n let header = headers[idx * 3 + 1];\n let values = [];\n if (q['si:realListXMLList'] && q['si:realListXMLList']['si:valueXMLList']) {\n values = q['si:realListXMLList']['si:valueXMLList'].trim().split(/\\s+/).map(v => parseFloat(v));\n }\n if (values.length === 1 && xValues.length > 1) {\n values = new Array(xValues.length).fill(values[0]);\n }\n let uncertainty = uncertaintiesArray[idx];\n let conformity = [];\n if (conformityArray[idx]) {\n conformity = conformityArray[idx];\n }\n // Use Tab10 palette; assign a color for this data quantity\n let traceColor = palette[idx % palette.length];\n unitGroups[unit].push({ name: header, y: values, uncertainty: uncertainty, conformity: conformity, color: traceColor, index: idx });\n });\n console.debug('Unit groups for plots:', unitGroups);\n\n // Build plots: one subplot per unit group, sharing the same X-axis\n const plotsContainer = document.getElementById('subplotsContainer');\n plotsContainer.innerHTML = '';\n const unitKeys = Object.keys(unitGroups);\n unitKeys.forEach((unit, groupIdx) => {\n const group = unitGroups[unit];\n const graphDiv = document.createElement('div');\n graphDiv.style.width = '100%';\n graphDiv.style.height = '300px';\n plotsContainer.appendChild(graphDiv);\n\n // Build traces for this group\n const groupTraces = group.map(trace => {\n // Prepare tooltip with short format\n const hovertemplate = 'X: %{x} ' + convertUnit(xUnit) + ' | ' + trace.name + ': %{y} ± %{error_y.array} ' + convertUnit(unit) + ' | Conformity: %{customdata}<extra></extra>';\n return {\n x: xValues,\n y: trace.y,\n error_y: { type: 'data', array: trace.uncertainty || new Array(xValues.length).fill(0), visible: true, color: trace.color },\n type: 'scatter',\n mode: 'lines+markers',\n name: trace.name,\n marker: { color: trace.color },\n hovertemplate: hovertemplate,\n customdata: (trace.conformity && trace.conformity.length === xValues.length) ? trace.conformity : new Array(xValues.length).fill('')\n };\n });\n\n // Only the last subplot gets an X-axis label\n let xaxisTitle = '';\n if (groupIdx === unitKeys.length - 1) {\n let xLabel = 'X: ';\n if (xQuantity['dcc:name'] && xQuantity['dcc:name']['dcc:content']) {\n let content = xQuantity['dcc:name']['dcc:content'];\n if (Array.isArray(content)) {\n const match = content.find(item => item.$ && item.$.lang === language) || content[0];\n xLabel += match._ || match;\n } else {\n xLabel += content._ || content;\n }\n }\n xaxisTitle = xLabel + ' in ' + convertUnit(xUnit);\n }\n\n const layout = {\n xaxis: { title: { text: xaxisTitle, font: { size: 18, family: 'Arial', color: 'black' } }, tickfont: { family: 'Arial', size: 14, color: 'black' } },\n yaxis: { title: { text: convertUnit(unit), font: { size: 18, family: 'Arial', color: 'black' } }, tickfont: { family: 'Arial', size: 14, color: 'black' } },\n hovermode: 'closest',\n margin: { t: 20, b: 40 }\n };\n\n Plotly.newPlot(graphDiv, groupTraces, layout).then(() => {\n console.debug('Plot rendered for unit:', unit);\n // Bold caption above the plot: \"QuantityName in Unit\"; remove duplicate unit if any\n const caption = document.createElement('div');\n caption.innerHTML = '<b>' + group[0].name + ' in ' + convertUnit(unit) + '</b>';\n caption.style.textAlign = 'center';\n caption.style.marginBottom = '5px';\n graphDiv.parentNode.insertBefore(caption, graphDiv);\n });\n\n graphDiv.on('plotly_hover', function(data) {\n if (data.points && data.points.length > 0) {\n const pointIndex = data.points[0].pointIndex + 1; // offset for header row\n highlightTableRow(pointIndex);\n }\n });\n graphDiv.on('plotly_unhover', function() {\n clearTableRowHighlights();\n });\n });\n }\n\n // Render table from a 2D array with colored backgrounds for data columns\n function renderTable(tableData) {\n const tableContainer = document.getElementById('tableContainer');\n tableContainer.innerHTML = '';\n const table = document.createElement('table');\n tableData.forEach((rowData, rowIndex) => {\n const tr = document.createElement('tr');\n rowData.forEach((cellData, cellIndex) => {\n const cell = document.createElement(rowIndex === 0 ? 'th' : 'td');\n cell.textContent = cellData;\n cell.style.padding = '4px';\n cell.style.border = '1px solid #ccc';\n if (rowIndex === 0 && cellIndex > 0) {\n const dataIndex = Math.floor((cellIndex - 1) / 3);\n cell.style.backgroundColor = lightPalette[dataIndex % lightPalette.length];\n }\n tr.appendChild(cell);\n });\n tr.addEventListener('mouseover', () => { tr.style.backgroundColor = '#eef'; });\n tr.addEventListener('mouseout', () => { tr.style.backgroundColor = ''; });\n table.appendChild(tr);\n });\n tableContainer.appendChild(table);\n }\n\n function highlightTableRow(rowIndex) {\n const rows = document.getElementById('tableContainer').querySelectorAll('tr');\n if (rows[rowIndex]) {\n rows[rowIndex].style.backgroundColor = '#fee';\n }\n }\n\n function clearTableRowHighlights() {\n const rows = document.getElementById('tableContainer').querySelectorAll('tr');\n rows.forEach(row => row.style.backgroundColor = '');\n }\n\n updateVisualization();\n\n const radios = document.querySelectorAll('input[name=\"xAxisSelect\"]');\n radios.forEach(radio => {\n radio.addEventListener('change', updateVisualization);\n });\n\n toleranceToggle.addEventListener('change', () => {\n console.log('Tolerance toggle:', toleranceToggle.checked);\n // Future: update plot/table for tolerance markings and color coding\n });\n}\n" } } } diff --git a/readME.md b/readME.md new file mode 100644 index 0000000000000000000000000000000000000000..e7dd35db0ad2249bca252e3e8f4811ad58bc080f --- /dev/null +++ b/readME.md @@ -0,0 +1,76 @@ +Below is a short README for your project that explains how to install, run in development mode, and serve a debug version with Python: + +--- + +# DCC Viewer + +This is a front-end only digital calibration certificate viewer built with vanilla JavaScript, Vite, Plotly, xml2js, and JSONEditor. It converts a DCC XML file into JSON and renders administrative and measurement data with interactive plots, tables, and tooltips. + +## Features + +- **XML-to-JSON conversion:** Uses [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js) to convert the calibration certificate XML into JSON. +- **Interactive Measurement Renderer:** Displays measurement results as grouped subplots with error bars, custom tooltips (including conformity status), and a correlated data table. +- **JSON Tree Viewer:** Uses [JSONEditor](https://github.com/josdejong/jsoneditor) for a modern, read-only view of the administrative data. +- **Responsive UI:** Supports language switching and dynamic updates. + +## Installation + +1. Make sure you have [Node.js](https://nodejs.org/) installed. +2. Clone the repository and navigate to the project directory. +3. Install dependencies: + + ```bash + npm install + ``` + +## Development + +Start the Vite development server: + +```bash +npm run dev +``` + +This will launch the app (usually on [http://localhost:5173/](http://localhost:5173/)). Changes to the source files will trigger hot reloading. + +## Serving the Debug Version with Python + +If you prefer to view the debug version using Python's built-in HTTP server, follow these steps: + +1. Build the project (or use the current static files in the project root): + + ```bash + npm run build + ``` + + The output will be placed in the `dist` folder. + +2. Navigate to the `dist` folder: + + ```bash + cd dist + ``` + +3. Serve the folder using Python (Python 3): + + ```bash + python3 -m http.server 8000 + ``` + + This command will start a local server on port 8000. + +4. Open your browser and navigate to [http://localhost:8000](http://localhost:8000) to view the debug version. + +*Note:* You can also serve the project directly from the project root (if your files are static) by running the Python server there, but using the Vite build output (in `dist`) is recommended for a production-like environment. + +## Debugging + +- **Chrome Developer Tools:** + Open the **Sources** tab and locate your source files (source maps are enabled by default with Vite) to set breakpoints and inspect variables. + +- **WebStorm:** + Create a JavaScript Debug configuration (with URL `http://localhost:5173/`) and set breakpoints directly in the IDE. + +--- + +This README should help you get started with development and debugging. Let me know if you need any additional information! \ No newline at end of file diff --git a/src/renderers/MeasurementRenderer.js b/src/renderers/MeasurementRenderer.js index 8e119c8fb3010fc4a3205d9e3d5422581a410e55..5c44671553319db29cbeb87abf857b9e7fb8e8f6 100644 --- a/src/renderers/MeasurementRenderer.js +++ b/src/renderers/MeasurementRenderer.js @@ -1,5 +1,10 @@ import Plotly from 'plotly.js-dist'; +// 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', @@ -88,7 +93,7 @@ export function renderMeasurementResults(measurementResults, language) { const listData = resultObj['dcc:data']['dcc:list']; console.debug('List data:', listData); - // Flatten all quantities from the list + // Flatten quantities from the list let quantities = []; if (listData['dcc:quantity']) { quantities = Array.isArray(listData['dcc:quantity']) ? listData['dcc:quantity'] : [listData['dcc:quantity']]; @@ -109,7 +114,7 @@ export function renderMeasurementResults(measurementResults, language) { } console.debug('Combined quantities:', quantities); - // Separate index quantities and data quantities; also extract extra info (uncertainty, comment, conformity) + // Separate index quantities and data quantities; extract extra info (uncertainty, comment, conformity) const indexQuantities = []; const dataQuantities = []; const extraInfo = []; @@ -126,40 +131,34 @@ export function renderMeasurementResults(measurementResults, language) { uncertainty = errStr.trim().split(/\s+/).map(v => parseFloat(v)); } let comment = ''; - let conformity = ''; + let validArray = []; + let conformity = null; if (q['dcc:measurementMetaData'] && q['dcc:measurementMetaData']['dcc:metaData']) { let md = q['dcc:measurementMetaData']['dcc:metaData']; - if (Array.isArray(md)) { - md.forEach(item => { - if (item.$ && item.$.refType && item.$.refType.includes('basic_tableRowComment')) { - if (item['dcc:description'] && item['dcc:description']['dcc:content']) { - comment = Array.isArray(item['dcc:description']['dcc:content']) ? - (item['dcc:description']['dcc:content'][0]._ || item['dcc:description']['dcc:content'][0]) : - (item['dcc:description']['dcc:content']._ || item['dcc:description']['dcc:content']); + if (!Array.isArray(md)) { md = [md]; } + md.forEach(item => { + if (item.$ && item.$.refType && item.$.refType.includes('basic_tableRowComment')) { + 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]; + comment = match._ || match; + } else { + comment = desc._ || desc; } } - if (item.$ && item.$.refType && item.$.refType.includes('basic_conformity')) { - if (item['dcc:conformityXMLList']) { - conformity = item['dcc:conformityXMLList'].trim(); - } - } - }); - } else { - if (md.$ && md.$.refType && md.$.refType.includes('basic_tableRowComment')) { - if (md['dcc:description'] && md['dcc:description']['dcc:content']) { - comment = Array.isArray(md['dcc:description']['dcc:content']) ? - (md['dcc:description']['dcc:content'][0]._ || md['dcc:description']['dcc:content'][0]) : - (md['dcc:description']['dcc:content']._ || md['dcc:description']['dcc:content']); + if (item['dcc:validXMLList']) { + validArray = item['dcc:validXMLList'].trim().split(/\s+/); } } - if (md.$ && md.$.refType && md.$.refType.includes('basic_conformity')) { - if (md['dcc:conformityXMLList']) { - conformity = md['dcc:conformityXMLList'].trim(); + if (item.$ && item.$.refType && item.$.refType.includes('basic_conformity')) { + if (item['dcc:conformityXMLList']) { + conformity = item['dcc:conformityXMLList'].trim().split(/\s+/); } } - } + }); } - extraInfo.push({ uncertainty, comment, conformity }); + extraInfo.push({ uncertainty, comment, valid: validArray, conformity }); } }); console.debug('Index Quantities:', indexQuantities); @@ -239,7 +238,7 @@ export function renderMeasurementResults(measurementResults, language) { // Build table headers and arrays for values, comments, conformity, uncertainties const headers = []; - let xHeader = 'X-Axis (' + xUnit + ')'; + let xHeader = 'X-Axis (selected) (' + convertUnit(xUnit) + ')'; headers.push(xHeader); const dataValues = []; @@ -261,9 +260,14 @@ export function renderMeasurementResults(measurementResults, language) { if (q['si:realListXMLList'] && q['si:realListXMLList']['si:unitXMLList']) { unit = q['si:realListXMLList']['si:unitXMLList'].trim(); } - headers.push(header + ' (' + unit + ')'); + headers.push(header + ' in ' + convertUnit(unit)); headers.push('Comments'); - headers.push('Conformity'); + // 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']) { @@ -281,8 +285,12 @@ export function renderMeasurementResults(measurementResults, language) { uncertaintiesArray.push(uncertainty); let comment = extraInfo[idx] ? extraInfo[idx].comment : ''; - let conformity = extraInfo[idx] ? extraInfo[idx].conformity : ''; commentsArray.push(comment); + + let conformity = []; + if (extraInfo[idx] && extraInfo[idx].conformity) { + conformity = extraInfo[idx].conformity; + } conformityArray.push(conformity); }); @@ -297,7 +305,11 @@ export function renderMeasurementResults(measurementResults, language) { } row.push(cellValue); row.push(commentsArray[idx] || ''); - row.push(conformityArray[idx] || ''); + if (conformityArray[idx] && conformityArray[idx][i]) { + row.push(conformityArray[idx][i]); + } else { + row.push(''); + } }); tableData.push(row); } @@ -323,31 +335,31 @@ export function renderMeasurementResults(measurementResults, language) { values = new Array(xValues.length).fill(values[0]); } let uncertainty = uncertaintiesArray[idx]; - let conformity = conformityArray[idx]; - // Assign color: use palette; override if conformity indicates pass/fail - let traceColor = palette[idx % palette.length]; - if (conformity.toLowerCase().includes('pass')) { - traceColor = '#2ca02c'; - } else if (conformity.toLowerCase().includes('fail')) { - traceColor = '#d62728'; + let conformity = []; + if (conformityArray[idx]) { + conformity = conformityArray[idx]; } - unitGroups[unit].push({ name: header, y: values, uncertainty: uncertainty, conformity: conformity, color: traceColor }); + // 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 }); }); console.debug('Unit groups for plots:', unitGroups); - // Build a single Plotly figure with subplots (here, one plot per unit group) + // Build plots: one subplot per unit group, sharing the same X-axis const plotsContainer = document.getElementById('subplotsContainer'); plotsContainer.innerHTML = ''; - Object.keys(unitGroups).forEach((unit, idx) => { + 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); - // Build traces for this unit group + // Build traces for this group const groupTraces = group.map(trace => { - const hovertemplate = 'X-Axis: %{x} ' + xUnit + ' | ' + trace.name + ': %{y} ± %{error_y.array} (' + unit + ') | Conformity: <b style="color:' + (trace.conformity.toLowerCase().includes('pass') ? '#2ca02c' : (trace.conformity.toLowerCase().includes('fail') ? '#d62728' : '#000')) + '">' + trace.conformity + '</b><extra></extra>'; + // Prepare tooltip with short format + const hovertemplate = 'X: %{x} ' + convertUnit(xUnit) + ' | ' + trace.name + ': %{y} ± %{error_y.array} ' + convertUnit(unit) + ' | Conformity: %{customdata}<extra></extra>'; return { x: xValues, y: trace.y, @@ -357,31 +369,46 @@ export function renderMeasurementResults(measurementResults, language) { name: trace.name, marker: { color: trace.color }, hovertemplate: hovertemplate, - customdata: trace.conformity + customdata: (trace.conformity && trace.conformity.length === xValues.length) ? trace.conformity : new Array(xValues.length).fill('') }; }); + // 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); + } + const layout = { - xaxis: { title: { text: xHeader, font: { size: 16, weight: 'bold' } } }, - yaxis: { title: { text: unit, font: { size: 16, weight: 'bold' } } }, + 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' } }, hovermode: 'closest', margin: { t: 20, b: 40 } }; Plotly.newPlot(graphDiv, groupTraces, layout).then(() => { console.debug('Plot rendered for unit:', unit); - // Add caption below the plot: italic text "QuantityName in Unit" + // Bold caption above the plot: "QuantityName in Unit"; remove duplicate unit if any const caption = document.createElement('div'); - // Use the name of the first trace as QuantityName - caption.innerHTML = '<i>' + group[0].name + ' in ' + unit + '</i>'; + caption.innerHTML = '<b>' + group[0].name + ' in ' + convertUnit(unit) + '</b>'; caption.style.textAlign = 'center'; - caption.style.fontStyle = 'italic'; - graphDiv.parentNode.insertBefore(caption, graphDiv.nextSibling); + 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; + const pointIndex = data.points[0].pointIndex + 1; // offset for header row highlightTableRow(pointIndex); } }); @@ -405,7 +432,6 @@ export function renderMeasurementResults(measurementResults, language) { cell.style.border = '1px solid #ccc'; if (rowIndex === 0 && cellIndex > 0) { const dataIndex = Math.floor((cellIndex - 1) / 3); - const color = palette[dataIndex % palette.length]; cell.style.backgroundColor = lightPalette[dataIndex % lightPalette.length]; } tr.appendChild(cell);