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.
Files changed (117) hide show
  1. package/README.md +120 -5
  2. package/dist/assets/workerRuntime-Dt94jbqq.js +1 -0
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.js +2683 -1153
  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/trackLayoutBuilder.d.ts +3 -1
  12. package/dist/types/components/core/types.d.ts +2 -0
  13. package/dist/types/components/core/useWaveformChartController.d.ts +1 -0
  14. package/dist/types/components/core/useWaveformData.d.ts +2 -0
  15. package/dist/types/components/core/useWaveformLayout.d.ts +2 -0
  16. package/dist/types/components/core/useWaveformRenderSampling.d.ts +19 -0
  17. package/dist/types/components/core/waveformChartTypes.d.ts +8 -1
  18. package/dist/types/components/core/workerSamplingClient.d.ts +8 -0
  19. package/dist/types/components/data/types.d.ts +1 -1
  20. package/dist/types/components/interaction/zoomConstraints.d.ts +2 -0
  21. package/dist/types/components/waveform.d.ts +1 -1
  22. package/dist/types/core/compactSamplePointSource.d.ts +3 -0
  23. package/dist/types/core/data.d.ts +7 -1
  24. package/dist/types/core/index.d.ts +2 -1
  25. package/dist/types/core/rendering.d.ts +3 -1
  26. package/dist/types/core/renderingOptions.d.ts +11 -1
  27. package/dist/types/core/samplingReference.d.ts +36 -0
  28. package/dist/types/core/samplingReferenceValues.d.ts +4 -0
  29. package/dist/types/core/samplingReferenceVectors.d.ts +17 -0
  30. package/dist/types/core/wasmSampling.d.ts +48 -0
  31. package/dist/types/core/waveformPointSource.d.ts +53 -0
  32. package/dist/types/core/workerSampling/index.d.ts +5 -0
  33. package/dist/types/core/workerSampling/javascriptBackend.d.ts +3 -0
  34. package/dist/types/core/workerSampling/multiresolution.d.ts +29 -0
  35. package/dist/types/core/workerSampling/protocol.d.ts +153 -0
  36. package/dist/types/core/workerSampling/repository.d.ts +36 -0
  37. package/dist/types/core/workerSampling/repositoryDataset.d.ts +29 -0
  38. package/dist/types/core/workerSampling/repositorySupport.d.ts +27 -0
  39. package/dist/types/core/workerSampling/samplingCache.d.ts +24 -0
  40. package/dist/types/core/workerSampling/workerRuntime.d.ts +1 -0
  41. package/dist/types/index.d.ts +2 -2
  42. package/dist/types/types/chart.d.ts +56 -0
  43. package/dist/types/types/data.d.ts +20 -2
  44. package/dist/types/types/index.d.ts +2 -2
  45. package/dist/types/views/WasmSamplingDemo.vue.d.ts +3 -0
  46. package/package.json +5 -4
  47. package/src/App.test.ts +11 -7
  48. package/src/App.vue +2 -2
  49. package/src/DemoRouterApp.vue +1 -0
  50. package/src/components/WaveformChart.css +2 -0
  51. package/src/components/annotation/WaveformAnnotationEditor.css +2 -2
  52. package/src/components/annotation/WaveformAnnotationEditor.vue +8 -4
  53. package/src/components/annotation/markup.ts +45 -26
  54. package/src/components/annotation/types.ts +2 -0
  55. package/src/components/annotation/useWaveformChartAnnotations.ts +12 -15
  56. package/src/components/core/latestTaskScheduler.test.ts +51 -0
  57. package/src/components/core/latestTaskScheduler.ts +75 -0
  58. package/src/components/core/trackLayoutBuilder.ts +3 -0
  59. package/src/components/core/types.ts +2 -0
  60. package/src/components/core/useWaveformChartController.ts +14 -0
  61. package/src/components/core/useWaveformData.test.ts +95 -0
  62. package/src/components/core/useWaveformData.ts +23 -32
  63. package/src/components/core/useWaveformLayout.ts +4 -4
  64. package/src/components/core/useWaveformRenderSampling.test.ts +17 -0
  65. package/src/components/core/useWaveformRenderSampling.ts +394 -0
  66. package/src/components/core/waveformChartTypes.ts +13 -0
  67. package/src/components/core/workerSamplingClient.ts +148 -0
  68. package/src/components/data/types.ts +9 -0
  69. package/src/components/interaction/WaveformTooltip.test.ts +1 -3
  70. package/src/components/interaction/WaveformTooltip.vue +13 -12
  71. package/src/components/interaction/useWaveformHover.ts +7 -7
  72. package/src/components/interaction/zoomConstraints.ts +5 -1
  73. package/src/components/waveform.ts +9 -0
  74. package/src/components/waveformChartCases/compactAndSharedHover.test.ts +15 -4
  75. package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +11 -6
  76. package/src/components/waveformChartCases/dataLifecycle.test.ts +1 -3
  77. package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +117 -0
  78. package/src/components/waveformChartCases/xDomainStrategy.test.ts +14 -2
  79. package/src/core/compactSamplePointSource.ts +76 -0
  80. package/src/core/data.test.ts +114 -0
  81. package/src/core/data.ts +177 -75
  82. package/src/core/index.ts +11 -0
  83. package/src/core/rendering.test.ts +87 -12
  84. package/src/core/rendering.ts +35 -1
  85. package/src/core/renderingOptions.ts +124 -15
  86. package/src/core/samplingReference.test.ts +124 -0
  87. package/src/core/samplingReference.ts +243 -0
  88. package/src/core/samplingReferenceValues.ts +169 -0
  89. package/src/core/samplingReferenceVectors.ts +46 -0
  90. package/src/core/wasmSampling.test.ts +205 -0
  91. package/src/core/wasmSampling.ts +230 -0
  92. package/src/core/waveformPointSource.ts +395 -0
  93. package/src/core/workerSampling/index.ts +30 -0
  94. package/src/core/workerSampling/javascriptBackend.ts +15 -0
  95. package/src/core/workerSampling/multiresolution.test.ts +69 -0
  96. package/src/core/workerSampling/multiresolution.ts +300 -0
  97. package/src/core/workerSampling/protocol.test.ts +34 -0
  98. package/src/core/workerSampling/protocol.ts +207 -0
  99. package/src/core/workerSampling/repository.test.ts +400 -0
  100. package/src/core/workerSampling/repository.ts +400 -0
  101. package/src/core/workerSampling/repositoryCache.test.ts +163 -0
  102. package/src/core/workerSampling/repositoryDataset.ts +194 -0
  103. package/src/core/workerSampling/repositorySupport.ts +225 -0
  104. package/src/core/workerSampling/repositoryTypedDataset.test.ts +89 -0
  105. package/src/core/workerSampling/repositoryWasmDataset.test.ts +70 -0
  106. package/src/core/workerSampling/samplingCache.ts +94 -0
  107. package/src/core/workerSampling/workerRuntime.ts +40 -0
  108. package/src/data/simulatedWaveforms.test.ts +19 -0
  109. package/src/data/simulatedWaveforms.ts +11 -8
  110. package/src/demoRoutes.css +177 -0
  111. package/src/index.ts +18 -0
  112. package/src/router.ts +5 -0
  113. package/src/types/chart.ts +63 -0
  114. package/src/types/data.ts +24 -1
  115. package/src/types/index.ts +9 -0
  116. package/src/views/WasmSamplingDemo.test.ts +13 -0
  117. package/src/views/WasmSamplingDemo.vue +180 -0
@@ -0,0 +1,124 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { WaveformPoint } from '../types'
4
+ import {
5
+ sampleWaveformReference,
6
+ type ReferenceSamplingStrategy,
7
+ type SourceIndexSamplingResult,
8
+ } from './samplingReference'
9
+ import {
10
+ REFERENCE_AGGREGATE_VECTORS,
11
+ REFERENCE_SAMPLING_POINTS,
12
+ REFERENCE_SOURCE_INDEX_VECTORS,
13
+ } from './samplingReferenceVectors'
14
+
15
+ function indexes(strategy: Exclude<ReferenceSamplingStrategy, 'average' | 'sum'>, target: number) {
16
+ const result = sampleWaveformReference({
17
+ points: REFERENCE_SAMPLING_POINTS,
18
+ strategy,
19
+ targetPointCount: target,
20
+ })
21
+ expect(result.kind).toBe('source-indexes')
22
+ return Array.from((result as SourceIndexSamplingResult).sourceIndexes)
23
+ }
24
+
25
+ describe('sampleWaveformReference', () => {
26
+ it('locks the source-index strategy vectors', () => {
27
+ for (const vector of REFERENCE_SOURCE_INDEX_VECTORS) {
28
+ expect(indexes(vector.strategy, vector.targetPointCount)).toEqual(
29
+ vector.expectedSourceIndexes,
30
+ )
31
+ }
32
+ })
33
+
34
+ it('locks the aggregate bucket and synthesized-X semantics', () => {
35
+ for (const vector of REFERENCE_AGGREGATE_VECTORS) {
36
+ const result = sampleWaveformReference({
37
+ points: REFERENCE_SAMPLING_POINTS,
38
+ strategy: vector.strategy,
39
+ targetPointCount: vector.targetPointCount,
40
+ })
41
+
42
+ expect(result).toMatchObject({ kind: 'aggregates', validPointCount: 8 })
43
+ expect(Array.from(result.kind === 'aggregates' ? result.x : [])).toEqual(vector.expectedX)
44
+ expect(Array.from(result.kind === 'aggregates' ? result.y : [])).toEqual(vector.expectedY)
45
+ }
46
+ })
47
+
48
+ it('uses the earliest point for equal extrema and removes duplicate peak entries', () => {
49
+ const tied = [
50
+ { x: 0, y: 0 },
51
+ { x: 1, y: -5 },
52
+ { x: 2, y: -5 },
53
+ { x: 3, y: 5 },
54
+ { x: 4, y: 5 },
55
+ { x: 5, y: 0 },
56
+ { x: 6, y: 0 },
57
+ ]
58
+
59
+ const minmax = sampleWaveformReference({
60
+ points: tied,
61
+ strategy: 'minmax',
62
+ targetPointCount: 4,
63
+ })
64
+ const peak = sampleWaveformReference({ points: tied, strategy: 'peak', targetPointCount: 6 })
65
+
66
+ expect(Array.from(minmax.kind === 'source-indexes' ? minmax.sourceIndexes : [])).toEqual([
67
+ 0, 1, 3, 6,
68
+ ])
69
+ expect(Array.from(peak.kind === 'source-indexes' ? peak.sourceIndexes : [])).toEqual([
70
+ 0, 1, 3, 5, 6,
71
+ ])
72
+ })
73
+
74
+ it('filters invalid points, stably orders X, and returns indexes into the original input', () => {
75
+ const input: WaveformPoint[] = [
76
+ { x: 2, y: 20 },
77
+ { x: Number.NaN, y: 99 },
78
+ { x: 1, y: 10 },
79
+ { x: 1, y: 11 },
80
+ { x: 3, y: Number.POSITIVE_INFINITY },
81
+ ]
82
+ const result = sampleWaveformReference({ points: input, strategy: 'none', targetPointCount: 1 })
83
+
84
+ expect(result).toMatchObject({ inputPointCount: 5, validPointCount: 3, kind: 'source-indexes' })
85
+ expect(Array.from(result.kind === 'source-indexes' ? result.sourceIndexes : [])).toEqual([
86
+ 2, 3, 0,
87
+ ])
88
+ expect(input).toEqual([
89
+ { x: 2, y: 20 },
90
+ { x: Number.NaN, y: 99 },
91
+ { x: 1, y: 10 },
92
+ { x: 1, y: 11 },
93
+ { x: 3, y: Number.POSITIVE_INFINITY },
94
+ ])
95
+ })
96
+
97
+ it('defines empty, short, and low-target behavior without exceeding the target', () => {
98
+ const strategies: ReferenceSamplingStrategy[] = [
99
+ 'none',
100
+ 'peak',
101
+ 'lttb',
102
+ 'average',
103
+ 'min',
104
+ 'max',
105
+ 'minmax',
106
+ 'sum',
107
+ ]
108
+ for (const strategy of strategies) {
109
+ const empty = sampleWaveformReference({ points: [], strategy, targetPointCount: 3 })
110
+ expect(empty.kind === 'aggregates' ? empty.x : empty.sourceIndexes).toHaveLength(0)
111
+
112
+ const short = sampleWaveformReference({
113
+ points: REFERENCE_SAMPLING_POINTS.slice(0, 2),
114
+ strategy,
115
+ targetPointCount: 10,
116
+ })
117
+ expect(short.kind === 'aggregates' ? short.x : short.sourceIndexes).toHaveLength(2)
118
+ }
119
+
120
+ expect(indexes('lttb', 2)).toEqual([0, 7])
121
+ expect(indexes('minmax', 3)).toEqual([0, 7])
122
+ expect(indexes('peak', 6)).toHaveLength(5)
123
+ })
124
+ })
@@ -0,0 +1,243 @@
1
+ import type { WaveformPoint } from '../types'
2
+
3
+ /** Sampling algorithms that can be implemented equivalently by the future WASM core. */
4
+ export type ReferenceSamplingStrategy =
5
+ 'none' | 'peak' | 'lttb' | 'average' | 'min' | 'max' | 'minmax' | 'sum'
6
+
7
+ /**
8
+ * A pure TypeScript reference request. Source indexes in the result address the original
9
+ * `points` array, after filtering non-finite points and applying a stable ascending-X order.
10
+ */
11
+ export interface ReferenceSamplingRequest {
12
+ points: readonly WaveformPoint[]
13
+ strategy: ReferenceSamplingStrategy
14
+ targetPointCount: number
15
+ }
16
+
17
+ interface ReferenceSamplingResultBase {
18
+ strategy: ReferenceSamplingStrategy
19
+ inputPointCount: number
20
+ validPointCount: number
21
+ }
22
+
23
+ /** A result made of real points from the input sequence. */
24
+ export interface SourceIndexSamplingResult extends ReferenceSamplingResultBase {
25
+ kind: 'source-indexes'
26
+ sourceIndexes: Uint32Array
27
+ }
28
+
29
+ /** A result whose points are synthesized from a contiguous source bucket. */
30
+ export interface AggregateSamplingResult extends ReferenceSamplingResultBase {
31
+ kind: 'aggregates'
32
+ x: Float64Array
33
+ y: Float64Array
34
+ }
35
+
36
+ export type ReferenceSamplingResult = SourceIndexSamplingResult | AggregateSamplingResult
37
+
38
+ interface IndexedPoint {
39
+ sourceIndex: number
40
+ x: number
41
+ y: number
42
+ }
43
+
44
+ function normalizedPoints(points: readonly WaveformPoint[]): IndexedPoint[] {
45
+ const result: IndexedPoint[] = []
46
+ let ordered = true
47
+ let previousX = Number.NEGATIVE_INFINITY
48
+
49
+ for (let sourceIndex = 0; sourceIndex < points.length; sourceIndex += 1) {
50
+ const point = points[sourceIndex]
51
+ if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y)) continue
52
+ if (point.x < previousX) ordered = false
53
+ previousX = point.x
54
+ result.push({ sourceIndex, x: point.x, y: point.y })
55
+ }
56
+
57
+ // ES2019 guarantees stable Array#sort, preserving duplicate-X source order.
58
+ if (!ordered) result.sort((left, right) => left.x - right.x)
59
+ return result
60
+ }
61
+
62
+ function targetCount(value: number, pointCount: number) {
63
+ if (pointCount === 0) return 0
64
+ if (!Number.isFinite(value)) return pointCount
65
+ return Math.min(pointCount, Math.max(1, Math.floor(value)))
66
+ }
67
+
68
+ function bucketBounds(bucket: number, bucketCount: number, start: number, end: number) {
69
+ const span = end - start
70
+ return {
71
+ start: start + Math.floor((bucket * span) / bucketCount),
72
+ end: start + Math.floor(((bucket + 1) * span) / bucketCount),
73
+ }
74
+ }
75
+
76
+ function addUniqueIndex(indexes: number[], index: number) {
77
+ if (indexes[indexes.length - 1] !== index) indexes.push(index)
78
+ }
79
+
80
+ function withEndpoints(points: IndexedPoint[], target: number, selectInterior: () => number[]) {
81
+ if (points.length <= target) return points.map((_, index) => index)
82
+ if (target === 1) return [0]
83
+ if (target === 2) return [0, points.length - 1]
84
+
85
+ const indexes = [0]
86
+ for (const index of selectInterior()) addUniqueIndex(indexes, index)
87
+ addUniqueIndex(indexes, points.length - 1)
88
+ return indexes
89
+ }
90
+
91
+ function selectExtrema(points: IndexedPoint[], target: number, kind: 'min' | 'max' | 'minmax') {
92
+ const bucketCount = kind === 'minmax' ? Math.floor((target - 2) / 2) : Math.max(0, target - 2)
93
+
94
+ return withEndpoints(points, target, () => {
95
+ if (bucketCount === 0) return []
96
+ const result: number[] = []
97
+ for (let bucket = 0; bucket < bucketCount; bucket += 1) {
98
+ const bounds = bucketBounds(bucket, bucketCount, 1, points.length - 1)
99
+ let minimum = bounds.start
100
+ let maximum = bounds.start
101
+ for (let index = bounds.start + 1; index < bounds.end; index += 1) {
102
+ if (points[index]!.y < points[minimum]!.y) minimum = index
103
+ if (points[index]!.y > points[maximum]!.y) maximum = index
104
+ }
105
+ if (kind === 'min') {
106
+ addUniqueIndex(result, minimum)
107
+ } else if (kind === 'max') {
108
+ addUniqueIndex(result, maximum)
109
+ } else if (minimum <= maximum) {
110
+ addUniqueIndex(result, minimum)
111
+ addUniqueIndex(result, maximum)
112
+ } else {
113
+ addUniqueIndex(result, maximum)
114
+ addUniqueIndex(result, minimum)
115
+ }
116
+ }
117
+ return result
118
+ })
119
+ }
120
+
121
+ function selectPeak(points: IndexedPoint[], target: number) {
122
+ const bucketCount = Math.floor((target - 2) / 4)
123
+ return withEndpoints(points, target, () => {
124
+ if (bucketCount === 0) return []
125
+ const result: number[] = []
126
+ for (let bucket = 0; bucket < bucketCount; bucket += 1) {
127
+ const bounds = bucketBounds(bucket, bucketCount, 1, points.length - 1)
128
+ let minimum = bounds.start
129
+ let maximum = bounds.start
130
+ for (let index = bounds.start + 1; index < bounds.end; index += 1) {
131
+ if (points[index]!.y < points[minimum]!.y) minimum = index
132
+ if (points[index]!.y > points[maximum]!.y) maximum = index
133
+ }
134
+ for (const index of [bounds.start, minimum, maximum, bounds.end - 1].sort((a, b) => a - b)) {
135
+ addUniqueIndex(result, index)
136
+ }
137
+ }
138
+ return result
139
+ })
140
+ }
141
+
142
+ function selectLttb(points: IndexedPoint[], target: number) {
143
+ return withEndpoints(points, target, () => {
144
+ const bucketSize = (points.length - 2) / (target - 2)
145
+ const result: number[] = []
146
+ let previous = 0
147
+
148
+ for (let bucket = 0; bucket < target - 2; bucket += 1) {
149
+ const averageStart = Math.min(points.length, Math.floor((bucket + 1) * bucketSize) + 1)
150
+ const averageEnd = Math.min(points.length, Math.floor((bucket + 2) * bucketSize) + 1)
151
+ let averageX = 0
152
+ let averageY = 0
153
+ for (let index = averageStart; index < averageEnd; index += 1) {
154
+ averageX += points[index]!.x
155
+ averageY += points[index]!.y
156
+ }
157
+ const averageCount = averageEnd - averageStart
158
+ if (averageCount > 0) {
159
+ averageX /= averageCount
160
+ averageY /= averageCount
161
+ } else {
162
+ averageX = points[points.length - 1]!.x
163
+ averageY = points[points.length - 1]!.y
164
+ }
165
+
166
+ const bounds = bucketBounds(bucket, target - 2, 1, points.length - 1)
167
+ let selected = bounds.start
168
+ let maximumArea = -1
169
+ for (let index = bounds.start; index < bounds.end; index += 1) {
170
+ const area = Math.abs(
171
+ (points[previous]!.x - averageX) * (points[index]!.y - points[previous]!.y) -
172
+ (points[previous]!.x - points[index]!.x) * (averageY - points[previous]!.y),
173
+ )
174
+ if (area > maximumArea) {
175
+ maximumArea = area
176
+ selected = index
177
+ }
178
+ }
179
+ result.push(selected)
180
+ previous = selected
181
+ }
182
+ return result
183
+ })
184
+ }
185
+
186
+ function aggregate(points: IndexedPoint[], target: number, strategy: 'average' | 'sum') {
187
+ const count = targetCount(target, points.length)
188
+ const x = new Float64Array(count)
189
+ const y = new Float64Array(count)
190
+ for (let bucket = 0; bucket < count; bucket += 1) {
191
+ const bounds = bucketBounds(bucket, count, 0, points.length)
192
+ for (let index = bounds.start; index < bounds.end; index += 1) {
193
+ x[bucket] += points[index]!.x
194
+ y[bucket] += points[index]!.y
195
+ }
196
+ const bucketSize = bounds.end - bounds.start
197
+ x[bucket] /= bucketSize
198
+ if (strategy === 'average') y[bucket] /= bucketSize
199
+ }
200
+ return { x, y }
201
+ }
202
+
203
+ /**
204
+ * Samples an immutable point sequence without changing its objects or order. Aggregate X values
205
+ * are the arithmetic mean of each contiguous bucket's X values; extrema ties select the earliest
206
+ * source point. Source-point strategies preserve first and last points whenever the target is 2+.
207
+ */
208
+ export function sampleWaveformReference(
209
+ request: ReferenceSamplingRequest,
210
+ ): ReferenceSamplingResult {
211
+ const points = normalizedPoints(request.points)
212
+ const base = {
213
+ strategy: request.strategy,
214
+ inputPointCount: request.points.length,
215
+ validPointCount: points.length,
216
+ }
217
+
218
+ if (request.strategy === 'average' || request.strategy === 'sum') {
219
+ return {
220
+ ...base,
221
+ kind: 'aggregates',
222
+ ...aggregate(points, request.targetPointCount, request.strategy),
223
+ }
224
+ }
225
+
226
+ const target = targetCount(request.targetPointCount, points.length)
227
+ let indexes: number[]
228
+ if (request.strategy === 'none' || points.length <= target) {
229
+ indexes = points.map((_, index) => index)
230
+ } else if (request.strategy === 'peak') {
231
+ indexes = selectPeak(points, target)
232
+ } else if (request.strategy === 'lttb') {
233
+ indexes = selectLttb(points, target)
234
+ } else {
235
+ indexes = selectExtrema(points, target, request.strategy)
236
+ }
237
+
238
+ return {
239
+ ...base,
240
+ kind: 'source-indexes',
241
+ sourceIndexes: Uint32Array.from(indexes, (index) => points[index]!.sourceIndex),
242
+ }
243
+ }
@@ -0,0 +1,169 @@
1
+ import type { ReferenceSamplingStrategy } from './samplingReference'
2
+ import type { WorkerSamplingOutput } from './workerSampling/protocol'
3
+
4
+ interface Bounds {
5
+ start: number
6
+ end: number
7
+ }
8
+
9
+ function targetCount(value: number, pointCount: number) {
10
+ if (pointCount === 0) return 0
11
+ if (!Number.isFinite(value)) return pointCount
12
+ return Math.min(pointCount, Math.max(1, Math.floor(value)))
13
+ }
14
+
15
+ function bucketBounds(bucket: number, bucketCount: number, start: number, end: number): Bounds {
16
+ const span = end - start
17
+ return {
18
+ start: start + Math.floor((bucket * span) / bucketCount),
19
+ end: start + Math.floor(((bucket + 1) * span) / bucketCount),
20
+ }
21
+ }
22
+
23
+ function addUnique(indexes: number[], index: number) {
24
+ if (indexes[indexes.length - 1] !== index) indexes.push(index)
25
+ }
26
+
27
+ function withEndpoints(length: number, target: number, selectInterior: () => number[]) {
28
+ if (length <= target) return Array.from({ length }, (_, index) => index)
29
+ if (target === 1) return [0]
30
+ if (target === 2) return [0, length - 1]
31
+ const indexes = [0]
32
+ selectInterior().forEach((index) => addUnique(indexes, index))
33
+ addUnique(indexes, length - 1)
34
+ return indexes
35
+ }
36
+
37
+ function extremaIndexes(y: Float64Array, target: number, strategy: 'min' | 'max' | 'minmax') {
38
+ const bucketCount = strategy === 'minmax' ? Math.floor((target - 2) / 2) : Math.max(0, target - 2)
39
+ return withEndpoints(y.length, target, () => {
40
+ if (!bucketCount) return []
41
+ const result: number[] = []
42
+ for (let bucket = 0; bucket < bucketCount; bucket += 1) {
43
+ const bounds = bucketBounds(bucket, bucketCount, 1, y.length - 1)
44
+ let minimum = bounds.start
45
+ let maximum = bounds.start
46
+ for (let index = bounds.start + 1; index < bounds.end; index += 1) {
47
+ if (y[index]! < y[minimum]!) minimum = index
48
+ if (y[index]! > y[maximum]!) maximum = index
49
+ }
50
+ if (strategy === 'min') addUnique(result, minimum)
51
+ else if (strategy === 'max') addUnique(result, maximum)
52
+ else if (minimum <= maximum) {
53
+ addUnique(result, minimum)
54
+ addUnique(result, maximum)
55
+ } else {
56
+ addUnique(result, maximum)
57
+ addUnique(result, minimum)
58
+ }
59
+ }
60
+ return result
61
+ })
62
+ }
63
+
64
+ function peakIndexes(y: Float64Array, target: number) {
65
+ const bucketCount = Math.floor((target - 2) / 4)
66
+ return withEndpoints(y.length, target, () => {
67
+ if (!bucketCount) return []
68
+ const result: number[] = []
69
+ for (let bucket = 0; bucket < bucketCount; bucket += 1) {
70
+ const bounds = bucketBounds(bucket, bucketCount, 1, y.length - 1)
71
+ let minimum = bounds.start
72
+ let maximum = bounds.start
73
+ for (let index = bounds.start + 1; index < bounds.end; index += 1) {
74
+ if (y[index]! < y[minimum]!) minimum = index
75
+ if (y[index]! > y[maximum]!) maximum = index
76
+ }
77
+ ;[bounds.start, minimum, maximum, bounds.end - 1]
78
+ .sort((left, right) => left - right)
79
+ .forEach((index) => addUnique(result, index))
80
+ }
81
+ return result
82
+ })
83
+ }
84
+
85
+ function lttbIndexes(x: Float64Array, y: Float64Array, target: number) {
86
+ return withEndpoints(x.length, target, () => {
87
+ const bucketSize = (x.length - 2) / (target - 2)
88
+ const result: number[] = []
89
+ let previous = 0
90
+ for (let bucket = 0; bucket < target - 2; bucket += 1) {
91
+ const averageStart = Math.min(x.length, Math.floor((bucket + 1) * bucketSize) + 1)
92
+ const averageEnd = Math.min(x.length, Math.floor((bucket + 2) * bucketSize) + 1)
93
+ let averageX = 0
94
+ let averageY = 0
95
+ for (let index = averageStart; index < averageEnd; index += 1) {
96
+ averageX += x[index]!
97
+ averageY += y[index]!
98
+ }
99
+ const averageCount = averageEnd - averageStart
100
+ if (averageCount) {
101
+ averageX /= averageCount
102
+ averageY /= averageCount
103
+ } else {
104
+ averageX = x[x.length - 1]!
105
+ averageY = y[y.length - 1]!
106
+ }
107
+ const bounds = bucketBounds(bucket, target - 2, 1, x.length - 1)
108
+ let selected = bounds.start
109
+ let maximumArea = -1
110
+ for (let index = bounds.start; index < bounds.end; index += 1) {
111
+ const area = Math.abs(
112
+ (x[previous]! - averageX) * (y[index]! - y[previous]!) -
113
+ (x[previous]! - x[index]!) * (averageY - y[previous]!),
114
+ )
115
+ if (area > maximumArea) {
116
+ maximumArea = area
117
+ selected = index
118
+ }
119
+ }
120
+ result.push(selected)
121
+ previous = selected
122
+ }
123
+ return result
124
+ })
125
+ }
126
+
127
+ function aggregates(
128
+ x: Float64Array,
129
+ y: Float64Array,
130
+ target: number,
131
+ strategy: 'average' | 'sum',
132
+ ): WorkerSamplingOutput {
133
+ const count = targetCount(target, x.length)
134
+ const outputX = new Float64Array(count)
135
+ const outputY = new Float64Array(count)
136
+ for (let bucket = 0; bucket < count; bucket += 1) {
137
+ const bounds = bucketBounds(bucket, count, 0, x.length)
138
+ for (let index = bounds.start; index < bounds.end; index += 1) {
139
+ outputX[bucket] += x[index]!
140
+ outputY[bucket] += y[index]!
141
+ }
142
+ const size = bounds.end - bounds.start
143
+ outputX[bucket] /= size
144
+ if (strategy === 'average') outputY[bucket] /= size
145
+ }
146
+ return { kind: 'aggregates', x: outputX, y: outputY }
147
+ }
148
+
149
+ /** Numeric JavaScript fallback matching the WASM source-index and aggregate contracts. */
150
+ export function sampleWaveformReferenceValues(
151
+ x: Float64Array,
152
+ y: Float64Array,
153
+ strategy: ReferenceSamplingStrategy,
154
+ targetPointCount: number,
155
+ ): WorkerSamplingOutput {
156
+ if (x.length !== y.length) throw new Error('x and y must have the same length')
157
+ if (strategy === 'average' || strategy === 'sum')
158
+ return aggregates(x, y, targetPointCount, strategy)
159
+ const target = targetCount(targetPointCount, x.length)
160
+ const indexes =
161
+ strategy === 'none' || x.length <= target
162
+ ? Array.from({ length: x.length }, (_, index) => index)
163
+ : strategy === 'peak'
164
+ ? peakIndexes(y, target)
165
+ : strategy === 'lttb'
166
+ ? lttbIndexes(x, y, target)
167
+ : extremaIndexes(y, target, strategy)
168
+ return { kind: 'source-indexes', sourceIndexes: Uint32Array.from(indexes) }
169
+ }
@@ -0,0 +1,46 @@
1
+ import type { WaveformPoint } from '../types'
2
+ import type { ReferenceSamplingStrategy } from './samplingReference'
3
+
4
+ /** Stable source data shared by the TypeScript reference and future WASM conformance suites. */
5
+ export const REFERENCE_SAMPLING_POINTS: readonly WaveformPoint[] = [
6
+ { x: 0, y: 4 },
7
+ { x: 1, y: 1 },
8
+ { x: 2, y: 8 },
9
+ { x: 3, y: 2 },
10
+ { x: 4, y: 7 },
11
+ { x: 5, y: 0 },
12
+ { x: 6, y: 9 },
13
+ { x: 7, y: 3 },
14
+ ]
15
+
16
+ export interface SourceIndexSamplingVector {
17
+ strategy: Exclude<ReferenceSamplingStrategy, 'average' | 'sum'>
18
+ targetPointCount: number
19
+ expectedSourceIndexes: readonly number[]
20
+ }
21
+
22
+ export const REFERENCE_SOURCE_INDEX_VECTORS: readonly SourceIndexSamplingVector[] = [
23
+ { strategy: 'none', targetPointCount: 3, expectedSourceIndexes: [0, 1, 2, 3, 4, 5, 6, 7] },
24
+ { strategy: 'peak', targetPointCount: 6, expectedSourceIndexes: [0, 1, 5, 6, 7] },
25
+ { strategy: 'min', targetPointCount: 6, expectedSourceIndexes: [0, 1, 3, 4, 5, 7] },
26
+ { strategy: 'max', targetPointCount: 6, expectedSourceIndexes: [0, 1, 2, 4, 6, 7] },
27
+ { strategy: 'minmax', targetPointCount: 6, expectedSourceIndexes: [0, 1, 2, 5, 6, 7] },
28
+ { strategy: 'lttb', targetPointCount: 5, expectedSourceIndexes: [0, 2, 3, 6, 7] },
29
+ ]
30
+
31
+ export interface AggregateSamplingVector {
32
+ strategy: Extract<ReferenceSamplingStrategy, 'average' | 'sum'>
33
+ targetPointCount: number
34
+ expectedX: readonly number[]
35
+ expectedY: readonly number[]
36
+ }
37
+
38
+ export const REFERENCE_AGGREGATE_VECTORS: readonly AggregateSamplingVector[] = [
39
+ {
40
+ strategy: 'average',
41
+ targetPointCount: 3,
42
+ expectedX: [0.5, 3, 6],
43
+ expectedY: [2.5, 17 / 3, 4],
44
+ },
45
+ { strategy: 'sum', targetPointCount: 3, expectedX: [0.5, 3, 6], expectedY: [5, 17, 12] },
46
+ ]