52 lines
2.3 KiB
JavaScript
52 lines
2.3 KiB
JavaScript
'use strict';
|
|
// Min/max (peak-envelope) decimation — O(n). Runs off-main-thread to avoid
|
|
// blocking the render loop.
|
|
//
|
|
// The range is split into threshold/2 equal buckets and each contributes its
|
|
// smallest and largest sample, in the order the two occurred — the way an
|
|
// oscilloscope draws a trace it cannot show pixel-for-pixel.
|
|
//
|
|
// This replaced LTTB, which picks the sample forming the largest triangle with
|
|
// its neighbours: a plausible-looking shape, but it silently drops a one-sample
|
|
// spike whenever a smoother neighbour scores higher — exactly the sample worth
|
|
// looking at. The envelope cannot drop it, because a spike is by definition its
|
|
// bucket's min or max. Every output point is a real sample at its real
|
|
// timestamp; nothing is interpolated or averaged.
|
|
//
|
|
// Kept identical to minMaxDecimate() in Common/Client/go/wshub/hub.go and to
|
|
// decimate() in app.js, so a trace looks the same whichever thinned it.
|
|
function decimate(t, v, threshold) {
|
|
const len = t.length;
|
|
if (len <= threshold || threshold < 4) {
|
|
// Copy to new arrays so we can transfer them back without detaching the input.
|
|
return { t: new Float64Array(t), v: new Float64Array(v) };
|
|
}
|
|
const buckets = threshold >> 1;
|
|
const outT = new Float64Array(threshold);
|
|
const outV = new Float64Array(threshold);
|
|
let n = 0;
|
|
for (let b = 0; b < buckets; b++) {
|
|
const lo = Math.floor(b * len / buckets);
|
|
const hi = (b === buckets - 1) ? len : Math.floor((b + 1) * len / buckets);
|
|
if (lo >= hi) continue;
|
|
let iMin = lo, iMax = lo;
|
|
for (let j = lo + 1; j < hi; j++) {
|
|
if (v[j] < v[iMin]) iMin = j;
|
|
if (v[j] > v[iMax]) iMax = j;
|
|
}
|
|
// Emit in time order so the result plots as one ascending trace.
|
|
if (iMin > iMax) { const s = iMin; iMin = iMax; iMax = s; }
|
|
outT[n] = t[iMin]; outV[n] = v[iMin]; n++;
|
|
// A bucket whose samples are all equal has one extreme, not two.
|
|
if (iMax !== iMin) { outT[n] = t[iMax]; outV[n] = v[iMax]; n++; }
|
|
}
|
|
// slice() so the transferred buffers are exactly the used length.
|
|
return { t: outT.slice(0, n), v: outV.slice(0, n) };
|
|
}
|
|
|
|
self.onmessage = function({ data: { id, t, v, threshold } }) {
|
|
const result = decimate(t, v, threshold);
|
|
// Transfer the output buffers back to the main thread zero-copy.
|
|
self.postMessage({ id, t: result.t, v: result.v }, [result.t.buffer, result.v.buffer]);
|
|
};
|