waveform-analysis 0.1.47 → 0.1.50

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.
Files changed (122) hide show
  1. package/README.md +127 -5
  2. package/dist/assets/workerRuntime-Dt94jbqq.js +1 -0
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.js +2701 -1165
  5. package/dist/style.css +1 -1
  6. package/dist/types/components/WaveformChart.vue.d.ts +14 -0
  7. package/dist/types/components/annotation/WaveformAnnotationEditor.vue.d.ts +0 -1
  8. package/dist/types/components/annotation/markup.d.ts +5 -6
  9. package/dist/types/components/annotation/types.d.ts +2 -0
  10. package/dist/types/components/core/latestTaskScheduler.d.ts +19 -0
  11. package/dist/types/components/core/layout.d.ts +2 -2
  12. package/dist/types/components/core/trackLayoutBuilder.d.ts +4 -1
  13. package/dist/types/components/core/types.d.ts +2 -0
  14. package/dist/types/components/core/useWaveformChartController.d.ts +1 -0
  15. package/dist/types/components/core/useWaveformData.d.ts +2 -0
  16. package/dist/types/components/core/useWaveformLayout.d.ts +2 -0
  17. package/dist/types/components/core/useWaveformRenderSampling.d.ts +19 -0
  18. package/dist/types/components/core/waveformChartTypes.d.ts +8 -1
  19. package/dist/types/components/core/workerSamplingClient.d.ts +8 -0
  20. package/dist/types/components/data/types.d.ts +1 -1
  21. package/dist/types/components/interaction/zoomConstraints.d.ts +2 -0
  22. package/dist/types/components/waveform.d.ts +1 -1
  23. package/dist/types/core/compactSamplePointSource.d.ts +3 -0
  24. package/dist/types/core/data.d.ts +7 -1
  25. package/dist/types/core/index.d.ts +2 -1
  26. package/dist/types/core/rendering.d.ts +3 -1
  27. package/dist/types/core/renderingOptions.d.ts +11 -1
  28. package/dist/types/core/samplingReference.d.ts +36 -0
  29. package/dist/types/core/samplingReferenceValues.d.ts +4 -0
  30. package/dist/types/core/samplingReferenceVectors.d.ts +17 -0
  31. package/dist/types/core/wasmSampling.d.ts +48 -0
  32. package/dist/types/core/waveformPointSource.d.ts +53 -0
  33. package/dist/types/core/workerSampling/index.d.ts +5 -0
  34. package/dist/types/core/workerSampling/javascriptBackend.d.ts +3 -0
  35. package/dist/types/core/workerSampling/multiresolution.d.ts +29 -0
  36. package/dist/types/core/workerSampling/protocol.d.ts +153 -0
  37. package/dist/types/core/workerSampling/repository.d.ts +36 -0
  38. package/dist/types/core/workerSampling/repositoryDataset.d.ts +29 -0
  39. package/dist/types/core/workerSampling/repositorySupport.d.ts +27 -0
  40. package/dist/types/core/workerSampling/samplingCache.d.ts +24 -0
  41. package/dist/types/core/workerSampling/workerRuntime.d.ts +1 -0
  42. package/dist/types/index.d.ts +2 -2
  43. package/dist/types/types/chart.d.ts +58 -0
  44. package/dist/types/types/data.d.ts +20 -2
  45. package/dist/types/types/index.d.ts +2 -2
  46. package/dist/types/views/WasmSamplingDemo.vue.d.ts +3 -0
  47. package/package.json +5 -4
  48. package/src/App.test.ts +11 -7
  49. package/src/App.vue +2 -2
  50. package/src/DemoRouterApp.vue +1 -0
  51. package/src/components/WaveformChart.css +2 -0
  52. package/src/components/annotation/WaveformAnnotationEditor.css +2 -2
  53. package/src/components/annotation/WaveformAnnotationEditor.vue +8 -4
  54. package/src/components/annotation/markup.ts +45 -26
  55. package/src/components/annotation/types.ts +2 -0
  56. package/src/components/annotation/useWaveformChartAnnotations.ts +12 -15
  57. package/src/components/core/latestTaskScheduler.test.ts +51 -0
  58. package/src/components/core/latestTaskScheduler.ts +75 -0
  59. package/src/components/core/layout.ts +15 -6
  60. package/src/components/core/trackLayoutBuilder.ts +8 -2
  61. package/src/components/core/types.ts +2 -0
  62. package/src/components/core/useWaveformChartController.ts +14 -0
  63. package/src/components/core/useWaveformData.test.ts +95 -0
  64. package/src/components/core/useWaveformData.ts +23 -32
  65. package/src/components/core/useWaveformLayout.ts +16 -10
  66. package/src/components/core/useWaveformRenderSampling.test.ts +17 -0
  67. package/src/components/core/useWaveformRenderSampling.ts +394 -0
  68. package/src/components/core/waveformChartTypes.ts +13 -0
  69. package/src/components/core/workerSamplingClient.ts +148 -0
  70. package/src/components/core/yAxisNice.test.ts +174 -0
  71. package/src/components/data/types.ts +9 -0
  72. package/src/components/interaction/WaveformTooltip.test.ts +1 -3
  73. package/src/components/interaction/WaveformTooltip.vue +13 -12
  74. package/src/components/interaction/useWaveformHover.ts +7 -7
  75. package/src/components/interaction/zoomConstraints.ts +5 -1
  76. package/src/components/waveform.ts +9 -0
  77. package/src/components/waveformChartCases/compactAndSharedHover.test.ts +15 -4
  78. package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +11 -6
  79. package/src/components/waveformChartCases/dataLifecycle.test.ts +1 -3
  80. package/src/components/waveformChartCases/trackOrder.test.ts +130 -0
  81. package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +117 -0
  82. package/src/components/waveformChartCases/xDomainStrategy.test.ts +14 -2
  83. package/src/core/compactSamplePointSource.ts +76 -0
  84. package/src/core/data.test.ts +114 -0
  85. package/src/core/data.ts +177 -75
  86. package/src/core/index.ts +11 -0
  87. package/src/core/rendering.test.ts +87 -12
  88. package/src/core/rendering.ts +35 -1
  89. package/src/core/renderingOptions.ts +124 -15
  90. package/src/core/samplingReference.test.ts +124 -0
  91. package/src/core/samplingReference.ts +243 -0
  92. package/src/core/samplingReferenceValues.ts +169 -0
  93. package/src/core/samplingReferenceVectors.ts +46 -0
  94. package/src/core/wasmSampling.test.ts +205 -0
  95. package/src/core/wasmSampling.ts +230 -0
  96. package/src/core/waveformPointSource.ts +395 -0
  97. package/src/core/workerSampling/index.ts +30 -0
  98. package/src/core/workerSampling/javascriptBackend.ts +15 -0
  99. package/src/core/workerSampling/multiresolution.test.ts +69 -0
  100. package/src/core/workerSampling/multiresolution.ts +300 -0
  101. package/src/core/workerSampling/protocol.test.ts +34 -0
  102. package/src/core/workerSampling/protocol.ts +207 -0
  103. package/src/core/workerSampling/repository.test.ts +400 -0
  104. package/src/core/workerSampling/repository.ts +400 -0
  105. package/src/core/workerSampling/repositoryCache.test.ts +163 -0
  106. package/src/core/workerSampling/repositoryDataset.ts +194 -0
  107. package/src/core/workerSampling/repositorySupport.ts +225 -0
  108. package/src/core/workerSampling/repositoryTypedDataset.test.ts +89 -0
  109. package/src/core/workerSampling/repositoryWasmDataset.test.ts +70 -0
  110. package/src/core/workerSampling/samplingCache.ts +94 -0
  111. package/src/core/workerSampling/workerRuntime.ts +40 -0
  112. package/src/data/simulatedWaveforms.test.ts +19 -0
  113. package/src/data/simulatedWaveforms.ts +11 -8
  114. package/src/demo/xAxisLabelControls.test.ts +1 -1
  115. package/src/demoRoutes.css +177 -0
  116. package/src/index.ts +18 -0
  117. package/src/router.ts +5 -0
  118. package/src/types/chart.ts +65 -0
  119. package/src/types/data.ts +24 -1
  120. package/src/types/index.ts +9 -0
  121. package/src/views/WasmSamplingDemo.test.ts +13 -0
  122. 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
+ })