v0.20.0 -- Full s3 event parse and PDF creation. #28
+185
-20
@@ -161,7 +161,7 @@
|
||||
background: #161b22;
|
||||
border: 1px solid #21262d;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px 8px;
|
||||
padding: 10px 30px 8px 12px; /* right padding leaves room for the "0.0" baseline label */
|
||||
}
|
||||
.chart-label {
|
||||
font-size: 11px;
|
||||
@@ -211,6 +211,72 @@
|
||||
font-size: 11px;
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
@@ -223,7 +289,8 @@
|
||||
</select>
|
||||
<input type="search" id="event-filter" placeholder="filter events…" />
|
||||
<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>
|
||||
|
||||
<div id="main">
|
||||
@@ -250,13 +317,16 @@
|
||||
<div id="status-bar">Ready.</div>
|
||||
|
||||
<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 = {
|
||||
Tran: '#58a6ff',
|
||||
MicL: '#e066ff',
|
||||
Long: '#3a80ff',
|
||||
Vert: '#3fb950',
|
||||
Long: '#d29922',
|
||||
MicL: '#bc8cff',
|
||||
Tran: '#f85149',
|
||||
};
|
||||
const CHANNEL_ORDER = ['Tran', 'Vert', 'Long', 'MicL'];
|
||||
const CHANNEL_ORDER = ['MicL', 'Long', 'Vert', 'Tran'];
|
||||
|
||||
let allEvents = [];
|
||||
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)` : '—'],
|
||||
['Project', ev?.project || '—'],
|
||||
['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>`
|
||||
).join('');
|
||||
|
||||
// Per-channel stats table mirroring the printout's middle block.
|
||||
// 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';
|
||||
}
|
||||
|
||||
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) {
|
||||
document.getElementById('empty-state').style.display = 'none';
|
||||
const chartsDiv = document.getElementById('charts');
|
||||
@@ -420,6 +524,15 @@ function renderWaveform(data) {
|
||||
const channels = data.channels || {};
|
||||
const timeAxis = data.time_axis || null; // ms relative to trigger
|
||||
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) {
|
||||
const chData = channels[ch];
|
||||
@@ -447,6 +560,9 @@ function renderWaveform(data) {
|
||||
? `peak ${(typeof peak === 'number' ? peak.toExponential(3) : peak)} ${unit}`
|
||||
+ (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');
|
||||
wrap.className = 'chart-wrap';
|
||||
@@ -510,40 +626,89 @@ function renderWaveform(data) {
|
||||
scales: {
|
||||
x: {
|
||||
type: 'category',
|
||||
display: showXAxis,
|
||||
ticks: {
|
||||
color: '#484f58',
|
||||
color: isPrintMode ? '#666' : '#484f58',
|
||||
maxTicksLimit: 10,
|
||||
maxRotation: 0,
|
||||
callback: (val, i) => rT[i] + ' ms',
|
||||
},
|
||||
grid: { color: '#21262d' },
|
||||
grid: { color: isPrintMode ? '#e0e0e0' : '#21262d', drawTicks: showXAxis },
|
||||
},
|
||||
y: {
|
||||
ticks: { color: '#484f58', maxTicksLimit: 5 },
|
||||
grid: { color: '#21262d' },
|
||||
title: { display: true, text: unit, color: '#484f58', font: { size: 10 } },
|
||||
ticks: { color: isPrintMode ? '#666' : '#484f58', maxTicksLimit: 5 },
|
||||
grid: { color: isPrintMode ? '#e0e0e0' : '#21262d' },
|
||||
title: { display: true, text: unit,
|
||||
color: isPrintMode ? '#666' : '#484f58', font: { size: 10 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [{
|
||||
// Vertical trigger line at t=0
|
||||
id: 'triggerLine',
|
||||
// Trigger line @ t=0 + triangle markers above/below + "0.0"
|
||||
// baseline label on the right edge. Matches the Instantel
|
||||
// BW Event Report printout style.
|
||||
id: 'instantelOverlays',
|
||||
afterDraw(chart) {
|
||||
const ctx = chart.ctx;
|
||||
const xAxis = chart.scales.x;
|
||||
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);
|
||||
if (zeroIdx < 0) return;
|
||||
if (zeroIdx >= 0) {
|
||||
const x = xAxis.getPixelForValue(zeroIdx);
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, yAxis.top);
|
||||
ctx.lineTo(x, yAxis.bottom);
|
||||
ctx.strokeStyle = 'rgba(248, 81, 73, 0.7)';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.strokeStyle = isPrintMode ? '#cc0000' : 'rgba(248, 81, 73, 0.8)';
|
||||
ctx.lineWidth = 1.2;
|
||||
ctx.setLineDash([4, 3]);
|
||||
ctx.stroke();
|
||||
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