-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathplotMath.js
More file actions
114 lines (108 loc) · 5.07 KB
/
Copy pathplotMath.js
File metadata and controls
114 lines (108 loc) · 5.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/** Return chronological extrema for each horizontal pixel bucket. */
function bucketExtrema(values, pixels) {
const size = Math.max(1, Math.ceil(values.length / Math.max(1, Math.floor(pixels))));
const result = [];
for (let start = 0; start < values.length; start += size) {
const end = Math.min(values.length, start + size);
let minIndex = -1, maxIndex = -1;
for (let i = start; i < end; i++) {
if (!Number.isFinite(values[i])) continue;
if (minIndex === -1 || values[i] < values[minIndex]) minIndex = i;
if (maxIndex === -1 || values[i] > values[maxIndex]) maxIndex = i;
}
if (minIndex === -1) continue;
if (minIndex <= maxIndex) {
result.push({ index: minIndex, value: values[minIndex] });
if (maxIndex !== minIndex) result.push({ index: maxIndex, value: values[maxIndex] });
} else {
result.push({ index: maxIndex, value: values[maxIndex] });
result.push({ index: minIndex, value: values[minIndex] });
}
}
if (values.length && Number.isFinite(values[0]) && result[0].index !== 0)
result.unshift({ index: 0, value: values[0] });
if (values.length && Number.isFinite(values[values.length - 1]) && result[result.length - 1].index !== values.length - 1)
result.push({ index: values.length - 1, value: values[values.length - 1] });
return result;
}
/** Keep chronological extrema for samples sharing the same horizontal pixel. */
function bucketAxisExtrema(values, xPositions) {
const result = [];
let pixel = -1, minIndex = -1, maxIndex = -1;
const flush = () => {
if (minIndex < 0) return;
const first = Math.min(minIndex, maxIndex);
const last = Math.max(minIndex, maxIndex);
result.push({ index: first, value: values[first] });
if (last !== first) result.push({ index: last, value: values[last] });
};
for (let i = 0; i < values.length; i++) {
if (!Number.isFinite(values[i]) || !Number.isFinite(xPositions[i])) continue;
const nextPixel = Math.floor(xPositions[i]);
if (nextPixel !== pixel) {
flush();
pixel = nextPixel;
minIndex = maxIndex = i;
} else {
if (values[i] < values[minIndex]) minIndex = i;
if (values[i] > values[maxIndex]) maxIndex = i;
}
}
flush();
const firstValid = values.findIndex(Number.isFinite);
if (firstValid >= 0 && result[0]?.index !== firstValid)
result.unshift({ index: firstValid, value: values[firstValid] });
let lastValid = values.length - 1;
while (lastValid >= 0 && !Number.isFinite(values[lastValid])) lastValid--;
if (lastValid >= 0 && result.at(-1)?.index !== lastValid)
result.push({ index: lastValid, value: values[lastValid] });
return result;
}
function csvField(value) {
const text = String(value);
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
}
/** Map a data value to its relative position on a linear or positive log axis. */
function axisFraction(value, min, max, scale = 'linear') {
if (!(max > min)) return NaN;
if (scale === 'log') {
if (!(value > 0) || !(min > 0)) return NaN;
return Math.log(value / min) / Math.log(max / min);
}
return (value - min) / (max - min);
}
function axisValueAtFraction(fraction, min, max, scale = 'linear') {
if (!(max > min)) return NaN;
if (scale === 'log') {
if (!(min > 0)) return NaN;
return min * Math.pow(max / min, fraction);
}
return min + (max - min) * fraction;
}
/** Convert a dragged plot rectangle into an inclusive sample window and Y range. */
function zoomRectToBounds({ x0, y0, x1, y1, plotWidth, plotHeight,
startIndex, visibleCount, min, max, xScale = 'linear', yScale = 'linear' }) {
if (plotWidth <= 0 || plotHeight <= 0 || visibleCount < 2 || max <= min) return null;
const left = Math.max(0, Math.min(plotWidth, Math.min(x0, x1)));
const right = Math.max(0, Math.min(plotWidth, Math.max(x0, x1)));
const top = Math.max(0, Math.min(plotHeight, Math.min(y0, y1)));
const bottom = Math.max(0, Math.min(plotHeight, Math.max(y0, y1)));
if (right - left < 6 || bottom - top < 6) return null;
const endIndex = startIndex + visibleCount - 1;
const first = Math.ceil(axisValueAtFraction(left / plotWidth, startIndex, endIndex, xScale));
const last = Math.floor(axisValueAtFraction(right / plotWidth, startIndex, endIndex, xScale));
if (last <= first) return null;
return {
scrollOffset: first,
displayCount: last - first + 1,
yMin: axisValueAtFraction(1 - bottom / plotHeight, min, max, yScale),
yMax: axisValueAtFraction(1 - top / plotHeight, min, max, yScale)
};
}
if (typeof module !== 'undefined') module.exports = {
axisFraction, axisValueAtFraction, bucketAxisExtrema, bucketExtrema, csvField, zoomRectToBounds
};
globalThis.SerialPlotter ??= {};
Object.assign(globalThis.SerialPlotter, {
axisFraction, axisValueAtFraction, bucketAxisExtrema, bucketExtrema, csvField, zoomRectToBounds
});