v0.20.0 -- Full s3 event parse and PDF creation. #28
+60
-6
@@ -289,7 +289,12 @@
|
||||
</select>
|
||||
<input type="search" id="event-filter" placeholder="filter events…" />
|
||||
<span class="pill" id="count-pill">—</span>
|
||||
<button id="print-btn" onclick="togglePrintView()" style="margin-left:auto;background:#21262d">Print view</button>
|
||||
<button id="mic-unit-toggle" style="margin-left:auto;background:#21262d"
|
||||
onclick="_setMicUnit(_getMicUnit() === 'dBL' ? 'psi' : 'dBL')"
|
||||
title="Toggle mic display unit (dBL ↔ psi). Persists across page loads.">
|
||||
Mic: dBL
|
||||
</button>
|
||||
<button id="print-btn" onclick="togglePrintView()" style="background:#21262d">Print view</button>
|
||||
<button id="reload-btn" onclick="loadSerials()">Reload</button>
|
||||
</header>
|
||||
|
||||
@@ -328,6 +333,29 @@ const CHANNEL_COLORS = {
|
||||
};
|
||||
const CHANNEL_ORDER = ['MicL', 'Long', 'Vert', 'Tran'];
|
||||
|
||||
// Reference pressure for dB(L) — 20 µPa expressed in psi (≈ 2.9e-9 psi).
|
||||
const DBL_REF = 2.9e-9;
|
||||
|
||||
// User-toggleable mic display unit: 'dBL' (default, matches BW printout
|
||||
// + the rest of SFM) or 'psi' (raw sample unit).
|
||||
function _getMicUnit() {
|
||||
return localStorage.getItem('sfm_mic_unit') === 'psi' ? 'psi' : 'dBL';
|
||||
}
|
||||
function _setMicUnit(u) {
|
||||
localStorage.setItem('sfm_mic_unit', u === 'psi' ? 'psi' : 'dBL');
|
||||
_refreshMicUnitToggle();
|
||||
if (currentEventId) loadEvent(currentEventId);
|
||||
}
|
||||
function _refreshMicUnitToggle() {
|
||||
const b = document.getElementById('mic-unit-toggle');
|
||||
if (b) b.textContent = `Mic: ${_getMicUnit()}`;
|
||||
}
|
||||
// psi → dB(L). Null for non-positive (log undefined; Chart.js renders as a gap).
|
||||
function _psiToDbl(psi) {
|
||||
if (psi == null || !(psi > 0)) return null;
|
||||
return 20 * Math.log10(psi / DBL_REF);
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -502,6 +530,19 @@ function renderMeta(data, ev) {
|
||||
['Vert', ev?.vert_ppv],
|
||||
['Long', ev?.long_ppv],
|
||||
];
|
||||
// Mic display honors the current user preference (dBL default).
|
||||
// mic_ppv is stored as raw psi on series3 events; convert when needed.
|
||||
const micPsi = ev?.mic_ppv;
|
||||
const micUnitDisplay = _getMicUnit();
|
||||
let micStr;
|
||||
if (micPsi == null) {
|
||||
micStr = '—';
|
||||
} else if (micUnitDisplay === 'dBL') {
|
||||
const d = _psiToDbl(Number(micPsi));
|
||||
micStr = (d != null ? d.toFixed(1) : '—') + ' dBL';
|
||||
} else {
|
||||
micStr = Number(micPsi).toExponential(2) + ' psi';
|
||||
}
|
||||
const statsHtml = `
|
||||
<table class="stats-table">
|
||||
<thead>
|
||||
@@ -509,7 +550,7 @@ function renderMeta(data, ev) {
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows.map(([ch, ppv]) => `<tr><td>${ch}</td><td>${fmt(ppv)}</td></tr>`).join('')}
|
||||
<tr><td>MicL</td><td>${fmt(ev?.mic_ppv)} psi</td></tr>
|
||||
<tr><td>MicL</td><td>${micStr}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
@@ -560,11 +601,11 @@ function renderWaveform(data) {
|
||||
);
|
||||
const lastDataCh = channelsWithData[channelsWithData.length - 1];
|
||||
|
||||
const micUnit = _getMicUnit();
|
||||
for (const ch of CHANNEL_ORDER) {
|
||||
const chData = channels[ch];
|
||||
if (!chData) continue;
|
||||
const values = chData.values || [];
|
||||
if (values.length === 0) {
|
||||
if ((chData.values || []).length === 0) {
|
||||
// Render an empty card so user sees the channel exists but is missing
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'chart-wrap';
|
||||
@@ -579,9 +620,19 @@ function renderWaveform(data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const unit = chData.unit || 'unit';
|
||||
const peak = chData.peak;
|
||||
// Mic channel: convert from raw psi to dB(L) when the user prefers dBL
|
||||
// (the default). We mutate `values`, `peak`, and `unit` locally so the
|
||||
// chart datasets + axis title + tooltip + peak label all stay aligned.
|
||||
let values = chData.values || [];
|
||||
let unit = chData.unit || 'unit';
|
||||
let peak = chData.peak;
|
||||
const peakT = chData.peak_t_ms;
|
||||
if (ch === 'MicL' && unit === 'psi' && micUnit === 'dBL') {
|
||||
values = values.map(_psiToDbl);
|
||||
peak = _psiToDbl(peak);
|
||||
unit = 'dB(L)';
|
||||
}
|
||||
|
||||
const peakLabel = peak != null
|
||||
? `peak ${_fmtPeak(peak, unit)}`
|
||||
+ (!isHistogram && peakT != null ? ` @ ${peakT.toFixed(1)} ms` : '')
|
||||
@@ -781,6 +832,9 @@ document.getElementById('serial-select').addEventListener('change', e => {
|
||||
});
|
||||
document.getElementById('event-filter').addEventListener('input', applyFilter);
|
||||
|
||||
// Reflect any persisted mic-unit preference in the header pill on load
|
||||
_refreshMicUnitToggle();
|
||||
|
||||
// Initial load
|
||||
loadSerials();
|
||||
</script>
|
||||
|
||||
+50
-5
@@ -818,6 +818,12 @@
|
||||
<span class="ft-dot"></span>
|
||||
<span>Force refresh</span>
|
||||
</label>
|
||||
<div class="hdr-sep"></div>
|
||||
<button id="mic-unit-toggle" class="section-btn"
|
||||
onclick="_setMicUnit(_getMicUnit() === 'dBL' ? 'psi' : 'dBL')"
|
||||
title="Toggle microphone display unit (dBL ↔ psi) for waveform plots. Affects all mic charts; persists across page loads.">
|
||||
Mic: dBL
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════
|
||||
@@ -2560,6 +2566,29 @@ const _SC_CHANNEL_COLORS = {
|
||||
const _SC_CHANNEL_ORDER = ['MicL', 'Long', 'Vert', 'Tran'];
|
||||
let _scCharts = {};
|
||||
|
||||
// User preference for how mic is displayed in plots — dBL (default,
|
||||
// matches BW printout convention + the rest of SFM) or psi (the raw
|
||||
// sample unit). Toggleable via the header pill; persists in localStorage.
|
||||
function _getMicUnit() {
|
||||
return localStorage.getItem('sfm_mic_unit') === 'psi' ? 'psi' : 'dBL';
|
||||
}
|
||||
function _setMicUnit(u) {
|
||||
localStorage.setItem('sfm_mic_unit', u === 'psi' ? 'psi' : 'dBL');
|
||||
_refreshMicUnitToggleLabel();
|
||||
// Re-render the open modal so the change is immediately visible.
|
||||
if (_scCurrentEventId) openSidecarModal(_scCurrentEventId);
|
||||
}
|
||||
function _refreshMicUnitToggleLabel() {
|
||||
const b = document.getElementById('mic-unit-toggle');
|
||||
if (b) b.textContent = `Mic: ${_getMicUnit()}`;
|
||||
}
|
||||
// Convert a psi value to dB(L). Returns null for non-positive values
|
||||
// (log of zero is undefined) — Chart.js handles null as a gap in the line.
|
||||
function _psiToDbl(psi) {
|
||||
if (psi == null || !(psi > 0)) return null;
|
||||
return 20 * Math.log10(psi / DBL_REF);
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -2610,17 +2639,30 @@ function _renderScWaveform(data) {
|
||||
);
|
||||
const lastCh = withData[withData.length - 1];
|
||||
|
||||
const micUnit = _getMicUnit(); // user preference: 'dBL' or 'psi'
|
||||
|
||||
for (const ch of _SC_CHANNEL_ORDER) {
|
||||
const chData = channels[ch];
|
||||
if (!chData) continue;
|
||||
const values = chData.values || [];
|
||||
let values = chData.values || [];
|
||||
let chUnit = chData.unit || '';
|
||||
let chPeak = chData.peak;
|
||||
|
||||
// Mic channel: convert from raw psi to dB(L) when user prefers dBL
|
||||
// (default). Mic samples that are zero/negative become null (Chart.js
|
||||
// renders them as gaps in line mode, zero-height bars in histogram mode).
|
||||
if (ch === 'MicL' && chUnit === 'psi' && micUnit === 'dBL') {
|
||||
values = values.map(_psiToDbl);
|
||||
chPeak = _psiToDbl(chPeak);
|
||||
chUnit = 'dB(L)';
|
||||
}
|
||||
|
||||
const wrap = document.createElement('div');
|
||||
wrap.style.cssText = 'background:var(--surface);border:1px solid var(--border2);border-radius:6px;padding:6px 30px 4px 10px';
|
||||
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`;
|
||||
const peakStr = chData.peak != null
|
||||
? `peak ${_fmtPeak(chData.peak, chData.unit)}`
|
||||
const peakStr = chPeak != null
|
||||
? `peak ${_fmtPeak(chPeak, chUnit)}`
|
||||
: '';
|
||||
lbl.innerHTML = `<span>${ch}</span><span style="color:var(--text-dim);font-weight:normal">${peakStr}</span>`;
|
||||
wrap.appendChild(lbl);
|
||||
@@ -2716,7 +2758,7 @@ function _renderScWaveform(data) {
|
||||
title: items => isHistogram
|
||||
? `interval ${items[0].label}`
|
||||
: `t = ${items[0].label} ms`,
|
||||
label: item => `${ch}: ${_fmtPeak(item.raw, chData.unit)}`,
|
||||
label: item => `${ch}: ${_fmtPeak(item.raw, chUnit)}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2730,7 +2772,7 @@ function _renderScWaveform(data) {
|
||||
...yBounds,
|
||||
ticks: { color: '#484f58', maxTicksLimit: 4 },
|
||||
grid: { color: '#21262d' },
|
||||
title: { display: true, text: chData.unit || '', color: '#484f58', font: { size: 9 } },
|
||||
title: { display: true, text: chUnit, color: '#484f58', font: { size: 9 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -3058,6 +3100,9 @@ document.addEventListener('keydown', e => {
|
||||
// hit localhost:8200, 10.0.0.44:8200, or anything else.
|
||||
document.getElementById('api-base').value = window.location.origin;
|
||||
|
||||
// Reflect any persisted mic-unit preference in the header pill on load
|
||||
_refreshMicUnitToggleLabel();
|
||||
|
||||
// We default to Database view → trigger initial history + units load
|
||||
// (switchSection handles this when clicked, but we never click on first paint).
|
||||
if (currentSection === 'db') {
|
||||
|
||||
Reference in New Issue
Block a user