waveform-analysis 0.1.62 → 0.1.64
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 +9 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +953 -946
- package/dist/style.css +1 -1
- package/dist/types/types/chart.d.ts +2 -1
- package/package.json +1 -1
- package/src/components/WaveformChart.vue +1 -1
- package/src/components/core/xDomain.ts +26 -2
- package/src/components/rendering/WaveformLegend.vue +1 -1
- package/src/components/rendering/WaveformSeriesLayer.vue +1 -1
- package/src/demo/DemoChartHost.vue +1 -0
- package/src/types/chart.ts +2 -1
- package/src/App.test.ts +0 -400
- package/src/components/annotation/components.test.ts +0 -372
- package/src/components/annotation/markup.test.ts +0 -371
- package/src/components/annotation/serialization.test.ts +0 -114
- package/src/components/core/constants.test.ts +0 -29
- package/src/components/core/fixedYDomainLayout.test.ts +0 -204
- package/src/components/core/grid.test.ts +0 -197
- package/src/components/core/latestTaskScheduler.test.ts +0 -51
- package/src/components/core/layout.test.ts +0 -373
- package/src/components/core/title.test.ts +0 -76
- package/src/components/core/useWaveformData.test.ts +0 -95
- package/src/components/core/useWaveformRenderSampling.test.ts +0 -17
- package/src/components/core/xDomain.test.ts +0 -28
- package/src/components/core/yAxisNice.test.ts +0 -174
- package/src/components/core/yAxisSlotLayout.test.ts +0 -165
- package/src/components/core/yDomain.test.ts +0 -56
- package/src/components/interaction/WaveformTooltip.test.ts +0 -276
- package/src/components/interaction/viewportInteractionState.test.ts +0 -101
- package/src/components/utils/useAnimationFrameThrottle.test.ts +0 -53
- package/src/components/waveformChartCases/annotationEditingAndReprojection.test.ts +0 -257
- package/src/components/waveformChartCases/compactAndSharedHover.test.ts +0 -358
- package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +0 -358
- package/src/components/waveformChartCases/dataLifecycle.test.ts +0 -310
- package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +0 -389
- package/src/components/waveformChartCases/gridFill.test.ts +0 -95
- package/src/components/waveformChartCases/incompleteDataHover.test.ts +0 -155
- package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +0 -328
- package/src/components/waveformChartCases/layoutAndDomains.test.ts +0 -371
- package/src/components/waveformChartCases/legendVisibility.test.ts +0 -350
- package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +0 -400
- package/src/components/waveformChartCases/multiAxisLabelAlignment.test.ts +0 -61
- package/src/components/waveformChartCases/presentationAndAxes.test.ts +0 -345
- package/src/components/waveformChartCases/presentationMode.test.ts +0 -327
- package/src/components/waveformChartCases/renderingStyles.test.ts +0 -283
- package/src/components/waveformChartCases/sizingAndTitles.test.ts +0 -370
- package/src/components/waveformChartCases/trackOrder.test.ts +0 -323
- package/src/components/waveformChartCases/viewportBoxAndPan.test.ts +0 -365
- package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +0 -311
- package/src/components/waveformChartCases/viewportLifecycle.test.ts +0 -132
- package/src/components/waveformChartCases/viewportReset.test.ts +0 -251
- package/src/components/waveformChartCases/wheelZoomOut.test.ts +0 -114
- package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +0 -117
- package/src/components/waveformChartCases/xDomainStrategy.test.ts +0 -168
- package/src/components/waveformChartCases/yAxisLayout.test.ts +0 -103
- package/src/components/waveformChartCases/zoomIntentAndDataRebind.test.ts +0 -213
- package/src/components/waveformChartCases/zoomPointConstraints.test.ts +0 -308
- package/src/components/waveformLegendGeometry.test.ts +0 -21
- package/src/components/waveformNormalization.test.ts +0 -170
- package/src/core/data.test.ts +0 -241
- package/src/core/rendering.test.ts +0 -267
- package/src/core/renderingStrategies.test.ts +0 -90
- package/src/core/samplingReference.test.ts +0 -124
- package/src/core/wasmSampling.test.ts +0 -205
- package/src/core/workerSampling/multiresolution.test.ts +0 -69
- package/src/core/workerSampling/protocol.test.ts +0 -34
- package/src/core/workerSampling/repository.test.ts +0 -400
- package/src/core/workerSampling/repositoryCache.test.ts +0 -163
- package/src/core/workerSampling/repositorySampling.test.ts +0 -106
- package/src/core/workerSampling/repositoryTypedDataset.test.ts +0 -89
- package/src/core/workerSampling/repositoryWasmDataset.test.ts +0 -70
- package/src/data/simulatedWaveforms.test.ts +0 -60
- package/src/demo/plotMarginControls.test.ts +0 -29
- package/src/demo/xAxisLabelControls.test.ts +0 -86
- package/src/test/setup.ts +0 -82
- package/src/test/waveformChart.ts +0 -75
- package/src/utils/domain.test.ts +0 -28
- package/src/utils/formatters.test.ts +0 -131
- package/src/utils/sampling.test.ts +0 -226
- package/src/views/FixedYDomainDemo.test.ts +0 -44
- package/src/views/WasmSamplingDemo.test.ts +0 -13
- package/src/views/wasmSamplingDemoSampling.test.ts +0 -27
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import type { WaveformPoint } from '../types'
|
|
4
|
-
import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from './renderingOptions'
|
|
5
|
-
import {
|
|
6
|
-
peakPreservingPointSelectionStrategy,
|
|
7
|
-
resolveRenderablePointSelectionStrategy,
|
|
8
|
-
type RenderablePointSelectionContext,
|
|
9
|
-
} from './renderingStrategies'
|
|
10
|
-
|
|
11
|
-
const denseOptions = {
|
|
12
|
-
...DEFAULT_WAVEFORM_RENDERING_OPTIONS,
|
|
13
|
-
downsampleThreshold: 0,
|
|
14
|
-
maxPointsPerPixel: 1,
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('renderable point selection strategies', () => {
|
|
18
|
-
it('resolves complete-point selection at the configured boundaries', () => {
|
|
19
|
-
const context: RenderablePointSelectionContext = {
|
|
20
|
-
points: [
|
|
21
|
-
{ x: 0, y: 5 },
|
|
22
|
-
{ x: 1, y: 1 },
|
|
23
|
-
{ x: 2, y: 10 },
|
|
24
|
-
{ x: 3, y: 3 },
|
|
25
|
-
{ x: 4, y: 7 },
|
|
26
|
-
],
|
|
27
|
-
range: { start: 0, end: 5 },
|
|
28
|
-
domain: [0, 4],
|
|
29
|
-
width: 100,
|
|
30
|
-
options: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
|
|
31
|
-
}
|
|
32
|
-
const complete = resolveRenderablePointSelectionStrategy({
|
|
33
|
-
visibleCount: 100,
|
|
34
|
-
width: 100,
|
|
35
|
-
options: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
|
|
36
|
-
})(context)
|
|
37
|
-
const disabled = resolveRenderablePointSelectionStrategy({
|
|
38
|
-
visibleCount: 10_000,
|
|
39
|
-
width: 100,
|
|
40
|
-
options: { ...denseOptions, downsample: false },
|
|
41
|
-
})(context)
|
|
42
|
-
|
|
43
|
-
expect(complete).toEqual(context.points)
|
|
44
|
-
expect(disabled).toEqual(context.points)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('resolves peak-preserving selection for dense visible data', () => {
|
|
48
|
-
const context: RenderablePointSelectionContext = {
|
|
49
|
-
points: [
|
|
50
|
-
{ x: 0, y: 5 },
|
|
51
|
-
{ x: 1, y: 1 },
|
|
52
|
-
{ x: 2, y: 10 },
|
|
53
|
-
{ x: 3, y: 3 },
|
|
54
|
-
{ x: 4, y: 7 },
|
|
55
|
-
],
|
|
56
|
-
range: { start: 0, end: 5 },
|
|
57
|
-
domain: [0, 4],
|
|
58
|
-
width: 4,
|
|
59
|
-
options: denseOptions,
|
|
60
|
-
}
|
|
61
|
-
const selected = resolveRenderablePointSelectionStrategy({
|
|
62
|
-
visibleCount: 1_000,
|
|
63
|
-
width: 4,
|
|
64
|
-
options: denseOptions,
|
|
65
|
-
})(context)
|
|
66
|
-
|
|
67
|
-
expect(selected).toEqual(expect.arrayContaining([context.points[1], context.points[2]]))
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('retains first, last, minimum, and maximum points in a peak bucket', () => {
|
|
71
|
-
const points: WaveformPoint[] = [
|
|
72
|
-
{ x: 0, y: 5 },
|
|
73
|
-
{ x: 1, y: 1 },
|
|
74
|
-
{ x: 2, y: 10 },
|
|
75
|
-
{ x: 3, y: 3 },
|
|
76
|
-
{ x: 4, y: 7 },
|
|
77
|
-
]
|
|
78
|
-
const selected = peakPreservingPointSelectionStrategy({
|
|
79
|
-
points,
|
|
80
|
-
range: { start: 0, end: points.length },
|
|
81
|
-
domain: [0, 4],
|
|
82
|
-
width: 4,
|
|
83
|
-
options: denseOptions,
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
expect(selected[0]).toBe(points[0])
|
|
87
|
-
expect(selected.at(-1)).toBe(points.at(-1))
|
|
88
|
-
expect(selected.map((point) => point.y)).toEqual(expect.arrayContaining([1, 10]))
|
|
89
|
-
})
|
|
90
|
-
})
|
|
@@ -1,124 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
|
|
3
|
-
import { readFile } from 'node:fs/promises'
|
|
4
|
-
import { resolve } from 'node:path'
|
|
5
|
-
|
|
6
|
-
import { beforeAll, describe, expect, it } from 'vitest'
|
|
7
|
-
|
|
8
|
-
import { sampleWaveformReference } from './samplingReference'
|
|
9
|
-
import {
|
|
10
|
-
REFERENCE_AGGREGATE_VECTORS,
|
|
11
|
-
REFERENCE_SAMPLING_POINTS,
|
|
12
|
-
REFERENCE_SOURCE_INDEX_VECTORS,
|
|
13
|
-
} from './samplingReferenceVectors'
|
|
14
|
-
import {
|
|
15
|
-
calculateWasmRange,
|
|
16
|
-
createInitializedWasmSamplingBackend,
|
|
17
|
-
findWasmVisibleRange,
|
|
18
|
-
initializeWasmSampling,
|
|
19
|
-
sampleWaveformWasm,
|
|
20
|
-
} from './wasmSampling'
|
|
21
|
-
|
|
22
|
-
const x = Float64Array.from(REFERENCE_SAMPLING_POINTS, (point) => point.x)
|
|
23
|
-
const y = Float64Array.from(REFERENCE_SAMPLING_POINTS, (point) => point.y)
|
|
24
|
-
|
|
25
|
-
describe('sampleWaveformWasm', () => {
|
|
26
|
-
beforeAll(async () => {
|
|
27
|
-
await initializeWasmSampling(
|
|
28
|
-
await readFile(resolve(process.cwd(), 'wasm/pkg/waveform_sampling_wasm_bg.wasm')),
|
|
29
|
-
)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('matches the shared source-index vectors and TypeScript reference', async () => {
|
|
33
|
-
for (const vector of REFERENCE_SOURCE_INDEX_VECTORS) {
|
|
34
|
-
const wasm = await sampleWaveformWasm({ x, y, ...vector })
|
|
35
|
-
const reference = sampleWaveformReference({
|
|
36
|
-
points: REFERENCE_SAMPLING_POINTS,
|
|
37
|
-
strategy: vector.strategy,
|
|
38
|
-
targetPointCount: vector.targetPointCount,
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
expect(wasm.kind).toBe('source-indexes')
|
|
42
|
-
expect(reference.kind).toBe('source-indexes')
|
|
43
|
-
if (wasm.kind === 'source-indexes' && reference.kind === 'source-indexes') {
|
|
44
|
-
expect(Array.from(wasm.sourceIndexes)).toEqual(vector.expectedSourceIndexes)
|
|
45
|
-
expect(Array.from(wasm.sourceIndexes)).toEqual(Array.from(reference.sourceIndexes))
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('matches the shared aggregate vectors and TypeScript reference', async () => {
|
|
51
|
-
for (const vector of REFERENCE_AGGREGATE_VECTORS) {
|
|
52
|
-
const wasm = await sampleWaveformWasm({ x, y, ...vector })
|
|
53
|
-
const reference = sampleWaveformReference({
|
|
54
|
-
points: REFERENCE_SAMPLING_POINTS,
|
|
55
|
-
strategy: vector.strategy,
|
|
56
|
-
targetPointCount: vector.targetPointCount,
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
expect(wasm.kind).toBe('aggregates')
|
|
60
|
-
expect(reference.kind).toBe('aggregates')
|
|
61
|
-
if (wasm.kind === 'aggregates' && reference.kind === 'aggregates') {
|
|
62
|
-
expect(Array.from(wasm.x)).toEqual(vector.expectedX)
|
|
63
|
-
expect(Array.from(wasm.y)).toEqual(vector.expectedY)
|
|
64
|
-
expect(Array.from(wasm.x)).toEqual(Array.from(reference.x))
|
|
65
|
-
expect(Array.from(wasm.y)).toEqual(Array.from(reference.y))
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('preserves reference filtering, stable sorting, and original source indexes', async () => {
|
|
71
|
-
const wasm = await sampleWaveformWasm({
|
|
72
|
-
x: Float64Array.from([2, Number.NaN, 1, 1, 3]),
|
|
73
|
-
y: Float64Array.from([20, 99, 10, 11, Number.POSITIVE_INFINITY]),
|
|
74
|
-
strategy: 'none',
|
|
75
|
-
targetPointCount: 1,
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
expect(wasm).toEqual({ kind: 'source-indexes', sourceIndexes: Uint32Array.from([2, 3, 0]) })
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('calculates full domains and normalized visible ranges', async () => {
|
|
82
|
-
const range = await calculateWasmRange(
|
|
83
|
-
Float64Array.from([2, Number.NaN, 1, 1, 3]),
|
|
84
|
-
Float64Array.from([20, 99, 10, 11, Number.POSITIVE_INFINITY]),
|
|
85
|
-
)
|
|
86
|
-
const visible = await findWasmVisibleRange(
|
|
87
|
-
Float64Array.from([2, Number.NaN, 1, 1, 3]),
|
|
88
|
-
Float64Array.from([20, 99, 10, 11, Number.POSITIVE_INFINITY]),
|
|
89
|
-
[1, 1],
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
expect(range).toEqual({ x: [1, 2], y: [10, 20] })
|
|
93
|
-
expect(visible).toEqual([0, 2])
|
|
94
|
-
await expect(calculateWasmRange(Float64Array.of(0), new Float64Array())).rejects.toThrow(
|
|
95
|
-
'x and y must have the same length',
|
|
96
|
-
)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('matches the reference across strategy and target boundary cases', async () => {
|
|
100
|
-
const points = [
|
|
101
|
-
{ x: 3, y: 3 },
|
|
102
|
-
{ x: 1, y: -2 },
|
|
103
|
-
{ x: 1, y: 4 },
|
|
104
|
-
{ x: Number.NaN, y: 6 },
|
|
105
|
-
{ x: 2, y: -1 },
|
|
106
|
-
{ x: 4, y: Number.POSITIVE_INFINITY },
|
|
107
|
-
{ x: 5, y: 0 },
|
|
108
|
-
]
|
|
109
|
-
const caseX = Float64Array.from(points, (point) => point.x)
|
|
110
|
-
const caseY = Float64Array.from(points, (point) => point.y)
|
|
111
|
-
const strategies = ['none', 'peak', 'lttb', 'average', 'min', 'max', 'minmax', 'sum'] as const
|
|
112
|
-
const targets = [0, 1, 2, 3, 6, 20, Number.POSITIVE_INFINITY]
|
|
113
|
-
|
|
114
|
-
for (const strategy of strategies) {
|
|
115
|
-
for (const targetPointCount of targets) {
|
|
116
|
-
const wasm = await sampleWaveformWasm({
|
|
117
|
-
x: caseX,
|
|
118
|
-
y: caseY,
|
|
119
|
-
strategy,
|
|
120
|
-
targetPointCount,
|
|
121
|
-
})
|
|
122
|
-
const reference = sampleWaveformReference({ points, strategy, targetPointCount })
|
|
123
|
-
|
|
124
|
-
expect(wasm.kind).toBe(reference.kind)
|
|
125
|
-
if (wasm.kind === 'source-indexes' && reference.kind === 'source-indexes') {
|
|
126
|
-
expect(Array.from(wasm.sourceIndexes)).toEqual(Array.from(reference.sourceIndexes))
|
|
127
|
-
}
|
|
128
|
-
if (wasm.kind === 'aggregates' && reference.kind === 'aggregates') {
|
|
129
|
-
expect(Array.from(wasm.x)).toEqual(Array.from(reference.x))
|
|
130
|
-
expect(Array.from(wasm.y)).toEqual(Array.from(reference.y))
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('rejects coordinate arrays with unequal lengths', async () => {
|
|
137
|
-
await expect(
|
|
138
|
-
sampleWaveformWasm({
|
|
139
|
-
x: Float64Array.of(0),
|
|
140
|
-
y: new Float64Array(),
|
|
141
|
-
strategy: 'none',
|
|
142
|
-
targetPointCount: 1,
|
|
143
|
-
}),
|
|
144
|
-
).rejects.toThrow('x and y must have the same length')
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('keeps a bounded dataset index in WASM for unaligned viewport queries and releases it', () => {
|
|
148
|
-
const backend = createInitializedWasmSamplingBackend()
|
|
149
|
-
const handle = backend.registerDataset(x, y, 1024 * 1024)
|
|
150
|
-
const visible = REFERENCE_SAMPLING_POINTS.slice(2, 9)
|
|
151
|
-
|
|
152
|
-
for (const strategy of ['peak', 'min', 'max', 'minmax'] as const) {
|
|
153
|
-
const actual = backend.sampleDataset(handle, 2, 9, strategy, 6)
|
|
154
|
-
const expected = sampleWaveformReference({
|
|
155
|
-
points: visible,
|
|
156
|
-
strategy,
|
|
157
|
-
targetPointCount: 6,
|
|
158
|
-
})
|
|
159
|
-
expect(actual.kind).toBe('source-indexes')
|
|
160
|
-
expect(expected.kind).toBe('source-indexes')
|
|
161
|
-
if (actual.kind === 'source-indexes' && expected.kind === 'source-indexes') {
|
|
162
|
-
expect(Array.from(actual.sourceIndexes)).toEqual(
|
|
163
|
-
Array.from(expected.sourceIndexes, (index) => index + 2),
|
|
164
|
-
)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const aggregates = backend.sampleDataset(handle, 2, 9, 'average', 4)
|
|
169
|
-
const expectedAggregates = sampleWaveformReference({
|
|
170
|
-
points: visible,
|
|
171
|
-
strategy: 'average',
|
|
172
|
-
targetPointCount: 4,
|
|
173
|
-
})
|
|
174
|
-
expect(aggregates.kind).toBe('aggregates')
|
|
175
|
-
expect(expectedAggregates.kind).toBe('aggregates')
|
|
176
|
-
if (aggregates.kind === 'aggregates' && expectedAggregates.kind === 'aggregates') {
|
|
177
|
-
aggregates.x.forEach((value, index) =>
|
|
178
|
-
expect(value).toBeCloseTo(expectedAggregates.x[index]!, 12),
|
|
179
|
-
)
|
|
180
|
-
aggregates.y.forEach((value, index) =>
|
|
181
|
-
expect(value).toBeCloseTo(expectedAggregates.y[index]!, 12),
|
|
182
|
-
)
|
|
183
|
-
}
|
|
184
|
-
expect(backend.datasetIndexBytes(handle)).toBeGreaterThan(0)
|
|
185
|
-
backend.disposeDataset(handle)
|
|
186
|
-
expect(backend.datasetIndexBytes(handle)).toBe(0)
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
it('registers evenly sampled values without an explicit X column', () => {
|
|
190
|
-
const backend = createInitializedWasmSamplingBackend()
|
|
191
|
-
const handle = backend.registerSamples(
|
|
192
|
-
new Float64Array([1, 9, 2]),
|
|
193
|
-
new Uint32Array([0, 2, 3]),
|
|
194
|
-
10,
|
|
195
|
-
2,
|
|
196
|
-
1024 * 1024,
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
expect(backend.sampleDataset(handle, 0, 3, 'max', 3)).toMatchObject({
|
|
200
|
-
kind: 'source-indexes',
|
|
201
|
-
sourceIndexes: Uint32Array.from([0, 1, 2]),
|
|
202
|
-
})
|
|
203
|
-
backend.disposeDataset(handle)
|
|
204
|
-
})
|
|
205
|
-
})
|
|
@@ -1,69 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { isCurrentWorkerSamplingResponse } from './protocol'
|
|
4
|
-
|
|
5
|
-
describe('worker sampling response validation', () => {
|
|
6
|
-
it('rejects old request ids and outdated per-series revisions before UI state is committed', () => {
|
|
7
|
-
const current = { requestId: 12, revisions: { alpha: 2, beta: 1 } }
|
|
8
|
-
const response = {
|
|
9
|
-
type: 'sample-viewport-response' as const,
|
|
10
|
-
requestId: 12,
|
|
11
|
-
results: [
|
|
12
|
-
{ datasetId: 'alpha', revision: 2 },
|
|
13
|
-
{ datasetId: 'beta', revision: 0 },
|
|
14
|
-
],
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
expect(isCurrentWorkerSamplingResponse({ ...response, requestId: 11 } as never, current)).toBe(
|
|
18
|
-
false,
|
|
19
|
-
)
|
|
20
|
-
expect(isCurrentWorkerSamplingResponse(response as never, current)).toBe(false)
|
|
21
|
-
expect(
|
|
22
|
-
isCurrentWorkerSamplingResponse(
|
|
23
|
-
{
|
|
24
|
-
...response,
|
|
25
|
-
results: [
|
|
26
|
-
{ datasetId: 'alpha', revision: 2 },
|
|
27
|
-
{ datasetId: 'beta', revision: 1 },
|
|
28
|
-
],
|
|
29
|
-
} as never,
|
|
30
|
-
current,
|
|
31
|
-
),
|
|
32
|
-
).toBe(true)
|
|
33
|
-
})
|
|
34
|
-
})
|