diff --git a/Client/WebUI/run.sh b/Client/WebUI/run.sh new file mode 100755 index 0000000..e326358 --- /dev/null +++ b/Client/WebUI/run.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -e +cd "$(dirname "$0")" + +COMMIT="$(git describe --always --dirty 2>/dev/null || echo 'unknown')" +BUILD_TIME="$(date -u +%Y%m%dT%H%M%SZ)" +VERSION="${COMMIT}-${BUILD_TIME}" + +echo "Building udpstreamer-webui ${VERSION}..." +go build -ldflags "-X main.version=${VERSION}" -o udpstreamer-webui . + +echo "Starting udpstreamer-webui ${VERSION}..." +exec ./udpstreamer-webui "$@" diff --git a/Client/WebUI/static/app.js b/Client/WebUI/static/app.js new file mode 100644 index 0000000..25417ff --- /dev/null +++ b/Client/WebUI/static/app.js @@ -0,0 +1,1432 @@ +'use strict'; +/* ════════════════════════════════════════════════════════════════ + Constants + ════════════════════════════════════════════════════════════════ */ +const DEFAULT_CAP = 10_000; +const TEMPORAL_CAP = 600_000; +const LTTB_MIN = 200; // never decimate below this many points + +const TRACE_COLORS = [ + '#89b4fa','#a6e3a1','#f38ba8','#fab387','#cba6f7', + '#94e2d5','#89dceb','#b4befe','#f9e2af','#f5c2e7', +]; + +/* ════════════════════════════════════════════════════════════════ + Globals + ════════════════════════════════════════════════════════════════ */ +let signals = []; +let buffers = {}; +let plots = []; +let nextPlotId = 1; +let windowSec = 5; +let globalPause = false; +let lastDataAt = 0; + +const traceColorMap = {}; +let colorIdx = 0; +function getTraceColor(key) { + if (!traceColorMap[key]) traceColorMap[key] = TRACE_COLORS[colorIdx++ % TRACE_COLORS.length]; + return traceColorMap[key]; +} + +// Sync: shared uPlot cursor crosshair across all live plots +const LIVE_SYNC = uPlot.sync('live'); +const TRIG_SYNC = uPlot.sync('trig'); + +// Zoom guard: prevents echo on cross-plot sync calls inside onZoom. +// zoomGuard prevents the setScale hook from calling onZoom when we programmatically +// set the scale (rolling window, zoom-back, fit, resize, pan, cross-plot sync). +// All programmatic setScale calls wrap with zoomGuard=true/false so that the hook +// only fires for genuine user drag-zoom or scroll-wheel gestures. +let zoomGuard = false; + +// Zoom history for Back button (global since plots are zoom-synced) +const zoomHistory = []; + +// Cursors A/B — stored in x-axis units of the current mode: +// live mode → Unix seconds +// trig mode → relative seconds from trigger +const cursors = { mode: 'off', tA: null, tB: null }; +let cursorsDirty = false; // if true, redraw all plots to update cursor lines + +// Layout — [label, cssClass, cols, rows] +const LAYOUTS = [ + ['1×1','l1x1',1,1],['1×2','l1x2',1,2],['2×1','l2x1',2,1],['1×3','l1x3',1,3], + ['3×1','l3x1',3,1],['2×2','l2x2',2,2],['1×4','l1x4',1,4],['4×1','l4x1',4,1], +]; +let currentLayout = 'l1x1'; + +/* ════════════════════════════════════════════════════════════════ + Trigger state + ════════════════════════════════════════════════════════════════ */ +const trig = { + enabled:false, signal:'', edge:'rising', threshold:0, windowSec:1, + prePercent:20, mode:'normal', stopped:false, + armed:false, prevVal:null, collecting:false, trigTime:null, snapshot:null, +}; +function trigPreSec() { return trig.windowSec * trig.prePercent / 100; } +function trigPostSec() { return trig.windowSec * (100 - trig.prePercent) / 100; } + +/* ════════════════════════════════════════════════════════════════ + WebSocket + ════════════════════════════════════════════════════════════════ */ +let ws = null, wsBackoff = 1000; +function connectWS() { + ws = new WebSocket('ws://' + location.host + '/ws'); + ws.onopen = () => { wsBackoff = 1000; setStatus('orange','Connected – waiting for data'); }; + ws.onclose = () => { + setStatus('red','Disconnected (reconnecting…)'); + setTimeout(connectWS, wsBackoff); + wsBackoff = Math.min(wsBackoff * 2, 30000); + }; + ws.onerror = () => {}; + ws.onmessage = evt => { + let msg; try { msg = JSON.parse(evt.data); } catch { return; } + if (msg.type === 'config') onConfig(msg); + else if (msg.type === 'data') onData(msg); + }; +} + +/* ════════════════════════════════════════════════════════════════ + Status LED + ════════════════════════════════════════════════════════════════ */ +function setStatus(s, t) { + document.getElementById('status-led').className = s; + document.getElementById('status-text').textContent = t; +} +setInterval(() => { + const tsEl = document.getElementById('sb-tsage'); + if (ws && ws.readyState === WebSocket.OPEN && lastDataAt > 0) { + const age = performance.now() - lastDataAt; + // Compute minimum lag: newest buffer timestamp vs browser wall clock. + let tsAge = null; + const wallNow = Date.now() / 1000; + Object.values(buffers).forEach(buf => { + if (buf.size === 0) return; + const newest = buf.t[(buf.head - 1 + buf.cap) % buf.cap]; + const a = wallNow - newest; + if (tsAge === null || a < tsAge) tsAge = a; + }); + if (tsEl && tsAge !== null) { + const ms = tsAge * 1000; + tsEl.textContent = '| lag: ' + (ms < 1000 ? ms.toFixed(0) + 'ms' : tsAge.toFixed(2) + 's'); + } else if (tsEl) { + tsEl.textContent = ''; + } + if (age > 1000) setStatus('orange', 'No data for ' + (age/1000).toFixed(1) + 's'); + else setStatus('green', 'Streaming'); + } else if (tsEl) { + tsEl.textContent = ''; + } +}, 500); + +/* ════════════════════════════════════════════════════════════════ + Config handler + ════════════════════════════════════════════════════════════════ */ +function numElements(sig) { return (sig.numRows || 1) * (sig.numCols || 1); } +function isTemporal(sig) { return numElements(sig) > 1 && (sig.timeMode || 0) !== 0; } + +function onConfig(msg) { + const newSigs = msg.signals || []; + const fp = s => s.name+':'+s.typeCode+':'+(s.numRows||1)+':'+(s.numCols||1)+':'+(s.timeMode||0); + const changed = newSigs.length !== signals.length || newSigs.some((s,i) => fp(s) !== fp(signals[i])); + signals = newSigs; + if (changed) { + buffers = {}; + signals.forEach(sig => { + const n = numElements(sig); + if (isTemporal(sig)) { buffers[sig.name] = makeBuffer(TEMPORAL_CAP); } + else if (n === 1) { buffers[sig.name] = makeBuffer(); } + else { for (let i = 0; i < n; i++) buffers[sig.name+'['+i+']'] = makeBuffer(); } + }); + trigDisarm(); trig.snapshot = null; trig.prevVal = null; + zoomHistory.length = 0; + document.getElementById('btn-zoom-back').style.display = 'none'; + } + buildSidebar(); + buildTrigSignalSelect(); +} + +/* ════════════════════════════════════════════════════════════════ + Data handler + ════════════════════════════════════════════════════════════════ */ +function onData(msg) { + lastDataAt = performance.now(); + const sigs = msg.signals; if (!sigs) return; + Object.keys(sigs).forEach(key => { + const buf = buffers[key]; if (!buf) return; + const sd = sigs[key]; if (!sd || !sd.t || !sd.v) return; + const len = Math.min(sd.t.length, sd.v.length); + for (let i = 0; i < len; i++) pushBuffer(buf, sd.t[i], sd.v[i]); + }); + if (trig.enabled && trig.armed && trig.signal) checkTrigger(sigs); + if (trig.enabled && trig.collecting && (Date.now()/1000) >= trig.trigTime + trigPostSec()) + finaliseTriggerCapture(); + if (!trig.enabled) { + plots.forEach(p => { + if (globalPause) return; + if (p.traces.some(t => buffers[t] !== undefined)) p.needsRedraw = true; + }); + } +} + +/* ════════════════════════════════════════════════════════════════ + Trigger logic + ════════════════════════════════════════════════════════════════ */ +function checkTrigger(sigs) { + const sd = sigs[trig.signal]; if (!sd || !sd.v || !sd.v.length) return; + for (let i = 0; i < sd.v.length; i++) { + const cur = sd.v[i], prev = trig.prevVal; trig.prevVal = cur; + if (prev === null) continue; + const e = trig.edge, thr = trig.threshold; + if ((e==='rising' &&prev=thr) || + (e==='falling'&&prev>thr&&cur<=thr) || + (e==='both'&&((prev=thr)||(prev>thr&&cur<=thr)))) { + fireTrigger(sd.t ? sd.t[i] : Date.now()/1000); break; + } + } +} +function fireTrigger(t) { + trig.armed=false; trig.collecting=true; trig.trigTime=t; trig.snapshot=null; + updateTrigStatusBadge('waiting'); setAllCardsCollecting(true); +} +function finaliseTriggerCapture() { + trig.collecting = false; + const t0 = trig.trigTime - trigPreSec(), t1 = trig.trigTime + trigPostSec(); + const snap = {}; + Object.keys(buffers).forEach(key => { + const sl = getBufferSliceRange(buffers[key], t0, t1); + if (sl.t.length > 0) snap[key] = sl; + }); + // Tag snapshot with the window parameters captured at trigger time so that + // the render loop uses the correct bounds even if UI controls are changed later. + snap._preS = trigPreSec(); + snap._postS = trigPostSec(); + trig.snapshot = snap; + setAllCardsCollecting(false); updateTrigStatusBadge('triggered'); + showRearmBtn(trig.mode === 'single'); + showStopBtn(trig.mode === 'normal'); + // Reset cursor positions (mode changed) and show cursor button now that snapshot exists + cursors.tA = null; cursors.tB = null; updateCursorReadout(); + updateCursorBtnVisibility(); + plots.forEach(p => { p.xRange = null; p.needsRedraw = true; }); + if (trig.mode === 'normal' && !trig.stopped) + setTimeout(() => { if (trig.enabled && trig.mode==='normal' && !trig.stopped) trigArm(); }, 200); +} +function trigArm() { + trig.snapshot=null; trig.armed=true; trig.collecting=false; trig.prevVal=null; + showRearmBtn(false); updateTrigStatusBadge('armed'); + cursors.tA = null; cursors.tB = null; updateCursorReadout(); + updateCursorBtnVisibility(); + plots.forEach(p => { p.xRange = null; p.needsRedraw = true; }); +} +function trigDisarm() { + trig.armed=false; trig.collecting=false; trig.trigTime=null; trig.stopped=false; + setAllCardsCollecting(false); showRearmBtn(false); showStopBtn(false); updateTrigStatusBadge('idle'); +} +function setAllCardsCollecting(on) { + document.querySelectorAll('.plot-card').forEach(c => c.classList.toggle('trig-collecting', on)); +} +function updateTrigStatusBadge(state) { + const el = document.getElementById('trig-status-badge'); + el.className = state; + el.textContent = {idle:'IDLE',armed:'ARMED',waiting:'COLLECTING',triggered:'TRIGGERED'}[state]||'IDLE'; +} +function showRearmBtn(v) { document.getElementById('btn-trig-rearm').style.display = v?'inline-block':'none'; } +function showStopBtn(v) { document.getElementById('btn-trig-stop').style.display = v?'inline-block':'none'; } +function updateStopBtn() { + const btn = document.getElementById('btn-trig-stop'); + btn.textContent = trig.stopped ? 'Resume' : 'Stop'; +} + +/* ════════════════════════════════════════════════════════════════ + Circular buffer + ════════════════════════════════════════════════════════════════ */ +function makeBuffer(cap) { + cap = cap || DEFAULT_CAP; + return { t: new Float64Array(cap), v: new Float64Array(cap), head:0, size:0, cap }; +} +function pushBuffer(buf, t, v) { + buf.t[buf.head] = t; buf.v[buf.head] = v; + buf.head = (buf.head + 1) % buf.cap; + if (buf.size < buf.cap) buf.size++; +} + +// Binary-search range slice of circular buffer — O(log n + window_size) +function getBufferSliceRange(buf, t0, t1) { + if (buf.size === 0) return { t: new Float64Array(0), v: new Float64Array(0) }; + const { cap, size, head } = buf; + const start = (size === cap) ? head : 0; + const physAt = k => (start + k) % cap; + + let lo = 0, hi = size; + while (lo < hi) { const m = (lo+hi)>>>1; if (buf.t[physAt(m)] < t0) lo=m+1; else hi=m; } + const kStart = lo; + lo = kStart; hi = size; + while (lo < hi) { const m = (lo+hi)>>>1; if (buf.t[physAt(m)] <= t1) lo=m+1; else hi=m; } + const kEnd = lo, len = kEnd - kStart; + if (len <= 0) return { t: new Float64Array(0), v: new Float64Array(0) }; + + const outT = new Float64Array(len), outV = new Float64Array(len); + const physStart = physAt(kStart), tail = cap - physStart; + if (tail >= len) { + outT.set(buf.t.subarray(physStart, physStart + len)); + outV.set(buf.v.subarray(physStart, physStart + len)); + } else { + outT.set(buf.t.subarray(physStart, physStart + tail)); + outT.set(buf.t.subarray(0, len - tail), tail); + outV.set(buf.v.subarray(physStart, physStart + tail)); + outV.set(buf.v.subarray(0, len - tail), tail); + } + return { t: outT, v: outV }; +} +// getGlobalNow returns the reference "now" for the rolling window. +// Always anchors to the newest timestamp found in any buffer so the rolling +// window tracks real data regardless of any clock skew between the Go server +// and the browser. Falls back to Date.now()/1000 only when all buffers are +// empty (no data yet received). +function getGlobalNow() { + let latest = -Infinity; + Object.values(buffers).forEach(buf => { + if (buf.size === 0) return; + const newestT = buf.t[(buf.head - 1 + buf.cap) % buf.cap]; + if (newestT > latest) latest = newestT; + }); + return isFinite(latest) ? latest : Date.now() / 1000; +} + +// getBufferNow returns the "now" anchor for a single buffer — the buffer's own +// newest timestamp. This avoids cross-signal interference when signals have +// different timescales or update rates. +function getBufferNow(buf) { + if (buf.size === 0) return Date.now() / 1000; + return buf.t[(buf.head - 1 + buf.cap) % buf.cap]; +} + +function getBufferSlice(buf) { + const now = getBufferNow(buf); + return getBufferSliceRange(buf, now - windowSec, now); +} + +// Binary-search slice of a sorted contiguous Float64Array pair +function sliceTypedArrayRange(t, v, t0, t1) { + let lo = 0, hi = t.length; + while (lo < hi) { const m = (lo+hi)>>>1; if (t[m] < t0) lo=m+1; else hi=m; } + const s = lo; lo = s; hi = t.length; + while (lo < hi) { const m = (lo+hi)>>>1; if (t[m] <= t1) lo=m+1; else hi=m; } + return { t: t.subarray(s, lo), v: v.subarray(s, lo) }; +} + + +// Return the configured samplingRate for a buffer key. +// Temporal array signals have a meaningful SamplingRate; scalars return 0. +// Used to prefer high-freq signals as the master time grid regardless of trace order. +function getKeySamplingRate(key) { + const direct = signals.find(s => s.name === key); + if (direct) return direct.samplingRate || 0; + // Array element key like "Sig[3]" — strip the index + const sig = signals.find(s => key.startsWith(s.name + '[')); + return sig ? (sig.samplingRate || 0) : 0; +} + +// LTTB decimation — O(n). Returns {t, v} unchanged when len ≤ threshold. +function lttb(t, v, threshold) { + const len = t.length; + if (len <= threshold) return { t, v }; + const outT = new Float64Array(threshold), outV = new Float64Array(threshold); + outT[0] = t[0]; outV[0] = v[0]; + outT[threshold-1] = t[len-1]; outV[threshold-1] = v[len-1]; + const every = (len - 2) / (threshold - 2); + let a = 0; + for (let i = 0; i < threshold - 2; i++) { + const avgS = Math.floor((i+1)*every)+1, avgE = Math.min(Math.floor((i+2)*every)+1, len); + let avgT = 0, avgV = 0, n = 0; + for (let j = avgS; j < avgE; j++) { avgT += t[j]; avgV += v[j]; n++; } + if (n) { avgT /= n; avgV /= n; } + const rS = Math.floor(i*every)+1, rE = Math.min(Math.floor((i+1)*every)+1, len); + let maxA = -1, next = rS; + const aT = t[a], aV = v[a]; + for (let j = rS; j < rE; j++) { + const area = Math.abs((aT-avgT)*(v[j]-aV) - (aT-t[j])*(avgV-aV)); + if (area > maxA) { maxA = area; next = j; } + } + outT[i+1] = t[next]; outV[i+1] = v[next]; a = next; + } + return { t: outT, v: outV }; +} + +/* ════════════════════════════════════════════════════════════════ + uPlot helpers + ════════════════════════════════════════════════════════════════ */ +// Format Unix seconds → HH:MM:SS.mmm (used for live x-axis ticks) +function fmtLiveTick(u, vals) { + return vals.map(v => { + if (v == null) return ''; + const d = new Date(v * 1000); + const hh = String(d.getHours()).padStart(2,'0'); + const mm = String(d.getMinutes()).padStart(2,'0'); + const ss = String(d.getSeconds()).padStart(2,'0'); + const ms = String(d.getMilliseconds()).padStart(3,'0'); + return hh+':'+mm+':'+ss+'.'+ms; + }); +} +// Format relative seconds → ±Xms or ±Xs (used for trigger x-axis ticks) +function fmtTrigTick(u, vals) { + return vals.map(v => { + if (v == null) return ''; + const abs = Math.abs(v), sign = v < 0 ? '-' : '+'; + if (abs < 1) return sign + (abs*1000).toFixed(1) + 'ms'; + return sign + abs.toFixed(3) + 's'; + }); +} + +// Draw custom cursor A/B lines onto the canvas (called from draw hook) +function drawCursorLines(u) { + const { ctx, bbox } = u; + if (!bbox) return; + const drawLine = (val, color) => { + if (val === null) return; + const x = u.valToPos(val, 'x', true); // canvas pixels + if (x < bbox.left || x > bbox.left + bbox.width) return; + ctx.save(); + ctx.strokeStyle = color; + ctx.lineWidth = 1.5; + ctx.setLineDash([5, 4]); + ctx.beginPath(); + ctx.moveTo(Math.round(x), bbox.top); + ctx.lineTo(Math.round(x), bbox.top + bbox.height); + ctx.stroke(); + ctx.restore(); + }; + drawLine(cursors.tA, 'rgba(137,220,235,0.85)'); + drawLine(cursors.tB, 'rgba(249,226,175,0.85)'); +} + +// Build uPlot opts for a given plot object +function makeUPlotOpts(p, inTrigMode) { + const seriesArr = [{}]; // time (index 0) + p.traces.forEach(key => { + seriesArr.push({ + label: key, + stroke: getTraceColor(key), + width: 1.5, + points: { show: false }, + spanGaps: true, + }); + }); + + const xVals = inTrigMode ? (u, vals) => fmtTrigTick(u, vals) + : (u, vals) => fmtLiveTick(u, vals); + + return { + width: Math.max(p.div.clientWidth || 100, 50), + height: Math.max(p.div.clientHeight || 100, 50), + cursor: { + sync: { key: inTrigMode ? 'trig' : 'live', setSeries: false }, + drag: { x: true, y: false, setScale: true, uni: 20 }, + lock: false, + }, + select: { show: true }, + scales: { + x: { time: false, auto: false, + min: p.xRange ? p.xRange[0] : 0, + max: p.xRange ? p.xRange[1] : windowSec }, + y: { auto: true }, + }, + series: seriesArr, + axes: [ + { stroke:'#7f849c', grid:{ stroke:'#313244', width:1 }, ticks:{ stroke:'#313244', width:1 }, + values: xVals, size: 40 }, + { stroke:'#7f849c', grid:{ stroke:'#313244', width:1 }, ticks:{ stroke:'#313244', width:1 }, size: 50 }, + ], + legend: { show: false }, + padding: [4, 8, 0, 0], + hooks: { + draw: [ u => drawCursorLines(u) ], + // Two-hook zoom detection: setSelect flags that the NEXT setScale is user-initiated. + // uPlot fires setSelect → then immediately setScale (when drag.setScale:true). + // All programmatic setScale calls happen without a preceding setSelect, so the + // flag is false and onZoom is never called unintentionally. + setSelect: [(u) => { u._userZoom = (u.select.width > 0); }], + setScale: [(u, key) => { + if (key !== 'x' || !u._userZoom || !u._ready) return; + u._userZoom = false; + const { min, max } = u.scales.x; + if (min == null || max == null || max <= min) return; + onZoom(p.id, min, max); + }], + ready: [ u => { u._ready = true; } ], + }, + }; +} + +// Create (or recreate) the uPlot instance for a plot, mounting into p.div +function createUPlot(p) { + // Destroy previous instance + if (p.uplot) { p.uplot.destroy(); p.uplot = null; } + + const inTrigMode = trig.enabled && trig.snapshot !== null; + const opts = makeUPlotOpts(p, inTrigMode); + const data = buildUPlotData(p, inTrigMode); + + // Mount into the plot body div (clear previous uPlot DOM) + p.div.querySelectorAll('.uplot').forEach(el => el.remove()); + + p.uplot = new uPlot(opts, data, p.div); + + // ── Cursor drag: place or drag A/B cursors ────────────────────────────── + // - Near an existing cursor line (within CURSOR_SNAP_PX): drag to move it. + // - cursor mode A or B active: click/drag places that cursor anywhere. + // - Intercepts mousedown before uPlot zoom so the selection rect never shows. + const CURSOR_SNAP_PX = 8; + + function _cursorAtClientX(clientX) { + const rect = p.uplot.over.getBoundingClientRect(); + const { min, max } = p.uplot.scales.x; + const toX = val => rect.left + ((val - min) / (max - min)) * rect.width; + if (cursors.tA !== null && Math.abs(clientX - toX(cursors.tA)) <= CURSOR_SNAP_PX) return 'A'; + if (cursors.tB !== null && Math.abs(clientX - toX(cursors.tB)) <= CURSOR_SNAP_PX) return 'B'; + return null; + } + + function _cursorValFromEvent(e) { + const rect = p.uplot.over.getBoundingClientRect(); + const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); + const { min, max } = p.uplot.scales.x; + return min + pct * (max - min); + } + + // Update pointer style based on what's under the mouse + p.uplot.over.addEventListener('mousemove', e => { + const snap = _cursorAtClientX(e.clientX); + p.uplot.over.style.cursor = snap ? 'ew-resize' : ''; + }); + p.uplot.over.addEventListener('mouseleave', () => { + p.uplot.over.style.cursor = ''; + }); + + // Mousedown: start drag (intercept before uPlot to suppress zoom selection) + p.uplot.over.addEventListener('mousedown', e => { + if (e.button !== 0 || e.shiftKey) return; // shift is pan + const snap = _cursorAtClientX(e.clientX); + const target = snap || (cursors.mode !== 'off' ? cursors.mode : null); + if (!target) return; + + e.stopImmediatePropagation(); // prevent uPlot drag-zoom + e.preventDefault(); + + // Set cursor position immediately on mousedown + if (target === 'A') cursors.tA = _cursorValFromEvent(e); + else cursors.tB = _cursorValFromEvent(e); + updateCursorReadout(); + cursorsDirty = true; + + const onMove = ev => { + if (target === 'A') cursors.tA = _cursorValFromEvent(ev); + else cursors.tB = _cursorValFromEvent(ev); + updateCursorReadout(); + cursorsDirty = true; + }; + const onUp = () => { + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + }; + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }, true); // capture:true so we fire before uPlot's own handlers + + // Pan support: Shift+left-drag pans the current view (synced across all plots). + // Works in both zoomed mode (xRange set) and rolling mode (freezes the window first). + let _panActive = false, _panAnchorX = 0, _panAnchorMin = 0, _panAnchorMax = 0; + p.uplot.over.addEventListener('mousedown', e => { + if (e.button !== 0 || !e.shiftKey) return; + e.stopImmediatePropagation(); + e.preventDefault(); + _panActive = true; + _panAnchorX = e.clientX; + const xr = p.xRange; + if (xr) { + _panAnchorMin = xr[0]; + _panAnchorMax = xr[1]; + } else { + // Rolling mode: capture the current window position and freeze it so we + // have a stable anchor to pan from. + let now = -Infinity; + p.traces.forEach(key => { + const buf = buffers[key]; + if (buf && buf.size > 0) { + const t = buf.t[(buf.head - 1 + buf.cap) % buf.cap]; + if (t > now) now = t; + } + }); + if (!isFinite(now)) now = Date.now() / 1000; + _panAnchorMin = now - windowSec; + _panAnchorMax = now; + // Freeze all plots at this position immediately. + const pMin = _panAnchorMin, pMax = _panAnchorMax; + zoomGuard = true; + plots.forEach(q => { + q.xRange = [pMin, pMax]; + if (q.uplot) q.uplot.setScale('x', { min: pMin, max: pMax }); + }); + zoomGuard = false; + if (!syncLocked) { + syncLocked = true; + const btnR = document.getElementById('btn-sync-resume'); + if (btnR) btnR.style.display = ''; + } + } + }, true); + const _onPanMove = e => { + if (!_panActive || !p.uplot) return; + const w = p.uplot.over.getBoundingClientRect().width; + const span = _panAnchorMax - _panAnchorMin; + const dt = -((e.clientX - _panAnchorX) / w) * span; + const newMin = _panAnchorMin + dt; + const newMax = _panAnchorMax + dt; + zoomGuard = true; + plots.forEach(q => { + q.xRange = [newMin, newMax]; + if (q.uplot) q.uplot.setScale('x', { min: newMin, max: newMax }); + q.needsRedraw = true; + }); + zoomGuard = false; + }; + const _onPanEnd = () => { _panActive = false; }; + document.addEventListener('mousemove', _onPanMove); + document.addEventListener('mouseup', _onPanEnd); + + // Resize observer so the uPlot fills its container. + // zoomGuard prevents setSize → setScale → hook from calling onZoom. + if (p.ro) { p.ro.disconnect(); } + p.ro = new ResizeObserver(() => { + if (!p.uplot) return; + const w = Math.max(p.div.clientWidth || 50, 50); + const h = Math.max(p.div.clientHeight || 50, 50); + zoomGuard = true; + p.uplot.setSize({ width: w, height: h }); + zoomGuard = false; + }); + p.ro.observe(p.div); +} + +// Build the uPlot data array from buffers / trigger snapshot +function buildUPlotData(p, inTrigMode) { + if (p.traces.length === 0) return [new Float64Array(0)]; + + // When trigger is enabled but no snapshot yet (armed/waiting), return + // the last-rendered data so the plot stays frozen. + if (trig.enabled && !inTrigMode) { + if (!p.uplot || !p.uplot.data || !p.uplot.data[0]) return [new Float64Array(0)]; + return p.uplot.data; + } + + if (inTrigMode && trig.snapshot) return buildTrigData(p); + return buildLiveData(p); +} + +// Resample (vSrc) from times (tSrc) onto target times (tDst) using linear interpolation. +// tSrc must be sorted ascending. Values outside tSrc range are clamped to the nearest +// endpoint (extrapolation is not safe for streaming data). +function resampleLinear(tSrc, vSrc, tDst) { + const n = tDst.length; + const out = new Float64Array(n); + if (tSrc.length === 0) return out; // all zeros + if (tSrc.length === 1) { out.fill(vSrc[0]); return out; } + let j = 0; + for (let i = 0; i < n; i++) { + const td = tDst[i]; + // Advance j so that tSrc[j] <= td < tSrc[j+1] (or j at last index) + while (j < tSrc.length - 2 && tSrc[j + 1] < td) j++; + if (td <= tSrc[0]) { + out[i] = vSrc[0]; + } else if (td >= tSrc[tSrc.length - 1]) { + out[i] = vSrc[vSrc.length - 1]; + } else { + const t0 = tSrc[j], t1 = tSrc[j + 1]; + const frac = (td - t0) / (t1 - t0); + out[i] = vSrc[j] + frac * (vSrc[j + 1] - vSrc[j]); + } + } + return out; +} + +function buildLiveData(p) { + if (p.traces.length === 0) return [new Float64Array(0)]; + + // plotNow = newest timestamp across ALL traces + let plotNow = -Infinity; + p.traces.forEach(key => { + const buf = buffers[key]; + if (buf && buf.size > 0) { + const t = buf.t[(buf.head - 1 + buf.cap) % buf.cap]; + if (t > plotNow) plotNow = t; + } + }); + if (!isFinite(plotNow)) plotNow = Date.now() / 1000; + + const t0 = p.xRange ? p.xRange[0] : plotNow - windowSec; + const t1 = p.xRange ? p.xRange[1] : plotNow; + + // Pixel-adaptive LTTB target: 2× plot width so zooming in automatically + // raises the effective sample cap and reveals full resolution. + const targetPts = Math.max(LTTB_MIN, ((p.uplot ? p.uplot.width : p.div.clientWidth) || 600) * 2); + + // Slice all traces once; pick the master time grid using configured samplingRate + // as the primary criterion (unambiguous, independent of buffer fill / trace order). + // Fall back to raw sample count for signals without a configured rate. + const slices = {}; + let masterKey = p.traces[0], masterCount = -1, masterRate = -1; + for (const key of p.traces) { + const buf = buffers[key]; + if (!buf || buf.size === 0) continue; + const sl = getBufferSliceRange(buf, t0, t1); + slices[key] = sl; + const rate = getKeySamplingRate(key); + if (rate > masterRate || (rate === masterRate && sl.t.length > masterCount)) { + masterRate = rate; masterCount = sl.t.length; masterKey = key; + } + } + + const masterRaw = slices[masterKey]; + if (!masterRaw || masterRaw.t.length === 0) + return [new Float64Array(0), ...p.traces.map(() => new Float64Array(0))]; + + // Decimate master with pixel-adaptive LTTB, use resulting grid for all others + const dec = lttb(masterRaw.t, masterRaw.v, targetPts); + const sharedT = dec.t; + + const yArrays = []; + for (const key of p.traces) { + if (key === masterKey) { yArrays.push(dec.v); continue; } + const sl = slices[key]; + if (!sl || sl.t.length === 0) { yArrays.push(new Float64Array(sharedT.length)); continue; } + yArrays.push(resampleLinear(sl.t, sl.v, sharedT)); + } + + return [sharedT, ...yArrays]; +} + +function buildTrigData(p) { + const trigT = trig.trigTime; + const preS = trig.snapshot._preS !== undefined ? trig.snapshot._preS : trigPreSec(); + const postS = trig.snapshot._postS !== undefined ? trig.snapshot._postS : trigPostSec(); + + if (p.traces.length === 0) return [new Float64Array(0)]; + + const t0 = p.xRange ? trigT + p.xRange[0] : trigT - preS; + const t1 = p.xRange ? trigT + p.xRange[1] : trigT + postS; + + const targetPts = Math.max(LTTB_MIN, ((p.uplot ? p.uplot.width : p.div.clientWidth) || 600) * 2); + + // Slice all traces; pick master by samplingRate first, then sample count + const slices = {}; + let masterKey = p.traces[0], masterCount = -1, masterRate = -1; + for (const key of p.traces) { + const snap = trig.snapshot[key]; + if (!snap) continue; + const sl = sliceTypedArrayRange(snap.t, snap.v, t0, t1); + slices[key] = sl; + const rate = getKeySamplingRate(key); + if (rate > masterRate || (rate === masterRate && sl.t.length > masterCount)) { + masterRate = rate; masterCount = sl.t.length; masterKey = key; + } + } + + const masterRaw = slices[masterKey]; + if (!masterRaw || masterRaw.t.length === 0) + return [new Float64Array(0), ...p.traces.map(() => new Float64Array(0))]; + + const dec = lttb(masterRaw.t, masterRaw.v, targetPts); + // Convert absolute → relative seconds + const sharedT = new Float64Array(dec.t.length); + for (let i = 0; i < dec.t.length; i++) sharedT[i] = dec.t[i] - trigT; + + const yArrays = []; + for (const key of p.traces) { + if (key === masterKey) { yArrays.push(dec.v); continue; } + const sl = slices[key]; + if (!sl || sl.t.length === 0) { yArrays.push(new Float64Array(sharedT.length)); continue; } + const relT = new Float64Array(sl.t.length); + for (let i = 0; i < sl.t.length; i++) relT[i] = sl.t[i] - trigT; + yArrays.push(resampleLinear(relT, sl.v, sharedT)); + } + + return [sharedT, ...yArrays]; +} + +/* ════════════════════════════════════════════════════════════════ + Zoom sync + ════════════════════════════════════════════════════════════════ */ +let syncLocked = false; + +function onZoom(sourcePlotId, min, max) { + // Push current range to history before applying new zoom + const prevRange = plots[0] && plots[0].xRange ? [...plots[0].xRange] : null; + zoomHistory.push(prevRange); + if (zoomHistory.length > 30) zoomHistory.shift(); + document.getElementById('btn-zoom-back').style.display = ''; + + // Store zoom on source plot + const src = plots.find(p => p.id === sourcePlotId); + if (src) src.xRange = [min, max]; + + // Show Auto button in live mode + if (!trig.enabled && !syncLocked) { + syncLocked = true; + document.getElementById('btn-sync-resume').style.display = ''; + } + + // Propagate to other plots + zoomGuard = true; + plots.forEach(p => { + if (p.id === sourcePlotId) return; + p.xRange = [min, max]; + if (p.uplot) p.uplot.setScale('x', { min, max }); + }); + zoomGuard = false; + + // Mark all plots dirty (re-slice data to the new range for full resolution) + plots.forEach(p => { p.needsRedraw = true; }); +} + +// Undo last zoom/pan action +function zoomBack() { + if (!zoomHistory.length) return; + const prev = zoomHistory.pop(); + if (!zoomHistory.length) document.getElementById('btn-zoom-back').style.display = 'none'; + + if (prev === null) { + // Was at auto/rolling state before the zoom + resetZoom(); + } else { + zoomGuard = true; + plots.forEach(p => { + p.xRange = [...prev]; + if (p.uplot) p.uplot.setScale('x', { min: prev[0], max: prev[1] }); + p.needsRedraw = true; + }); + zoomGuard = false; + } +} + +// Reset to auto/rolling window (clears all zoom) +function resetZoom() { + syncLocked = false; + document.getElementById('btn-sync-resume').style.display = 'none'; + if (trig.enabled && trig.snapshot) { + const preS = trig.snapshot._preS || trigPreSec(); + const postS = trig.snapshot._postS || trigPostSec(); + zoomGuard = true; + plots.forEach(p => { + p.xRange = null; + if (p.uplot) p.uplot.setScale('x', { min: -preS, max: postS }); + p.needsRedraw = true; + }); + zoomGuard = false; + } else { + // Back to rolling window — setScale to current window, render loop keeps it moving + plots.forEach(p => { + p.xRange = null; + if (!globalPause) p.needsRedraw = true; + }); + } +} + +// Fit x-axis to all data currently in buffers (or full trigger snapshot) +function zoomFit() { + if (trig.enabled && trig.snapshot) { + resetZoom(); // "Fit" in trigger mode = show full trigger window + return; + } + // Find oldest/newest timestamps across all visible signals + let gMin = Infinity, gMax = -Infinity; + plots.forEach(p => { + p.traces.forEach(key => { + const buf = buffers[key]; if (!buf || buf.size === 0) return; + const startIdx = (buf.size === buf.cap) ? buf.head : 0; + const oldestT = buf.t[startIdx]; + const newestT = buf.t[(buf.head - 1 + buf.cap) % buf.cap]; + if (oldestT < gMin) gMin = oldestT; + if (newestT > gMax) gMax = newestT; + }); + }); + if (!isFinite(gMin) || gMin >= gMax) return; + + // Push to history + const prevRange = plots[0] && plots[0].xRange ? [...plots[0].xRange] : null; + zoomHistory.push(prevRange); + document.getElementById('btn-zoom-back').style.display = ''; + if (!syncLocked) { syncLocked = true; document.getElementById('btn-sync-resume').style.display = ''; } + + zoomGuard = true; + plots.forEach(p => { + p.xRange = [gMin, gMax]; + if (p.uplot) p.uplot.setScale('x', { min: gMin, max: gMax }); + p.needsRedraw = true; + }); + zoomGuard = false; +} + +// Auto = return to rolling window / full trigger window +function exitSyncLock() { resetZoom(); } + +document.getElementById('btn-sync-resume').addEventListener('click', resetZoom); +document.getElementById('btn-zoom-back').addEventListener('click', zoomBack); +document.getElementById('btn-zoom-fit').addEventListener('click', zoomFit); + +/* ════════════════════════════════════════════════════════════════ + Cursor controls + ════════════════════════════════════════════════════════════════ */ +const CURSOR_LABELS = { off:'Cursor', A:'Cursor A', B:'Cursor B' }; + +// Show the cursor button only when paused or in trigger-snapshot mode. +// Hides and clears cursors whenever neither condition holds. +function updateCursorBtnVisibility() { + const canUseCursors = globalPause || (trig.enabled && trig.snapshot !== null); + const btn = document.getElementById('btn-cursor'); + btn.style.display = canUseCursors ? '' : 'none'; + if (!canUseCursors && cursors.mode !== 'off') { + cursors.mode = 'off'; + cursors.tA = null; cursors.tB = null; + btn.textContent = CURSOR_LABELS.off; + btn.className = 'ctrl-btn'; + document.getElementById('cursor-readout').classList.remove('visible'); + cursorsDirty = true; + } +} +const CURSOR_MODES = ['off','A','B']; + +document.getElementById('btn-cursor').addEventListener('click', () => { + cursors.mode = CURSOR_MODES[(CURSOR_MODES.indexOf(cursors.mode)+1) % 3]; + const btn = document.getElementById('btn-cursor'); + btn.textContent = CURSOR_LABELS[cursors.mode]; + btn.className = 'ctrl-btn' + + (cursors.mode==='A' ? ' cursor-a' : cursors.mode==='B' ? ' cursor-b' : ''); + if (cursors.mode === 'off') { + cursors.tA = null; cursors.tB = null; + document.getElementById('cursor-readout').classList.remove('visible'); + cursorsDirty = true; + } else { + document.getElementById('cursor-readout').classList.add('visible'); + } +}); + +function updateCursorReadout() { + const ro = document.getElementById('cursor-readout'); + const active = cursors.mode !== 'off' || cursors.tA !== null || cursors.tB !== null; + ro.classList.toggle('visible', active); + if (!active) return; + + // Format depends on mode: live = HH:MM:SS.mmm, trigger = ±Xms + const fmt = v => { + if (v === null) return '—'; + if (trig.enabled && trig.snapshot) { + const abs = Math.abs(v), sign = v < 0 ? '-' : '+'; + return abs < 1 ? sign+(abs*1000).toFixed(3)+'ms' : sign+abs.toFixed(6)+'s'; + } + const d = new Date(v * 1000); + return String(d.getHours()).padStart(2,'0') + ':' + + String(d.getMinutes()).padStart(2,'0') + ':' + + String(d.getSeconds()).padStart(2,'0') + '.' + + String(d.getMilliseconds()).padStart(3,'0'); + }; + document.getElementById('cur-ta').textContent = 'A: ' + fmt(cursors.tA); + document.getElementById('cur-tb').textContent = 'B: ' + fmt(cursors.tB); + if (cursors.tA !== null && cursors.tB !== null) { + const dt = cursors.tB - cursors.tA, abs = Math.abs(dt); + const s = dt >= 0 ? '+' : '-'; + const str = abs < 1 ? s+(abs*1000).toFixed(3)+'ms' : s+abs.toFixed(6)+'s'; + document.getElementById('cur-dt').textContent = 'ΔT: ' + str; + } else { + document.getElementById('cur-dt').textContent = 'ΔT: —'; + } +} + +/* ════════════════════════════════════════════════════════════════ + Trigger bar controls + ════════════════════════════════════════════════════════════════ */ +function openTrigBar(open) { + trig.enabled = open; + document.getElementById('trigbar').classList.toggle('open', open); + document.getElementById('btn-trigger').classList.toggle('trig-active', open); + document.documentElement.style.setProperty('--trigbar-h', open ? '48px' : '0px'); + // Streaming-only controls are irrelevant while the trigger is active + const none = open ? 'none' : ''; + document.getElementById('btn-pause-global').style.display = none; + document.getElementById('window-select').style.display = none; + document.getElementById('lbl-window').style.display = none; + if (!open) { + trigDisarm(); trig.snapshot = null; + cursors.tA = null; cursors.tB = null; updateCursorReadout(); + updateCursorBtnVisibility(); + zoomHistory.length = 0; + document.getElementById('btn-zoom-back').style.display = 'none'; + plots.forEach(p => { p.xRange = null; p.needsRedraw = true; }); + } else { + cursors.tA = null; cursors.tB = null; updateCursorReadout(); + updateCursorBtnVisibility(); + zoomHistory.length = 0; + document.getElementById('btn-zoom-back').style.display = 'none'; + resetZoom(); + if (trig.signal) trigArm(); else updateTrigStatusBadge('idle'); + } + // Resize uPlot instances after trigbar height changes + setTimeout(() => { plots.forEach(p => { + if (!p.uplot) return; + p.uplot.setSize({ width: p.div.clientWidth, height: p.div.clientHeight }); + }); }, 220); +} +document.getElementById('btn-trigger').addEventListener('click', () => openTrigBar(!trig.enabled)); +document.getElementById('trig-signal').addEventListener('change', e => { + trig.signal = e.target.value; trig.prevVal = null; + if (trig.enabled && trig.signal) trigArm(); else if (!trig.signal) trigDisarm(); +}); +document.getElementById('trig-edge').addEventListener('change', e => { trig.edge = e.target.value; if (trig.armed) trig.prevVal = null; }); +document.getElementById('trig-threshold').addEventListener('change', e => { trig.threshold = parseFloat(e.target.value)||0; if (trig.armed) trig.prevVal = null; }); +document.getElementById('trig-window').addEventListener('change', e => { trig.windowSec = parseFloat(e.target.value); }); +document.getElementById('trig-pre').addEventListener('input', e => { + trig.prePercent = parseInt(e.target.value, 10); + document.getElementById('trig-pre-val').textContent = trig.prePercent + '%'; +}); +document.getElementById('trig-mode').addEventListener('change', e => { trig.mode = e.target.value; }); +document.getElementById('btn-trig-rearm').addEventListener('click', () => { if (trig.enabled) trigArm(); }); +document.getElementById('btn-trig-stop').addEventListener('click', () => { + if (!trig.enabled || trig.mode !== 'normal') return; + trig.stopped = !trig.stopped; + updateStopBtn(); + if (!trig.stopped) trigArm(); // resume: re-arm immediately +}); + +/* ════════════════════════════════════════════════════════════════ + Trigger signal selector + ════════════════════════════════════════════════════════════════ */ +function buildTrigSignalSelect() { + const sel = document.getElementById('trig-signal'), cur = sel.value; + sel.innerHTML = ''; + signals.forEach(sig => { + const n = numElements(sig); + if (isTemporal(sig) || n === 1) { + const o = document.createElement('option'); o.value = sig.name; o.textContent = sig.name; sel.appendChild(o); + } else { + for (let i = 0; i < n; i++) { + const key = sig.name+'['+i+']', o = document.createElement('option'); + o.value = key; o.textContent = key; sel.appendChild(o); + } + } + }); + if (cur && [...sel.options].some(o => o.value === cur)) sel.value = cur; + trig.signal = sel.value; +} + +/* ════════════════════════════════════════════════════════════════ + Sidebar + ════════════════════════════════════════════════════════════════ */ +function buildSidebar() { + const list = document.getElementById('signal-list'); + list.innerHTML = ''; + if (!signals.length) { + list.innerHTML = '
No signals
'; + return; + } + const typeNames = ['u8','i8','u16','i16','u32','i32','u64','i64','f32','f64']; + signals.forEach(sig => { + const n = numElements(sig), temporal = isTemporal(sig); + const typeName = typeNames[sig.typeCode] || '?'; + if (n === 1 || temporal) { + list.appendChild(makeDraggable(sig.name, sig.name, temporal?'['+n+'] '+typeName:typeName, sig.unit||'')); + } else { + const group = document.createElement('div'); group.className = 'array-group'; + const header = document.createElement('div'); header.className = 'array-header'; + header.innerHTML = ''+escHtml(sig.name)+'' + + (sig.unit?''+escHtml(sig.unit)+'':'') + + '['+n+'] '+typeName+''; + header.addEventListener('click', () => header.classList.toggle('open')); + const children = document.createElement('div'); children.className = 'array-children'; + for (let i = 0; i < n; i++) { + const key = sig.name+'['+i+']', child = makeDraggable(key, key, typeName, sig.unit||''); + child.className = 'array-child'; children.appendChild(child); + } + group.appendChild(header); group.appendChild(children); list.appendChild(group); + } + }); +} +function makeDraggable(key, label, typeName, unit) { + const item = document.createElement('div'); + item.className = 'sig-item'; item.draggable = true; + item.innerHTML = ''+escHtml(label)+'' + + (unit?''+escHtml(unit)+'':'') + + ''+escHtml(typeName)+''; + item.addEventListener('dragstart', e => { + e.dataTransfer.setData('signal', key); e.dataTransfer.effectAllowed = 'copy'; + requestAnimationFrame(() => item.classList.add('dragging')); + }); + item.addEventListener('dragend', () => item.classList.remove('dragging')); + return item; +} + +/* ════════════════════════════════════════════════════════════════ + Layout management + ════════════════════════════════════════════════════════════════ */ +// Returns the number of plot cells in a layout (cols × rows). +function layoutPlotCount(cls) { + const m = cls.match(/^l(\d+)x(\d+)$/); + return m ? parseInt(m[1]) * parseInt(m[2]) : 1; +} + +// Build a small SVG grid thumbnail for a given cols×rows layout. +function layoutSVG(cols, rows) { + const W = 40, H = 28, GAP = 2, PAD = 2; + const cw = (W - PAD * 2 - GAP * (cols - 1)) / cols; + const ch = (H - PAD * 2 - GAP * (rows - 1)) / rows; + let rects = ''; + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const x = (PAD + c * (cw + GAP)).toFixed(1); + const y = (PAD + r * (ch + GAP)).toFixed(1); + rects += ``; + } + } + return `` + + `${rects}`; +} + +// Apply a new layout: switch the grid class and auto-add/remove plots. +// Plots with traces are preserved; empty plots are removed first when shrinking. +function applyLayout(cls) { + const entry = LAYOUTS.find(l => l[1] === cls); + if (!entry) return; + const [label,, cols, rows] = entry; + currentLayout = cls; + document.getElementById('plot-grid').className = cls; + + // Update button label + const btn = document.getElementById('btn-layout'); + if (btn) btn.innerHTML = layoutSVG(cols, rows) + ' ' + label + ' ▾'; + + // Update active state in menu + document.querySelectorAll('.layout-menu-item') + .forEach(el => el.classList.toggle('active', el.dataset.layout === cls)); + + const needed = layoutPlotCount(cls); + + // Remove excess plots — prefer empty ones to preserve trace assignments. + while (plots.length > needed) { + let removeId = plots[plots.length - 1].id; + for (let i = plots.length - 1; i >= 0; i--) { + if (plots[i].traces.length === 0) { removeId = plots[i].id; break; } + } + deletePlot(removeId); + } + + // Add missing plots. + while (plots.length < needed) addPlot(); + + setTimeout(() => plots.forEach(p => { + if (p.uplot) p.uplot.setSize({ width: p.div.clientWidth, height: p.div.clientHeight }); + }), 120); +} + +function buildLayoutMenu() { + const menu = document.getElementById('layout-menu'); + + LAYOUTS.forEach(([label, cls, cols, rows]) => { + const item = document.createElement('button'); + item.className = 'layout-menu-item' + (cls === currentLayout ? ' active' : ''); + item.dataset.layout = cls; + item.innerHTML = layoutSVG(cols, rows) + '' + label + ''; + item.addEventListener('click', () => { + applyLayout(cls); + menu.classList.remove('open'); + }); + menu.appendChild(item); + }); + + // Toggle menu open/close + document.getElementById('btn-layout').addEventListener('click', e => { + e.stopPropagation(); + const r = e.currentTarget.getBoundingClientRect(); + menu.style.left = r.left + 'px'; + menu.style.top = (r.bottom + 4) + 'px'; + menu.classList.toggle('open'); + }); + + // Close when clicking outside + document.addEventListener('click', e => { + if (!e.target.closest('#layout-menu') && !e.target.closest('#btn-layout')) + menu.classList.remove('open'); + }); +} + +/* ════════════════════════════════════════════════════════════════ + Export CSV (all plots) + ════════════════════════════════════════════════════════════════ */ +function exportAllCSV() { + const inTrigMode = trig.enabled && trig.snapshot !== null; + // Collect unique signal keys across all plots (preserving order) + const keys = []; + plots.forEach(p => p.traces.forEach(k => { if (!keys.includes(k)) keys.push(k); })); + if (!keys.length) return; + + const slices = keys.map(key => { + if (inTrigMode) { + const raw = trig.snapshot[key] || { t: new Float64Array(0), v: new Float64Array(0) }; + return { t: Array.from(raw.t).map(ts => ts - trig.trigTime), v: Array.from(raw.v) }; + } + const buf = buffers[key]; if (!buf) return { t: [], v: [] }; + const sl = getBufferSlice(buf); + return { t: Array.from(sl.t), v: Array.from(sl.v) }; + }); + + const allT = new Set(); slices.forEach(s => s.t.forEach(t => allT.add(t))); + const sortedT = Array.from(allT).sort((a, b) => a - b); + const lookups = slices.map(s => { const m = new Map(); s.t.forEach((t, i) => m.set(t, s.v[i])); return m; }); + const hdr = [(inTrigMode ? 'time_rel_s' : 'time_s'), ...keys].join(','); + const rows = sortedT.map(t => [t.toFixed(9), ...lookups.map(lk => lk.has(t) ? lk.get(t) : '')].join(',')); + const blob = new Blob([hdr + '\n' + rows.join('\n')], { type: 'text/csv' }); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); a.download = 'signals_' + Date.now() + '.csv'; + a.click(); URL.revokeObjectURL(a.href); +} + +/* ════════════════════════════════════════════════════════════════ + Plot management + ════════════════════════════════════════════════════════════════ */ +function addPlot() { + const id = nextPlotId++; + const card = document.createElement('div'); + card.className = 'plot-card'; card.dataset.plotId = id; + card.innerHTML = ` +
+
Plot ${id}
+
+
+
+
Drop signals here
+
⚡ Collecting…
+
`; + + card.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect='copy'; card.classList.add('drag-over'); }); + card.addEventListener('dragleave', () => card.classList.remove('drag-over')); + card.addEventListener('drop', e => { + e.preventDefault(); card.classList.remove('drag-over'); + const key = e.dataTransfer.getData('signal'); if (key) addTraceTo(id, key); + }); + + document.getElementById('plot-grid').appendChild(card); + + const plotBody = card.querySelector('#pbody-'+id); + const p = { id, traces:[], div: plotBody, needsRedraw:false, xRange:null, uplot:null, ro:null }; + plots.push(p); + + // Create the uPlot instance (empty) — deferred so DOM has settled + requestAnimationFrame(() => { createUPlot(p); }); + return id; +} + +function addTraceTo(plotId, signalKey) { + const p = plots.find(p => p.id === plotId); if (!p) return; + if (p.traces.includes(signalKey)) return; + p.traces.push(signalKey); + document.querySelector('#hint-'+plotId).style.display = 'none'; + addBadge(plotId, signalKey); + // Recreate uPlot with new series list + createUPlot(p); + p.needsRedraw = true; +} + +function removeTraceFrom(plotId, signalKey) { + const p = plots.find(p => p.id === plotId); if (!p) return; + p.traces = p.traces.filter(t => t !== signalKey); + removeBadge(plotId, signalKey); + createUPlot(p); + p.needsRedraw = true; + if (!p.traces.length) document.querySelector('#hint-'+plotId).style.display = ''; +} + +function addBadge(plotId, key) { + const c = document.getElementById('badges-'+plotId); if (!c) return; + if (c.querySelector('[data-key="'+CSS.escape(key)+'"]')) return; + const color = getTraceColor(key); + const badge = document.createElement('span'); + badge.className = 'sig-badge'; badge.dataset.key = key; + const dot = document.createElement('span'); dot.className = 'trace-dot'; dot.style.background = color; + const x = document.createElement('span'); x.className = 'sig-badge-x'; x.title = 'Remove'; x.textContent = '×'; + x.addEventListener('click', () => removeTraceFrom(plotId, key)); + badge.appendChild(dot); badge.appendChild(document.createTextNode(key)); badge.appendChild(x); + c.appendChild(badge); +} +function removeBadge(plotId, key) { + const c = document.getElementById('badges-'+plotId); if (!c) return; + const b = c.querySelector('[data-key="'+CSS.escape(key)+'"]'); if (b) b.remove(); +} + + +function deletePlot(plotId) { + const idx = plots.findIndex(p => p.id === plotId); if (idx === -1) return; + const p = plots[idx]; + if (p.ro) p.ro.disconnect(); + if (p.uplot) p.uplot.destroy(); + plots.splice(idx, 1); + document.querySelector('[data-plot-id="'+plotId+'"]').remove(); +} + +/* ════════════════════════════════════════════════════════════════ + Render loop + ════════════════════════════════════════════════════════════════ */ +let _dbgTick = 0; +function renderDirtyPlots() { + const inTrigMode = trig.enabled && trig.snapshot !== null; + + // Diagnostic: every ~5 s print buffer state to the browser console. + // Open DevTools → Console to see timestamps and sizes. + if (++_dbgTick % 300 === 0) { + const wallNow = Date.now() / 1000; + let anyData = false; + Object.entries(buffers).forEach(([k, b]) => { + if (b.size === 0) return; + anyData = true; + const newest = b.t[(b.head - 1 + b.cap) % b.cap]; + const oldest = b.t[(b.size === b.cap ? b.head : 0) % b.cap]; + const sliceLen = getBufferSlice(b).t.length; + console.log(`[buf] ${k}: size=${b.size} oldest=${oldest.toFixed(3)} newest=${newest.toFixed(3)} wallNow=${wallNow.toFixed(3)} ts-age=${(wallNow-newest).toFixed(3)}s slice(${windowSec}s)=${sliceLen}pts`); + }); + if (!anyData) console.log('[buf] all buffers empty — no data received yet'); + plots.forEach(p => console.log(`[plot${p.id}] traces=${JSON.stringify(p.traces)} xRange=${JSON.stringify(p.xRange)} needsRedraw=${p.needsRedraw}`)); + } + + // Rolling-window mode: detect stale xRange (buffer has scrolled past a frozen + // zoom window) and clear it back to rolling. Run unconditionally before the + // data-rebuild loop so the stale plot is correctly handled this frame. + if (!trig.enabled && !globalPause) { + plots.forEach(p => { + if (!p.uplot || !p.xRange) return; + const hasData = p.traces.some(k => buffers[k] && buffers[k].size > 0); + if (!hasData) return; + const stale = p.traces.every(key => { + const buf = buffers[key]; + if (!buf || buf.size === 0) return true; + const oldest = buf.t[buf.size === buf.cap ? buf.head : 0]; + return p.xRange[1] < oldest; + }); + if (stale) { + p.xRange = null; + syncLocked = false; + const btnR = document.getElementById('btn-sync-resume'); + if (btnR) btnR.style.display = 'none'; + } + }); + } + + // Fast path: cursor-only redraw (no data rebuild needed) + if (cursorsDirty) { + cursorsDirty = false; + plots.forEach(p => { if (p.uplot) p.uplot.redraw(false); }); + } + + // Rolling-window plots: mark dirty every frame for smooth continuous scrolling. + // setScale is called AFTER setData inside the rebuild loop so the viewport and + // data slice are always computed with the same plotNow anchor. + if (!trig.enabled && !globalPause) { + plots.forEach(p => { + if (!p.uplot || p.xRange) return; + p.needsRedraw = true; + }); + } + + plots.forEach(p => { + if (!p.needsRedraw || !p.uplot) return; + p.needsRedraw = false; + + const data = buildUPlotData(p, inTrigMode); + + // setData internally triggers the setScale hook in uPlot (it reaffirms the + // current scale even with auto:false). Keep zoomGuard raised across the + // entire setData + setScale block so the hook never calls onZoom and freezes + // p.xRange unintentionally. The guard is safe here: JS is single-threaded so + // no genuine user drag-zoom event can fire during this synchronous block. + zoomGuard = true; + p.uplot.setData(data); + + // Re-apply the x-scale after setData so the viewport stays correct. + if (trig.enabled && !inTrigMode) { + // Armed / waiting for trigger: keep the current scale frozen. + } else if (inTrigMode) { + const preS = trig.snapshot._preS !== undefined ? trig.snapshot._preS : trigPreSec(); + const postS = trig.snapshot._postS !== undefined ? trig.snapshot._postS : trigPostSec(); + p.uplot.setScale('x', { min: p.xRange ? p.xRange[0] : -preS, + max: p.xRange ? p.xRange[1] : postS }); + } else if (p.xRange) { + // Zoomed: re-apply so scale is correct after setData + p.uplot.setScale('x', { min: p.xRange[0], max: p.xRange[1] }); + } else { + // Rolling window: compute plotNow fresh each frame (same anchor as buildLiveData) + // and set the scale immediately after setData for correct alignment. + let plotNow = -Infinity; + p.traces.forEach(key => { + const buf = buffers[key]; + if (!buf || buf.size === 0) return; + const t = buf.t[(buf.head - 1 + buf.cap) % buf.cap]; + if (t > plotNow) plotNow = t; + }); + if (!isFinite(plotNow)) plotNow = Date.now() / 1000; + p.uplot.setScale('x', { min: plotNow - windowSec, max: plotNow }); + } + zoomGuard = false; + }); + + requestAnimationFrame(renderDirtyPlots); +} + + +/* ════════════════════════════════════════════════════════════════ + Global controls + ════════════════════════════════════════════════════════════════ */ +document.getElementById('btn-pause-global').addEventListener('click', () => { + globalPause = !globalPause; + const btn = document.getElementById('btn-pause-global'); + btn.textContent = globalPause ? '▶ Resume' : '⏸ Pause'; + btn.classList.toggle('active', globalPause); + if (!globalPause) plots.forEach(p => { p.needsRedraw = true; }); + updateCursorBtnVisibility(); +}); + +document.getElementById('window-select').addEventListener('change', e => { + windowSec = parseFloat(e.target.value); + // Don't trigger redraws while in trigger mode without a snapshot + if (trig.enabled && !trig.snapshot) return; + plots.forEach(p => { if (!p.xRange) p.needsRedraw = true; }); +}); + +/* ════════════════════════════════════════════════════════════════ + Sidebar toggle + ════════════════════════════════════════════════════════════════ */ +let sidebarOpen = true; +function setSidebar(open) { + sidebarOpen = open; + document.getElementById('sidebar').classList.toggle('collapsed', !open); + document.getElementById('btn-sidebar').classList.toggle('active', open); + setTimeout(() => { plots.forEach(p => { + if (p.uplot) p.uplot.setSize({ width: p.div.clientWidth, height: p.div.clientHeight }); + }); }, 200); +} +document.getElementById('btn-sidebar').addEventListener('click', () => setSidebar(!sidebarOpen)); + +/* ════════════════════════════════════════════════════════════════ + Utility + ════════════════════════════════════════════════════════════════ */ +function escHtml(s) { + return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); +} + +/* ════════════════════════════════════════════════════════════════ + Init + ════════════════════════════════════════════════════════════════ */ +buildLayoutMenu(); +applyLayout('l1x1'); +document.getElementById('btn-csv-all').addEventListener('click', exportAllCSV); +connectWS(); +requestAnimationFrame(renderDirtyPlots); +fetch('/version').then(r => r.text()).then(v => { + document.getElementById('build-version').textContent = 'v' + v; +}).catch(() => {}); diff --git a/Client/WebUI/static/favicon.svg b/Client/WebUI/static/favicon.svg new file mode 100644 index 0000000..9ddd8db --- /dev/null +++ b/Client/WebUI/static/favicon.svg @@ -0,0 +1,5 @@ + + + + diff --git a/Client/WebUI/static/style.css b/Client/WebUI/static/style.css new file mode 100644 index 0000000..dd695f8 --- /dev/null +++ b/Client/WebUI/static/style.css @@ -0,0 +1,309 @@ +/* ── Catppuccin Mocha palette ──────────────────────────────── */ +:root { + --bg: #1e1e2e; --mantle: #181825; --crust: #11111b; + --surface0: #313244; --surface1: #45475a; --surface2: #585b70; + --overlay0: #6c7086; --overlay1: #7f849c; --text: #cdd6f4; + --subtext0: #a6adc8; --subtext1: #bac2de; + --accent: #89b4fa; --green: #a6e3a1; --red: #f38ba8; + --yellow: #f9e2af; --peach: #fab387; --mauve: #cba6f7; + --teal: #94e2d5; --sky: #89dceb; --lavender: #b4befe; + --pink: #f5c2e7; + --radius: 8px; --sidebar-w: 280px; --topbar-h: 52px; + --trigbar-h: 0px; --statusbar-h: 20px; --transition: 0.18s ease; +} +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html, body { height:100%; background:var(--bg); color:var(--text); + font-family:'Segoe UI',system-ui,sans-serif; font-size:14px; overflow:hidden; } +::-webkit-scrollbar { width:6px; } +::-webkit-scrollbar-track { background:var(--mantle); } +::-webkit-scrollbar-thumb { background:var(--surface1); border-radius:3px; } + +/* ── uPlot overrides ─────────────────────────────────────────── */ +.uplot { background:transparent !important; } +.uplot .u-over { cursor: crosshair; } +.uplot .u-cursor-x, .uplot .u-cursor-y { border-color: #585b70 !important; } +.uplot .u-select { background: rgba(137,180,250,0.1) !important; border: 1px solid rgba(137,180,250,0.4) !important; } + +/* ── Top bar ─────────────────────────────────────────────────── */ +#topbar { + position:fixed; top:0; left:0; right:0; height:var(--topbar-h); + background:var(--mantle); border-bottom:1px solid var(--surface0); + display:flex; align-items:center; gap:10px; padding:0 14px; + z-index:100; box-shadow:0 2px 8px rgba(0,0,0,0.4); overflow:hidden; +} +#app-title { font-weight:700; font-size:15px; color:var(--accent); + white-space:nowrap; letter-spacing:0.3px; flex-shrink:0; } +.topbar-sep { flex:1; min-width:4px; } +#status-led { width:8px; height:8px; border-radius:50%; + background:var(--red); flex-shrink:0; transition:background var(--transition); } +#status-led.green { background:var(--green); animation:pulse-green 2s infinite; } +#status-led.orange { background:var(--yellow); animation:pulse-orange 1.5s infinite; } +@keyframes pulse-green { 0%,100%{box-shadow:0 0 0 0 rgba(166,227,161,0.4)} 50%{box-shadow:0 0 0 4px rgba(166,227,161,0)} } +@keyframes pulse-orange { 0%,100%{box-shadow:0 0 0 0 rgba(249,226,175,0.4)} 50%{box-shadow:0 0 0 4px rgba(249,226,175,0)} } +#status-text { font-size:11px; color:var(--subtext0); white-space:nowrap; } +#sb-tsage { font-size:11px; color:var(--overlay0); white-space:nowrap; font-family:monospace; } + +#cursor-readout { + display:none; align-items:center; gap:8px; + font-size:11px; font-family:monospace; + background:var(--surface0); border:1px solid var(--surface1); + border-radius:5px; padding:3px 8px; white-space:nowrap; flex-shrink:0; +} +#cursor-readout.visible { display:flex; } +#cur-ta { color:var(--sky); } #cur-tb { color:var(--yellow); } +#cur-dt { color:var(--subtext1); } .cur-sep { color:var(--surface2); } + +.topbar-vsep { width:1px; height:22px; background:var(--surface0); flex-shrink:0; margin:0 2px; } +#layout-btns { display:flex; gap:2px; align-items:center; flex-shrink:0; } +.ctrl-label { font-size:12px; color:var(--subtext0); white-space:nowrap; flex-shrink:0; } +select.ctrl-select { + background:var(--surface0); color:var(--text); + border:1px solid var(--surface1); border-radius:var(--radius); + padding:4px 6px; font-size:12px; cursor:pointer; outline:none; flex-shrink:0; +} +select.ctrl-select:hover { border-color:var(--accent); } +button.ctrl-btn { + background:var(--surface0); color:var(--text); + border:1px solid var(--surface1); border-radius:var(--radius); + padding:4px 12px; font-size:12px; cursor:pointer; white-space:nowrap; flex-shrink:0; + transition:background var(--transition),border-color var(--transition); +} +button.ctrl-btn:hover { background:var(--surface1); border-color:var(--accent); } +button.ctrl-btn.active { background:var(--surface1); border-color:var(--accent); color:var(--accent); } +button.ctrl-btn.trig-active { background:rgba(203,166,247,0.15); border-color:var(--mauve); color:var(--mauve); } +button.ctrl-btn.cursor-a { border-color:var(--sky); color:var(--sky); } +button.ctrl-btn.cursor-b { border-color:var(--yellow); color:var(--yellow); } +button.ctrl-btn.resume-btn { border-color:var(--teal); color:var(--teal); } + +/* ── Trigger bar ──────────────────────────────────────────────── */ +#trigbar { + position:fixed; top:var(--topbar-h); left:0; right:0; + background:var(--crust); border-bottom:1px solid var(--surface0); + display:flex; align-items:center; flex-wrap:wrap; gap:10px; + padding:0 16px; z-index:99; + height:0; overflow:hidden; + transition:height var(--transition),padding var(--transition); +} +#trigbar.open { height:48px; padding:0 16px; } +.trig-group { display:flex; align-items:center; gap:6px; } +.trig-sep { width:1px; height:24px; background:var(--surface0); flex-shrink:0; } +.trig-label { font-size:11px; color:var(--subtext0); white-space:nowrap; } +select.trig-select { + background:var(--surface0); color:var(--text); + border:1px solid var(--surface1); border-radius:5px; + padding:3px 6px; font-size:12px; cursor:pointer; outline:none; +} +select.trig-select:hover { border-color:var(--mauve); } +input.trig-input { + background:var(--surface0); color:var(--text); + border:1px solid var(--surface1); border-radius:5px; + padding:3px 6px; font-size:12px; outline:none; width:80px; +} +input.trig-input:focus { border-color:var(--mauve); } +input[type=range].trig-range { + -webkit-appearance:none; width:90px; height:4px; + background:var(--surface1); border-radius:2px; outline:none; cursor:pointer; +} +input[type=range].trig-range::-webkit-slider-thumb { + -webkit-appearance:none; width:12px; height:12px; + border-radius:50%; background:var(--mauve); cursor:pointer; +} +.trig-range-val { font-size:11px; color:var(--mauve); min-width:28px; } +#trig-status-badge { + font-size:11px; font-weight:700; letter-spacing:0.8px; + padding:3px 10px; border-radius:12px; + border:1px solid var(--surface1); background:var(--surface0); color:var(--subtext0); + min-width:80px; text-align:center; white-space:nowrap; +} +#trig-status-badge.armed { background:rgba(166,227,161,0.12); border-color:var(--green); color:var(--green); } +#trig-status-badge.waiting { background:rgba(249,226,175,0.12); border-color:var(--yellow); color:var(--yellow); } +#trig-status-badge.triggered { background:rgba(203,166,247,0.15); border-color:var(--mauve); color:var(--mauve); } +#btn-trig-rearm, #btn-trig-stop { + border:none; border-radius:5px; + padding:4px 12px; font-size:12px; font-weight:600; cursor:pointer; display:none; +} +#btn-trig-rearm { background:var(--mauve); color:var(--crust); } +#btn-trig-stop { background:var(--surface1); color:var(--yellow); border:1px solid var(--yellow); } +#btn-trig-rearm:hover, #btn-trig-stop:hover { opacity:0.85; } + +/* ── Status bar ───────────────────────────────────────────────── */ +#statusbar { + position:fixed; bottom:0; left:0; right:0; height:var(--statusbar-h); + background:var(--crust); border-top:1px solid var(--surface0); + display:flex; align-items:center; justify-content:space-between; + padding:0 10px; z-index:100; +} +#sb-left { display:flex; align-items:center; gap:6px; } +#build-version { font-size:10px; color:var(--overlay0); font-family:monospace; white-space:nowrap; } + +/* ── Body ─────────────────────────────────────────────────────── */ +#body { + position:fixed; top:calc(var(--topbar-h) + var(--trigbar-h)); + left:0; right:0; bottom:var(--statusbar-h); display:flex; overflow:hidden; + transition:top var(--transition); +} + +/* ── Sidebar ──────────────────────────────────────────────────── */ +#sidebar { + width:var(--sidebar-w); min-width:var(--sidebar-w); + background:var(--mantle); border-right:1px solid var(--surface0); + display:flex; flex-direction:column; + transition:width var(--transition),min-width var(--transition); overflow:hidden; +} +#sidebar.collapsed { width:0; min-width:0; } +#sidebar-header { + display:flex; align-items:center; justify-content:space-between; + padding:10px 14px; border-bottom:1px solid var(--surface0); + font-weight:600; color:var(--subtext1); font-size:12px; + text-transform:uppercase; letter-spacing:0.8px; flex-shrink:0; +} +#signal-list { flex:1; overflow-y:auto; padding:8px 0; } +.sig-item { + padding:6px 14px; cursor:grab; border-radius:6px; margin:1px 6px; + transition:background var(--transition); display:flex; align-items:center; gap:8px; + user-select:none; +} +.sig-item:hover { background:var(--surface0); } +.sig-item:active { cursor:grabbing; } +.sig-item.dragging { opacity:0.4; } +.sig-name { flex:1; font-size:13px; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } +.sig-unit { font-size:11px; color:var(--subtext0); font-style:italic; } +.type-badge { font-size:10px; background:var(--surface1); color:var(--subtext1); padding:1px 5px; border-radius:3px; white-space:nowrap; } +.array-group {} +.array-header { + padding:6px 14px 6px 10px; cursor:pointer; border-radius:6px; margin:1px 6px; + transition:background var(--transition); display:flex; align-items:center; gap:6px; user-select:none; +} +.array-header:hover { background:var(--surface0); } +.array-arrow { font-size:10px; color:var(--subtext0); transition:transform var(--transition); display:inline-block; } +.array-header.open .array-arrow { transform:rotate(90deg); } +.array-children { display:none; padding-left:16px; } +.array-header.open + .array-children { display:block; } +.array-child { + padding:4px 14px 4px 8px; cursor:grab; border-radius:6px; margin:1px 6px; + transition:background var(--transition); display:flex; align-items:center; gap:8px; + user-select:none; color:var(--subtext1); font-size:12px; +} +.array-child:hover { background:var(--surface0); } +.array-child:active { cursor:grabbing; } + +/* ── Main area ────────────────────────────────────────────────── */ +#main { flex:1; display:flex; flex-direction:column; overflow:hidden; min-width:0; } +/* Layout toggle button in topbar */ +#btn-layout { + display:flex; align-items:center; gap:6px; + font-size:12px; padding:4px 10px; +} +#btn-layout svg { flex-shrink:0; } +#btn-layout span { flex-shrink:0; } + +/* Layout dropdown menu */ +#layout-menu { + position:fixed; z-index:200; + background:var(--mantle); border:1px solid var(--surface1); border-radius:var(--radius); + box-shadow:0 6px 20px rgba(0,0,0,0.5); + display:none; grid-template-columns:1fr 1fr; gap:4px; padding:6px; +} +#layout-menu.open { display:grid; } +.layout-menu-item { + display:flex; flex-direction:column; align-items:center; gap:5px; + padding:8px 14px; cursor:pointer; + background:var(--surface0); border:1px solid var(--surface1); border-radius:6px; + color:var(--subtext1); font-size:11px; font-family:monospace; + transition:background var(--transition),border-color var(--transition),color var(--transition); +} +.layout-menu-item:hover { background:var(--surface1); border-color:var(--accent); color:var(--accent); } +.layout-menu-item.active { background:var(--surface1); border-color:var(--accent); color:var(--accent); } + +/* ── Plot grid ────────────────────────────────────────────────── */ +#plot-grid { + flex:1; min-height:0; display:grid; gap:8px; padding:8px; overflow:hidden; +} +#plot-grid.l1x1 { grid-template-columns:1fr; grid-template-rows:1fr; } +#plot-grid.l2x1 { grid-template-columns:1fr 1fr; grid-template-rows:1fr; } +#plot-grid.l1x2 { grid-template-columns:1fr; grid-template-rows:1fr 1fr; } +#plot-grid.l2x2 { grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr; } +#plot-grid.l3x1 { grid-template-columns:1fr 1fr 1fr; grid-template-rows:1fr; } +#plot-grid.l1x3 { grid-template-columns:1fr; grid-template-rows:1fr 1fr 1fr; } +#plot-grid.l3x2 { grid-template-columns:1fr 1fr 1fr; grid-template-rows:1fr 1fr; } +#plot-grid.l2x3 { grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr 1fr; } +#plot-grid.l1x4 { grid-template-columns:1fr; grid-template-rows:1fr 1fr 1fr 1fr; } +#plot-grid.l4x1 { grid-template-columns:1fr 1fr 1fr 1fr; grid-template-rows:1fr; } + +/* ── Plot card ────────────────────────────────────────────────── */ +.plot-card { + background:var(--surface0); border:1px solid var(--surface1); + border-radius:var(--radius); display:flex; flex-direction:column; + min-height:0; position:relative; overflow:hidden; + transition:border-color var(--transition); +} +.plot-card.drag-over { border-color:var(--accent); box-shadow:0 0 0 2px rgba(137,180,250,0.25); } +.plot-card-header { + display:flex; align-items:center; gap:5px; + padding:4px 8px; border-bottom:1px solid var(--surface1); + flex-shrink:0; min-height:30px; max-height:30px; overflow:hidden; +} +.plot-title { + font-size:11px; color:var(--subtext1); font-weight:600; + cursor:text; outline:none; border:1px solid transparent; border-radius:3px; + padding:1px 3px; background:transparent; white-space:nowrap; + flex-shrink:0; max-width:80px; overflow:hidden; text-overflow:ellipsis; +} +.plot-title:focus { border-color:var(--accent); background:var(--surface1); color:var(--text); } +.sig-badges { display:flex; flex-wrap:nowrap; gap:3px; flex:1; overflow:hidden; min-width:0; } +.sig-badge { + display:inline-flex; align-items:center; gap:3px; + background:rgba(69,71,90,0.6); color:var(--subtext1); + border-radius:10px; padding:1px 6px 1px 4px; + font-size:10px; white-space:nowrap; flex-shrink:0; +} +.trace-dot { width:7px; height:7px; border-radius:50%; flex-shrink:0; display:inline-block; } +.sig-badge-x { + cursor:pointer; color:var(--overlay0); font-size:11px; line-height:1; margin-left:1px; + transition:color var(--transition); +} +.sig-badge-x:hover { color:var(--red); } +.plot-icons { display:flex; gap:2px; flex-shrink:0; } +.plot-icon-btn { + background:none; border:none; cursor:pointer; color:var(--subtext0); + font-size:13px; line-height:1; padding:2px 4px; border-radius:4px; + transition:color var(--transition),background var(--transition); +} +.plot-icon-btn:hover { color:var(--accent); background:var(--surface1); } +.plot-icon-btn.danger:hover { color:var(--red); } +.plot-body { flex:1; position:relative; min-height:0; overflow:hidden; } +.drop-hint { + position:absolute; inset:0; display:flex; align-items:center; justify-content:center; + color:var(--overlay0); font-size:13px; pointer-events:none; +} +.pause-overlay { + position:absolute; inset:0; display:none; + align-items:center; justify-content:center; pointer-events:none; z-index:10; +} +.plot-card.paused .pause-overlay { display:flex; } +.pause-overlay-text { + background:rgba(49,50,68,0.82); color:var(--yellow); + font-size:11px; font-weight:600; padding:4px 12px; border-radius:20px; + border:1px solid var(--yellow); +} +.trig-collect-overlay { + position:absolute; inset:0; display:none; + align-items:center; justify-content:center; pointer-events:none; z-index:10; +} +.plot-card.trig-collecting .trig-collect-overlay { display:flex; } +.trig-collect-text { + background:rgba(49,50,68,0.82); color:var(--mauve); + font-size:11px; font-weight:600; padding:4px 12px; border-radius:20px; + border:1px solid var(--mauve); +} + +/* ── Empty state ──────────────────────────────────────────────── */ +#empty-state { + position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); + text-align:center; color:var(--subtext0); pointer-events:none; display:none; +} +#empty-state.visible { display:block; } +#empty-state h2 { font-size:20px; margin-bottom:8px; color:var(--surface2); } +#empty-state p { font-size:13px; } + +@media (max-width:700px) { #sidebar { width:0; min-width:0; } :root { --sidebar-w:240px; } } diff --git a/Client/WebUI/static/uPlot.iife.min.js b/Client/WebUI/static/uPlot.iife.min.js new file mode 100644 index 0000000..be69d13 --- /dev/null +++ b/Client/WebUI/static/uPlot.iife.min.js @@ -0,0 +1,2 @@ +/*! https://github.com/leeoniya/uPlot (v1.6.31) */ +var uPlot=function(){"use strict";const e="u-off",l="u-label",t="width",n="height",i="top",o="bottom",s="left",r="right",u="#000",a=u+"0",f="mousemove",c="mousedown",h="mouseup",d="mouseenter",p="mouseleave",m="dblclick",g="change",x="dppxchange",w="--",_="undefined"!=typeof window,b=_?document:null,v=_?window:null,k=_?navigator:null;let y,M;function S(e,l){if(null!=l){let t=e.classList;!t.contains(l)&&t.add(l)}}function E(e,l){let t=e.classList;t.contains(l)&&t.remove(l)}function T(e,l,t){e.style[l]=t+"px"}function z(e,l,t,n){let i=b.createElement(e);return null!=l&&S(i,l),null!=t&&t.insertBefore(i,n),i}function D(e,l){return z("div",e,l)}const P=new WeakMap;function A(l,t,n,i,o){let s="translate("+t+"px,"+n+"px)";s!=P.get(l)&&(l.style.transform=s,P.set(l,s),0>t||0>n||t>i||n>o?S(l,e):E(l,e))}const W=new WeakMap;function Y(e,l,t){let n=l+t;n!=W.get(e)&&(W.set(e,n),e.style.background=l,e.style.borderColor=t)}const C=new WeakMap;function F(e,l,t,n){let i=l+""+t;i!=C.get(e)&&(C.set(e,i),e.style.height=t+"px",e.style.width=l+"px",e.style.marginLeft=n?-l/2+"px":0,e.style.marginTop=n?-t/2+"px":0)}const H={passive:!0},R={...H,capture:!0};function G(e,l,t,n){l.addEventListener(e,t,n?R:H)}function I(e,l,t){l.removeEventListener(e,t,H)}function O(e,l,t,n){let i;t=t||0;let o=2147483647>=(n=n||l.length-1);for(;n-t>1;)i=o?t+n>>1:te((t+n)/2),e>l[i]?t=i:n=i;return e-l[t]>l[n]-e?n:t}function L(e,l,t,n){for(let i=1==n?l:t;i>=l&&t>=i;i+=n)if(null!=e[i])return i;return-1}function N(e,l,t,n){let i=ue(e),o=ue(l);e==l&&(-1==i?(e*=t,l/=t):(e/=t,l*=t));let s=10==t?ae:fe,r=1==o?ie:te,u=(1==i?te:ie)(s(le(e))),a=r(s(le(l))),f=re(t,u),c=re(t,a);return 10==t&&(0>u&&(f=Ee(f,-u)),0>a&&(c=Ee(c,-a))),n||2==t?(e=f*i,l=c*o):(e=Se(e,f),l=Me(l,c)),[e,l]}function j(e,l,t,n){let i=N(e,l,t,n);return 0==e&&(i[0]=0),0==l&&(i[1]=0),i}_&&function e(){let l=devicePixelRatio;y!=l&&(y=l,M&&I(g,M,e),M=matchMedia(`(min-resolution: ${y-.001}dppx) and (max-resolution: ${y+.001}dppx)`),G(g,M,e),v.dispatchEvent(new CustomEvent(x)))}();const U=.1,B={mode:3,pad:U},V={pad:0,soft:null,mode:0},$={min:V,max:V};function J(e,l,t,n){return He(t)?K(e,l,t):(V.pad=t,V.soft=n?0:null,V.mode=n?3:0,K(e,l,$))}function q(e,l){return null==e?l:e}function K(e,l,t){let n=t.min,i=t.max,o=q(n.pad,0),s=q(i.pad,0),r=q(n.hard,-he),u=q(i.hard,he),a=q(n.soft,he),f=q(i.soft,-he),c=q(n.mode,0),h=q(i.mode,0),d=l-e,p=ae(d),m=se(le(e),le(l)),g=ae(m),x=le(g-p);(1e-24>d||x>10)&&(d=0,0!=e&&0!=l||(d=1e-24,2==c&&a!=he&&(o=0),2==h&&f!=-he&&(s=0)));let w=d||m||1e3,_=ae(w),b=re(10,te(_)),v=Ee(Se(e-w*(0==d?0==e?.1:1:o),b/10),24),k=a>e||1!=c&&(3!=c||v>a)&&(2!=c||a>v)?he:a,y=se(r,k>v&&e>=k?k:oe(k,v)),M=Ee(Me(l+w*(0==d?0==l?.1:1:s),b/10),24),S=l>f||1!=h&&(3!=h||f>M)&&(2!=h||M>f)?-he:f,E=oe(u,M>S&&S>=l?S:se(S,M));return y==E&&0==y&&(E=100),[y,E]}const X=new Intl.NumberFormat(_?k.language:"en-US"),Z=e=>X.format(e),Q=Math,ee=Q.PI,le=Q.abs,te=Q.floor,ne=Q.round,ie=Q.ceil,oe=Q.min,se=Q.max,re=Q.pow,ue=Q.sign,ae=Q.log10,fe=Q.log2,ce=(e,l=1)=>Q.asinh(e/l),he=1/0;function de(e){return 1+(0|ae((e^e>>31)-(e>>31)))}function pe(e,l,t){return oe(se(e,l),t)}function me(e){return"function"==typeof e?e:()=>e}const ge=e=>e,xe=(e,l)=>l,we=()=>null,_e=()=>!0,be=(e,l)=>e==l,ve=/\.\d*?(?=9{6,}|0{6,})/gm,ke=e=>{if(Ce(e)||Te.has(e))return e;const l=""+e,t=l.match(ve);if(null==t)return e;let n=t[0].length-1;if(-1!=l.indexOf("e-")){let[e,t]=l.split("e");return+`${ke(e)}e${t}`}return Ee(e,n)};function ye(e,l){return ke(Ee(ke(e/l))*l)}function Me(e,l){return ke(ie(ke(e/l))*l)}function Se(e,l){return ke(te(ke(e/l))*l)}function Ee(e,l=0){if(Ce(e))return e;let t=10**l;return ne(e*t*(1+Number.EPSILON))/t}const Te=new Map;function ze(e){return((""+e).split(".")[1]||"").length}function De(e,l,t,n){let i=[],o=n.map(ze);for(let s=l;t>s;s++){let l=le(s),t=Ee(re(e,s),l);for(let r=0;n.length>r;r++){let u=10==e?+`${n[r]}e${s}`:n[r]*t,a=(0>s?l:0)+(o[r]>s?o[r]:0),f=10==e?u:Ee(u,a);i.push(f),Te.set(f,a)}}return i}const Pe={},Ae=[],We=[null,null],Ye=Array.isArray,Ce=Number.isInteger;function Fe(e){return"string"==typeof e}function He(e){let l=!1;if(null!=e){let t=e.constructor;l=null==t||t==Object}return l}function Re(e){return null!=e&&"object"==typeof e}const Ge=Object.getPrototypeOf(Uint8Array),Ie="__proto__";function Oe(e,l=He){let t;if(Ye(e)){let n=e.find((e=>null!=e));if(Ye(n)||l(n)){t=Array(e.length);for(let n=0;e.length>n;n++)t[n]=Oe(e[n],l)}else t=e.slice()}else if(e instanceof Ge)t=e.slice();else if(l(e)){t={};for(let n in e)n!=Ie&&(t[n]=Oe(e[n],l))}else t=e;return t}function Le(e){let l=arguments;for(let t=1;l.length>t;t++){let n=l[t];for(let l in n)l!=Ie&&(He(e[l])?Le(e[l],Oe(n[l])):e[l]=Oe(n[l]))}return e}function Ne(e,l,t){for(let n,i=0,o=-1;l.length>i;i++){let s=l[i];if(s>o){for(n=s-1;n>=0&&null==e[n];)e[n--]=null;for(n=s+1;t>n&&null==e[n];)e[o=n++]=null}}}const je="undefined"==typeof queueMicrotask?e=>Promise.resolve().then(e):queueMicrotask,Ue=["January","February","March","April","May","June","July","August","September","October","November","December"],Be=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];function Ve(e){return e.slice(0,3)}const $e=Be.map(Ve),Je=Ue.map(Ve),qe={MMMM:Ue,MMM:Je,WWWW:Be,WWW:$e};function Ke(e){return(10>e?"0":"")+e}const Xe={YYYY:e=>e.getFullYear(),YY:e=>(e.getFullYear()+"").slice(2),MMMM:(e,l)=>l.MMMM[e.getMonth()],MMM:(e,l)=>l.MMM[e.getMonth()],MM:e=>Ke(e.getMonth()+1),M:e=>e.getMonth()+1,DD:e=>Ke(e.getDate()),D:e=>e.getDate(),WWWW:(e,l)=>l.WWWW[e.getDay()],WWW:(e,l)=>l.WWW[e.getDay()],HH:e=>Ke(e.getHours()),H:e=>e.getHours(),h:e=>{let l=e.getHours();return 0==l?12:l>12?l-12:l},AA:e=>12>e.getHours()?"AM":"PM",aa:e=>12>e.getHours()?"am":"pm",a:e=>12>e.getHours()?"a":"p",mm:e=>Ke(e.getMinutes()),m:e=>e.getMinutes(),ss:e=>Ke(e.getSeconds()),s:e=>e.getSeconds(),fff:e=>function(e){return(10>e?"00":100>e?"0":"")+e}(e.getMilliseconds())};function Ze(e,l){l=l||qe;let t,n=[],i=/\{([a-z]+)\}|[^{]+/gi;for(;t=i.exec(e);)n.push("{"==t[0][0]?Xe[t[1]]:t[0]);return e=>{let t="";for(let i=0;n.length>i;i++)t+="string"==typeof n[i]?n[i]:n[i](e,l);return t}}const Qe=(new Intl.DateTimeFormat).resolvedOptions().timeZone,el=e=>e%1==0,ll=[1,2,2.5,5],tl=De(10,-32,0,ll),nl=De(10,0,32,ll),il=nl.filter(el),ol=tl.concat(nl),sl="{YYYY}",rl="\n"+sl,ul="{M}/{D}",al="\n"+ul,fl=al+"/{YY}",cl="{aa}",hl="{h}:{mm}"+cl,dl="\n"+hl,pl=":{ss}",ml=null;function gl(e){let l=1e3*e,t=60*l,n=60*t,i=24*n,o=30*i,s=365*i;return[(1==e?De(10,0,3,ll).filter(el):De(10,-3,0,ll)).concat([l,5*l,10*l,15*l,30*l,t,5*t,10*t,15*t,30*t,n,2*n,3*n,4*n,6*n,8*n,12*n,i,2*i,3*i,4*i,5*i,6*i,7*i,8*i,9*i,10*i,15*i,o,2*o,3*o,4*o,6*o,s,2*s,5*s,10*s,25*s,50*s,100*s]),[[s,sl,ml,ml,ml,ml,ml,ml,1],[28*i,"{MMM}",rl,ml,ml,ml,ml,ml,1],[i,ul,rl,ml,ml,ml,ml,ml,1],[n,"{h}"+cl,fl,ml,al,ml,ml,ml,1],[t,hl,fl,ml,al,ml,ml,ml,1],[l,pl,fl+" "+hl,ml,al+" "+hl,ml,dl,ml,1],[e,pl+".{fff}",fl+" "+hl,ml,al+" "+hl,ml,dl,ml,1]],function(l){return(r,u,a,f,c,h)=>{let d=[],p=c>=s,m=c>=o&&s>c,g=l(a),x=Ee(g*e,3),w=Sl(g.getFullYear(),p?0:g.getMonth(),m||p?1:g.getDate()),_=Ee(w*e,3);if(m||p){let t=m?c/o:0,n=p?c/s:0,i=x==_?x:Ee(Sl(w.getFullYear()+n,w.getMonth()+t,1)*e,3),r=new Date(ne(i/e)),u=r.getFullYear(),a=r.getMonth();for(let o=0;f>=i;o++){let s=Sl(u+n*o,a+t*o,1),r=s-l(Ee(s*e,3));i=Ee((+s+r)*e,3),i>f||d.push(i)}}else{let o=i>c?c:i,s=_+(te(a)-te(x))+Me(x-_,o);d.push(s);let p=l(s),m=p.getHours()+p.getMinutes()/t+p.getSeconds()/n,g=c/n,w=h/r.axes[u]._space;for(;s=Ee(s+c,1==e?0:3),f>=s;)if(g>1){let e=te(Ee(m+g,6))%24,t=l(s).getHours()-e;t>1&&(t=-1),s-=t*n,m=(m+g)%24,.7>Ee((s-d[d.length-1])/c,3)*w||d.push(s)}else d.push(s)}return d}}]}const[xl,wl,_l]=gl(1),[bl,vl,kl]=gl(.001);function yl(e,l){return e.map((e=>e.map(((t,n)=>0==n||8==n||null==t?t:l(1==n||0==e[8]?t:e[1]+t)))))}function Ml(e,l){return(t,n,i,o,s)=>{let r,u,a,f,c,h,d=l.find((e=>s>=e[0]))||l[l.length-1];return n.map((l=>{let t=e(l),n=t.getFullYear(),i=t.getMonth(),o=t.getDate(),s=t.getHours(),p=t.getMinutes(),m=t.getSeconds(),g=n!=r&&d[2]||i!=u&&d[3]||o!=a&&d[4]||s!=f&&d[5]||p!=c&&d[6]||m!=h&&d[7]||d[1];return r=n,u=i,a=o,f=s,c=p,h=m,g(t)}))}}function Sl(e,l,t){return new Date(e,l,t)}function El(e,l){return l(e)}function Tl(e,l){return(t,n,i,o)=>null==o?w:l(e(n))}De(2,-53,53,[1]);const zl={show:!0,live:!0,isolate:!1,mount:()=>{},markers:{show:!0,width:2,stroke:function(e,l){let t=e.series[l];return t.width?t.stroke(e,l):t.points.width?t.points.stroke(e,l):null},fill:function(e,l){return e.series[l].fill(e,l)},dash:"solid"},idx:null,idxs:null,values:[]},Dl=[0,0];function Pl(e,l,t,n=!0){return e=>{0==e.button&&(!n||e.target==l)&&t(e)}}function Al(e,l,t,n=!0){return e=>{(!n||e.target==l)&&t(e)}}const Wl={show:!0,x:!0,y:!0,lock:!1,move:function(e,l,t){return Dl[0]=l,Dl[1]=t,Dl},points:{one:!1,show:function(e,l){let i=e.cursor.points,o=D(),s=i.size(e,l);T(o,t,s),T(o,n,s);let r=s/-2;T(o,"marginLeft",r),T(o,"marginTop",r);let u=i.width(e,l,s);return u&&T(o,"borderWidth",u),o},size:function(e,l){return e.series[l].points.size},width:0,stroke:function(e,l){let t=e.series[l].points;return t._stroke||t._fill},fill:function(e,l){let t=e.series[l].points;return t._fill||t._stroke}},bind:{mousedown:Pl,mouseup:Pl,click:Pl,dblclick:Pl,mousemove:Al,mouseleave:Al,mouseenter:Al},drag:{setScale:!0,x:!0,y:!1,dist:0,uni:null,click:(e,l)=>{l.stopPropagation(),l.stopImmediatePropagation()},_x:!1,_y:!1},focus:{dist:(e,l,t,n,i)=>n-i,prox:-1,bias:0},hover:{skip:[void 0],prox:null,bias:0},left:-10,top:-10,idx:null,dataIdx:null,idxs:null,event:null},Yl={show:!0,stroke:"rgba(0,0,0,0.07)",width:2},Cl=Le({},Yl,{filter:xe}),Fl=Le({},Cl,{size:10}),Hl=Le({},Yl,{show:!1}),Rl='12px system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',Gl="bold "+Rl,Il={show:!0,scale:"x",stroke:u,space:50,gap:5,size:50,labelGap:0,labelSize:30,labelFont:Gl,side:2,grid:Cl,ticks:Fl,border:Hl,font:Rl,lineGap:1.5,rotate:0},Ol={show:!0,scale:"x",auto:!1,sorted:1,min:he,max:-he,idxs:[]};function Ll(e,l){return l.map((e=>null==e?"":Z(e)))}function Nl(e,l,t,n,i,o,s){let r=[],u=Te.get(i)||0;for(let e=t=s?t:Ee(Me(t,i),u);n>=e;e=Ee(e+i,u))r.push(Object.is(e,-0)?0:e);return r}function jl(e,l,t,n,i){const o=[],s=e.scales[e.axes[l].scale].log,r=te((10==s?ae:fe)(t));i=re(s,r),10==s&&(i=ol[O(i,ol)]);let u=t,a=i*s;10==s&&(a=ol[O(a,ol)]);do{o.push(u),u+=i,10!=s||Te.has(u)||(u=Ee(u,Te.get(i))),a>u||(a=(i=u)*s,10==s&&(a=ol[O(a,ol)]))}while(n>=u);return o}function Ul(e,l,t,n,i){let o=e.scales[e.axes[l].scale].asinh,s=n>o?jl(e,l,se(o,t),n,i):[o],r=0>n||t>0?[]:[0];return(-o>t?jl(e,l,se(o,-n),-t,i):[o]).reverse().map((e=>-e)).concat(r,s)}const Bl=/./,Vl=/[12357]/,$l=/[125]/,Jl=/1/,ql=(e,l,t,n)=>e.map(((e,i)=>4==l&&0==e||i%n==0&&t.test(e.toExponential()[0>e?1:0])?e:null));function Kl(e,l,t){let n=e.axes[t],i=n.scale,o=e.scales[i],s=e.valToPos,r=n._space,u=s(10,i),a=s(9,i)-ue)return ql(l.slice().reverse(),o.distr,a,ie(r/e)).reverse()}return ql(l,o.distr,a,1)}function Xl(e,l,t){let n=e.axes[t],i=n.scale,o=n._space,s=e.valToPos,r=le(s(1,i)-s(2,i));return o>r?ql(l.slice().reverse(),3,Bl,ie(o/r)).reverse():l}function Zl(e,l,t,n){return null==n?w:null==l?"":Z(l)}const Ql={show:!0,scale:"y",stroke:u,space:30,gap:5,size:50,labelGap:0,labelSize:30,labelFont:Gl,side:3,grid:Cl,ticks:Fl,border:Hl,font:Rl,lineGap:1.5,rotate:0},et={scale:null,auto:!0,sorted:0,min:he,max:-he},lt=(e,l,t,n,i)=>i,tt={show:!0,auto:!0,sorted:0,gaps:lt,alpha:1,facets:[Le({},et,{scale:"x"}),Le({},et,{scale:"y"})]},nt={scale:"y",auto:!0,sorted:0,show:!0,spanGaps:!1,gaps:lt,alpha:1,points:{show:function(e,l){let{scale:t,idxs:n}=e.series[0],i=e._data[0],o=e.valToPos(i[n[0]],t,!0),s=e.valToPos(i[n[1]],t,!0);return le(s-o)/(e.series[l].points.space*y)>=n[1]-n[0]},filter:null},values:null,min:he,max:-he,idxs:[],path:null,clip:null};function it(e,l,t){return t/10}const ot={time:!0,auto:!0,distr:1,log:10,asinh:1,min:null,max:null,dir:1,ori:0},st=Le({},ot,{time:!1,ori:1}),rt={};function ut(e){let l=rt[e];return l||(l={key:e,plots:[],sub(e){l.plots.push(e)},unsub(e){l.plots=l.plots.filter((l=>l!=e))},pub(e,t,n,i,o,s,r){for(let u=0;l.plots.length>u;u++)l.plots[u]!=t&&l.plots[u].pub(e,t,n,i,o,s,r)}},null!=e&&(rt[e]=l)),l}function at(e,l,t){const n=e.mode,i=e.series[l],o=2==n?e._data[l]:e._data,s=e.scales,r=e.bbox;let u=o[0],a=2==n?o[1]:o[l],f=2==n?s[i.facets[0].scale]:s[e.series[0].scale],c=2==n?s[i.facets[1].scale]:s[i.scale],h=r.left,d=r.top,p=r.width,m=r.height,g=e.valToPosH,x=e.valToPosV;return 0==f.ori?t(i,u,a,f,c,g,x,h,d,p,m,xt,_t,vt,yt,St):t(i,u,a,f,c,x,g,d,h,m,p,wt,bt,kt,Mt,Et)}function ft(e,l){let t=0,n=0,i=q(e.bands,Ae);for(let e=0;i.length>e;e++){let o=i[e];o.series[0]==l?t=o.dir:o.series[1]==l&&(n|=1==o.dir?1:2)}return[t,1==n?-1:2==n?1:3==n?2:0]}function ct(e,l,t,n,i){let o=e.series[l],s=e.scales[2==e.mode?o.facets[1].scale:o.scale];return-1==i?s.min:1==i?s.max:3==s.distr?1==s.dir?s.min:s.max:0}function ht(e,l,t,n,i,o){return at(e,l,((e,l,s,r,u,a,f,c,h,d,p)=>{let m=e.pxRound;const g=0==r.ori?_t:bt;let x,w;1==r.dir*(0==r.ori?1:-1)?(x=t,w=n):(x=n,w=t);let _=m(a(l[x],r,d,c)),b=m(f(s[x],u,p,h)),v=m(a(l[w],r,d,c)),k=m(f(1==o?u.max:u.min,u,p,h)),y=new Path2D(i);return g(y,v,k),g(y,_,k),g(y,_,b),y}))}function dt(e,l,t,n,i,o){let s=null;if(e.length>0){s=new Path2D;const r=0==l?vt:kt;let u=t;for(let l=0;e.length>l;l++){let t=e[l];if(t[1]>t[0]){let e=t[0]-u;e>0&&r(s,u,n,e,n+o),u=t[1]}}let a=t+i-u,f=10;a>0&&r(s,u,n-f/2,a,n+o+f)}return s}function pt(e,l,t,n,i,o,s){let r=[],u=e.length;for(let a=1==i?t:n;a>=t&&n>=a;a+=i)if(null===l[a]){let f=a,c=a;if(1==i)for(;++a<=n&&null===l[a];)c=a;else for(;--a>=t&&null===l[a];)c=a;let h=o(e[f]),d=c==f?h:o(e[c]),p=f-i;h=s>0||0>p||p>=u?h:o(e[p]);let m=c+i;d=0>s||0>m||m>=u?d:o(e[m]),h>d||r.push([h,d])}return r}function mt(e){return 0==e?ge:1==e?ne:l=>ye(l,e)}function gt(e){let l=0==e?xt:wt,t=0==e?(e,l,t,n,i,o)=>{e.arcTo(l,t,n,i,o)}:(e,l,t,n,i,o)=>{e.arcTo(t,l,i,n,o)},n=0==e?(e,l,t,n,i)=>{e.rect(l,t,n,i)}:(e,l,t,n,i)=>{e.rect(t,l,i,n)};return(e,i,o,s,r,u=0,a=0)=>{0==u&&0==a?n(e,i,o,s,r):(u=oe(u,s/2,r/2),a=oe(a,s/2,r/2),l(e,i+u,o),t(e,i+s,o,i+s,o+r,u),t(e,i+s,o+r,i,o+r,a),t(e,i,o+r,i,o,a),t(e,i,o,i+s,o,u),e.closePath())}}const xt=(e,l,t)=>{e.moveTo(l,t)},wt=(e,l,t)=>{e.moveTo(t,l)},_t=(e,l,t)=>{e.lineTo(l,t)},bt=(e,l,t)=>{e.lineTo(t,l)},vt=gt(0),kt=gt(1),yt=(e,l,t,n,i,o)=>{e.arc(l,t,n,i,o)},Mt=(e,l,t,n,i,o)=>{e.arc(t,l,n,i,o)},St=(e,l,t,n,i,o,s)=>{e.bezierCurveTo(l,t,n,i,o,s)},Et=(e,l,t,n,i,o,s)=>{e.bezierCurveTo(t,l,i,n,s,o)};function Tt(){return(e,l,t,n,i)=>at(e,l,((l,o,s,r,u,a,f,c,h,d,p)=>{let m,g,{pxRound:x,points:w}=l;0==r.ori?(m=xt,g=yt):(m=wt,g=Mt);const _=Ee(w.width*y,3);let b=(w.size-w.width)/2*y,v=Ee(2*b,3),k=new Path2D,M=new Path2D,{left:S,top:E,width:T,height:z}=e.bbox;vt(M,S-v,E-v,T+2*v,z+2*v);const D=e=>{if(null!=s[e]){let l=x(a(o[e],r,d,c)),t=x(f(s[e],u,p,h));m(k,l+b,t),g(k,l,t,b,0,2*ee)}};if(i)i.forEach(D);else for(let e=t;n>=e;e++)D(e);return{stroke:_>0?k:null,fill:k,clip:M,flags:3}}))}function zt(e){return(l,t,n,i,o,s)=>{n!=i&&(o!=n&&s!=n&&e(l,t,n),o!=i&&s!=i&&e(l,t,i),e(l,t,s))}}const Dt=zt(_t),Pt=zt(bt);function At(e){const l=q(e?.alignGaps,0);return(e,t,n,i)=>at(e,t,((o,s,r,u,a,f,c,h,d,p,m)=>{let g,x,w=o.pxRound,_=e=>w(f(e,u,p,h)),b=e=>w(c(e,a,m,d));0==u.ori?(g=_t,x=Dt):(g=bt,x=Pt);const v=u.dir*(0==u.ori?1:-1),k={stroke:new Path2D,fill:null,clip:null,band:null,gaps:null,flags:1},y=k.stroke;let M,S,E,T=he,z=-he,D=_(s[1==v?n:i]),P=L(r,n,i,1*v),A=L(r,n,i,-1*v),W=_(s[P]),Y=_(s[A]),C=!1;for(let e=1==v?n:i;e>=n&&i>=e;e+=v){let l=_(s[e]),t=r[e];l==D?null!=t?(S=b(t),T==he&&(g(y,l,S),M=S),T=oe(S,T),z=se(S,z)):null===t&&(C=!0):(T!=he&&(x(y,D,T,z,M,S),E=D),null!=t?(S=b(t),g(y,l,S),T=z=M=S):(T=he,z=-he,null===t&&(C=!0)),D=l)}T!=he&&T!=z&&E!=D&&x(y,D,T,z,M,S);let[F,H]=ft(e,t);if(null!=o.fill||0!=F){let l=k.fill=new Path2D(y),n=b(o.fillTo(e,t,o.min,o.max,F));g(l,Y,n),g(l,W,n)}if(!o.spanGaps){let a=[];C&&a.push(...pt(s,r,n,i,v,_,l)),k.gaps=a=o.gaps(e,t,n,i,a),k.clip=dt(a,u.ori,h,d,p,m)}return 0!=H&&(k.band=2==H?[ht(e,t,n,i,y,-1),ht(e,t,n,i,y,1)]:ht(e,t,n,i,y,H)),k}))}function Wt(e,l,t,n,i,o,s=he){if(e.length>1){let r=null;for(let u=0,a=1/0;e.length>u;u++)if(void 0!==l[u]){if(null!=r){let l=le(e[u]-e[r]);a>l&&(a=l,s=le(t(e[u],n,i,o)-t(e[r],n,i,o)))}r=u}}return s}function Yt(e,l,t,n,i){const o=e.length;if(2>o)return null;const s=new Path2D;if(t(s,e[0],l[0]),2==o)n(s,e[1],l[1]);else{let t=Array(o),n=Array(o-1),r=Array(o-1),u=Array(o-1);for(let t=0;o-1>t;t++)r[t]=l[t+1]-l[t],u[t]=e[t+1]-e[t],n[t]=r[t]/u[t];t[0]=n[0];for(let e=1;o-1>e;e++)0===n[e]||0===n[e-1]||n[e-1]>0!=n[e]>0?t[e]=0:(t[e]=3*(u[e-1]+u[e])/((2*u[e]+u[e-1])/n[e-1]+(u[e]+2*u[e-1])/n[e]),isFinite(t[e])||(t[e]=0));t[o-1]=n[o-2];for(let n=0;o-1>n;n++)i(s,e[n]+u[n]/3,l[n]+t[n]*u[n]/3,e[n+1]-u[n]/3,l[n+1]-t[n+1]*u[n]/3,e[n+1],l[n+1])}return s}const Ct=new Set;function Ft(){for(let e of Ct)e.syncRect(!0)}_&&(G("resize",v,Ft),G("scroll",v,Ft,!0),G(x,v,(()=>{Kt.pxRatio=y})));const Ht=At(),Rt=Tt();function Gt(e,l,t,n){return(n?[e[0],e[1]].concat(e.slice(2)):[e[0]].concat(e.slice(1))).map(((e,n)=>It(e,n,l,t)))}function It(e,l,t,n){return Le({},0==l?t:n,e)}function Ot(e,l,t){return null==l?We:[l,t]}const Lt=Ot;function Nt(e,l,t){return null==l?We:J(l,t,U,!0)}function jt(e,l,t,n){return null==l?We:N(l,t,e.scales[n].log,!1)}const Ut=jt;function Bt(e,l,t,n){return null==l?We:j(l,t,e.scales[n].log,!1)}const Vt=Bt;function $t(e,l,t,n,i){let o=se(de(e),de(l)),s=l-e,r=O(i/n*s,t);do{let e=t[r],l=n*e/s;if(l>=i&&17>=o+(5>e?Te.get(e):0))return[e,l]}while(++r(l=ne((t=+n)*y))+"px")),l,t]}function qt(e){e.show&&[e.font,e.labelFont].forEach((e=>{let l=Ee(e[2]*y,1);e[0]=e[0].replace(/[0-9.]+px/,l+"px"),e[1]=l}))}function Kt(u,g,_){const k={mode:q(u.mode,1)},M=k.mode;function P(e,l){return((3==l.distr?ae(e>0?e:l.clamp(k,e,l.min,l.max,l.key)):4==l.distr?ce(e,l.asinh):100==l.distr?l.fwd(e):e)-l._min)/(l._max-l._min)}function W(e,l,t,n){let i=P(e,l);return n+t*(-1==l.dir?1-i:i)}function C(e,l,t,n){let i=P(e,l);return n+t*(-1==l.dir?i:1-i)}function H(e,l,t,n){return 0==l.ori?W(e,l,t,n):C(e,l,t,n)}k.valToPosH=W,k.valToPosV=C;let R=!1;k.status=0;const L=k.root=D("uplot");null!=u.id&&(L.id=u.id),S(L,u.class),u.title&&(D("u-title",L).textContent=u.title);const V=z("canvas"),$=k.ctx=V.getContext("2d"),K=D("u-wrap",L);G("click",K,(e=>{e.target===Z&&(Hn!=Wn||Rn!=Yn)&&Vn.click(k,e)}),!0);const X=k.under=D("u-under",K);K.appendChild(V);const Z=k.over=D("u-over",K),te=+q((u=Oe(u)).pxAlign,1),ue=mt(te);(u.plugins||[]).forEach((e=>{e.opts&&(u=e.opts(k,u)||u)}));const fe=u.ms||.001,de=k.series=1==M?Gt(u.series||[],Ol,nt,!1):function(e,l){return e.map(((e,t)=>0==t?{}:Le({},l,e)))}(u.series||[null],tt),ge=k.axes=Gt(u.axes||[],Il,Ql,!0),ve=k.scales={},ke=k.bands=u.bands||[];ke.forEach((e=>{e.fill=me(e.fill||null),e.dir=q(e.dir,-1)}));const Me=2==M?de[1].facets[0].scale:de[0].scale,Se={axes:function(){for(let e=0;ge.length>e;e++){let l=ge[e];if(!l.show||!l._show)continue;let t,n,u=l.side,a=u%2,f=l.stroke(k,e),c=0==u||3==u?-1:1;if(l.label){let e=ne((l._lpos+l.labelGap*c)*y);dn(l.labelFont[0],f,"center",2==u?i:o),$.save(),1==a?(t=n=0,$.translate(e,ne($l+ql/2)),$.rotate((3==u?-ee:ee)/2)):(t=ne(Vl+Jl/2),n=e),$.fillText(l.label,t,n),$.restore()}let[h,d]=l._found;if(0==d)continue;let p=ve[l.scale],m=0==a?Jl:ql,g=0==a?Vl:$l,x=ne(l.gap*y),w=l._splits,_=2==p.distr?w.map((e=>un[e])):w,b=2==p.distr?un[w[1]]-un[w[0]]:h,v=l.ticks,M=l.border,S=v.show?ne(v.size*y):0,E=l._rotate*-ee/180,T=ue(l._pos*y),z=T+(S+x)*c;n=0==a?z:0,t=1==a?z:0,dn(l.font[0],f,1==l.align?s:2==l.align?r:E>0?s:0>E?r:0==a?"center":3==u?r:s,E||1==a?"middle":2==u?i:o);let D=l.font[1]*l.lineGap,P=w.map((e=>ue(H(e,p,m,g)))),A=l._values;for(let e=0;A.length>e;e++){let l=A[e];if(null!=l){0==a?t=P[e]:n=P[e],l=""+l;let i=-1==l.indexOf("\n")?[l]:l.split(/\n/gm);for(let e=0;i.length>e;e++){let l=i[e];E?($.save(),$.translate(t,n+e*D),$.rotate(E),$.fillText(l,0,0),$.restore()):$.fillText(l,t,n+e*D)}}}v.show&&kn(P,v.filter(k,_,e,d,b),a,u,T,S,Ee(v.width*y,3),v.stroke(k,e),v.dash,v.cap);let W=l.grid;W.show&&kn(P,W.filter(k,_,e,d,b),a,0==a?2:1,0==a?$l:Vl,0==a?ql:Jl,Ee(W.width*y,3),W.stroke(k,e),W.dash,W.cap),M.show&&kn([T],[1],0==a?1:0,0==a?1:2,1==a?$l:Vl,1==a?ql:Jl,Ee(M.width*y,3),M.stroke(k,e),M.dash,M.cap)}zi("drawAxes")},series:function(){Wt>0&&(de.forEach(((e,l)=>{if(l>0&&e.show&&(gn(l,!1),gn(l,!0),null==e._paths)){rn!=e.alpha&&($.globalAlpha=rn=e.alpha);let t=2==M?[0,g[l][0].length-1]:function(e){let l=pe(Yt-1,0,Wt-1),t=pe(Ft+1,0,Wt-1);for(;null==e[l]&&l>0;)l--;for(;null==e[t]&&Wt-1>t;)t++;return[l,t]}(g[l]);e._paths=e.paths(k,l,t[0],t[1]),1!=rn&&($.globalAlpha=rn=1)}})),de.forEach(((e,l)=>{if(l>0&&e.show){rn!=e.alpha&&($.globalAlpha=rn=e.alpha),null!=e._paths&&xn(l,!1);{let t=null!=e._paths?e._paths.gaps:null,n=e.points.show(k,l,Yt,Ft,t),i=e.points.filter(k,l,n,t);(n||i)&&(e.points._paths=e.points.paths(k,l,Yt,Ft,i),xn(l,!0))}1!=rn&&($.globalAlpha=rn=1),zi("drawSeries",l)}})))}},De=(u.drawOrder||["axes","series"]).map((e=>Se[e]));function Ce(e){let l=ve[e];if(null==l){let t=(u.scales||Pe)[e]||Pe;if(null!=t.from)Ce(t.from),ve[e]=Le({},ve[t.from],t,{key:e});else{l=ve[e]=Le({},e==Me?ot:st,t),l.key=e;let n=l.time,i=l.range,o=Ye(i);if((e!=Me||2==M&&!n)&&(!o||null!=i[0]&&null!=i[1]||(i={min:null==i[0]?B:{mode:1,hard:i[0],soft:i[0]},max:null==i[1]?B:{mode:1,hard:i[1],soft:i[1]}},o=!1),!o&&He(i))){let e=i;i=(l,t,n)=>null==t?We:J(t,n,e)}l.range=me(i||(n?Lt:e==Me?3==l.distr?Ut:4==l.distr?Vt:Ot:3==l.distr?jt:4==l.distr?Bt:Nt)),l.auto=me(!o&&l.auto),l.clamp=me(l.clamp||it),l._min=l._max=null}}}Ce("x"),Ce("y"),1==M&&de.forEach((e=>{Ce(e.scale)})),ge.forEach((e=>{Ce(e.scale)}));for(let e in u.scales)Ce(e);const Ge=ve[Me],Ie=Ge.distr;let Ne,Ue;0==Ge.ori?(S(L,"u-hz"),Ne=W,Ue=C):(S(L,"u-vt"),Ne=C,Ue=W);const Be={};for(let e in ve){let l=ve[e];null==l.min&&null==l.max||(Be[e]={min:l.min,max:l.max},l.min=l.max=null)}const Ve=u.tzDate||(e=>new Date(ne(e/fe))),$e=u.fmtDate||Ze,Je=1==fe?_l(Ve):kl(Ve),qe=Ml(Ve,yl(1==fe?wl:vl,$e)),Ke=Tl(Ve,El("{YYYY}-{MM}-{DD} {h}:{mm}{aa}",$e)),Xe=[],Qe=k.legend=Le({},zl,u.legend),el=Qe.show,ll=Qe.markers;let tl,nl,sl;Qe.idxs=Xe,ll.width=me(ll.width),ll.dash=me(ll.dash),ll.stroke=me(ll.stroke),ll.fill=me(ll.fill);let rl,ul=[],al=[],fl=!1,cl={};if(Qe.live){const e=de[1]?de[1].values:null;fl=null!=e,rl=fl?e(k,1,0):{_:0};for(let e in rl)cl[e]=w}if(el)if(tl=z("table","u-legend",L),sl=z("tbody",null,tl),Qe.mount(k,tl),fl){nl=z("thead",null,tl,sl);let e=z("tr",null,nl);for(var hl in z("th",null,e),rl)z("th",l,e).textContent=hl}else S(tl,"u-inline"),Qe.live&&S(tl,"u-live");const dl={show:!0},pl={show:!1},ml=new Map;function gl(e,l,t,n=!0){const i=ml.get(l)||{},o=xt.bind[e](k,l,t,n);o&&(G(e,l,i[e]=o),ml.set(l,i))}function Sl(e,l){const t=ml.get(l)||{};for(let n in t)null!=e&&n!=e||(I(n,l,t[n]),delete t[n]);null==e&&ml.delete(l)}let Dl=0,Pl=0,Al=0,Yl=0,Cl=0,Fl=0,Hl=Cl,Rl=Fl,Gl=Al,Bl=Yl,Vl=0,$l=0,Jl=0,ql=0;k.bbox={};let et=!1,lt=!1,rt=!1,at=!1,ft=!1,ht=!1;function dt(e,l,t){(t||e!=k.width||l!=k.height)&&pt(e,l),Sn(!1),rt=!0,lt=!0,Nn()}function pt(e,l){k.width=Dl=Al=e,k.height=Pl=Yl=l,Cl=Fl=0,function(){let e=!1,l=!1,t=!1,n=!1;ge.forEach((i=>{if(i.show&&i._show){let{side:o,_size:s}=i,r=s+(null!=i.label?i.labelSize:0);r>0&&(o%2?(Al-=r,3==o?(Cl+=r,n=!0):t=!0):(Yl-=r,0==o?(Fl+=r,e=!0):l=!0))}})),zt[0]=e,zt[1]=t,zt[2]=l,zt[3]=n,Al-=At[1]+At[3],Cl+=At[3],Yl-=At[2]+At[0],Fl+=At[0]}(),function(){let e=Cl+Al,l=Fl+Yl,t=Cl,n=Fl;function i(i,o){switch(i){case 1:return e+=o,e-o;case 2:return l+=o,l-o;case 3:return t-=o,t+o;case 0:return n-=o,n+o}}ge.forEach((e=>{if(e.show&&e._show){let l=e.side;e._pos=i(l,e._size),null!=e.label&&(e._lpos=i(l,e.labelSize))}}))}();let t=k.bbox;Vl=t.left=ye(Cl*y,.5),$l=t.top=ye(Fl*y,.5),Jl=t.width=ye(Al*y,.5),ql=t.height=ye(Yl*y,.5)}const gt=3;k.setSize=function({width:e,height:l}){dt(e,l)};const xt=k.cursor=Le({},Wl,{drag:{y:2==M}},u.cursor);if(null==xt.dataIdx){let e=xt.hover,l=e.skip=new Set(e.skip??[]);l.add(void 0);let t=e.prox=me(e.prox),n=e.bias??=0;xt.dataIdx=(e,i,o,s)=>{if(0==i)return o;let r=o,u=t(e,i,o,s)??he,a=u>=0&&he>u,f=0==Ge.ori?Al:Yl,c=xt.left,h=g[0],d=g[i];if(l.has(d[o])){r=null;let e,t=null,i=null;if(0==n||-1==n)for(e=o;null==t&&e-- >0;)l.has(d[e])||(t=e);if(0==n||1==n)for(e=o;null==i&&e++l?l>u||(r=i):e>u||(r=t)}else r=null==i?t:null==t||o-t>i-o?i:t}else a&&le(c-Ne(h[o],Ge,f,0))>u&&(r=null);return r}}const wt=e=>{xt.event=e};xt.idxs=Xe,xt._lock=!1;let _t=xt.points;_t.show=me(_t.show),_t.size=me(_t.size),_t.stroke=me(_t.stroke),_t.width=me(_t.width),_t.fill=me(_t.fill);const bt=k.focus=Le({},u.focus||{alpha:.3},xt.focus),vt=bt.prox>=0,kt=vt&&_t.one;let yt=[],Mt=[],St=[];function Et(e,l){let t=_t.show(k,l);if(t)return S(t,"u-cursor-pt"),S(t,e.class),A(t,-10,-10,Al,Yl),Z.insertBefore(t,yt[l]),t}function Tt(t,n){if(1==M||n>0){let e=1==M&&ve[t.scale].time,l=t.value;t.value=e?Fe(l)?Tl(Ve,El(l,$e)):l||Ke:l||Zl,t.label=t.label||(e?"Time":"Value")}if(kt||n>0){t.width=null==t.width?1:t.width,t.paths=t.paths||Ht||we,t.fillTo=me(t.fillTo||ct),t.pxAlign=+q(t.pxAlign,te),t.pxRound=mt(t.pxAlign),t.stroke=me(t.stroke||null),t.fill=me(t.fill||null),t._stroke=t._fill=t._paths=t._focus=null;let e=function(e){return Ee(1*(3+2*(e||1)),3)}(se(1,t.width)),l=t.points=Le({},{size:e,width:se(1,.2*e),stroke:t.stroke,space:2*e,paths:Rt,_stroke:null,_fill:null},t.points);l.show=me(l.show),l.filter=me(l.filter),l.fill=me(l.fill),l.stroke=me(l.stroke),l.paths=me(l.paths),l.pxAlign=t.pxAlign}if(el){let i=function(t,n){if(0==n&&(fl||!Qe.live||2==M))return We;let i=[],o=z("tr","u-series",sl,sl.childNodes[n]);S(o,t.class),t.show||S(o,e);let s=z("th",null,o);if(ll.show){let e=D("u-marker",s);if(n>0){let l=ll.width(k,n);l&&(e.style.border=l+"px "+ll.dash(k,n)+" "+ll.stroke(k,n)),e.style.background=ll.fill(k,n)}}let r=D(l,s);for(var u in r.textContent=t.label,n>0&&(ll.show||(r.style.color=t.width>0?ll.stroke(k,n):ll.fill(k,n)),gl("click",s,(e=>{if(xt._lock)return;wt(e);let l=de.indexOf(t);if((e.ctrlKey||e.metaKey)!=Qe.isolate){let e=de.some(((e,t)=>t>0&&t!=l&&e.show));de.forEach(((t,n)=>{n>0&&Qn(n,e?n==l?dl:pl:dl,!0,Pi.setSeries)}))}else Qn(l,{show:!t.show},!0,Pi.setSeries)}),!1),vt&&gl(d,s,(e=>{xt._lock||(wt(e),Qn(de.indexOf(t),ni,!0,Pi.setSeries))}),!1)),rl){let e=z("td","u-value",o);e.textContent="--",i.push(e)}return[o,i]}(t,n);ul.splice(n,0,i[0]),al.splice(n,0,i[1]),Qe.values.push(null)}if(xt.show){Xe.splice(n,0,null);let e=null;kt?0==n&&(e=Et(t,n)):n>0&&(e=Et(t,n)),yt.splice(n,0,e),Mt.splice(n,0,0),St.splice(n,0,0)}zi("addSeries",n)}k.addSeries=function(e,l){l=null==l?de.length:l,e=1==M?It(e,l,Ol,nt):It(e,l,{},tt),de.splice(l,0,e),Tt(de[l],l)},k.delSeries=function(e){if(de.splice(e,1),el){Qe.values.splice(e,1),al.splice(e,1);let l=ul.splice(e,1)[0];Sl(null,l.firstChild),l.remove()}xt.show&&(Xe.splice(e,1),yt.splice(e,1)[0].remove(),Mt.splice(e,1),St.splice(e,1)),zi("delSeries",e)};const zt=[!1,!1,!1,!1];function Dt(e,l,t){let[n,i,o,s]=t,r=l%2,u=0;return 0==r&&(s||i)&&(u=0==l&&!n||2==l&&!o?ne(Il.size/3):0),1==r&&(n||o)&&(u=1==l&&!i||3==l&&!s?ne(Ql.size/2):0),u}const Pt=k.padding=(u.padding||[Dt,Dt,Dt,Dt]).map((e=>me(q(e,Dt)))),At=k._padding=Pt.map(((e,l)=>e(k,l,zt,0)));let Wt,Yt=null,Ft=null;const Kt=1==M?de[0].idxs:null;let Xt,Zt,Qt,en,ln,tn,nn,on,sn,rn,un=null,an=!1;function fn(e,l){if(k.data=k._data=g=null==e?[]:e,2==M){Wt=0;for(let e=1;de.length>e;e++)Wt+=g[e][0].length}else{0==g.length&&(k.data=k._data=g=[[]]),un=g[0],Wt=un.length;let e=g;if(2==Ie){e=g.slice();let l=e[0]=Array(Wt);for(let e=0;Wt>e;e++)l[e]=e}k._data=g=e}if(Sn(!0),zi("setData"),2==Ie&&(rt=!0),!1!==l){let e=Ge;e.auto(k,an)?cn():Zn(Me,e.min,e.max),at=at||xt.left>=0,ht=!0,Nn()}}function cn(){let e,l;an=!0,1==M&&(Wt>0?(Yt=Kt[0]=0,Ft=Kt[1]=Wt-1,e=g[0][Yt],l=g[0][Ft],2==Ie?(e=Yt,l=Ft):e==l&&(3==Ie?[e,l]=N(e,e,Ge.log,!1):4==Ie?[e,l]=j(e,e,Ge.log,!1):Ge.time?l=e+ne(86400/fe):[e,l]=J(e,l,U,!0))):(Yt=Kt[0]=e=null,Ft=Kt[1]=l=null)),Zn(Me,e,l)}function hn(e,l,t,n,i,o){e??=a,t??=Ae,n??="butt",i??=a,o??="round",e!=Xt&&($.strokeStyle=Xt=e),i!=Zt&&($.fillStyle=Zt=i),l!=Qt&&($.lineWidth=Qt=l),o!=ln&&($.lineJoin=ln=o),n!=tn&&($.lineCap=tn=n),t!=en&&$.setLineDash(en=t)}function dn(e,l,t,n){l!=Zt&&($.fillStyle=Zt=l),e!=nn&&($.font=nn=e),t!=on&&($.textAlign=on=t),n!=sn&&($.textBaseline=sn=n)}function pn(e,l,t,n,i=0){if(n.length>0&&e.auto(k,an)&&(null==l||null==l.min)){let l=q(Yt,0),o=q(Ft,n.length-1),s=null==t.min?3==e.distr?function(e,l,t){let n=he,i=-he;for(let o=l;t>=o;o++){let l=e[o];null!=l&&l>0&&(n>l&&(n=l),l>i&&(i=l))}return[n,i]}(n,l,o):function(e,l,t,n){let i=he,o=-he;if(1==n)i=e[l],o=e[t];else if(-1==n)i=e[t],o=e[l];else for(let n=l;t>=n;n++){let l=e[n];null!=l&&(i>l&&(i=l),l>o&&(o=l))}return[i,o]}(n,l,o,i):[t.min,t.max];e.min=oe(e.min,t.min=s[0]),e.max=se(e.max,t.max=s[1])}}k.setData=fn;const mn={min:null,max:null};function gn(e,l){let t=l?de[e].points:de[e];t._stroke=t.stroke(k,e),t._fill=t.fill(k,e)}function xn(e,l){let t=l?de[e].points:de[e],{stroke:n,fill:i,clip:o,flags:s,_stroke:r=t._stroke,_fill:u=t._fill,_width:a=t.width}=t._paths;a=Ee(a*y,3);let f=null,c=a%2/2;l&&null==u&&(u=a>0?"#fff":r);let h=1==t.pxAlign&&c>0;if(h&&$.translate(c,c),!l){let e=Vl-a/2,l=$l-a/2,t=Jl+a,n=ql+a;f=new Path2D,f.rect(e,l,t,n)}l?_n(r,a,t.dash,t.cap,u,n,i,s,o):function(e,l,t,n,i,o,s,r,u,a,f){let c=!1;0!=u&&ke.forEach(((h,d)=>{if(h.series[0]==e){let e,p=de[h.series[1]],m=g[h.series[1]],x=(p._paths||Pe).band;Ye(x)&&(x=1==h.dir?x[0]:x[1]);let w=null;p.show&&x&&function(e,l,t){for(l=q(l,0),t=q(t,e.length-1);t>=l;){if(null!=e[l])return!0;l++}return!1}(m,Yt,Ft)?(w=h.fill(k,d)||o,e=p._paths.clip):x=null,_n(l,t,n,i,w,s,r,u,a,f,e,x),c=!0}})),c||_n(l,t,n,i,o,s,r,u,a,f)}(e,r,a,t.dash,t.cap,u,n,i,s,f,o),h&&$.translate(-c,-c)}const wn=3;function _n(e,l,t,n,i,o,s,r,u,a,f,c){hn(e,l,t,n,i),(u||a||c)&&($.save(),u&&$.clip(u),a&&$.clip(a)),c?(r&wn)==wn?($.clip(c),f&&$.clip(f),vn(i,s),bn(e,o,l)):2&r?(vn(i,s),$.clip(c),bn(e,o,l)):1&r&&($.save(),$.clip(c),f&&$.clip(f),vn(i,s),$.restore(),bn(e,o,l)):(vn(i,s),bn(e,o,l)),(u||a||c)&&$.restore()}function bn(e,l,t){t>0&&(l instanceof Map?l.forEach(((e,l)=>{$.strokeStyle=Xt=l,$.stroke(e)})):null!=l&&e&&$.stroke(l))}function vn(e,l){l instanceof Map?l.forEach(((e,l)=>{$.fillStyle=Zt=l,$.fill(e)})):null!=l&&e&&$.fill(l)}function kn(e,l,t,n,i,o,s,r,u,a){let f=s%2/2;1==te&&$.translate(f,f),hn(r,s,u,a,r),$.beginPath();let c,h,d,p,m=i+(0==n||3==n?-o:o);0==t?(h=i,p=m):(c=i,d=m);for(let n=0;e.length>n;n++)null!=l[n]&&(0==t?c=d=e[n]:h=p=e[n],$.moveTo(c,h),$.lineTo(d,p));$.stroke(),1==te&&$.translate(-f,-f)}function yn(e){let l=!0;return ge.forEach(((t,n)=>{if(!t.show)return;let i=ve[t.scale];if(null==i.min)return void(t._show&&(l=!1,t._show=!1,Sn(!1)));t._show||(l=!1,t._show=!0,Sn(!1));let o=t.side,s=o%2,{min:r,max:u}=i,[a,f]=function(e,l,t,n){let i,o=ge[e];if(n>0){let s=o._space=o.space(k,e,l,t,n);i=$t(l,t,o._incrs=o.incrs(k,e,l,t,n,s),n,s)}else i=[0,0];return o._found=i}(n,r,u,0==s?Al:Yl);if(0==f)return;let c=t._splits=t.splits(k,n,r,u,a,f,2==i.distr),h=2==i.distr?c.map((e=>un[e])):c,d=2==i.distr?un[c[1]]-un[c[0]]:a,p=t._values=t.values(k,t.filter(k,h,n,f,d),n,f,d);t._rotate=2==o?t.rotate(k,p,n,f):0;let m=t._size;t._size=ie(t.size(k,p,n,e)),null!=m&&t._size!=m&&(l=!1)})),l}function Mn(e){let l=!0;return Pt.forEach(((t,n)=>{let i=t(k,n,zt,e);i!=At[n]&&(l=!1),At[n]=i})),l}function Sn(e){de.forEach(((l,t)=>{t>0&&(l._paths=null,e&&(1==M?(l.min=null,l.max=null):l.facets.forEach((e=>{e.min=null,e.max=null}))))}))}let En,Tn,zn,Dn,Pn,An,Wn,Yn,Cn,Fn,Hn,Rn,Gn=!1,In=!1,On=[];function Ln(){In=!1;for(let e=0;On.length>e;e++)zi(...On[e]);On.length=0}function Nn(){Gn||(je(jn),Gn=!0)}function jn(){if(et&&(function(){for(let e in ve){let l=ve[e];null==Be[e]&&(null==l.min||null!=Be[Me]&&l.auto(k,an))&&(Be[e]=mn)}for(let e in ve){let l=ve[e];null==Be[e]&&null!=l.from&&null!=Be[l.from]&&(Be[e]=mn)}null!=Be[Me]&&Sn(!0);let e={};for(let l in Be){let t=Be[l];if(null!=t){let n=e[l]=Oe(ve[l],Re);if(null!=t.min)Le(n,t);else if(l!=Me||2==M)if(0==Wt&&null==n.from){let e=n.range(k,null,null,l);n.min=e[0],n.max=e[1]}else n.min=he,n.max=-he}}if(Wt>0){de.forEach(((l,t)=>{if(1==M){let n=l.scale,i=Be[n];if(null==i)return;let o=e[n];if(0==t){let e=o.range(k,o.min,o.max,n);o.min=e[0],o.max=e[1],Yt=O(o.min,g[0]),Ft=O(o.max,g[0]),Ft-Yt>1&&(o.min>g[0][Yt]&&Yt++,g[0][Ft]>o.max&&Ft--),l.min=un[Yt],l.max=un[Ft]}else l.show&&l.auto&&pn(o,i,l,g[t],l.sorted);l.idxs[0]=Yt,l.idxs[1]=Ft}else if(t>0&&l.show&&l.auto){let[n,i]=l.facets,o=n.scale,s=i.scale,[r,u]=g[t],a=e[o],f=e[s];null!=a&&pn(a,Be[o],n,r,n.sorted),null!=f&&pn(f,Be[s],i,u,i.sorted),l.min=i.min,l.max=i.max}}));for(let l in e){let t=e[l],n=Be[l];if(null==t.from&&(null==n||null==n.min)){let e=t.range(k,t.min==he?null:t.min,t.max==-he?null:t.max,l);t.min=e[0],t.max=e[1]}}}for(let l in e){let t=e[l];if(null!=t.from){let n=e[t.from];if(null==n.min)t.min=t.max=null;else{let e=t.range(k,n.min,n.max,l);t.min=e[0],t.max=e[1]}}}let l={},t=!1;for(let n in e){let i=e[n],o=ve[n];if(o.min!=i.min||o.max!=i.max){o.min=i.min,o.max=i.max;let e=o.distr;o._min=3==e?ae(o.min):4==e?ce(o.min,o.asinh):100==e?o.fwd(o.min):o.min,o._max=3==e?ae(o.max):4==e?ce(o.max,o.asinh):100==e?o.fwd(o.max):o.max,l[n]=t=!0}}if(t){de.forEach(((e,t)=>{2==M?t>0&&l.y&&(e._paths=null):l[e.scale]&&(e._paths=null)}));for(let e in l)rt=!0,zi("setScale",e);xt.show&&xt.left>=0&&(at=ht=!0)}for(let e in Be)Be[e]=null}(),et=!1),rt&&(function(){let e=!1,l=0;for(;!e;){l++;let t=yn(l),n=Mn(l);e=l==gt||t&&n,e||(pt(k.width,k.height),lt=!0)}}(),rt=!1),lt){if(T(X,s,Cl),T(X,i,Fl),T(X,t,Al),T(X,n,Yl),T(Z,s,Cl),T(Z,i,Fl),T(Z,t,Al),T(Z,n,Yl),T(K,t,Dl),T(K,n,Pl),V.width=ne(Dl*y),V.height=ne(Pl*y),ge.forEach((({_el:l,_show:t,_size:n,_pos:i,side:o})=>{if(null!=l)if(t){let t=o%2==1;T(l,t?"left":"top",i-(3===o||0===o?n:0)),T(l,t?"width":"height",n),T(l,t?"top":"left",t?Fl:Cl),T(l,t?"height":"width",t?Yl:Al),E(l,e)}else S(l,e)})),Xt=Zt=Qt=ln=tn=nn=on=sn=en=null,rn=1,di(!0),Cl!=Hl||Fl!=Rl||Al!=Gl||Yl!=Bl){Sn(!1);let e=Al/Gl,l=Yl/Bl;if(xt.show&&!at&&xt.left>=0){xt.left*=e,xt.top*=l,zn&&A(zn,ne(xt.left),0,Al,Yl),Dn&&A(Dn,0,ne(xt.top),Al,Yl);for(let t=0;yt.length>t;t++){let n=yt[t];null!=n&&(Mt[t]*=e,St[t]*=l,A(n,ie(Mt[t]),ie(St[t]),Al,Yl))}}if(qn.show&&!ft&&qn.left>=0&&qn.width>0){qn.left*=e,qn.width*=e,qn.top*=l,qn.height*=l;for(let e in gi)T(Kn,e,qn[e])}Hl=Cl,Rl=Fl,Gl=Al,Bl=Yl}zi("setSize"),lt=!1}Dl>0&&Pl>0&&($.clearRect(0,0,V.width,V.height),zi("drawClear"),De.forEach((e=>e())),zi("draw")),qn.show&&ft&&(Xn(qn),ft=!1),xt.show&&at&&(ci(null,!0,!1),at=!1),Qe.show&&Qe.live&&ht&&(ai(),ht=!1),R||(R=!0,k.status=1,zi("ready")),an=!1,Gn=!1}function Un(e,l){let t=ve[e];if(null==t.from){if(0==Wt){let n=t.range(k,l.min,l.max,e);l.min=n[0],l.max=n[1]}if(l.min>l.max){let e=l.min;l.min=l.max,l.max=e}if(Wt>1&&null!=l.min&&null!=l.max&&1e-16>l.max-l.min)return;e==Me&&2==t.distr&&Wt>0&&(l.min=O(l.min,g[0]),l.max=O(l.max,g[0]),l.min==l.max&&l.max++),Be[e]=l,et=!0,Nn()}}k.batch=function(e,l=!1){Gn=!0,In=l,e(k),jn(),l&&On.length>0&&queueMicrotask(Ln)},k.redraw=(e,l)=>{rt=l||!1,!1!==e?Zn(Me,Ge.min,Ge.max):Nn()},k.setScale=Un;let Bn=!1;const Vn=xt.drag;let $n=Vn.x,Jn=Vn.y;xt.show&&(xt.x&&(En=D("u-cursor-x",Z)),xt.y&&(Tn=D("u-cursor-y",Z)),0==Ge.ori?(zn=En,Dn=Tn):(zn=Tn,Dn=En),Hn=xt.left,Rn=xt.top);const qn=k.select=Le({show:!0,over:!0,left:0,width:0,top:0,height:0},u.select),Kn=qn.show?D("u-select",qn.over?Z:X):null;function Xn(e,l){if(qn.show){for(let l in e)qn[l]=e[l],l in gi&&T(Kn,l,e[l]);!1!==l&&zi("setSelect")}}function Zn(e,l,t){Un(e,{min:l,max:t})}function Qn(l,t,n,i){null!=t.focus&&function(e){if(e!=ti){let l=null==e,t=1!=bt.alpha;de.forEach(((n,i)=>{if(1==M||i>0){let o=l||0==i||i==e;n._focus=l?null:o,t&&function(e,l){de[e].alpha=l,xt.show&&yt[e]&&(yt[e].style.opacity=l),el&&ul[e]&&(ul[e].style.opacity=l)}(i,o?1:bt.alpha)}})),ti=e,t&&Nn()}}(l),null!=t.show&&de.forEach(((n,i)=>{0>=i||l!=i&&null!=l||(n.show=t.show,function(l){let t=el?ul[l]:null;de[l].show?t&&E(t,e):(t&&S(t,e),A(kt?yt[0]:yt[l],-10,-10,Al,Yl))}(i),2==M?(Zn(n.facets[0].scale,null,null),Zn(n.facets[1].scale,null,null)):Zn(n.scale,null,null),Nn())})),!1!==n&&zi("setSeries",l,t),i&&Yi("setSeries",k,l,t)}let ei,li,ti;k.setSelect=Xn,k.setSeries=Qn,k.addBand=function(e,l){e.fill=me(e.fill||null),e.dir=q(e.dir,-1),ke.splice(l=null==l?ke.length:l,0,e)},k.setBand=function(e,l){Le(ke[e],l)},k.delBand=function(e){null==e?ke.length=0:ke.splice(e,1)};const ni={focus:!0};function ii(e,l,t){let n=ve[l];t&&(e=e/y-(1==n.ori?Fl:Cl));let i=Al;1==n.ori&&(i=Yl,e=i-e),-1==n.dir&&(e=i-e);let o=n._min,s=o+e/i*(n._max-o),r=n.distr;return 3==r?re(10,s):4==r?((e,l=1)=>Q.sinh(e)*l)(s,n.asinh):100==r?n.bwd(s):s}function oi(e,l){T(Kn,s,qn.left=e),T(Kn,t,qn.width=l)}function si(e,l){T(Kn,i,qn.top=e),T(Kn,n,qn.height=l)}el&&vt&&gl(p,tl,(e=>{xt._lock||(wt(e),null!=ti&&Qn(null,ni,!0,Pi.setSeries))})),k.valToIdx=e=>O(e,g[0]),k.posToIdx=function(e,l){return O(ii(e,Me,l),g[0],Yt,Ft)},k.posToVal=ii,k.valToPos=(e,l,t)=>0==ve[l].ori?W(e,ve[l],t?Jl:Al,t?Vl:0):C(e,ve[l],t?ql:Yl,t?$l:0),k.setCursor=(e,l,t)=>{Hn=e.left,Rn=e.top,ci(null,l,t)};let ri=0==Ge.ori?oi:si,ui=1==Ge.ori?oi:si;function ai(e,l){if(null!=e&&(e.idxs?e.idxs.forEach(((e,l)=>{Xe[l]=e})):(e=>void 0===e)(e.idx)||Xe.fill(e.idx),Qe.idx=Xe[0]),el&&Qe.live){for(let e=0;de.length>e;e++)(e>0||1==M&&!fl)&&fi(e,Xe[e]);!function(){if(el&&Qe.live)for(let e=2==M?1:0;de.length>e;e++){if(0==e&&fl)continue;let l=Qe.values[e],t=0;for(let n in l)al[e][t++].firstChild.nodeValue=l[n]}}()}ht=!1,!1!==l&&zi("setLegend")}function fi(e,l){let t,n=de[e],i=0==e&&2==Ie?un:g[e];fl?t=n.values(k,e,l)??cl:(t=n.value(k,null==l?null:i[l],e,l),t=null==t?cl:{_:t}),Qe.values[e]=t}function ci(e,l,t){let n;Cn=Hn,Fn=Rn,[Hn,Rn]=xt.move(k,Hn,Rn),xt.left=Hn,xt.top=Rn,xt.show&&(zn&&A(zn,ne(Hn),0,Al,Yl),Dn&&A(Dn,0,ne(Rn),Al,Yl)),ei=he,li=null;let i=0==Ge.ori?Al:Yl,o=1==Ge.ori?Al:Yl;if(0>Hn||0==Wt||Yt>Ft){n=xt.idx=null;for(let e=0;de.length>e;e++){let l=yt[e];null!=l&&A(l,-10,-10,Al,Yl)}vt&&Qn(null,ni,!0,null==e&&Pi.setSeries),Qe.live&&(Xe.fill(n),ht=!0)}else{let e,l,t;1==M&&(e=0==Ge.ori?Hn:Rn,l=ii(e,Me),n=xt.idx=O(l,g[0],Yt,Ft),t=Ne(g[0][n],Ge,i,0));let s=-10,r=-10,u=0,a=0,f=!0,c="",h="";for(let e=2==M?1:0;de.length>e;e++){let d=de[e],p=Xe[e],m=null==p?null:1==M?g[e][p]:g[e][1][p],x=xt.dataIdx(k,e,n,l),w=null==x?null:1==M?g[e][x]:g[e][1][x];ht=ht||w!=m||x!=p,Xe[e]=x;let _=x==n?t:Ne(1==M?g[0][x]:g[e][0][x],Ge,i,0);if(e>0&&d.show){let l=null==w?-10:Ue(w,1==M?ve[d.scale]:ve[d.facets[1].scale],o,0);if(vt&&null!=w){let t=1==Ge.ori?Hn:Rn,n=le(bt.dist(k,e,x,l,t));if(ei>n){let l=bt.bias;if(0!=l){let i=ii(t,d.scale),o=0>i?-1:1;o!=(0>w?-1:1)||(1==o?1==l?i>w:w>i:1==l?w>i:i>w)||(ei=n,li=e)}else ei=n,li=e}}if(ht||kt){let t,n;0==Ge.ori?(t=_,n=l):(t=l,n=_);let i,o,d,p,m,g,x=!0,w=_t.bbox;if(null!=w){x=!1;let l=w(k,e);d=l.left,p=l.top,i=l.width,o=l.height}else d=t,p=n,i=o=_t.size(k,e);if(g=_t.fill(k,e),m=_t.stroke(k,e),kt)e!=li||ei>bt.prox||(s=d,r=p,u=i,a=o,f=x,c=g,h=m);else{let l=yt[e];null!=l&&(Mt[e]=d,St[e]=p,F(l,i,o,x),Y(l,g,m),A(l,ie(d),ie(p),Al,Yl))}}}}if(kt){let e=bt.prox;if(ht||(null==ti?e>=ei:ei>e||li!=ti)){let e=yt[0];Mt[0]=s,St[0]=r,F(e,u,a,f),Y(e,c,h),A(e,ie(s),ie(r),Al,Yl)}}}if(qn.show&&Bn)if(null!=e){let[l,t]=Pi.scales,[n,s]=Pi.match,[r,u]=e.cursor.sync.scales,a=e.cursor.drag;if($n=a._x,Jn=a._y,$n||Jn){let a,f,c,h,d,{left:p,top:m,width:g,height:x}=e.select,w=e.scales[r].ori,_=e.posToVal,b=null!=l&&n(l,r),v=null!=t&&s(t,u);b&&$n?(0==w?(a=p,f=g):(a=m,f=x),c=ve[l],h=Ne(_(a,r),c,i,0),d=Ne(_(a+f,r),c,i,0),ri(oe(h,d),le(d-h))):ri(0,i),v&&Jn?(1==w?(a=p,f=g):(a=m,f=x),c=ve[t],h=Ue(_(a,u),c,o,0),d=Ue(_(a+f,u),c,o,0),ui(oe(h,d),le(d-h))):ui(0,o)}else xi()}else{let e=le(Cn-Pn),l=le(Fn-An);if(1==Ge.ori){let t=e;e=l,l=t}$n=Vn.x&&e>=Vn.dist,Jn=Vn.y&&l>=Vn.dist;let t,n,s=Vn.uni;null!=s?$n&&Jn&&($n=e>=s,Jn=l>=s,$n||Jn||(l>e?Jn=!0:$n=!0)):Vn.x&&Vn.y&&($n||Jn)&&($n=Jn=!0),$n&&(0==Ge.ori?(t=Wn,n=Hn):(t=Yn,n=Rn),ri(oe(t,n),le(n-t)),Jn||ui(0,o)),Jn&&(1==Ge.ori?(t=Wn,n=Hn):(t=Yn,n=Rn),ui(oe(t,n),le(n-t)),$n||ri(0,i)),$n||Jn||(ri(0,0),ui(0,0))}if(Vn._x=$n,Vn._y=Jn,null==e){if(t){if(null!=Ai){let[e,l]=Pi.scales;Pi.values[0]=null!=e?ii(0==Ge.ori?Hn:Rn,e):null,Pi.values[1]=null!=l?ii(1==Ge.ori?Hn:Rn,l):null}Yi(f,k,Hn,Rn,Al,Yl,n)}if(vt){let e=t&&Pi.setSeries,l=bt.prox;null==ti?ei>l||Qn(li,ni,!0,e):ei>l?Qn(null,ni,!0,e):li!=ti&&Qn(li,ni,!0,e)}}ht&&(Qe.idx=n,ai()),!1!==l&&zi("setCursor")}k.setLegend=ai;let hi=null;function di(e=!1){e?hi=null:(hi=Z.getBoundingClientRect(),zi("syncRect",hi))}function pi(e,l,t,n,i,o){xt._lock||Bn&&null!=e&&0==e.movementX&&0==e.movementY||(mi(e,l,t,n,i,o,0,!1,null!=e),null!=e?ci(null,!0,!0):ci(l,!0,!1))}function mi(e,l,t,n,i,o,s,r,u){if(null==hi&&di(!1),wt(e),null!=e)t=e.clientX-hi.left,n=e.clientY-hi.top;else{if(0>t||0>n)return Hn=-10,void(Rn=-10);let[e,s]=Pi.scales,r=l.cursor.sync,[u,a]=r.values,[f,c]=r.scales,[h,d]=Pi.match,p=l.axes[0].side%2==1,m=0==Ge.ori?Al:Yl,g=1==Ge.ori?Al:Yl,x=p?o:i,w=p?i:o,_=p?n:t,b=p?t:n;if(t=null!=f?h(e,f)?H(u,ve[e],m,0):-10:m*(_/x),n=null!=c?d(s,c)?H(a,ve[s],g,0):-10:g*(b/w),1==Ge.ori){let e=t;t=n,n=e}}u&&(t>1&&Al-1>t||(t=ye(t,Al)),n>1&&Yl-1>n||(n=ye(n,Yl))),r?(Pn=t,An=n,[Wn,Yn]=xt.move(k,t,n)):(Hn=t,Rn=n)}Object.defineProperty(k,"rect",{get:()=>(null==hi&&di(!1),hi)});const gi={width:0,height:0,left:0,top:0};function xi(){Xn(gi,!1)}let wi,_i,bi,vi;function ki(e,l,t,n,i,o){Bn=!0,$n=Jn=Vn._x=Vn._y=!1,mi(e,l,t,n,i,o,0,!0,!1),null!=e&&(gl(h,b,yi,!1),Yi(c,k,Wn,Yn,Al,Yl,null));let{left:s,top:r,width:u,height:a}=qn;wi=s,_i=r,bi=u,vi=a,xi()}function yi(e,l,t,n,i,o){Bn=Vn._x=Vn._y=!1,mi(e,l,t,n,i,o,0,!1,!0);let{left:s,top:r,width:u,height:a}=qn,f=u>0||a>0,c=wi!=s||_i!=r||bi!=u||vi!=a;if(f&&c&&Xn(qn),Vn.setScale&&f&&c){let e=s,l=u,t=r,n=a;if(1==Ge.ori&&(e=r,l=a,t=s,n=u),$n&&Zn(Me,ii(e,Me),ii(e+l,Me)),Jn)for(let e in ve){let l=ve[e];e!=Me&&null==l.from&&l.min!=he&&Zn(e,ii(t+n,e),ii(t,e))}xi()}else xt.lock&&(xt._lock=!xt._lock,ci(null,!0,!1));null!=e&&(Sl(h,b),Yi(h,k,Hn,Rn,Al,Yl,null))}function Mi(e){xt._lock||(wt(e),cn(),xi(),null!=e&&Yi(m,k,Hn,Rn,Al,Yl,null))}function Si(){ge.forEach(qt),dt(k.width,k.height,!0)}G(x,v,Si);const Ei={};Ei.mousedown=ki,Ei.mousemove=pi,Ei.mouseup=yi,Ei.dblclick=Mi,Ei.setSeries=(e,l,t,n)=>{-1!=(t=(0,Pi.match[2])(k,l,t))&&Qn(t,n,!0,!1)},xt.show&&(gl(c,Z,ki),gl(f,Z,pi),gl(d,Z,(e=>{wt(e),di(!1)})),gl(p,Z,(function(e){if(xt._lock)return;wt(e);let l=Bn;if(Bn){let e,l,t=!0,n=!0,i=10;0==Ge.ori?(e=$n,l=Jn):(e=Jn,l=$n),e&&l&&(t=i>=Hn||Hn>=Al-i,n=i>=Rn||Rn>=Yl-i),e&&t&&(Hn=Wn>Hn?0:Al),l&&n&&(Rn=Yn>Rn?0:Yl),ci(null,!0,!0),Bn=!1}Hn=-10,Rn=-10,ci(null,!0,!0),l&&(Bn=l)})),gl(m,Z,Mi),Ct.add(k),k.syncRect=di);const Ti=k.hooks=u.hooks||{};function zi(e,l,t){In?On.push([e,l,t]):e in Ti&&Ti[e].forEach((e=>{e.call(null,k,l,t)}))}(u.plugins||[]).forEach((e=>{for(let l in e.hooks)Ti[l]=(Ti[l]||[]).concat(e.hooks[l])}));const Di=(e,l,t)=>t,Pi=Le({key:null,setSeries:!1,filters:{pub:_e,sub:_e},scales:[Me,de[1]?de[1].scale:null],match:[be,be,Di],values:[null,null]},xt.sync);2==Pi.match.length&&Pi.match.push(Di),xt.sync=Pi;const Ai=Pi.key,Wi=ut(Ai);function Yi(e,l,t,n,i,o,s){Pi.filters.pub(e,l,t,n,i,o,s)&&Wi.pub(e,l,t,n,i,o,s)}function Ci(){zi("init",u,g),fn(g||u.data,!1),Be[Me]?Un(Me,Be[Me]):cn(),ft=qn.show&&(qn.width>0||qn.height>0),at=ht=!0,dt(u.width,u.height)}return Wi.sub(k),k.pub=function(e,l,t,n,i,o,s){Pi.filters.sub(e,l,t,n,i,o,s)&&Ei[e](null,l,t,n,i,o,s)},k.destroy=function(){Wi.unsub(k),Ct.delete(k),ml.clear(),I(x,v,Si),L.remove(),tl?.remove(),zi("destroy")},de.forEach(Tt),ge.forEach((function(e,l){if(e._show=e.show,e.show){let t=ve[e.scale];null==t&&(e.scale=e.side%2?de[1].scale:Me,t=ve[e.scale]);let n=t.time;e.size=me(e.size),e.space=me(e.space),e.rotate=me(e.rotate),Ye(e.incrs)&&e.incrs.forEach((e=>{!Te.has(e)&&Te.set(e,ze(e))})),e.incrs=me(e.incrs||(2==t.distr?il:n?1==fe?xl:bl:ol)),e.splits=me(e.splits||(n&&1==t.distr?Je:3==t.distr?jl:4==t.distr?Ul:Nl)),e.stroke=me(e.stroke),e.grid.stroke=me(e.grid.stroke),e.ticks.stroke=me(e.ticks.stroke),e.border.stroke=me(e.border.stroke);let i=e.values;e.values=Ye(i)&&!Ye(i[0])?me(i):n?Ye(i)?Ml(Ve,yl(i,$e)):Fe(i)?function(e,l){let t=Ze(l);return(l,n)=>n.map((l=>t(e(l))))}(Ve,i):i||qe:i||Ll,e.filter=me(e.filter||(3>t.distr||10!=t.log?3==t.distr&&2==t.log?Xl:xe:Kl)),e.font=Jt(e.font),e.labelFont=Jt(e.labelFont),e._size=e.size(k,null,l,0),e._space=e._rotate=e._incrs=e._found=e._splits=e._values=null,e._size>0&&(zt[l]=!0,e._el=D("u-axis",K))}})),_?_ instanceof HTMLElement?(_.appendChild(L),Ci()):_(k,Ci):Ci(),k}Kt.assign=Le,Kt.fmtNum=Z,Kt.rangeNum=J,Kt.rangeLog=N,Kt.rangeAsinh=j,Kt.orient=at,Kt.pxRatio=y,Kt.join=function(e,l){if(function(e){let l=e[0][0],t=l.length;for(let n=1;e.length>n;n++){let i=e[n][0];if(i.length!=t)return!1;if(i!=l)for(let e=0;t>e;e++)if(i[e]!=l[e])return!1}return!0}(e)){let l=e[0].slice();for(let t=1;e.length>t;t++)l.push(...e[t].slice(1));return function(e,l=100){const t=e.length;if(1>=t)return!0;let n=0,i=t-1;for(;i>=n&&null==e[n];)n++;for(;i>=n&&null==e[i];)i--;if(n>=i)return!0;const o=se(1,te((i-n+1)/l));for(let l=e[n],t=n+o;i>=t;t+=o){const n=e[t];if(null!=n){if(l>=n)return!1;l=n}}return!0}(l[0])||(l=function(e){let l=e[0],t=l.length,n=Array(t);for(let e=0;n.length>e;e++)n[e]=e;n.sort(((e,t)=>l[e]-l[t]));let i=[];for(let l=0;e.length>l;l++){let o=e[l],s=Array(t);for(let e=0;t>e;e++)s[e]=o[n[e]];i.push(s)}return i}(l)),l}let t=new Set;for(let l=0;e.length>l;l++){let n=e[l][0],i=n.length;for(let e=0;i>e;e++)t.add(n[e])}let n=[Array.from(t).sort(((e,l)=>e-l))],i=n[0].length,o=new Map;for(let e=0;i>e;e++)o.set(n[0][e],e);for(let t=0;e.length>t;t++){let s=e[t],r=s[0];for(let e=1;s.length>e;e++){let u=s[e],a=Array(i).fill(void 0),f=l?l[t][e]:1,c=[];for(let e=0;u.length>e;e++){let l=u[e],t=o.get(r[e]);null===l?0!=f&&(a[t]=l,2==f&&c.push(t)):a[t]=l}Ne(a,c,i),n.push(a)}}return n},Kt.fmtDate=Ze,Kt.tzDate=function(e,l){let t;return"UTC"==l||"Etc/UTC"==l?t=new Date(+e+6e4*e.getTimezoneOffset()):l==Qe?t=e:(t=new Date(e.toLocaleString("en-US",{timeZone:l})),t.setMilliseconds(e.getMilliseconds())),t},Kt.sync=ut;{Kt.addGap=function(e,l,t){let n=e[e.length-1];n&&n[0]==l?n[1]=t:e.push([l,t])},Kt.clipGaps=dt;let e=Kt.paths={points:Tt};e.linear=At,e.stepped=function(e){const l=q(e.align,1),t=q(e.ascDesc,!1),n=q(e.alignGaps,0),i=q(e.extend,!1);return(e,o,s,r)=>at(e,o,((u,a,f,c,h,d,p,m,g,x,w)=>{let _=u.pxRound,{left:b,width:v}=e.bbox,k=e=>_(d(e,c,x,m)),M=e=>_(p(e,h,w,g)),S=0==c.ori?_t:bt;const E={stroke:new Path2D,fill:null,clip:null,band:null,gaps:null,flags:1},T=E.stroke,z=c.dir*(0==c.ori?1:-1);s=L(f,s,r,1),r=L(f,s,r,-1);let D=M(f[1==z?s:r]),P=k(a[1==z?s:r]),A=P,W=P;i&&-1==l&&(W=b,S(T,W,D)),S(T,P,D);for(let e=1==z?s:r;e>=s&&r>=e;e+=z){let t=f[e];if(null==t)continue;let n=k(a[e]),i=M(t);1==l?S(T,n,D):S(T,A,i),S(T,n,i),D=i,A=n}let Y=A;i&&1==l&&(Y=b+v,S(T,Y,D));let[C,F]=ft(e,o);if(null!=u.fill||0!=C){let l=E.fill=new Path2D(T),t=M(u.fillTo(e,o,u.min,u.max,C));S(l,Y,t),S(l,W,t)}if(!u.spanGaps){let i=[];i.push(...pt(a,f,s,r,z,k,n));let h=u.width*y/2,d=t||1==l?h:-h,p=t||-1==l?-h:h;i.forEach((e=>{e[0]+=d,e[1]+=p})),E.gaps=i=u.gaps(e,o,s,r,i),E.clip=dt(i,c.ori,m,g,x,w)}return 0!=F&&(E.band=2==F?[ht(e,o,s,r,T,-1),ht(e,o,s,r,T,1)]:ht(e,o,s,r,T,F)),E}))},e.bars=function(e){const l=q((e=e||Pe).size,[.6,he,1]),t=e.align||0,n=e.gap||0;let i=e.radius;i=null==i?[0,0]:"number"==typeof i?[i,0]:i;const o=me(i),s=1-l[0],r=q(l[1],he),u=q(l[2],1),a=q(e.disp,Pe),f=q(e.each,(()=>{})),{fill:c,stroke:h}=a;return(e,l,i,d)=>at(e,l,((p,m,g,x,w,_,b,v,k,M,S)=>{let E,T,z=p.pxRound,D=t,P=n*y,A=r*y,W=u*y;0==x.ori?[E,T]=o(e,l):[T,E]=o(e,l);const Y=x.dir*(0==x.ori?1:-1);let C,F,H,R=0==x.ori?vt:kt,G=0==x.ori?f:(e,l,t,n,i,o,s)=>{f(e,l,t,i,n,s,o)},I=q(e.bands,Ae).find((e=>e.series[0]==l)),O=p.fillTo(e,l,p.min,p.max,null!=I?I.dir:0),L=z(b(O,w,S,k)),N=M,j=z(p.width*y),U=!1,B=null,V=null,$=null,J=null;null==c||0!=j&&null==h||(U=!0,B=c.values(e,l,i,d),V=new Map,new Set(B).forEach((e=>{null!=e&&V.set(e,new Path2D)})),j>0&&($=h.values(e,l,i,d),J=new Map,new Set($).forEach((e=>{null!=e&&J.set(e,new Path2D)}))));let{x0:K,size:X}=a;if(null!=K&&null!=X){D=1,m=K.values(e,l,i,d),2==K.unit&&(m=m.map((l=>e.posToVal(v+l*M,x.key,!0))));let t=X.values(e,l,i,d);F=2==X.unit?t[0]*M:_(t[0],x,M,v)-_(0,x,M,v),N=Wt(m,g,_,x,M,v,N),H=N-F+P}else N=Wt(m,g,_,x,M,v,N),H=N*s+P,F=N-H;1>H&&(H=0),F/2>j||(j=0),5>H&&(z=ge);let Z=H>0;F=z(pe(N-H-(Z?j:0),W,A)),C=(0==D?F/2:D==Y?0:F)-D*Y*((0==D?P/2:0)+(Z?j/2:0));const Q={stroke:null,fill:null,clip:null,band:null,gaps:null,flags:0},ee=U?null:new Path2D;let le=null;if(null!=I)le=e.data[I.series[1]];else{let{y0:t,y1:n}=a;null!=t&&null!=n&&(g=n.values(e,l,i,d),le=t.values(e,l,i,d))}let ne=E*F,ie=T*F;for(let t=1==Y?i:d;t>=i&&d>=t;t+=Y){let n=g[t];if(null==n)continue;if(null!=le){let e=le[t]??0;if(n-e==0)continue;L=b(e,w,S,k)}let i=_(2!=x.distr||null!=a?m[t]:t,x,M,v),o=b(q(n,O),w,S,k),s=z(i-C),r=z(se(o,L)),u=z(oe(o,L)),f=r-u;if(null!=n){let i=0>n?ie:ne,o=0>n?ne:ie;U?(j>0&&null!=$[t]&&R(J.get($[t]),s,u+te(j/2),F,se(0,f-j),i,o),null!=B[t]&&R(V.get(B[t]),s,u+te(j/2),F,se(0,f-j),i,o)):R(ee,s,u+te(j/2),F,se(0,f-j),i,o),G(e,l,t,s-j/2,u,F+j,f)}}return j>0?Q.stroke=U?J:ee:U||(Q._fill=0==p.width?p._fill:p._stroke??p._fill,Q.width=0),Q.fill=U?V:ee,Q}))},e.spline=function(e){return function(e,l){const t=q(l?.alignGaps,0);return(l,n,i,o)=>at(l,n,((s,r,u,a,f,c,h,d,p,m,g)=>{let x,w,_,b=s.pxRound,v=e=>b(c(e,a,m,d)),k=e=>b(h(e,f,g,p));0==a.ori?(x=xt,_=_t,w=St):(x=wt,_=bt,w=Et);const y=a.dir*(0==a.ori?1:-1);i=L(u,i,o,1),o=L(u,i,o,-1);let M=v(r[1==y?i:o]),S=M,E=[],T=[];for(let e=1==y?i:o;e>=i&&o>=e;e+=y)if(null!=u[e]){let l=v(r[e]);E.push(S=l),T.push(k(u[e]))}const z={stroke:e(E,T,x,_,w,b),fill:null,clip:null,band:null,gaps:null,flags:1},D=z.stroke;let[P,A]=ft(l,n);if(null!=s.fill||0!=P){let e=z.fill=new Path2D(D),t=k(s.fillTo(l,n,s.min,s.max,P));_(e,S,t),_(e,M,t)}if(!s.spanGaps){let e=[];e.push(...pt(r,u,i,o,y,v,t)),z.gaps=e=s.gaps(l,n,i,o,e),z.clip=dt(e,a.ori,d,p,m,g)}return 0!=A&&(z.band=2==A?[ht(l,n,i,o,D,-1),ht(l,n,i,o,D,1)]:ht(l,n,i,o,D,A)),z}))}(Yt,e)}}return Kt}(); diff --git a/Client/WebUI/static/uPlot.min.css b/Client/WebUI/static/uPlot.min.css new file mode 100644 index 0000000..a030d63 --- /dev/null +++ b/Client/WebUI/static/uPlot.min.css @@ -0,0 +1 @@ +.uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: min-content;}.u-title {text-align: center;font-size: 18px;font-weight: bold;}.u-wrap {position: relative;user-select: none;}.u-over, .u-under {position: absolute;}.u-under {overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.u-axis {position: absolute;}.u-legend {font-size: 14px;margin: auto;text-align: center;}.u-inline {display: block;}.u-inline * {display: inline-block;}.u-inline tr {margin-right: 16px;}.u-legend th {font-weight: 600;}.u-legend th > * {vertical-align: middle;display: inline-block;}.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}.u-inline.u-live th::after {content: ":";vertical-align: middle;}.u-inline:not(.u-live) .u-value {display: none;}.u-series > * {padding: 4px;}.u-series th {cursor: pointer;}.u-legend .u-off > * {opacity: 0.3;}.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;}.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;} \ No newline at end of file