v0.20.0 -- Full s3 event parse and PDF creation. #28
+55
-12
@@ -328,6 +328,23 @@ const CHANNEL_COLORS = {
|
|||||||
};
|
};
|
||||||
const CHANNEL_ORDER = ['MicL', 'Long', 'Vert', 'Tran'];
|
const CHANNEL_ORDER = ['MicL', 'Long', 'Vert', 'Tran'];
|
||||||
|
|
||||||
|
// Adaptive decimal formatter — scientific notation only for truly extreme
|
||||||
|
// values. Normal-range peaks render as plain decimals with sensible
|
||||||
|
// precision (was previously forcing toExponential(3) which produced ugly
|
||||||
|
// "2.500E-2 IN/S" labels).
|
||||||
|
function _fmtPeak(v, unit) {
|
||||||
|
if (v == null || (typeof v === 'number' && !isFinite(v))) return '';
|
||||||
|
if (typeof v !== 'number') return String(v) + (unit ? ' ' + unit : '');
|
||||||
|
if (v === 0) return '0' + (unit ? ' ' + unit : '');
|
||||||
|
const a = Math.abs(v);
|
||||||
|
const u = unit ? ' ' + unit : '';
|
||||||
|
if (a >= 0.0001 && a < 10000) {
|
||||||
|
const d = a >= 100 ? 1 : a >= 10 ? 2 : a >= 1 ? 3 : a >= 0.1 ? 4 : 5;
|
||||||
|
return v.toFixed(d) + u;
|
||||||
|
}
|
||||||
|
return v.toExponential(2) + u;
|
||||||
|
}
|
||||||
|
|
||||||
let allEvents = [];
|
let allEvents = [];
|
||||||
let filteredEvents = [];
|
let filteredEvents = [];
|
||||||
let currentEventId = null;
|
let currentEventId = null;
|
||||||
@@ -530,6 +547,10 @@ function renderWaveform(data) {
|
|||||||
const dtMs = ta.dt_ms || (1000.0 / sr);
|
const dtMs = ta.dt_ms || (1000.0 / sr);
|
||||||
const t0Ms = ta.t0_ms != null ? ta.t0_ms : 0;
|
const t0Ms = ta.t0_ms != null ? ta.t0_ms : 0;
|
||||||
const isPrintMode = document.body.classList.contains('print-view');
|
const isPrintMode = document.body.classList.contains('print-view');
|
||||||
|
// Histograms record per-interval peaks (typically 1 per minute/5-min),
|
||||||
|
// not per-sample waveforms. Render as a tight bar graph instead of a
|
||||||
|
// line plot — matches the BW Event Report's histogram presentation.
|
||||||
|
const isHistogram = String(data.record_type || '').toLowerCase().includes('histogram');
|
||||||
|
|
||||||
// Which channels actually have data → determines which one renders the
|
// Which channels actually have data → determines which one renders the
|
||||||
// shared x-axis at the bottom (Instantel printout has the time scale
|
// shared x-axis at the bottom (Instantel printout has the time scale
|
||||||
@@ -562,8 +583,8 @@ function renderWaveform(data) {
|
|||||||
const peak = chData.peak;
|
const peak = chData.peak;
|
||||||
const peakT = chData.peak_t_ms;
|
const peakT = chData.peak_t_ms;
|
||||||
const peakLabel = peak != null
|
const peakLabel = peak != null
|
||||||
? `peak ${(typeof peak === 'number' ? peak.toExponential(3) : peak)} ${unit}`
|
? `peak ${_fmtPeak(peak, unit)}`
|
||||||
+ (peakT != null ? ` @ ${peakT.toFixed(1)} ms` : '')
|
+ (!isHistogram && peakT != null ? ` @ ${peakT.toFixed(1)} ms` : '')
|
||||||
: '';
|
: '';
|
||||||
// Hide x-axis on every chart except the bottom-most data channel —
|
// Hide x-axis on every chart except the bottom-most data channel —
|
||||||
// gives the "single shared time axis" feel of the BW printout.
|
// gives the "single shared time axis" feel of the BW printout.
|
||||||
@@ -583,8 +604,12 @@ function renderWaveform(data) {
|
|||||||
wrap.appendChild(canvasWrap);
|
wrap.appendChild(canvasWrap);
|
||||||
chartsDiv.appendChild(wrap);
|
chartsDiv.appendChild(wrap);
|
||||||
|
|
||||||
// Per-sample time in ms relative to trigger. Negative for pre-trigger samples.
|
// Waveform: per-sample time in ms relative to trigger (negative for pretrig).
|
||||||
const times = values.map((_, i) => t0Ms + i * dtMs);
|
// Histogram: interval index (1..N); sample_rate-based time math doesn't
|
||||||
|
// apply to per-interval peaks.
|
||||||
|
const times = isHistogram
|
||||||
|
? values.map((_, i) => i + 1)
|
||||||
|
: values.map((_, i) => t0Ms + i * dtMs);
|
||||||
|
|
||||||
// Downsample for rendering
|
// Downsample for rendering
|
||||||
const MAX_POINTS = 4000;
|
const MAX_POINTS = 4000;
|
||||||
@@ -595,11 +620,26 @@ function renderWaveform(data) {
|
|||||||
rV = values.filter((_, i) => i % step === 0);
|
rV = values.filter((_, i) => i % step === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tick formatter — round to 1 decimal so we don't get
|
||||||
|
// "11.7187040000000002 ms" garbage from floating-point accumulation.
|
||||||
|
const xAxisUnit = isHistogram ? '' : ' ms';
|
||||||
|
const fmtTick = i => {
|
||||||
|
const v = rT[i];
|
||||||
|
if (typeof v !== 'number') return String(v) + xAxisUnit;
|
||||||
|
return (Number.isInteger(v) ? String(v) : v.toFixed(1)) + xAxisUnit;
|
||||||
|
};
|
||||||
|
|
||||||
const chart = new Chart(canvas, {
|
const chart = new Chart(canvas, {
|
||||||
type: 'line',
|
type: isHistogram ? 'bar' : 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: rT.map(t => (typeof t === 'number' ? t.toFixed(2) : t)),
|
labels: rT.map(t => (typeof t === 'number' ? (Number.isInteger(t) ? String(t) : t.toFixed(2)) : t)),
|
||||||
datasets: [{
|
datasets: isHistogram ? [{
|
||||||
|
data: rV,
|
||||||
|
backgroundColor: CHANNEL_COLORS[ch],
|
||||||
|
borderWidth: 0,
|
||||||
|
barPercentage: 1.0,
|
||||||
|
categoryPercentage: 1.0, // bars touch — tight bargraph
|
||||||
|
}] : [{
|
||||||
data: rV,
|
data: rV,
|
||||||
borderColor: CHANNEL_COLORS[ch],
|
borderColor: CHANNEL_COLORS[ch],
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
@@ -617,8 +657,10 @@ function renderWaveform(data) {
|
|||||||
mode: 'index',
|
mode: 'index',
|
||||||
intersect: false,
|
intersect: false,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
title: items => `t = ${items[0].label} ms`,
|
title: items => isHistogram
|
||||||
label: item => `${ch}: ${item.raw} ${unit}`,
|
? `interval ${items[0].label}`
|
||||||
|
: `t = ${items[0].label} ms`,
|
||||||
|
label: item => `${ch}: ${_fmtPeak(item.raw, unit)}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -630,7 +672,7 @@ function renderWaveform(data) {
|
|||||||
color: isPrintMode ? '#666' : '#484f58',
|
color: isPrintMode ? '#666' : '#484f58',
|
||||||
maxTicksLimit: 10,
|
maxTicksLimit: 10,
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
callback: (val, i) => rT[i] + ' ms',
|
callback: (val, i) => fmtTick(i),
|
||||||
},
|
},
|
||||||
grid: { color: isPrintMode ? '#e0e0e0' : '#21262d', drawTicks: showXAxis },
|
grid: { color: isPrintMode ? '#e0e0e0' : '#21262d', drawTicks: showXAxis },
|
||||||
},
|
},
|
||||||
@@ -642,10 +684,11 @@ function renderWaveform(data) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [{
|
plugins: isHistogram ? [] : [{
|
||||||
// Trigger line @ t=0 + triangle markers above/below + "0.0"
|
// Trigger line @ t=0 + triangle markers above/below + "0.0"
|
||||||
// baseline label on the right edge. Matches the Instantel
|
// baseline label on the right edge. Matches the Instantel
|
||||||
// BW Event Report printout style.
|
// BW Event Report printout style. Skipped for histograms —
|
||||||
|
// they have no trigger event.
|
||||||
id: 'instantelOverlays',
|
id: 'instantelOverlays',
|
||||||
afterDraw(chart) {
|
afterDraw(chart) {
|
||||||
const ctx = chart.ctx;
|
const ctx = chart.ctx;
|
||||||
|
|||||||
+59
-10
@@ -2560,6 +2560,23 @@ const _SC_CHANNEL_COLORS = {
|
|||||||
const _SC_CHANNEL_ORDER = ['MicL', 'Long', 'Vert', 'Tran'];
|
const _SC_CHANNEL_ORDER = ['MicL', 'Long', 'Vert', 'Tran'];
|
||||||
let _scCharts = {};
|
let _scCharts = {};
|
||||||
|
|
||||||
|
// Adaptive decimal formatter — scientific notation is reserved for truly
|
||||||
|
// extreme values (10000+ or sub-0.0001). Normal-range values (most peaks
|
||||||
|
// fall here) render as decimals with sensible precision. Replaces the
|
||||||
|
// previous .toExponential(3) call that turned every peak into ugly "2.500E-2".
|
||||||
|
function _fmtPeak(v, unit) {
|
||||||
|
if (v == null || (typeof v === 'number' && !isFinite(v))) return '';
|
||||||
|
if (typeof v !== 'number') return String(v) + (unit ? ' ' + unit : '');
|
||||||
|
if (v === 0) return '0' + (unit ? ' ' + unit : '');
|
||||||
|
const a = Math.abs(v);
|
||||||
|
const u = unit ? ' ' + unit : '';
|
||||||
|
if (a >= 0.0001 && a < 10000) {
|
||||||
|
const d = a >= 100 ? 1 : a >= 10 ? 2 : a >= 1 ? 3 : a >= 0.1 ? 4 : 5;
|
||||||
|
return v.toFixed(d) + u;
|
||||||
|
}
|
||||||
|
return v.toExponential(2) + u;
|
||||||
|
}
|
||||||
|
|
||||||
function _destroyScCharts() {
|
function _destroyScCharts() {
|
||||||
Object.values(_scCharts).forEach(c => { try { c.destroy(); } catch {} });
|
Object.values(_scCharts).forEach(c => { try { c.destroy(); } catch {} });
|
||||||
_scCharts = {};
|
_scCharts = {};
|
||||||
@@ -2580,6 +2597,12 @@ function _renderScWaveform(data) {
|
|||||||
const sr = ta.sample_rate || 1024;
|
const sr = ta.sample_rate || 1024;
|
||||||
const dtMs = ta.dt_ms || (1000.0 / sr);
|
const dtMs = ta.dt_ms || (1000.0 / sr);
|
||||||
const t0Ms = ta.t0_ms != null ? ta.t0_ms : 0;
|
const t0Ms = ta.t0_ms != null ? ta.t0_ms : 0;
|
||||||
|
// Histogram events have per-interval peaks, not per-sample data.
|
||||||
|
// Render as bars (one per interval) instead of a connected line, and
|
||||||
|
// suppress trigger/zero overlays which don't apply. X-axis becomes
|
||||||
|
// interval index since the sample_rate-based time math is meaningless
|
||||||
|
// here (each "sample" is one interval, typically 1-5 minutes long).
|
||||||
|
const isHistogram = String(data.record_type || '').toLowerCase().includes('histogram');
|
||||||
|
|
||||||
// Which channels have data — determines which one renders the shared bottom axis.
|
// Which channels have data — determines which one renders the shared bottom axis.
|
||||||
const withData = _SC_CHANNEL_ORDER.filter(ch =>
|
const withData = _SC_CHANNEL_ORDER.filter(ch =>
|
||||||
@@ -2597,7 +2620,7 @@ function _renderScWaveform(data) {
|
|||||||
const lbl = document.createElement('div');
|
const lbl = document.createElement('div');
|
||||||
lbl.style.cssText = `font-size:10px;font-weight:600;letter-spacing:0.05em;text-transform:uppercase;margin-bottom:2px;color:${_SC_CHANNEL_COLORS[ch]};display:flex;justify-content:space-between`;
|
lbl.style.cssText = `font-size:10px;font-weight:600;letter-spacing:0.05em;text-transform:uppercase;margin-bottom:2px;color:${_SC_CHANNEL_COLORS[ch]};display:flex;justify-content:space-between`;
|
||||||
const peakStr = chData.peak != null
|
const peakStr = chData.peak != null
|
||||||
? `peak ${(typeof chData.peak === 'number' ? chData.peak.toExponential(3) : chData.peak)} ${chData.unit || ''}`
|
? `peak ${_fmtPeak(chData.peak, chData.unit)}`
|
||||||
: '';
|
: '';
|
||||||
lbl.innerHTML = `<span>${ch}</span><span style="color:var(--text-dim);font-weight:normal">${peakStr}</span>`;
|
lbl.innerHTML = `<span>${ch}</span><span style="color:var(--text-dim);font-weight:normal">${peakStr}</span>`;
|
||||||
wrap.appendChild(lbl);
|
wrap.appendChild(lbl);
|
||||||
@@ -2618,8 +2641,11 @@ function _renderScWaveform(data) {
|
|||||||
wrap.appendChild(canvasWrap);
|
wrap.appendChild(canvasWrap);
|
||||||
chartsDiv.appendChild(wrap);
|
chartsDiv.appendChild(wrap);
|
||||||
|
|
||||||
// Per-sample time in ms relative to trigger. Negative for pre-trigger samples.
|
// Waveform: per-sample time in ms relative to trigger (negative for pretrig).
|
||||||
const times = values.map((_, i) => t0Ms + i * dtMs);
|
// Histogram: interval index (1..N); time math doesn't apply to per-interval peaks.
|
||||||
|
const times = isHistogram
|
||||||
|
? values.map((_, i) => i + 1)
|
||||||
|
: values.map((_, i) => t0Ms + i * dtMs);
|
||||||
|
|
||||||
// Downsample for rendering when very long.
|
// Downsample for rendering when very long.
|
||||||
const MAX = 3000;
|
const MAX = 3000;
|
||||||
@@ -2631,11 +2657,30 @@ function _renderScWaveform(data) {
|
|||||||
}
|
}
|
||||||
const showX = (ch === lastCh);
|
const showX = (ch === lastCh);
|
||||||
|
|
||||||
|
// Tick label formatter: snap floats to 1 decimal place so we don't get
|
||||||
|
// "11.7187040000000002 ms" garbage from accumulated floating-point error.
|
||||||
|
const xAxisLabel = isHistogram ? '' : ' ms';
|
||||||
|
const fmtTick = i => {
|
||||||
|
const v = rT[i];
|
||||||
|
if (typeof v === 'number') {
|
||||||
|
// Whole numbers (intervals) → no decimals. Sub-integer ms → 1 decimal.
|
||||||
|
const s = Number.isInteger(v) ? String(v) : v.toFixed(1);
|
||||||
|
return s + xAxisLabel;
|
||||||
|
}
|
||||||
|
return String(v) + xAxisLabel;
|
||||||
|
};
|
||||||
|
|
||||||
_scCharts[ch] = new Chart(canvas, {
|
_scCharts[ch] = new Chart(canvas, {
|
||||||
type: 'line',
|
type: isHistogram ? 'bar' : 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: rT.map(t => (typeof t === 'number' ? t.toFixed(2) : t)),
|
labels: rT.map(t => (typeof t === 'number' ? (Number.isInteger(t) ? String(t) : t.toFixed(2)) : t)),
|
||||||
datasets: [{
|
datasets: isHistogram ? [{
|
||||||
|
data: rV,
|
||||||
|
backgroundColor: _SC_CHANNEL_COLORS[ch],
|
||||||
|
borderWidth: 0,
|
||||||
|
barPercentage: 1.0,
|
||||||
|
categoryPercentage: 1.0, // bars touch — "tight bargraph" look
|
||||||
|
}] : [{
|
||||||
data: rV,
|
data: rV,
|
||||||
borderColor: _SC_CHANNEL_COLORS[ch],
|
borderColor: _SC_CHANNEL_COLORS[ch],
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
@@ -2650,15 +2695,17 @@ function _renderScWaveform(data) {
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
mode: 'index', intersect: false,
|
mode: 'index', intersect: false,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
title: items => `t = ${items[0].label} ms`,
|
title: items => isHistogram
|
||||||
label: item => `${ch}: ${item.raw} ${chData.unit || ''}`,
|
? `interval ${items[0].label}`
|
||||||
|
: `t = ${items[0].label} ms`,
|
||||||
|
label: item => `${ch}: ${_fmtPeak(item.raw, chData.unit)}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
type: 'category', display: showX,
|
type: 'category', display: showX,
|
||||||
ticks: { color: '#484f58', maxTicksLimit: 8, maxRotation: 0, callback: (v, i) => rT[i] + ' ms' },
|
ticks: { color: '#484f58', maxTicksLimit: 8, maxRotation: 0, callback: (v, i) => fmtTick(i) },
|
||||||
grid: { color: '#21262d', drawTicks: showX },
|
grid: { color: '#21262d', drawTicks: showX },
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
@@ -2668,7 +2715,9 @@ function _renderScWaveform(data) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [{
|
plugins: isHistogram ? [] : [{
|
||||||
|
// Trigger line + triangle markers + zero baseline — only meaningful
|
||||||
|
// for waveform-mode events. Histograms have no trigger.
|
||||||
id: 'overlays',
|
id: 'overlays',
|
||||||
afterDraw(chart) {
|
afterDraw(chart) {
|
||||||
const ctx = chart.ctx, x = chart.scales.x, y = chart.scales.y;
|
const ctx = chart.ctx, x = chart.scales.x, y = chart.scales.y;
|
||||||
|
|||||||
Reference in New Issue
Block a user