event_browser: Instantel-printout-style polish
Apply the cheap visual wins from the BW Event Report layout:
1. Channel order reversed → MicL (top), Long, Vert, Tran (bottom)
to match the Instantel printout.
2. Shared bottom time axis — x-axis ticks only render on the
bottom-most data channel; other channels hide ticks so all four
visually share one time scale.
3. Triangle trigger markers above and below the t=0 dashed line.
4. Horizontal zero-baseline (dotted) per channel with "0.0" label
on the right edge — Instantel convention.
5. "Print view" toggle that flips dark→light theme (white panels,
light grids, dark text) so the viewer can render usefully on
paper-style output / @media print.
6. Per-channel PPV stats table in the metadata header, with Peak
Vector Sum displayed prominently.
7. Colors adjusted to approximate BW trace colors (magenta MicL,
blue Long, green Vert, red Tran).
Future PDF-export work will reproduce the same layout server-side
once you upload a real example PDF and we pick a rendering pipeline
(weasyprint / chromium --print-to-pdf / etc.).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+185
-20
@@ -161,7 +161,7 @@
|
|||||||
background: #161b22;
|
background: #161b22;
|
||||||
border: 1px solid #21262d;
|
border: 1px solid #21262d;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 10px 12px 8px;
|
padding: 10px 30px 8px 12px; /* right padding leaves room for the "0.0" baseline label */
|
||||||
}
|
}
|
||||||
.chart-label {
|
.chart-label {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -211,6 +211,72 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Per-channel stats table in the metadata header */
|
||||||
|
.stats-table {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.stats-table th, .stats-table td {
|
||||||
|
padding: 3px 14px 3px 0;
|
||||||
|
text-align: left;
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
.stats-table th {
|
||||||
|
color: #484f58;
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Print view (light theme matching the Instantel printout) ─── */
|
||||||
|
body.print-view {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
body.print-view header,
|
||||||
|
body.print-view #event-list-wrap,
|
||||||
|
body.print-view #event-list-header,
|
||||||
|
body.print-view #event-meta,
|
||||||
|
body.print-view #status-bar,
|
||||||
|
body.print-view .chart-wrap {
|
||||||
|
background: #ffffff;
|
||||||
|
border-color: #cccccc;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
body.print-view .event-row { color: #000; border-bottom-color: #eee; }
|
||||||
|
body.print-view .event-row:hover { background: #f4f4f4; }
|
||||||
|
body.print-view .event-row.active {
|
||||||
|
background: #e6f0ff;
|
||||||
|
border-left-color: #1f6feb;
|
||||||
|
}
|
||||||
|
body.print-view .er-ts { color: #000; }
|
||||||
|
body.print-view .er-pvs { color: #003a8c; }
|
||||||
|
body.print-view .er-meta,
|
||||||
|
body.print-view #event-list-header,
|
||||||
|
body.print-view .meta-field .mf-label,
|
||||||
|
body.print-view .stats-table th {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
body.print-view .mf-value { color: #000; }
|
||||||
|
body.print-view .mf-value.highlight { color: #003a8c; }
|
||||||
|
body.print-view label { color: #444; }
|
||||||
|
body.print-view input, body.print-view select {
|
||||||
|
background: #fff; color: #000; border-color: #ccc;
|
||||||
|
}
|
||||||
|
/* In print theme, the channel-label colors stay (they identify
|
||||||
|
the trace). Only the chart panel background flips. */
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
header, #event-list-wrap, #status-bar, button { display: none !important; }
|
||||||
|
body { overflow: visible; height: auto; }
|
||||||
|
#main, #viewer { overflow: visible; }
|
||||||
|
#charts { overflow: visible; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -223,7 +289,8 @@
|
|||||||
</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="reload-btn" onclick="loadSerials()" style="margin-left:auto">Reload</button>
|
<button id="print-btn" onclick="togglePrintView()" style="margin-left:auto;background:#21262d">Print view</button>
|
||||||
|
<button id="reload-btn" onclick="loadSerials()">Reload</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="main">
|
<div id="main">
|
||||||
@@ -250,13 +317,16 @@
|
|||||||
<div id="status-bar">Ready.</div>
|
<div id="status-bar">Ready.</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Channel colors and rendering order mirror Instantel's BW Event Report
|
||||||
|
// printout: MicL at the top, Tran at the bottom. Colors approximate
|
||||||
|
// what BW renders (magenta mic, blue long, green vert, red tran).
|
||||||
const CHANNEL_COLORS = {
|
const CHANNEL_COLORS = {
|
||||||
Tran: '#58a6ff',
|
MicL: '#e066ff',
|
||||||
|
Long: '#3a80ff',
|
||||||
Vert: '#3fb950',
|
Vert: '#3fb950',
|
||||||
Long: '#d29922',
|
Tran: '#f85149',
|
||||||
MicL: '#bc8cff',
|
|
||||||
};
|
};
|
||||||
const CHANNEL_ORDER = ['Tran', 'Vert', 'Long', 'MicL'];
|
const CHANNEL_ORDER = ['MicL', 'Long', 'Vert', 'Tran'];
|
||||||
|
|
||||||
let allEvents = [];
|
let allEvents = [];
|
||||||
let filteredEvents = [];
|
let filteredEvents = [];
|
||||||
@@ -401,14 +471,48 @@ function renderMeta(data, ev) {
|
|||||||
['Geo range', data.geo_range ? `${data.geo_range} (${data.geo_full_scale_ips} in/s FS)` : '—'],
|
['Geo range', data.geo_range ? `${data.geo_range} (${data.geo_full_scale_ips} in/s FS)` : '—'],
|
||||||
['Project', ev?.project || '—'],
|
['Project', ev?.project || '—'],
|
||||||
['Location', ev?.sensor_location || '—'],
|
['Location', ev?.sensor_location || '—'],
|
||||||
['PVS', ev?.peak_vector_sum != null ? `${ev.peak_vector_sum.toFixed(4)} in/s` : '—'],
|
['Peak Vector Sum',
|
||||||
|
ev?.peak_vector_sum != null ? `${ev.peak_vector_sum.toFixed(4)} in/s` : '—'],
|
||||||
];
|
];
|
||||||
metaDiv.innerHTML = fields.map(([l, v]) =>
|
|
||||||
`<div class="meta-field"><span class="mf-label">${l}</span><span class="mf-value${l === 'PVS' ? ' highlight' : ''}">${v}</span></div>`
|
// Per-channel stats table mirroring the printout's middle block.
|
||||||
).join('');
|
// Pulls per-channel PPV from the events row (DB columns) and additional
|
||||||
|
// details (peak time, peak accel, peak displacement, sensor check) from
|
||||||
|
// bw_report when present.
|
||||||
|
const fmt = v => (v == null ? '—' : (typeof v === 'number' ? v.toFixed(3) : v));
|
||||||
|
const rows = [
|
||||||
|
['Tran', ev?.tran_ppv],
|
||||||
|
['Vert', ev?.vert_ppv],
|
||||||
|
['Long', ev?.long_ppv],
|
||||||
|
];
|
||||||
|
const statsHtml = `
|
||||||
|
<table class="stats-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Channel</th><th>PPV (in/s)</th></tr>
|
||||||
|
</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>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
|
||||||
|
metaDiv.innerHTML =
|
||||||
|
fields.map(([l, v]) =>
|
||||||
|
`<div class="meta-field"><span class="mf-label">${l}</span><span class="mf-value${l === 'Peak Vector Sum' ? ' highlight' : ''}">${v}</span></div>`
|
||||||
|
).join('') + statsHtml;
|
||||||
metaDiv.style.display = 'grid';
|
metaDiv.style.display = 'grid';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function togglePrintView() {
|
||||||
|
document.body.classList.toggle('print-view');
|
||||||
|
// Force chart redraw so axis/grid colors are re-evaluated against the
|
||||||
|
// new background. Easiest: re-render the current event.
|
||||||
|
if (currentEventId) {
|
||||||
|
loadEvent(currentEventId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderWaveform(data) {
|
function renderWaveform(data) {
|
||||||
document.getElementById('empty-state').style.display = 'none';
|
document.getElementById('empty-state').style.display = 'none';
|
||||||
const chartsDiv = document.getElementById('charts');
|
const chartsDiv = document.getElementById('charts');
|
||||||
@@ -420,6 +524,15 @@ function renderWaveform(data) {
|
|||||||
const channels = data.channels || {};
|
const channels = data.channels || {};
|
||||||
const timeAxis = data.time_axis || null; // ms relative to trigger
|
const timeAxis = data.time_axis || null; // ms relative to trigger
|
||||||
const triggerMs = data.trigger_ms ?? 0;
|
const triggerMs = data.trigger_ms ?? 0;
|
||||||
|
const isPrintMode = document.body.classList.contains('print-view');
|
||||||
|
|
||||||
|
// Which channels actually have data → determines which one renders the
|
||||||
|
// shared x-axis at the bottom (Instantel printout has the time scale
|
||||||
|
// only on the bottom-most chart).
|
||||||
|
const channelsWithData = CHANNEL_ORDER.filter(ch =>
|
||||||
|
channels[ch] && (channels[ch].values || []).length > 0
|
||||||
|
);
|
||||||
|
const lastDataCh = channelsWithData[channelsWithData.length - 1];
|
||||||
|
|
||||||
for (const ch of CHANNEL_ORDER) {
|
for (const ch of CHANNEL_ORDER) {
|
||||||
const chData = channels[ch];
|
const chData = channels[ch];
|
||||||
@@ -447,6 +560,9 @@ function renderWaveform(data) {
|
|||||||
? `peak ${(typeof peak === 'number' ? peak.toExponential(3) : peak)} ${unit}`
|
? `peak ${(typeof peak === 'number' ? peak.toExponential(3) : peak)} ${unit}`
|
||||||
+ (peakT != null ? ` @ ${peakT.toFixed(1)} ms` : '')
|
+ (peakT != null ? ` @ ${peakT.toFixed(1)} ms` : '')
|
||||||
: '';
|
: '';
|
||||||
|
// Hide x-axis on every chart except the bottom-most data channel —
|
||||||
|
// gives the "single shared time axis" feel of the BW printout.
|
||||||
|
const showXAxis = (ch === lastDataCh);
|
||||||
|
|
||||||
const wrap = document.createElement('div');
|
const wrap = document.createElement('div');
|
||||||
wrap.className = 'chart-wrap';
|
wrap.className = 'chart-wrap';
|
||||||
@@ -510,40 +626,89 @@ function renderWaveform(data) {
|
|||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
|
display: showXAxis,
|
||||||
ticks: {
|
ticks: {
|
||||||
color: '#484f58',
|
color: isPrintMode ? '#666' : '#484f58',
|
||||||
maxTicksLimit: 10,
|
maxTicksLimit: 10,
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
callback: (val, i) => rT[i] + ' ms',
|
callback: (val, i) => rT[i] + ' ms',
|
||||||
},
|
},
|
||||||
grid: { color: '#21262d' },
|
grid: { color: isPrintMode ? '#e0e0e0' : '#21262d', drawTicks: showXAxis },
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
ticks: { color: '#484f58', maxTicksLimit: 5 },
|
ticks: { color: isPrintMode ? '#666' : '#484f58', maxTicksLimit: 5 },
|
||||||
grid: { color: '#21262d' },
|
grid: { color: isPrintMode ? '#e0e0e0' : '#21262d' },
|
||||||
title: { display: true, text: unit, color: '#484f58', font: { size: 10 } },
|
title: { display: true, text: unit,
|
||||||
|
color: isPrintMode ? '#666' : '#484f58', font: { size: 10 } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [{
|
plugins: [{
|
||||||
// Vertical trigger line at t=0
|
// Trigger line @ t=0 + triangle markers above/below + "0.0"
|
||||||
id: 'triggerLine',
|
// baseline label on the right edge. Matches the Instantel
|
||||||
|
// BW Event Report printout style.
|
||||||
|
id: 'instantelOverlays',
|
||||||
afterDraw(chart) {
|
afterDraw(chart) {
|
||||||
const ctx = chart.ctx;
|
const ctx = chart.ctx;
|
||||||
const xAxis = chart.scales.x;
|
const xAxis = chart.scales.x;
|
||||||
const yAxis = chart.scales.y;
|
const yAxis = chart.scales.y;
|
||||||
|
const fgPrim = isPrintMode ? '#000' : '#c9d1d9';
|
||||||
|
const fgTrigger = '#f85149';
|
||||||
|
|
||||||
|
// Dashed vertical trigger line at t=0
|
||||||
const zeroIdx = rT.findIndex(t => parseFloat(t) >= 0);
|
const zeroIdx = rT.findIndex(t => parseFloat(t) >= 0);
|
||||||
if (zeroIdx < 0) return;
|
if (zeroIdx >= 0) {
|
||||||
const x = xAxis.getPixelForValue(zeroIdx);
|
const x = xAxis.getPixelForValue(zeroIdx);
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(x, yAxis.top);
|
ctx.moveTo(x, yAxis.top);
|
||||||
ctx.lineTo(x, yAxis.bottom);
|
ctx.lineTo(x, yAxis.bottom);
|
||||||
ctx.strokeStyle = 'rgba(248, 81, 73, 0.7)';
|
ctx.strokeStyle = isPrintMode ? '#cc0000' : 'rgba(248, 81, 73, 0.8)';
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 1.2;
|
||||||
ctx.setLineDash([4, 3]);
|
ctx.setLineDash([4, 3]);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
|
// Triangles above and below the chart at the trigger column
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = fgTrigger;
|
||||||
|
ctx.beginPath(); // top triangle pointing down
|
||||||
|
ctx.moveTo(x - 5, yAxis.top - 8);
|
||||||
|
ctx.lineTo(x + 5, yAxis.top - 8);
|
||||||
|
ctx.lineTo(x, yAxis.top - 1);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
ctx.beginPath(); // bottom triangle pointing up
|
||||||
|
ctx.moveTo(x - 5, yAxis.bottom + 8);
|
||||||
|
ctx.lineTo(x + 5, yAxis.bottom + 8);
|
||||||
|
ctx.lineTo(x, yAxis.bottom + 1);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// "0.0" baseline label on the right edge — printout convention.
|
||||||
|
// Position vertically at the zero-amplitude level.
|
||||||
|
const zeroY = yAxis.getPixelForValue(0);
|
||||||
|
if (zeroY >= yAxis.top && zeroY <= yAxis.bottom) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = isPrintMode ? '#aaa' : '#30363d';
|
||||||
|
ctx.lineWidth = 0.8;
|
||||||
|
ctx.setLineDash([2, 2]);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(xAxis.left, zeroY);
|
||||||
|
ctx.lineTo(xAxis.right, zeroY);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = fgPrim;
|
||||||
|
ctx.font = '11px monospace';
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText('0.0', xAxis.right + 6, zeroY);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user