clock:internal internal_ref_hz:2000000 start:di_syn2_rise stop:di_syn2_fall sample_clock_hz:max range:0.2 di1:trace di1_group_avg full_res_live live_history_packets:8 duration_ms:100 packet_limit:0 csv:capture.csv svg:capture.svgC
This commit is contained in:
114
live_plot.html
114
live_plot.html
@ -42,20 +42,23 @@
|
||||
<span id="updateInfo">Auto-refresh every 500 ms</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status" id="statusText">Open this page once and leave it open. It refreshes its data script to pick up new packets.</div>
|
||||
<div class="status" id="statusText">Open this page once and leave it open. It refreshes its data script to pick up a rolling continuous packet window.</div>
|
||||
<div class="controls">
|
||||
<button type="button" id="zoomXIn">X+</button>
|
||||
<button type="button" id="zoomXOut">X-</button>
|
||||
<button type="button" id="panLeft">Left</button>
|
||||
<button type="button" id="panRight">Right</button>
|
||||
<button type="button" id="zoomYIn">Y+</button>
|
||||
<button type="button" id="zoomYOut">Y-</button>
|
||||
<button type="button" id="resetZoom">Reset</button>
|
||||
<label><input type="checkbox" id="autoZoom" checked/>Auto view</label>
|
||||
<span class="hint">Wheel: X zoom, Shift+wheel: Y zoom, double-click: reset</span>
|
||||
<span class="hint">Wheel: X zoom, Shift+wheel: Y zoom, Left/Right buttons or arrow keys: pan X, double-click: reset. The live view shows a rolling continuous packet window; CSV stores all samples.</span>
|
||||
</div>
|
||||
<canvas id="plot" width="1200" height="620"></canvas>
|
||||
<div class="legend">
|
||||
<span id="legendCh1"><span class="sw" style="background:#005bbb"></span>CH1</span>
|
||||
<span id="legendCh2"><span class="sw" style="background:#d62828"></span>CH2</span>
|
||||
<span id="legendRss"><span class="sw" style="background:#ee8a12"></span>sqrt(CH1^2 + CH2^2)</span>
|
||||
<span id="legendDi1"><span class="sw" style="background:#1b8f3a"></span>DI1</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -72,9 +75,12 @@
|
||||
const packetRateInfo = document.getElementById('packetRateInfo');
|
||||
const updateInfo = document.getElementById('updateInfo');
|
||||
const legendCh2 = document.getElementById('legendCh2');
|
||||
const legendRss = document.getElementById('legendRss');
|
||||
const legendDi1 = document.getElementById('legendDi1');
|
||||
const zoomXIn = document.getElementById('zoomXIn');
|
||||
const zoomXOut = document.getElementById('zoomXOut');
|
||||
const panLeft = document.getElementById('panLeft');
|
||||
const panRight = document.getElementById('panRight');
|
||||
const zoomYIn = document.getElementById('zoomYIn');
|
||||
const zoomYOut = document.getElementById('zoomYOut');
|
||||
const resetZoom = document.getElementById('resetZoom');
|
||||
@ -165,6 +171,30 @@
|
||||
refreshAutoCheckbox();
|
||||
renderPacket(latestPacket);
|
||||
}
|
||||
function applyPanX(direction) {
|
||||
if (!latestAutoBounds || !latestPacket) return;
|
||||
const current = currentViewBounds(latestAutoBounds);
|
||||
const totalX = Math.max(1e-9, latestAutoBounds.maxT);
|
||||
const span = Math.max(1e-9, current.xMax - current.xMin);
|
||||
if (span >= totalX) return;
|
||||
const shift = span * 0.25 * direction;
|
||||
let newMin = current.xMin + shift;
|
||||
let newMax = current.xMax + shift;
|
||||
if (newMin < 0) {
|
||||
newMax -= newMin;
|
||||
newMin = 0;
|
||||
}
|
||||
if (newMax > totalX) {
|
||||
newMin -= (newMax - totalX);
|
||||
newMax = totalX;
|
||||
}
|
||||
newMin = Math.max(0, newMin);
|
||||
newMax = Math.min(totalX, newMax);
|
||||
viewState = { auto: false, xMin: newMin, xMax: newMax, yMin: current.yMin, yMax: current.yMax };
|
||||
saveViewState();
|
||||
refreshAutoCheckbox();
|
||||
renderPacket(latestPacket);
|
||||
}
|
||||
function resetView() {
|
||||
viewState = defaultViewState();
|
||||
saveViewState();
|
||||
@ -222,6 +252,32 @@
|
||||
});
|
||||
ctx.strokeStyle = '#1b8f3a'; ctx.lineWidth = 1.4; ctx.stroke();
|
||||
}
|
||||
function drawPacketMarkers(markers, box, xMin, xMax) {
|
||||
if (!Array.isArray(markers) || markers.length === 0) return;
|
||||
const spanT = Math.max(1e-9, xMax - xMin);
|
||||
ctx.save();
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.strokeStyle = '#9db0c2';
|
||||
ctx.lineWidth = 1.0;
|
||||
ctx.fillStyle = '#5c6f82';
|
||||
ctx.font = '11px Segoe UI';
|
||||
markers.forEach((m) => {
|
||||
if (!m || !Number.isFinite(m.t) || (m.t < xMin) || (m.t > xMax)) return;
|
||||
const x = box.left + ((m.t - xMin) / spanT) * box.width;
|
||||
ctx.beginPath(); ctx.moveTo(x, box.top); ctx.lineTo(x, box.top + box.height); ctx.stroke();
|
||||
if (m.label) ctx.fillText(m.label, x + 4, box.top + 16);
|
||||
});
|
||||
ctx.restore();
|
||||
}
|
||||
function pickTrace(base, mid, high, maxT, view) {
|
||||
if (!Array.isArray(base) || base.length === 0) return [];
|
||||
const totalT = Math.max(1e-9, maxT || 0);
|
||||
const spanT = Math.max(1e-9, view.xMax - view.xMin);
|
||||
const ratio = spanT / totalT;
|
||||
if (ratio <= 0.10 && Array.isArray(high) && high.length > 0) return high;
|
||||
if (ratio <= 0.35 && Array.isArray(mid) && mid.length > 0) return mid;
|
||||
return base;
|
||||
}
|
||||
function renderWaiting(message) {
|
||||
packetInfo.textContent = 'Waiting for packets...';
|
||||
timingInfo.textContent = '';
|
||||
@ -229,6 +285,7 @@
|
||||
zoomInfo.textContent = 'View: auto';
|
||||
packetRateInfo.textContent = '';
|
||||
legendCh2.style.display = '';
|
||||
legendRss.style.display = 'none';
|
||||
legendDi1.style.display = 'none';
|
||||
refreshAutoCheckbox();
|
||||
updateInfo.textContent = 'Polling every 500 ms';
|
||||
@ -240,13 +297,30 @@
|
||||
function renderPacket(data) {
|
||||
latestPacket = data;
|
||||
const ch1 = data.ch1 || [];
|
||||
const ch1Mid = data.ch1Mid || [];
|
||||
const ch1High = data.ch1High || [];
|
||||
const ch2 = data.ch2 || [];
|
||||
const ch2Mid = data.ch2Mid || [];
|
||||
const ch2High = data.ch2High || [];
|
||||
const rss = data.rss || [];
|
||||
const rssMid = data.rssMid || [];
|
||||
const rssHigh = data.rssHigh || [];
|
||||
const ch1Grouped = data.ch1Grouped || [];
|
||||
const ch2Grouped = data.ch2Grouped || [];
|
||||
const rssGrouped = data.rssGrouped || [];
|
||||
const di1 = data.di1 || [];
|
||||
const packetMarkers = data.packetMarkers || [];
|
||||
const di1Grouped = !!data.di1Grouped;
|
||||
const hasCh2 = (data.channelCount || 2) > 1 && ch2.length > 0;
|
||||
const hasRss = hasCh2 && rss.length > 0;
|
||||
const hasDi1Trace = !!data.hasDi1Trace && di1.length > 0;
|
||||
const values = [];
|
||||
ch1.forEach(p => values.push(p[1]));
|
||||
if (hasCh2) ch2.forEach(p => values.push(p[1]));
|
||||
const yCh1 = di1Grouped && ch1Grouped.length > 0 ? ch1Grouped : ch1;
|
||||
const yCh2 = di1Grouped && ch2Grouped.length > 0 ? ch2Grouped : ch2;
|
||||
const yRss = di1Grouped && rssGrouped.length > 0 ? rssGrouped : rss;
|
||||
yCh1.forEach(p => values.push(p[1]));
|
||||
if (hasCh2) yCh2.forEach(p => values.push(p[1]));
|
||||
if (hasRss) yRss.forEach(p => values.push(p[1]));
|
||||
let minY = Math.min(...values);
|
||||
let maxY = Math.max(...values);
|
||||
if (!Number.isFinite(minY) || !Number.isFinite(maxY) || minY === maxY) {
|
||||
@ -258,22 +332,30 @@
|
||||
const maxT = Math.max(data.durationMs / 1000.0, 1e-9);
|
||||
latestAutoBounds = { minY, maxY, maxT };
|
||||
const view = currentViewBounds(latestAutoBounds);
|
||||
const plotCh1 = di1Grouped ? ch1Grouped : pickTrace(ch1, ch1Mid, ch1High, maxT, view);
|
||||
const plotCh2 = hasCh2 ? (di1Grouped ? ch2Grouped : pickTrace(ch2, ch2Mid, ch2High, maxT, view)) : [];
|
||||
const plotRss = hasRss ? (di1Grouped ? rssGrouped : pickTrace(rss, rssMid, rssHigh, maxT, view)) : [];
|
||||
const box = drawAxes(view.yMin, view.yMax, view.xMin, view.xMax);
|
||||
drawTrace(ch1, '#005bbb', box, view.yMin, view.yMax, view.xMin, view.xMax);
|
||||
if (hasCh2) drawTrace(ch2, '#d62828', box, view.yMin, view.yMax, view.xMin, view.xMax);
|
||||
drawPacketMarkers(packetMarkers, box, view.xMin, view.xMax);
|
||||
drawTrace(plotCh1, '#005bbb', box, view.yMin, view.yMax, view.xMin, view.xMax);
|
||||
if (hasCh2) drawTrace(plotCh2, '#d62828', box, view.yMin, view.yMax, view.xMin, view.xMax);
|
||||
if (hasRss) drawTrace(plotRss, '#ee8a12', box, view.yMin, view.yMax, view.xMin, view.xMax);
|
||||
if (hasDi1Trace) drawDigitalTrace(di1, box, view.xMin, view.xMax);
|
||||
legendCh2.style.display = hasCh2 ? '' : 'none';
|
||||
legendRss.style.display = hasRss ? '' : 'none';
|
||||
legendDi1.style.display = hasDi1Trace ? '' : 'none';
|
||||
refreshAutoCheckbox();
|
||||
packetInfo.textContent = 'Packet #' + data.packetIndex + ' of ' + data.packetsSeen + ', channels: ' + (data.channelCount || 2);
|
||||
timingInfo.textContent = 'Frames/ch: ' + data.framesPerChannel + ', duration: ' + data.durationMs.toFixed(3) + ' ms';
|
||||
const firstPacket = data.firstPacketIndex || data.packetIndex;
|
||||
const lastPacket = data.lastPacketIndex || data.packetIndex;
|
||||
packetInfo.textContent = 'Packets #' + firstPacket + '..' + lastPacket + ' of ' + data.packetsSeen + ', channels: ' + (data.channelCount || 2);
|
||||
timingInfo.textContent = 'Window frames/ch: ' + data.framesPerChannel + ', window: ' + data.durationMs.toFixed(3) + ' ms, last packet: ' + data.latestPacketDurationMs.toFixed(3) + ' ms';
|
||||
packetRateInfo.textContent = 'Packets/s: ' + data.packetsPerSecond.toFixed(3);
|
||||
zeroInfo.textContent = hasDi1Trace
|
||||
? ('DI1 trace: ' + data.di1Frames + ' frame samples')
|
||||
: ('Zeroed on DI1 change: ' + data.zeroedPercent.toFixed(3) + '% (' + data.zeroedSamples + '/' + data.storedSamples + ')');
|
||||
zoomInfo.textContent = viewState.auto ? 'View: auto' : ('View: X ' + view.xMin.toFixed(6) + '..' + view.xMax.toFixed(6) + ' s, Y ' + view.yMin.toFixed(3) + '..' + view.yMax.toFixed(3) + ' V');
|
||||
updateInfo.textContent = 'Snapshot: ' + data.updatedAt;
|
||||
statusText.textContent = 'Close reason: ' + data.closeReason + '. This page refreshes its data every 500 ms.';
|
||||
updateInfo.textContent = 'Snapshot: ' + data.updatedAt + (data.fullResLive ? ' | adaptive live resolution on' : '') + (di1Grouped ? (' | DI1-group avg: ' + (data.groupedPointCount || 0) + ' pts') : '');
|
||||
statusText.textContent = 'Latest packet close reason: ' + data.closeReason + '. This page refreshes its rolling packet window every 500 ms.';
|
||||
}
|
||||
function applyLiveData(data) {
|
||||
if (!data || data.status === 'waiting') {
|
||||
@ -300,6 +382,8 @@
|
||||
}
|
||||
zoomXIn.addEventListener('click', () => applyZoomX(0.5));
|
||||
zoomXOut.addEventListener('click', () => applyZoomX(2.0));
|
||||
panLeft.addEventListener('click', () => applyPanX(-1.0));
|
||||
panRight.addEventListener('click', () => applyPanX(1.0));
|
||||
zoomYIn.addEventListener('click', () => applyZoomY(0.5));
|
||||
zoomYOut.addEventListener('click', () => applyZoomY(2.0));
|
||||
resetZoom.addEventListener('click', () => resetView());
|
||||
@ -324,6 +408,16 @@
|
||||
}
|
||||
}, { passive: false });
|
||||
canvas.addEventListener('dblclick', () => resetView());
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (!latestPacket) return;
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
applyPanX(-1.0);
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
applyPanX(1.0);
|
||||
}
|
||||
});
|
||||
renderWaiting('Waiting for first packet...');
|
||||
loadLatestData();
|
||||
window.setInterval(loadLatestData, 500);
|
||||
|
||||
Reference in New Issue
Block a user