viewers: render pre-trigger samples (time_axis is metadata, not an array)

The /db/events/{id}/waveform.json endpoint returns `time_axis` as a
metadata object — {sample_rate, pretrig_samples, t0_ms, dt_ms,
n_samples, total_samples, rectime_seconds} — not a per-sample times
array.  Both viewers (sfm_webapp.html sidecar modal + event_browser.html)
were treating it as an array, silently falling back to a derived path
that ignored pretrig entirely and started the time axis at 0.

Symptom: trigger line drawn at the very left edge of every chart, no
visible "leading up to the event" samples even though they're in the
decoded data.

Fix: read time_axis.t0_ms (negative when pretrig samples exist),
time_axis.dt_ms, build per-sample times as `t0_ms + i * dt_ms`.  Trigger
line lands at sample where t crosses 0; pretrig samples render at
negative t to the left of it.

Confirmed on a K558 event with 208 pretrig samples + 2 sec rectime at
1024 sps — time axis now spans -203 ms to +2046 ms, trigger line at
~9% from the left edge as expected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 21:58:20 +00:00
parent fd0e28657d
commit 6abfadae4f
2 changed files with 19 additions and 20 deletions
+9 -10
View File
@@ -522,8 +522,13 @@ function renderWaveform(data) {
charts = {}; charts = {};
const channels = data.channels || {}; const channels = data.channels || {};
const timeAxis = data.time_axis || null; // ms relative to trigger // time_axis is METADATA from sfm.plot.v1 — sample_rate, pretrig_samples,
const triggerMs = data.trigger_ms ?? 0; // t0_ms (first-sample time relative to trigger; negative when pretrig
// exists), dt_ms. Trigger is at t=0 by convention.
const ta = data.time_axis || {};
const sr = ta.sample_rate || 1024;
const dtMs = ta.dt_ms || (1000.0 / sr);
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');
// Which channels actually have data → determines which one renders the // Which channels actually have data → determines which one renders the
@@ -578,14 +583,8 @@ function renderWaveform(data) {
wrap.appendChild(canvasWrap); wrap.appendChild(canvasWrap);
chartsDiv.appendChild(wrap); chartsDiv.appendChild(wrap);
// Build time labels — use server-provided time_axis if present, else derive from sample_rate // Per-sample time in ms relative to trigger. Negative for pre-trigger samples.
let times; const times = values.map((_, i) => t0Ms + i * dtMs);
if (timeAxis && timeAxis.length === values.length) {
times = timeAxis;
} else {
const sr = data.sample_rate || 1024;
times = values.map((_, i) => (i / sr * 1000 - triggerMs));
}
// Downsample for rendering // Downsample for rendering
const MAX_POINTS = 4000; const MAX_POINTS = 4000;
+10 -10
View File
@@ -2572,8 +2572,14 @@ function _renderScWaveform(data) {
_destroyScCharts(); _destroyScCharts();
const channels = data.channels || {}; const channels = data.channels || {};
const timeAxis = data.time_axis || null; // time_axis is METADATA, not an array — it carries sample_rate,
const triggerMs = data.trigger_ms ?? 0; // pretrig_samples, t0_ms (first-sample time relative to trigger,
// negative when pretrig samples exist), and dt_ms. Trigger is at
// t=0 by convention.
const ta = data.time_axis || {};
const sr = ta.sample_rate || 1024;
const dtMs = ta.dt_ms || (1000.0 / sr);
const t0Ms = ta.t0_ms != null ? ta.t0_ms : 0;
// 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 =>
@@ -2612,14 +2618,8 @@ function _renderScWaveform(data) {
wrap.appendChild(canvasWrap); wrap.appendChild(canvasWrap);
chartsDiv.appendChild(wrap); chartsDiv.appendChild(wrap);
// Build time axis. Prefer server-provided time_axis; else derive from sample_rate. // Per-sample time in ms relative to trigger. Negative for pre-trigger samples.
let times; const times = values.map((_, i) => t0Ms + i * dtMs);
if (timeAxis && timeAxis.length === values.length) {
times = timeAxis;
} else {
const sr = data.sample_rate || 1024;
times = values.map((_, i) => (i / sr * 1000 - triggerMs));
}
// Downsample for rendering when very long. // Downsample for rendering when very long.
const MAX = 3000; const MAX = 3000;