v0.20.0 -- Full s3 event parse and PDF creation. #28
+193
-28
@@ -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