/** * SPC Charts — Plotly.js helper functions for Metrologist dashboard. * * Requires Plotly.js loaded via CDN before this script. * Requires window.SPC_I18N object set in the template for translations. * * Brand colors: * Primary: #2563EB * Pass: #10b981 * Warning: #f59e0b * Fail: #ef4444 */ // i18n helper — falls back to Italian defaults const _t = (key) => (window.SPC_I18N && window.SPC_I18N[key]) || key; const SPC_COLORS = { primary: '#2563EB', primaryLight: '#3B82F6', pass: '#10b981', warning: '#f59e0b', fail: '#ef4444', steel: '#64748B', steelLight: '#94A3B8', nominal: '#8b5cf6', ucl_lcl: '#ef4444', uwl_lwl: '#f59e0b', utl_ltl: '#dc2626', bg: '#ffffff', gridColor: '#e2e8f0', textColor: '#334155', }; const PLOTLY_LAYOUT_DEFAULTS = { font: { family: 'Inter, system-ui, sans-serif', color: SPC_COLORS.textColor }, paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', margin: { t: 20, r: 30, b: 50, l: 60 }, xaxis: { gridcolor: SPC_COLORS.gridColor, zeroline: false }, yaxis: { gridcolor: SPC_COLORS.gridColor, zeroline: false }, }; const PLOTLY_CONFIG = { responsive: true, displayModeBar: 'hover', modeBarButtonsToRemove: ['lasso2d', 'select2d'], displaylogo: false, }; /** * Render a control chart (X-bar) with tolerance and control limits. * * @param {string} containerId - DOM element ID for the chart. * @param {Object} data - ControlChartData from API. */ function renderControlChart(containerId, data) { if (!data || !data.values || data.values.length === 0) { document.getElementById(containerId).innerHTML = '
' + _t('noData') + '
'; return; } const n = data.values.length; const xLabels = data.timestamps.map((t, i) => i + 1); // Separate in-control and out-of-control points const oocSet = new Set(data.out_of_control || []); const inControlX = [], inControlY = []; const oocX = [], oocY = []; for (let i = 0; i < n; i++) { if (oocSet.has(i)) { oocX.push(xLabels[i]); oocY.push(data.values[i]); } else { inControlX.push(xLabels[i]); inControlY.push(data.values[i]); } } const traces = [ // In-control points { x: inControlX, y: inControlY, mode: 'markers', type: 'scatter', name: _t('inControl'), marker: { color: SPC_COLORS.primary, size: 6 }, }, // Out-of-control points { x: oocX, y: oocY, mode: 'markers', type: 'scatter', name: _t('outOfControl'), marker: { color: SPC_COLORS.fail, size: 9, symbol: 'diamond' }, }, // Line connecting all points { x: xLabels, y: data.values, mode: 'lines', type: 'scatter', name: '', line: { color: SPC_COLORS.steelLight, width: 1 }, showlegend: false, }, ]; // Helper to add horizontal line const shapes = []; function addLine(yVal, color, dash) { if (yVal == null) return; shapes.push({ type: 'line', xref: 'paper', x0: 0, x1: 1, y0: yVal, y1: yVal, line: { color, width: 1.5, dash }, }); } // Mean line addLine(data.mean, SPC_COLORS.primary, 'solid'); // UCL / LCL addLine(data.ucl, SPC_COLORS.ucl_lcl, 'dash'); addLine(data.lcl, SPC_COLORS.ucl_lcl, 'dash'); // UTL / LTL addLine(data.utl, SPC_COLORS.utl_ltl, 'dot'); addLine(data.ltl, SPC_COLORS.utl_ltl, 'dot'); // UWL / LWL addLine(data.uwl, SPC_COLORS.uwl_lwl, 'dashdot'); addLine(data.lwl, SPC_COLORS.uwl_lwl, 'dashdot'); // Nominal addLine(data.nominal, SPC_COLORS.nominal, 'longdash'); // Build annotations for limit labels const annotations = []; function addLabel(yVal, text, color) { if (yVal == null) return; annotations.push({ x: 1, xref: 'paper', xanchor: 'left', y: yVal, yanchor: 'middle', text: text, showarrow: false, font: { size: 10, color }, }); } addLabel(data.mean, 'X\u0304', SPC_COLORS.primary); addLabel(data.ucl, 'UCL', SPC_COLORS.ucl_lcl); addLabel(data.lcl, 'LCL', SPC_COLORS.ucl_lcl); addLabel(data.utl, 'UTL', SPC_COLORS.utl_ltl); addLabel(data.ltl, 'LTL', SPC_COLORS.utl_ltl); addLabel(data.uwl, 'UWL', SPC_COLORS.uwl_lwl); addLabel(data.lwl, 'LWL', SPC_COLORS.uwl_lwl); addLabel(data.nominal, 'NOM', SPC_COLORS.nominal); const layout = { ...PLOTLY_LAYOUT_DEFAULTS, xaxis: { ...PLOTLY_LAYOUT_DEFAULTS.xaxis, title: _t('measureNum') }, yaxis: { ...PLOTLY_LAYOUT_DEFAULTS.yaxis, title: _t('value') }, shapes, annotations, showlegend: true, legend: { orientation: 'h', y: -0.15 }, margin: { ...PLOTLY_LAYOUT_DEFAULTS.margin, r: 50 }, }; Plotly.newPlot(containerId, traces, layout, PLOTLY_CONFIG); } /** * Render histogram with normal curve overlay. * * @param {string} containerId - DOM element ID. * @param {Object} data - HistogramData from API. * @param {Object} [tol] - Optional tolerance limits {utl, ltl, nominal}. */ function renderHistogram(containerId, data, tol) { if (!data || !data.bins || data.bins.length === 0 || data.n === 0) { document.getElementById(containerId).innerHTML = '' + _t('noData') + '
'; return; } // Bin centers for bar chart const binCenters = []; const binWidth = data.bins[1] - data.bins[0]; for (let i = 0; i < data.counts.length; i++) { binCenters.push((data.bins[i] + data.bins[i + 1]) / 2); } const traces = [ // Histogram bars { x: binCenters, y: data.counts, type: 'bar', name: _t('frequency'), marker: { color: SPC_COLORS.primaryLight, opacity: 0.7 }, width: binWidth * 0.9, }, ]; // Normal curve overlay if (data.normal_x && data.normal_x.length > 0) { traces.push({ x: data.normal_x, y: data.normal_y, type: 'scatter', mode: 'lines', name: _t('normalCurve'), line: { color: SPC_COLORS.fail, width: 2 }, }); } const shapes = []; function addVertLine(xVal, color, dash) { if (xVal == null) return; shapes.push({ type: 'line', yref: 'paper', y0: 0, y1: 1, x0: xVal, x1: xVal, line: { color, width: 1.5, dash }, }); } if (tol) { addVertLine(tol.utl, SPC_COLORS.utl_ltl, 'dot'); addVertLine(tol.ltl, SPC_COLORS.utl_ltl, 'dot'); addVertLine(tol.nominal, SPC_COLORS.nominal, 'longdash'); } // Mean line addVertLine(data.mean, SPC_COLORS.primary, 'dash'); const layout = { ...PLOTLY_LAYOUT_DEFAULTS, xaxis: { ...PLOTLY_LAYOUT_DEFAULTS.xaxis, title: _t('value') }, yaxis: { ...PLOTLY_LAYOUT_DEFAULTS.yaxis, title: _t('frequency') }, shapes, bargap: 0.05, showlegend: true, legend: { orientation: 'h', y: -0.15 }, }; Plotly.newPlot(containerId, traces, layout, PLOTLY_CONFIG); } /** * Render a capability gauge (horizontal bar indicator for Cpk). * * @param {string} containerId - DOM element ID. * @param {Object} data - CapabilityData from API. */ function renderCapabilityGauge(containerId, data) { if (!data || data.cpk == null) { document.getElementById(containerId).innerHTML = '' + _t('indicesNotAvailable') + '
'; return; } const cpk = data.cpk; // Color zones: <1.0 = fail, 1.0-1.33 = warning, >=1.33 = pass let barColor = SPC_COLORS.fail; if (cpk >= 1.33) barColor = SPC_COLORS.pass; else if (cpk >= 1.0) barColor = SPC_COLORS.warning; const trace = { type: 'indicator', mode: 'gauge+number', value: cpk, title: { text: 'Cpk', font: { size: 14 } }, number: { font: { family: 'JetBrains Mono, monospace', size: 28 } }, gauge: { axis: { range: [0, 2.5], tickwidth: 1 }, bar: { color: barColor, thickness: 0.6 }, bgcolor: '#f1f5f9', borderwidth: 0, steps: [ { range: [0, 1.0], color: '#fecaca' }, { range: [1.0, 1.33], color: '#fef3c7' }, { range: [1.33, 2.5], color: '#d1fae5' }, ], threshold: { line: { color: SPC_COLORS.primary, width: 3 }, thickness: 0.75, value: cpk, }, }, }; const layout = { ...PLOTLY_LAYOUT_DEFAULTS, height: 200, margin: { t: 30, r: 20, b: 10, l: 20 }, }; Plotly.newPlot(containerId, [trace], layout, { ...PLOTLY_CONFIG, displayModeBar: false }); }