viewers: default mic to dB(L) + add Mic-unit toggle (dBL ↔ psi)
The sidecar-modal waveform plot was rendering mic in raw psi, while the
rest of SFM (history table column, peaks block, live-device chart,
event detail modal mic field) had already converted to dB(L) — matching
the BW Event Report convention. Unifying.
Both viewers now:
- Default mic chart values + axis title + peak label to dB(L)
- Provide a header toggle ("Mic: dBL" pill) to flip to psi
- Persist the preference via localStorage (sfm_mic_unit)
- Re-render the open chart immediately on toggle
Conversion: dBL = 20 * log10(psi / 2.9e-9), where 2.9e-9 psi is the
20 µPa reference pressure already defined for the rest of the webapp.
Non-positive psi samples (log undefined) render as null; Chart.js
handles them as gaps in line mode and missing bars in histogram mode.
Also fixes event_browser.html's stats table — the MicL row was
hard-coding "<value> psi"; now honors the same toggle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+60
-6
@@ -289,7 +289,12 @@
|
|||||||
</select>
|
</select>
|
||||||
<input type="search" id="event-filter" placeholder="filter events…" />
|
<input type="search" id="event-filter" placeholder="filter events…" />
|
||||||
<span class="pill" id="count-pill">—</span>
|
<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>
|
<button id="reload-btn" onclick="loadSerials()">Reload</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -328,6 +333,29 @@ const CHANNEL_COLORS = {
|
|||||||
};
|
};
|
||||||
const CHANNEL_ORDER = ['MicL', 'Long', 'Vert', 'Tran'];
|
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
|
// Adaptive decimal formatter — scientific notation only for truly extreme
|
||||||
// values. Normal-range peaks render as plain decimals with sensible
|
// values. Normal-range peaks render as plain decimals with sensible
|
||||||
// precision (was previously forcing toExponential(3) which produced ugly
|
// precision (was previously forcing toExponential(3) which produced ugly
|
||||||
@@ -502,6 +530,19 @@ function renderMeta(data, ev) {
|
|||||||
['Vert', ev?.vert_ppv],
|
['Vert', ev?.vert_ppv],
|
||||||
['Long', ev?.long_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 = `
|
const statsHtml = `
|
||||||
<table class="stats-table">
|
<table class="stats-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -509,7 +550,7 @@ function renderMeta(data, ev) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${rows.map(([ch, ppv]) => `<tr><td>${ch}</td><td>${fmt(ppv)}</td></tr>`).join('')}
|
${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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
`;
|
`;
|
||||||
@@ -560,11 +601,11 @@ function renderWaveform(data) {
|
|||||||
);
|
);
|
||||||
const lastDataCh = channelsWithData[channelsWithData.length - 1];
|
const lastDataCh = channelsWithData[channelsWithData.length - 1];
|
||||||
|
|
||||||
|
const micUnit = _getMicUnit();
|
||||||
for (const ch of CHANNEL_ORDER) {
|
for (const ch of CHANNEL_ORDER) {
|
||||||
const chData = channels[ch];
|
const chData = channels[ch];
|
||||||
if (!chData) continue;
|
if (!chData) continue;
|
||||||
const values = chData.values || [];
|
if ((chData.values || []).length === 0) {
|
||||||
if (values.length === 0) {
|
|
||||||
// Render an empty card so user sees the channel exists but is missing
|
// Render an empty card so user sees the channel exists but is missing
|
||||||
const wrap = document.createElement('div');
|
const wrap = document.createElement('div');
|
||||||
wrap.className = 'chart-wrap';
|
wrap.className = 'chart-wrap';
|
||||||
@@ -579,9 +620,19 @@ function renderWaveform(data) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const unit = chData.unit || 'unit';
|
// Mic channel: convert from raw psi to dB(L) when the user prefers dBL
|
||||||
const peak = chData.peak;
|
// (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;
|
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
|
const peakLabel = peak != null
|
||||||
? `peak ${_fmtPeak(peak, unit)}`
|
? `peak ${_fmtPeak(peak, unit)}`
|
||||||
+ (!isHistogram && peakT != null ? ` @ ${peakT.toFixed(1)} ms` : '')
|
+ (!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);
|
document.getElementById('event-filter').addEventListener('input', applyFilter);
|
||||||
|
|
||||||
|
// Reflect any persisted mic-unit preference in the header pill on load
|
||||||
|
_refreshMicUnitToggle();
|
||||||
|
|
||||||
// Initial load
|
// Initial load
|
||||||
loadSerials();
|
loadSerials();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+50
-5
@@ -818,6 +818,12 @@
|
|||||||
<span class="ft-dot"></span>
|
<span class="ft-dot"></span>
|
||||||
<span>Force refresh</span>
|
<span>Force refresh</span>
|
||||||
</label>
|
</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>
|
</header>
|
||||||
|
|
||||||
<!-- ════════════════════════════════════════════════════════════════
|
<!-- ════════════════════════════════════════════════════════════════
|
||||||
@@ -2560,6 +2566,29 @@ const _SC_CHANNEL_COLORS = {
|
|||||||
const _SC_CHANNEL_ORDER = ['MicL', 'Long', 'Vert', 'Tran'];
|
const _SC_CHANNEL_ORDER = ['MicL', 'Long', 'Vert', 'Tran'];
|
||||||
let _scCharts = {};
|
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
|
// Adaptive decimal formatter — scientific notation is reserved for truly
|
||||||
// extreme values (10000+ or sub-0.0001). Normal-range values (most peaks
|
// extreme values (10000+ or sub-0.0001). Normal-range values (most peaks
|
||||||
// fall here) render as decimals with sensible precision. Replaces the
|
// fall here) render as decimals with sensible precision. Replaces the
|
||||||
@@ -2610,17 +2639,30 @@ function _renderScWaveform(data) {
|
|||||||
);
|
);
|
||||||
const lastCh = withData[withData.length - 1];
|
const lastCh = withData[withData.length - 1];
|
||||||
|
|
||||||
|
const micUnit = _getMicUnit(); // user preference: 'dBL' or 'psi'
|
||||||
|
|
||||||
for (const ch of _SC_CHANNEL_ORDER) {
|
for (const ch of _SC_CHANNEL_ORDER) {
|
||||||
const chData = channels[ch];
|
const chData = channels[ch];
|
||||||
if (!chData) continue;
|
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');
|
const wrap = document.createElement('div');
|
||||||
wrap.style.cssText = 'background:var(--surface);border:1px solid var(--border2);border-radius:6px;padding:6px 30px 4px 10px';
|
wrap.style.cssText = 'background:var(--surface);border:1px solid var(--border2);border-radius:6px;padding:6px 30px 4px 10px';
|
||||||
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 = chPeak != null
|
||||||
? `peak ${_fmtPeak(chData.peak, chData.unit)}`
|
? `peak ${_fmtPeak(chPeak, chUnit)}`
|
||||||
: '';
|
: '';
|
||||||
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);
|
||||||
@@ -2716,7 +2758,7 @@ function _renderScWaveform(data) {
|
|||||||
title: items => isHistogram
|
title: items => isHistogram
|
||||||
? `interval ${items[0].label}`
|
? `interval ${items[0].label}`
|
||||||
: `t = ${items[0].label} ms`,
|
: `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,
|
...yBounds,
|
||||||
ticks: { color: '#484f58', maxTicksLimit: 4 },
|
ticks: { color: '#484f58', maxTicksLimit: 4 },
|
||||||
grid: { color: '#21262d' },
|
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.
|
// hit localhost:8200, 10.0.0.44:8200, or anything else.
|
||||||
document.getElementById('api-base').value = window.location.origin;
|
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
|
// We default to Database view → trigger initial history + units load
|
||||||
// (switchSection handles this when clicked, but we never click on first paint).
|
// (switchSection handles this when clicked, but we never click on first paint).
|
||||||
if (currentSection === 'db') {
|
if (currentSection === 'db') {
|
||||||
|
|||||||
Reference in New Issue
Block a user