diff --git a/sfm/event_browser.html b/sfm/event_browser.html
index 0dce1b0..c3b7516 100644
--- a/sfm/event_browser.html
+++ b/sfm/event_browser.html
@@ -522,8 +522,13 @@ function renderWaveform(data) {
charts = {};
const channels = data.channels || {};
- const timeAxis = data.time_axis || null; // ms relative to trigger
- const triggerMs = data.trigger_ms ?? 0;
+ // time_axis is METADATA from sfm.plot.v1 — sample_rate, pretrig_samples,
+ // 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');
// Which channels actually have data → determines which one renders the
@@ -578,14 +583,8 @@ function renderWaveform(data) {
wrap.appendChild(canvasWrap);
chartsDiv.appendChild(wrap);
- // Build time labels — use server-provided time_axis if present, else derive from sample_rate
- let times;
- if (timeAxis && timeAxis.length === values.length) {
- times = timeAxis;
- } else {
- const sr = data.sample_rate || 1024;
- times = values.map((_, i) => (i / sr * 1000 - triggerMs));
- }
+ // Per-sample time in ms relative to trigger. Negative for pre-trigger samples.
+ const times = values.map((_, i) => t0Ms + i * dtMs);
// Downsample for rendering
const MAX_POINTS = 4000;
diff --git a/sfm/sfm_webapp.html b/sfm/sfm_webapp.html
index 6c68b38..df23b3a 100644
--- a/sfm/sfm_webapp.html
+++ b/sfm/sfm_webapp.html
@@ -2572,8 +2572,14 @@ function _renderScWaveform(data) {
_destroyScCharts();
const channels = data.channels || {};
- const timeAxis = data.time_axis || null;
- const triggerMs = data.trigger_ms ?? 0;
+ // time_axis is METADATA, not an array — it carries sample_rate,
+ // 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.
const withData = _SC_CHANNEL_ORDER.filter(ch =>
@@ -2612,14 +2618,8 @@ function _renderScWaveform(data) {
wrap.appendChild(canvasWrap);
chartsDiv.appendChild(wrap);
- // Build time axis. Prefer server-provided time_axis; else derive from sample_rate.
- let times;
- if (timeAxis && timeAxis.length === values.length) {
- times = timeAxis;
- } else {
- const sr = data.sample_rate || 1024;
- times = values.map((_, i) => (i / sr * 1000 - triggerMs));
- }
+ // Per-sample time in ms relative to trigger. Negative for pre-trigger samples.
+ const times = values.map((_, i) => t0Ms + i * dtMs);
// Downsample for rendering when very long.
const MAX = 3000;