waveform-analysis 0.1.48 → 0.1.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -5
- package/dist/assets/workerRuntime-Dt94jbqq.js +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +2683 -1153
- package/dist/style.css +1 -1
- package/dist/types/components/WaveformChart.vue.d.ts +14 -0
- package/dist/types/components/annotation/WaveformAnnotationEditor.vue.d.ts +0 -1
- package/dist/types/components/annotation/markup.d.ts +5 -6
- package/dist/types/components/annotation/types.d.ts +2 -0
- package/dist/types/components/core/latestTaskScheduler.d.ts +19 -0
- package/dist/types/components/core/trackLayoutBuilder.d.ts +3 -1
- package/dist/types/components/core/types.d.ts +2 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +1 -0
- package/dist/types/components/core/useWaveformData.d.ts +2 -0
- package/dist/types/components/core/useWaveformLayout.d.ts +2 -0
- package/dist/types/components/core/useWaveformRenderSampling.d.ts +19 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +8 -1
- package/dist/types/components/core/workerSamplingClient.d.ts +8 -0
- package/dist/types/components/data/types.d.ts +1 -1
- package/dist/types/components/interaction/zoomConstraints.d.ts +2 -0
- package/dist/types/components/waveform.d.ts +1 -1
- package/dist/types/core/compactSamplePointSource.d.ts +3 -0
- package/dist/types/core/data.d.ts +7 -1
- package/dist/types/core/index.d.ts +2 -1
- package/dist/types/core/rendering.d.ts +3 -1
- package/dist/types/core/renderingOptions.d.ts +11 -1
- package/dist/types/core/samplingReference.d.ts +36 -0
- package/dist/types/core/samplingReferenceValues.d.ts +4 -0
- package/dist/types/core/samplingReferenceVectors.d.ts +17 -0
- package/dist/types/core/wasmSampling.d.ts +48 -0
- package/dist/types/core/waveformPointSource.d.ts +53 -0
- package/dist/types/core/workerSampling/index.d.ts +5 -0
- package/dist/types/core/workerSampling/javascriptBackend.d.ts +3 -0
- package/dist/types/core/workerSampling/multiresolution.d.ts +29 -0
- package/dist/types/core/workerSampling/protocol.d.ts +153 -0
- package/dist/types/core/workerSampling/repository.d.ts +36 -0
- package/dist/types/core/workerSampling/repositoryDataset.d.ts +29 -0
- package/dist/types/core/workerSampling/repositorySupport.d.ts +27 -0
- package/dist/types/core/workerSampling/samplingCache.d.ts +24 -0
- package/dist/types/core/workerSampling/workerRuntime.d.ts +1 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/types/chart.d.ts +56 -0
- package/dist/types/types/data.d.ts +20 -2
- package/dist/types/types/index.d.ts +2 -2
- package/dist/types/views/WasmSamplingDemo.vue.d.ts +3 -0
- package/package.json +5 -4
- package/src/App.test.ts +11 -7
- package/src/App.vue +2 -2
- package/src/DemoRouterApp.vue +1 -0
- package/src/components/WaveformChart.css +2 -0
- package/src/components/annotation/WaveformAnnotationEditor.css +2 -2
- package/src/components/annotation/WaveformAnnotationEditor.vue +8 -4
- package/src/components/annotation/markup.ts +45 -26
- package/src/components/annotation/types.ts +2 -0
- package/src/components/annotation/useWaveformChartAnnotations.ts +12 -15
- package/src/components/core/latestTaskScheduler.test.ts +51 -0
- package/src/components/core/latestTaskScheduler.ts +75 -0
- package/src/components/core/trackLayoutBuilder.ts +3 -0
- package/src/components/core/types.ts +2 -0
- package/src/components/core/useWaveformChartController.ts +14 -0
- package/src/components/core/useWaveformData.test.ts +95 -0
- package/src/components/core/useWaveformData.ts +23 -32
- package/src/components/core/useWaveformLayout.ts +4 -4
- package/src/components/core/useWaveformRenderSampling.test.ts +17 -0
- package/src/components/core/useWaveformRenderSampling.ts +394 -0
- package/src/components/core/waveformChartTypes.ts +13 -0
- package/src/components/core/workerSamplingClient.ts +148 -0
- package/src/components/data/types.ts +9 -0
- package/src/components/interaction/WaveformTooltip.test.ts +1 -3
- package/src/components/interaction/WaveformTooltip.vue +13 -12
- package/src/components/interaction/useWaveformHover.ts +7 -7
- package/src/components/interaction/zoomConstraints.ts +5 -1
- package/src/components/waveform.ts +9 -0
- package/src/components/waveformChartCases/compactAndSharedHover.test.ts +15 -4
- package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +11 -6
- package/src/components/waveformChartCases/dataLifecycle.test.ts +1 -3
- package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +117 -0
- package/src/components/waveformChartCases/xDomainStrategy.test.ts +14 -2
- package/src/core/compactSamplePointSource.ts +76 -0
- package/src/core/data.test.ts +114 -0
- package/src/core/data.ts +177 -75
- package/src/core/index.ts +11 -0
- package/src/core/rendering.test.ts +87 -12
- package/src/core/rendering.ts +35 -1
- package/src/core/renderingOptions.ts +124 -15
- package/src/core/samplingReference.test.ts +124 -0
- package/src/core/samplingReference.ts +243 -0
- package/src/core/samplingReferenceValues.ts +169 -0
- package/src/core/samplingReferenceVectors.ts +46 -0
- package/src/core/wasmSampling.test.ts +205 -0
- package/src/core/wasmSampling.ts +230 -0
- package/src/core/waveformPointSource.ts +395 -0
- package/src/core/workerSampling/index.ts +30 -0
- package/src/core/workerSampling/javascriptBackend.ts +15 -0
- package/src/core/workerSampling/multiresolution.test.ts +69 -0
- package/src/core/workerSampling/multiresolution.ts +300 -0
- package/src/core/workerSampling/protocol.test.ts +34 -0
- package/src/core/workerSampling/protocol.ts +207 -0
- package/src/core/workerSampling/repository.test.ts +400 -0
- package/src/core/workerSampling/repository.ts +400 -0
- package/src/core/workerSampling/repositoryCache.test.ts +163 -0
- package/src/core/workerSampling/repositoryDataset.ts +194 -0
- package/src/core/workerSampling/repositorySupport.ts +225 -0
- package/src/core/workerSampling/repositoryTypedDataset.test.ts +89 -0
- package/src/core/workerSampling/repositoryWasmDataset.test.ts +70 -0
- package/src/core/workerSampling/samplingCache.ts +94 -0
- package/src/core/workerSampling/workerRuntime.ts +40 -0
- package/src/data/simulatedWaveforms.test.ts +19 -0
- package/src/data/simulatedWaveforms.ts +11 -8
- package/src/demoRoutes.css +177 -0
- package/src/index.ts +18 -0
- package/src/router.ts +5 -0
- package/src/types/chart.ts +63 -0
- package/src/types/data.ts +24 -1
- package/src/types/index.ts +9 -0
- package/src/views/WasmSamplingDemo.test.ts +13 -0
- package/src/views/WasmSamplingDemo.vue +180 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import type { SingleWaveformData, WaveformPoint, WaveformTypedValues } from '../types'
|
|
2
|
+
import { createCompactSamplePointSource } from './compactSamplePointSource'
|
|
3
|
+
|
|
4
|
+
export interface WaveformPointRange {
|
|
5
|
+
start: number
|
|
6
|
+
end: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WaveformPointMetrics {
|
|
10
|
+
xMinimum: number
|
|
11
|
+
xMaximum: number
|
|
12
|
+
yMinimum: number
|
|
13
|
+
yMaximum: number
|
|
14
|
+
hasErrorPoints: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WorkerSamplingNumericDataset {
|
|
18
|
+
kind: 'typed'
|
|
19
|
+
x: Float64Array
|
|
20
|
+
y: WaveformTypedValues
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface WorkerSamplingSampleDataset {
|
|
24
|
+
kind: 'samples'
|
|
25
|
+
values: WaveformTypedValues
|
|
26
|
+
sampleRate: number
|
|
27
|
+
startTime: number
|
|
28
|
+
/** Present only when invalid input samples created gaps in the original time sequence. */
|
|
29
|
+
sourceIndexes?: Uint32Array
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WorkerSamplingPointDataset {
|
|
33
|
+
kind: 'points'
|
|
34
|
+
points: readonly WaveformPoint[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type WorkerSamplingDataset =
|
|
38
|
+
WorkerSamplingNumericDataset | WorkerSamplingSampleDataset | WorkerSamplingPointDataset
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Internal source abstraction. Compact inputs stay as numeric columns until a caller needs a
|
|
42
|
+
* specific point object for SVG, hover, or annotation work.
|
|
43
|
+
*/
|
|
44
|
+
export interface WaveformPointSource {
|
|
45
|
+
readonly length: number
|
|
46
|
+
readonly isCompact: boolean
|
|
47
|
+
pointAt(index: number): WaveformPoint | undefined
|
|
48
|
+
pointsInRange(start?: number, end?: number): WaveformPoint[]
|
|
49
|
+
visibleRange(domain: [number, number]): WaveformPointRange
|
|
50
|
+
nearestPoint(x: number): WaveformPoint | undefined
|
|
51
|
+
metrics(includeErrors?: boolean): WaveformPointMetrics
|
|
52
|
+
toWorkerDataset(): WorkerSamplingDataset
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeError(value: number | undefined): number | undefined {
|
|
56
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function clampRange(start: number, end: number, length: number): WaveformPointRange {
|
|
60
|
+
const boundedStart = Math.max(0, Math.min(length, Math.floor(start)))
|
|
61
|
+
return { start: boundedStart, end: Math.max(boundedStart, Math.min(length, Math.floor(end))) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function lowerBound(length: number, valueAt: (index: number) => number, value: number) {
|
|
65
|
+
let start = 0
|
|
66
|
+
let end = length
|
|
67
|
+
while (start < end) {
|
|
68
|
+
const middle = start + Math.floor((end - start) / 2)
|
|
69
|
+
if (valueAt(middle) < value) start = middle + 1
|
|
70
|
+
else end = middle
|
|
71
|
+
}
|
|
72
|
+
return start
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function upperBound(length: number, valueAt: (index: number) => number, value: number) {
|
|
76
|
+
let start = 0
|
|
77
|
+
let end = length
|
|
78
|
+
while (start < end) {
|
|
79
|
+
const middle = start + Math.floor((end - start) / 2)
|
|
80
|
+
if (valueAt(middle) <= value) start = middle + 1
|
|
81
|
+
else end = middle
|
|
82
|
+
}
|
|
83
|
+
return start
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function pointMetrics(
|
|
87
|
+
length: number,
|
|
88
|
+
pointAt: (index: number) => WaveformPoint | undefined,
|
|
89
|
+
includeErrors: boolean,
|
|
90
|
+
): WaveformPointMetrics {
|
|
91
|
+
let xMinimum = Number.POSITIVE_INFINITY
|
|
92
|
+
let xMaximum = Number.NEGATIVE_INFINITY
|
|
93
|
+
let yMinimum = Number.POSITIVE_INFINITY
|
|
94
|
+
let yMaximum = Number.NEGATIVE_INFINITY
|
|
95
|
+
let hasErrorPoints = false
|
|
96
|
+
for (let index = 0; index < length; index += 1) {
|
|
97
|
+
const point = pointAt(index)
|
|
98
|
+
if (!point) continue
|
|
99
|
+
xMinimum = Math.min(xMinimum, point.x)
|
|
100
|
+
xMaximum = Math.max(xMaximum, point.x)
|
|
101
|
+
yMinimum = Math.min(yMinimum, point.y)
|
|
102
|
+
yMaximum = Math.max(yMaximum, point.y)
|
|
103
|
+
if (!includeErrors) continue
|
|
104
|
+
const lower = normalizeError(point.lowerError) ?? normalizeError(point.error) ?? 0
|
|
105
|
+
const upper = normalizeError(point.upperError) ?? normalizeError(point.error) ?? 0
|
|
106
|
+
hasErrorPoints ||= lower !== 0 || upper !== 0
|
|
107
|
+
yMinimum = Math.min(yMinimum, point.y - lower)
|
|
108
|
+
yMaximum = Math.max(yMaximum, point.y + upper)
|
|
109
|
+
}
|
|
110
|
+
return { xMinimum, xMaximum, yMinimum, yMaximum, hasErrorPoints }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
class ObjectWaveformPointSource implements WaveformPointSource {
|
|
114
|
+
readonly isCompact = false
|
|
115
|
+
|
|
116
|
+
constructor(private readonly sourcePoints: readonly WaveformPoint[]) {}
|
|
117
|
+
|
|
118
|
+
get length() {
|
|
119
|
+
return this.sourcePoints.length
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
pointAt(index: number) {
|
|
123
|
+
return this.sourcePoints[index]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
pointsInRange(start = 0, end = this.length) {
|
|
127
|
+
const range = clampRange(start, end, this.length)
|
|
128
|
+
return this.sourcePoints.slice(range.start, range.end)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
visibleRange(domain: [number, number]) {
|
|
132
|
+
const start = Math.min(domain[0], domain[1])
|
|
133
|
+
const end = Math.max(domain[0], domain[1])
|
|
134
|
+
return {
|
|
135
|
+
start: lowerBound(this.length, (index) => this.sourcePoints[index]!.x, start),
|
|
136
|
+
end: upperBound(this.length, (index) => this.sourcePoints[index]!.x, end),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
nearestPoint(x: number) {
|
|
141
|
+
if (!Number.isFinite(x) || !this.length) return undefined
|
|
142
|
+
const first = this.sourcePoints[0]!
|
|
143
|
+
const last = this.sourcePoints[this.length - 1]!
|
|
144
|
+
if (x <= first.x) return first
|
|
145
|
+
if (x >= last.x) return last
|
|
146
|
+
const right = lowerBound(this.length, (index) => this.sourcePoints[index]!.x, x)
|
|
147
|
+
const candidate = this.sourcePoints[Math.min(right, this.length - 1)]!
|
|
148
|
+
const previous = this.sourcePoints[Math.max(0, right - 1)]!
|
|
149
|
+
return Math.abs(x - previous.x) < Math.abs(candidate.x - x) ? previous : candidate
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
metrics(includeErrors = false) {
|
|
153
|
+
return pointMetrics(this.length, (index) => this.sourcePoints[index], includeErrors)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
toWorkerDataset(): WorkerSamplingDataset {
|
|
157
|
+
return { kind: 'points', points: this.sourcePoints }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
class CompactWaveformPointSource implements WaveformPointSource {
|
|
162
|
+
readonly isCompact = true
|
|
163
|
+
|
|
164
|
+
constructor(
|
|
165
|
+
private readonly x: Float64Array,
|
|
166
|
+
private readonly y: WaveformTypedValues,
|
|
167
|
+
private readonly error?: WaveformTypedValues,
|
|
168
|
+
private readonly lowerError?: WaveformTypedValues,
|
|
169
|
+
private readonly upperError?: WaveformTypedValues,
|
|
170
|
+
) {}
|
|
171
|
+
|
|
172
|
+
get length() {
|
|
173
|
+
return this.x.length
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
pointAt(index: number) {
|
|
177
|
+
if (index < 0 || index >= this.length) return undefined
|
|
178
|
+
const error = normalizeError(this.error?.[index])
|
|
179
|
+
const lowerError = normalizeError(this.lowerError?.[index])
|
|
180
|
+
const upperError = normalizeError(this.upperError?.[index])
|
|
181
|
+
return {
|
|
182
|
+
x: this.x[index]!,
|
|
183
|
+
y: this.y[index]!,
|
|
184
|
+
...(error === undefined ? {} : { error }),
|
|
185
|
+
...(lowerError === undefined ? {} : { lowerError }),
|
|
186
|
+
...(upperError === undefined ? {} : { upperError }),
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
pointsInRange(start = 0, end = this.length) {
|
|
191
|
+
const range = clampRange(start, end, this.length)
|
|
192
|
+
const points: WaveformPoint[] = []
|
|
193
|
+
for (let index = range.start; index < range.end; index += 1) points.push(this.pointAt(index)!)
|
|
194
|
+
return points
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
visibleRange(domain: [number, number]) {
|
|
198
|
+
const start = Math.min(domain[0], domain[1])
|
|
199
|
+
const end = Math.max(domain[0], domain[1])
|
|
200
|
+
return {
|
|
201
|
+
start: lowerBound(this.length, (index) => this.x[index]!, start),
|
|
202
|
+
end: upperBound(this.length, (index) => this.x[index]!, end),
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
nearestPoint(x: number) {
|
|
207
|
+
if (!Number.isFinite(x) || !this.length) return undefined
|
|
208
|
+
if (x <= this.x[0]!) return this.pointAt(0)
|
|
209
|
+
if (x >= this.x[this.length - 1]!) return this.pointAt(this.length - 1)
|
|
210
|
+
const right = lowerBound(this.length, (index) => this.x[index]!, x)
|
|
211
|
+
const candidate = Math.min(right, this.length - 1)
|
|
212
|
+
const previous = Math.max(0, right - 1)
|
|
213
|
+
return Math.abs(x - this.x[previous]!) < Math.abs(this.x[candidate]! - x)
|
|
214
|
+
? this.pointAt(previous)
|
|
215
|
+
: this.pointAt(candidate)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
metrics(includeErrors = false) {
|
|
219
|
+
let xMinimum = Number.POSITIVE_INFINITY
|
|
220
|
+
let xMaximum = Number.NEGATIVE_INFINITY
|
|
221
|
+
let yMinimum = Number.POSITIVE_INFINITY
|
|
222
|
+
let yMaximum = Number.NEGATIVE_INFINITY
|
|
223
|
+
let hasErrorPoints = false
|
|
224
|
+
for (let index = 0; index < this.length; index += 1) {
|
|
225
|
+
const x = this.x[index]!
|
|
226
|
+
const y = this.y[index]!
|
|
227
|
+
xMinimum = Math.min(xMinimum, x)
|
|
228
|
+
xMaximum = Math.max(xMaximum, x)
|
|
229
|
+
yMinimum = Math.min(yMinimum, y)
|
|
230
|
+
yMaximum = Math.max(yMaximum, y)
|
|
231
|
+
if (!includeErrors) continue
|
|
232
|
+
const lower =
|
|
233
|
+
normalizeError(this.lowerError?.[index]) ?? normalizeError(this.error?.[index]) ?? 0
|
|
234
|
+
const upper =
|
|
235
|
+
normalizeError(this.upperError?.[index]) ?? normalizeError(this.error?.[index]) ?? 0
|
|
236
|
+
hasErrorPoints ||= lower !== 0 || upper !== 0
|
|
237
|
+
yMinimum = Math.min(yMinimum, y - lower)
|
|
238
|
+
yMaximum = Math.max(yMaximum, y + upper)
|
|
239
|
+
}
|
|
240
|
+
return { xMinimum, xMaximum, yMinimum, yMaximum, hasErrorPoints }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
toWorkerDataset(): WorkerSamplingDataset {
|
|
244
|
+
// Send private copies, never the consumer's ArrayBuffers. The Worker receives typed messages.
|
|
245
|
+
return { kind: 'typed', x: this.x.slice(), y: this.y.slice() }
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isTypedValues(value: unknown): value is WaveformTypedValues {
|
|
250
|
+
return value instanceof Float32Array || value instanceof Float64Array
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function typedPointsAreValid(data: Extract<SingleWaveformData, { kind: 'typed-points' }>) {
|
|
254
|
+
const values = [data.y, data.error, data.lowerError, data.upperError]
|
|
255
|
+
return (
|
|
256
|
+
data.x instanceof Float64Array &&
|
|
257
|
+
values.every(
|
|
258
|
+
(value) => value === undefined || (isTypedValues(value) && value.length === data.x.length),
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function compactSamples(data: Extract<SingleWaveformData, { kind: 'typed-samples' }>) {
|
|
264
|
+
if (
|
|
265
|
+
!isTypedValues(data.values) ||
|
|
266
|
+
!Number.isFinite(data.sampleRate) ||
|
|
267
|
+
data.sampleRate <= 0 ||
|
|
268
|
+
(data.startTime !== undefined && !Number.isFinite(data.startTime))
|
|
269
|
+
) {
|
|
270
|
+
return createCompactSamplePointSource(new Float64Array(), 1, 0)
|
|
271
|
+
}
|
|
272
|
+
const startTime = data.startTime ?? 0
|
|
273
|
+
let count = 0
|
|
274
|
+
for (let index = 0; index < data.values.length; index += 1)
|
|
275
|
+
count += Number.isFinite(data.values[index]) ? 1 : 0
|
|
276
|
+
const y = data.values instanceof Float32Array ? new Float32Array(count) : new Float64Array(count)
|
|
277
|
+
const sourceIndexes = count === data.values.length ? undefined : new Uint32Array(count)
|
|
278
|
+
for (let sourceIndex = 0, targetIndex = 0; sourceIndex < data.values.length; sourceIndex += 1) {
|
|
279
|
+
const value = data.values[sourceIndex]
|
|
280
|
+
if (!Number.isFinite(value)) continue
|
|
281
|
+
y[targetIndex] = value
|
|
282
|
+
if (sourceIndexes) sourceIndexes[targetIndex] = sourceIndex
|
|
283
|
+
targetIndex += 1
|
|
284
|
+
}
|
|
285
|
+
return createCompactSamplePointSource(y, data.sampleRate, startTime, sourceIndexes)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function compactPoints(data: Extract<SingleWaveformData, { kind: 'typed-points' }>) {
|
|
289
|
+
if (!typedPointsAreValid(data))
|
|
290
|
+
return new CompactWaveformPointSource(new Float64Array(), new Float64Array())
|
|
291
|
+
const indexes: number[] = []
|
|
292
|
+
let ordered = true
|
|
293
|
+
let previousX = Number.NEGATIVE_INFINITY
|
|
294
|
+
for (let index = 0; index < data.x.length; index += 1) {
|
|
295
|
+
const x = data.x[index]
|
|
296
|
+
const y = data.y[index]
|
|
297
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue
|
|
298
|
+
if (x < previousX) ordered = false
|
|
299
|
+
previousX = x
|
|
300
|
+
indexes.push(index)
|
|
301
|
+
}
|
|
302
|
+
if (!ordered) indexes.sort((left, right) => data.x[left]! - data.x[right]!)
|
|
303
|
+
const x = new Float64Array(indexes.length)
|
|
304
|
+
const y =
|
|
305
|
+
data.y instanceof Float32Array
|
|
306
|
+
? new Float32Array(indexes.length)
|
|
307
|
+
: new Float64Array(indexes.length)
|
|
308
|
+
const copyField = (field: WaveformTypedValues | undefined) =>
|
|
309
|
+
field
|
|
310
|
+
? field instanceof Float32Array
|
|
311
|
+
? new Float32Array(indexes.length)
|
|
312
|
+
: new Float64Array(indexes.length)
|
|
313
|
+
: undefined
|
|
314
|
+
const error = copyField(data.error)
|
|
315
|
+
const lowerError = copyField(data.lowerError)
|
|
316
|
+
const upperError = copyField(data.upperError)
|
|
317
|
+
indexes.forEach((sourceIndex, targetIndex) => {
|
|
318
|
+
x[targetIndex] = data.x[sourceIndex]!
|
|
319
|
+
y[targetIndex] = data.y[sourceIndex]!
|
|
320
|
+
if (error) error[targetIndex] = data.error![sourceIndex]!
|
|
321
|
+
if (lowerError) lowerError[targetIndex] = data.lowerError![sourceIndex]!
|
|
322
|
+
if (upperError) upperError[targetIndex] = data.upperError![sourceIndex]!
|
|
323
|
+
})
|
|
324
|
+
return new CompactWaveformPointSource(x, y, error, lowerError, upperError)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function createWaveformPointSource(
|
|
328
|
+
data: SingleWaveformData,
|
|
329
|
+
normalizeObjectData: (value: SingleWaveformData) => WaveformPoint[],
|
|
330
|
+
): WaveformPointSource {
|
|
331
|
+
if (data.kind === 'typed-samples') return compactSamples(data)
|
|
332
|
+
if (data.kind === 'typed-points') return compactPoints(data)
|
|
333
|
+
return new ObjectWaveformPointSource(normalizeObjectData(data))
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function pointSourceFromPoints(points: readonly WaveformPoint[]): WaveformPointSource {
|
|
337
|
+
return new ObjectWaveformPointSource(points)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Keeps existing array-oriented component code compatible while constructing compact points only
|
|
342
|
+
* when an index is actually read. Native array methods use `has` before indexed reads, so both
|
|
343
|
+
* iteration and `slice` retain their normal semantics without eagerly allocating point objects.
|
|
344
|
+
*/
|
|
345
|
+
export function lazyPointArray(source: WaveformPointSource): WaveformPoint[] {
|
|
346
|
+
if (!source.isCompact) return source.pointsInRange()
|
|
347
|
+
const cache = new Map<number, WaveformPoint>()
|
|
348
|
+
const maximumCachedPoints = 128
|
|
349
|
+
const valueAt = (index: number) => {
|
|
350
|
+
const cached = cache.get(index)
|
|
351
|
+
if (cached) return cached
|
|
352
|
+
const point = source.pointAt(index)
|
|
353
|
+
if (point) {
|
|
354
|
+
cache.set(index, point)
|
|
355
|
+
if (cache.size > maximumCachedPoints) cache.delete(cache.keys().next().value!)
|
|
356
|
+
}
|
|
357
|
+
return point
|
|
358
|
+
}
|
|
359
|
+
return new Proxy(Array.from<WaveformPoint>({ length: source.length }), {
|
|
360
|
+
get(target, property, receiver) {
|
|
361
|
+
if (typeof property === 'string' && /^(0|[1-9]\d*)$/.test(property)) {
|
|
362
|
+
return valueAt(Number(property))
|
|
363
|
+
}
|
|
364
|
+
return Reflect.get(target, property, receiver)
|
|
365
|
+
},
|
|
366
|
+
has(target, property) {
|
|
367
|
+
if (typeof property === 'string' && /^(0|[1-9]\d*)$/.test(property)) {
|
|
368
|
+
const index = Number(property)
|
|
369
|
+
return index >= 0 && index < source.length
|
|
370
|
+
}
|
|
371
|
+
return Reflect.has(target, property)
|
|
372
|
+
},
|
|
373
|
+
ownKeys(target) {
|
|
374
|
+
return [
|
|
375
|
+
...Array.from({ length: source.length }, (_, index) => String(index)),
|
|
376
|
+
...Reflect.ownKeys(target).filter((key) => key === 'length' || typeof key === 'symbol'),
|
|
377
|
+
]
|
|
378
|
+
},
|
|
379
|
+
getOwnPropertyDescriptor(target, property) {
|
|
380
|
+
if (typeof property === 'string' && /^(0|[1-9]\d*)$/.test(property)) {
|
|
381
|
+
const index = Number(property)
|
|
382
|
+
if (index >= 0 && index < source.length) {
|
|
383
|
+
return { configurable: true, enumerable: true, writable: true, value: valueAt(index) }
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return Reflect.getOwnPropertyDescriptor(target, property)
|
|
387
|
+
},
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function isWaveformPointSource(value: unknown): value is WaveformPointSource {
|
|
392
|
+
return (
|
|
393
|
+
typeof value === 'object' && value !== null && 'pointAt' in value && 'visibleRange' in value
|
|
394
|
+
)
|
|
395
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export { javascriptSamplingBackend } from './javascriptBackend'
|
|
2
|
+
export {
|
|
3
|
+
MultiResolutionSamplingIndex,
|
|
4
|
+
type MultiResolutionSamplingIndexOptions,
|
|
5
|
+
} from './multiresolution'
|
|
6
|
+
export { WorkerSamplingRepository, type WorkerSamplingRepositoryOptions } from './repository'
|
|
7
|
+
export {
|
|
8
|
+
SamplingOutputCache,
|
|
9
|
+
type SamplingCacheMetrics,
|
|
10
|
+
type SamplingOutputCacheOptions,
|
|
11
|
+
} from './samplingCache'
|
|
12
|
+
export {
|
|
13
|
+
DEFAULT_WORKER_SAMPLING_AUTO_THRESHOLD,
|
|
14
|
+
DEFAULT_WORKER_SAMPLING_MAX_POINTS_PER_PIXEL,
|
|
15
|
+
isCurrentWorkerSamplingResponse,
|
|
16
|
+
type DatasetResponse,
|
|
17
|
+
type DisposeAllResponse,
|
|
18
|
+
type FindNearestPointResponse,
|
|
19
|
+
type SampleViewportResponse,
|
|
20
|
+
type WorkerSamplingBackend,
|
|
21
|
+
type WorkerSamplingBackendKind,
|
|
22
|
+
type WorkerSamplingDatasetMetrics,
|
|
23
|
+
type WorkerSamplingDiagnostics,
|
|
24
|
+
type WorkerSamplingOutput,
|
|
25
|
+
type WorkerSamplingRequest,
|
|
26
|
+
type WorkerSamplingResponse,
|
|
27
|
+
type WorkerSamplingSeriesRequest,
|
|
28
|
+
type WorkerSamplingSeriesResponse,
|
|
29
|
+
type WorkerSamplingStatus,
|
|
30
|
+
} from './protocol'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { sampleWaveformReferenceValues } from '../samplingReferenceValues'
|
|
2
|
+
import type { WorkerSamplingBackend, WorkerSamplingBackendRequest } from './protocol'
|
|
3
|
+
|
|
4
|
+
/** The phase 3 fallback backend. A future WASM backend implements the same narrow interface. */
|
|
5
|
+
export const javascriptSamplingBackend: WorkerSamplingBackend = {
|
|
6
|
+
kind: 'javascript',
|
|
7
|
+
sample(request: WorkerSamplingBackendRequest) {
|
|
8
|
+
return sampleWaveformReferenceValues(
|
|
9
|
+
request.x,
|
|
10
|
+
request.y,
|
|
11
|
+
request.strategy,
|
|
12
|
+
request.targetPointCount,
|
|
13
|
+
)
|
|
14
|
+
},
|
|
15
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { WaveformPoint } from '@/types'
|
|
4
|
+
import { sampleWaveformReference } from '../samplingReference'
|
|
5
|
+
import { MultiResolutionSamplingIndex } from './multiresolution'
|
|
6
|
+
|
|
7
|
+
const points: WaveformPoint[] = Array.from({ length: 65 }, (_, index) => ({
|
|
8
|
+
x: index,
|
|
9
|
+
y: index === 1 ? -90 : index === 31 ? 120 : Math.sin(index / 3) * 10 + index / 9,
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
function outputPoints(result: ReturnType<MultiResolutionSamplingIndex['sample']>) {
|
|
13
|
+
if (!result || result.kind !== 'source-indexes') return []
|
|
14
|
+
return [...result.sourceIndexes]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('MultiResolutionSamplingIndex', () => {
|
|
18
|
+
it('matches reference source indexes for unaligned viewport boundaries', () => {
|
|
19
|
+
const index = new MultiResolutionSamplingIndex(points)
|
|
20
|
+
const visible = points.slice(3, 61)
|
|
21
|
+
|
|
22
|
+
for (const strategy of ['peak', 'min', 'max', 'minmax'] as const) {
|
|
23
|
+
const expected = sampleWaveformReference({
|
|
24
|
+
points: visible,
|
|
25
|
+
strategy,
|
|
26
|
+
targetPointCount: 17,
|
|
27
|
+
})
|
|
28
|
+
expect(expected.kind).toBe('source-indexes')
|
|
29
|
+
if (expected.kind !== 'source-indexes') continue
|
|
30
|
+
expect(outputPoints(index.sample(3, 61, strategy, 17))).toEqual(
|
|
31
|
+
[...expected.sourceIndexes].map((sourceIndex) => sourceIndex + 3),
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
expect(index.byteLength).toBeGreaterThan(0)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('uses reusable sum and count layers for average and sum buckets', () => {
|
|
38
|
+
const index = new MultiResolutionSamplingIndex(points)
|
|
39
|
+
for (const strategy of ['average', 'sum'] as const) {
|
|
40
|
+
const expected = sampleWaveformReference({
|
|
41
|
+
points: points.slice(5, 62),
|
|
42
|
+
strategy,
|
|
43
|
+
targetPointCount: 13,
|
|
44
|
+
})
|
|
45
|
+
const actual = index.sample(5, 62, strategy, 13)
|
|
46
|
+
expect(expected.kind).toBe('aggregates')
|
|
47
|
+
expect(actual).toMatchObject({ kind: 'aggregates' })
|
|
48
|
+
if (expected.kind !== 'aggregates' || actual?.kind !== 'aggregates') continue
|
|
49
|
+
actual.x.forEach((value, index) => expect(value).toBeCloseTo(expected.x[index]!, 12))
|
|
50
|
+
actual.y.forEach((value, index) => expect(value).toBeCloseTo(expected.y[index]!, 12))
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('falls back when its bounded index memory cannot serve a query and releases all layers', () => {
|
|
55
|
+
const index = new MultiResolutionSamplingIndex(points, { maxBytes: 8 })
|
|
56
|
+
|
|
57
|
+
expect(index.sample(0, points.length, 'peak', 8)).toBeUndefined()
|
|
58
|
+
expect(index.byteLength).toBe(0)
|
|
59
|
+
index.dispose()
|
|
60
|
+
expect(index.byteLength).toBe(0)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('leaves LTTB as a viewport-local calculation', () => {
|
|
64
|
+
const index = new MultiResolutionSamplingIndex(points)
|
|
65
|
+
|
|
66
|
+
expect(index.sample(5, 62, 'lttb', 13)).toBeUndefined()
|
|
67
|
+
expect(index.byteLength).toBe(0)
|
|
68
|
+
})
|
|
69
|
+
})
|