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
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { scaleLinear, zoomIdentity, type ZoomTransform } from 'd3'
|
|
2
2
|
|
|
3
3
|
import type { WaveformPoint } from '../../types'
|
|
4
|
+
import { pointSourceFromPoints, type WaveformPointSource } from '../../core/waveformPointSource'
|
|
4
5
|
import { ZOOM_CONSTRAINTS } from '../core/constants'
|
|
5
6
|
|
|
6
7
|
interface PointSeriesSource {
|
|
7
8
|
points: WaveformPoint[]
|
|
9
|
+
source?: WaveformPointSource
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export type ZoomSeriesGroup = readonly PointSeriesSource[]
|
|
@@ -30,7 +32,9 @@ function uniqueXValues(group: ZoomSeriesGroup, boundary: [number, number]): numb
|
|
|
30
32
|
if (cached) return cached
|
|
31
33
|
const values = new Set<number>()
|
|
32
34
|
for (const series of group) {
|
|
33
|
-
|
|
35
|
+
const source = series.source ?? pointSourceFromPoints(series.points)
|
|
36
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
37
|
+
const point = source.pointAt(index)!
|
|
34
38
|
if (point.x >= boundary[0] && point.x <= boundary[1]) values.add(point.x)
|
|
35
39
|
}
|
|
36
40
|
}
|
|
@@ -12,12 +12,21 @@ export type {
|
|
|
12
12
|
WaveformXDomainStrategy,
|
|
13
13
|
WaveformAnnotationStyle,
|
|
14
14
|
WaveformAnnotation,
|
|
15
|
+
WaveformSamplingMode,
|
|
16
|
+
WaveformSamplingStrategy,
|
|
17
|
+
WaveformSamplingOptions,
|
|
18
|
+
WaveformSamplingBackend,
|
|
19
|
+
WaveformSamplingDiagnostics,
|
|
20
|
+
WaveformSamplingError,
|
|
15
21
|
WaveformRenderingOptions,
|
|
16
22
|
WaveformPlotMargin,
|
|
17
23
|
WaveformTitleTextStyle,
|
|
18
24
|
WaveformTitleOptions,
|
|
19
25
|
WaveformFrameStyle,
|
|
20
26
|
SingleWaveformData,
|
|
27
|
+
WaveformTypedValues,
|
|
28
|
+
TypedSampleData,
|
|
29
|
+
TypedPointData,
|
|
21
30
|
WaveformSeries,
|
|
22
31
|
WaveformLineStyle,
|
|
23
32
|
WaveformData,
|
|
@@ -74,8 +74,7 @@ describe('WaveformChart', () => {
|
|
|
74
74
|
|
|
75
75
|
it('removes the later compact row top tick when the Y-axis end has a floating-point tail', async () => {
|
|
76
76
|
const yDomain: [number, number] = [-1, 0.2]
|
|
77
|
-
const calculatedAxisEnd =
|
|
78
|
-
yDomain[0] + ((yDomain[1] - yDomain[0]) * 4) / 4
|
|
77
|
+
const calculatedAxisEnd = yDomain[0] + ((yDomain[1] - yDomain[0]) * 4) / 4
|
|
79
78
|
expect(calculatedAxisEnd).not.toBe(yDomain[1])
|
|
80
79
|
|
|
81
80
|
const wrapper = await mountSizedChart(
|
|
@@ -84,11 +83,23 @@ describe('WaveformChart', () => {
|
|
|
84
83
|
series: [
|
|
85
84
|
{
|
|
86
85
|
name: 'top',
|
|
87
|
-
data: {
|
|
86
|
+
data: {
|
|
87
|
+
kind: 'points',
|
|
88
|
+
points: [
|
|
89
|
+
{ x: 0, y: -0.8 },
|
|
90
|
+
{ x: 1, y: 0.1 },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
88
93
|
},
|
|
89
94
|
{
|
|
90
95
|
name: 'bottom',
|
|
91
|
-
data: {
|
|
96
|
+
data: {
|
|
97
|
+
kind: 'points',
|
|
98
|
+
points: [
|
|
99
|
+
{ x: 0, y: -0.7 },
|
|
100
|
+
{ x: 1, y: 0 },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
92
103
|
},
|
|
93
104
|
],
|
|
94
105
|
},
|
|
@@ -99,12 +99,17 @@ describe('WaveformChart', () => {
|
|
|
99
99
|
{ rendering: { downsampleThreshold: 1_000, maxPointsPerPixel: 4 } },
|
|
100
100
|
)
|
|
101
101
|
const overlayWidth = Number(wrapper.get('.waveform-chart__overlay').attributes('width'))
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
await vi.waitFor(
|
|
103
|
+
() => {
|
|
104
|
+
expect(wrapper.emitted('sampling-complete')?.length).toBeGreaterThan(0)
|
|
105
|
+
const path = wrapper.get('.waveform-chart__line').attributes('d') ?? ''
|
|
106
|
+
const renderedPointCount = path.match(/[ML]/g)?.length ?? 0
|
|
107
|
+
expect(renderedPointCount).toBeGreaterThan(0)
|
|
108
|
+
expect(renderedPointCount).toBeLessThanOrEqual(Math.floor(overlayWidth * 4) + 2)
|
|
109
|
+
expect(path).toContain(',0')
|
|
110
|
+
},
|
|
111
|
+
{ timeout: 5_000 },
|
|
112
|
+
)
|
|
108
113
|
})
|
|
109
114
|
|
|
110
115
|
it('bounds dense decorations by pixel spacing while keeping one SVG path per series', async () => {
|
|
@@ -169,9 +169,7 @@ describe('WaveformChart', () => {
|
|
|
169
169
|
yAxisLabels[0].attributes('transform')?.match(/^translate\(([-\d.]+),/)?.[1],
|
|
170
170
|
)
|
|
171
171
|
expect(labelX).toBeLessThan(-46)
|
|
172
|
-
expect(Number(wrapper.get('.waveform-chart__y-axis-label-bg').attributes('x'))).toBe(
|
|
173
|
-
labelX - 6,
|
|
174
|
-
)
|
|
172
|
+
expect(Number(wrapper.get('.waveform-chart__y-axis-label-bg').attributes('x'))).toBe(labelX - 6)
|
|
175
173
|
expect(yAxisLabels.every((label) => !label.text().includes('(T)'))).toBe(true)
|
|
176
174
|
expect(wrapper.findAll('.waveform-chart__track-label')).toHaveLength(0)
|
|
177
175
|
expect(
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { flushPromises } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { flushAnimationFrames } from '../../test/setup'
|
|
5
|
+
import { mountSizedChart } from '../../test/waveformChart'
|
|
6
|
+
|
|
7
|
+
function densePoints(count: number) {
|
|
8
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
9
|
+
x: index,
|
|
10
|
+
y: index === count - 1 ? 100 : Math.sin(index / 10),
|
|
11
|
+
}))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function samplingEvents(count: number, rendering = {}) {
|
|
15
|
+
const wrapper = await mountSizedChart(
|
|
16
|
+
{ kind: 'points', points: densePoints(count) },
|
|
17
|
+
{ rendering, grid: { rowCount: 1, columnCount: 1 } },
|
|
18
|
+
)
|
|
19
|
+
await flushPromises()
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, 120))
|
|
21
|
+
return wrapper
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function compactSamplingEvents(count: number, rendering = {}) {
|
|
25
|
+
const wrapper = await mountSizedChart(
|
|
26
|
+
{
|
|
27
|
+
kind: 'typed-points',
|
|
28
|
+
x: Float64Array.from({ length: count }, (_, index) => index),
|
|
29
|
+
y: Float32Array.from({ length: count }, (_, index) =>
|
|
30
|
+
index === count - 1 ? 100 : Math.sin(index / 10),
|
|
31
|
+
),
|
|
32
|
+
},
|
|
33
|
+
{ rendering, grid: { rowCount: 1, columnCount: 1 } },
|
|
34
|
+
)
|
|
35
|
+
await flushPromises()
|
|
36
|
+
await new Promise((resolve) => setTimeout(resolve, 120))
|
|
37
|
+
return wrapper
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('WaveformChart worker sampling integration', () => {
|
|
41
|
+
it('uses raw source points at the auto threshold and JavaScript fallback above it', async () => {
|
|
42
|
+
const threshold = await samplingEvents(1_000)
|
|
43
|
+
const sampled = await samplingEvents(1_001)
|
|
44
|
+
|
|
45
|
+
expect(threshold.emitted('sampling-complete')?.at(-1)?.[0]).toMatchObject({
|
|
46
|
+
mode: 'auto',
|
|
47
|
+
backend: 'raw',
|
|
48
|
+
visiblePointCount: 1_000,
|
|
49
|
+
renderedPointCount: 1_000,
|
|
50
|
+
})
|
|
51
|
+
expect(sampled.emitted('sampling-complete')?.at(-1)?.[0]).toMatchObject({
|
|
52
|
+
mode: 'auto',
|
|
53
|
+
backend: 'javascript',
|
|
54
|
+
visiblePointCount: 1_001,
|
|
55
|
+
})
|
|
56
|
+
const path = sampled.get('.waveform-chart__line').attributes('d') ?? ''
|
|
57
|
+
expect(path.match(/[ML]/g)?.length).toBeLessThan(4_000)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('bypasses worker sampling in raw mode and reports a forced-WASM failure without a fallback', async () => {
|
|
61
|
+
const raw = await samplingEvents(1_001, { sampling: { mode: 'raw' } })
|
|
62
|
+
const wasm = await samplingEvents(1_001, {
|
|
63
|
+
sampling: { mode: 'wasm', wasmFailureFallback: 'error' },
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
expect(raw.emitted('sampling-complete')?.at(-1)?.[0]).toMatchObject({
|
|
67
|
+
mode: 'raw',
|
|
68
|
+
backend: 'raw',
|
|
69
|
+
renderedPointCount: 1_001,
|
|
70
|
+
})
|
|
71
|
+
expect(raw.emitted('sampling-error')).toBeUndefined()
|
|
72
|
+
expect(wasm.emitted('sampling-complete')?.at(-1)?.[0]).toMatchObject({
|
|
73
|
+
mode: 'wasm',
|
|
74
|
+
backend: 'unavailable',
|
|
75
|
+
})
|
|
76
|
+
expect(wasm.emitted('sampling-error')?.at(-1)?.[0]).toMatchObject({
|
|
77
|
+
mode: 'wasm',
|
|
78
|
+
fallback: 'none',
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('keeps hover bound to complete source data after sampled SVG geometry is applied', async () => {
|
|
83
|
+
const wrapper = await samplingEvents(1_001)
|
|
84
|
+
const overlay = wrapper.get('.waveform-chart__overlay')
|
|
85
|
+
const width = Number(overlay.attributes('width'))
|
|
86
|
+
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
87
|
+
value: () => ({ left: 0, top: 0, width, height: 290 }),
|
|
88
|
+
})
|
|
89
|
+
overlay.element.dispatchEvent(
|
|
90
|
+
new MouseEvent('pointermove', { clientX: width, clientY: 120, bubbles: true }),
|
|
91
|
+
)
|
|
92
|
+
flushAnimationFrames()
|
|
93
|
+
await flushPromises()
|
|
94
|
+
|
|
95
|
+
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: 1_000, y: 100 }])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('samples dense typed coordinates while retaining the full compact source for hover', async () => {
|
|
99
|
+
const wrapper = await compactSamplingEvents(1_001)
|
|
100
|
+
expect(wrapper.emitted('sampling-complete')?.at(-1)?.[0]).toMatchObject({
|
|
101
|
+
backend: 'javascript',
|
|
102
|
+
sourcePointCount: 1_001,
|
|
103
|
+
visiblePointCount: 1_001,
|
|
104
|
+
})
|
|
105
|
+
const overlay = wrapper.get('.waveform-chart__overlay')
|
|
106
|
+
const width = Number(overlay.attributes('width'))
|
|
107
|
+
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
108
|
+
value: () => ({ left: 0, top: 0, width, height: 290 }),
|
|
109
|
+
})
|
|
110
|
+
overlay.element.dispatchEvent(
|
|
111
|
+
new MouseEvent('pointermove', { clientX: width, clientY: 120, bubbles: true }),
|
|
112
|
+
)
|
|
113
|
+
flushAnimationFrames()
|
|
114
|
+
await flushPromises()
|
|
115
|
+
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: 1_000, y: 100 }])
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -128,12 +128,24 @@ describe('WaveformChart x domain strategy', () => {
|
|
|
128
128
|
{
|
|
129
129
|
id: 'first',
|
|
130
130
|
name: '第一轨',
|
|
131
|
-
data: {
|
|
131
|
+
data: {
|
|
132
|
+
kind: 'points',
|
|
133
|
+
points: [
|
|
134
|
+
{ x: 0, y: 0 },
|
|
135
|
+
{ x: 10, y: 1 },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
132
138
|
},
|
|
133
139
|
{
|
|
134
140
|
id: 'second',
|
|
135
141
|
name: '第二轨',
|
|
136
|
-
data: {
|
|
142
|
+
data: {
|
|
143
|
+
kind: 'points',
|
|
144
|
+
points: [
|
|
145
|
+
{ x: 100, y: 0 },
|
|
146
|
+
{ x: 110, y: 1 },
|
|
147
|
+
],
|
|
148
|
+
},
|
|
137
149
|
},
|
|
138
150
|
],
|
|
139
151
|
},
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { WaveformPoint, WaveformTypedValues } from '../types'
|
|
2
|
+
import type { WaveformPointSource, WorkerSamplingDataset } from './waveformPointSource'
|
|
3
|
+
|
|
4
|
+
function bound(length: number, xAt: (index: number) => number, value: number, upper: boolean) {
|
|
5
|
+
let start = 0
|
|
6
|
+
let end = length
|
|
7
|
+
while (start < end) {
|
|
8
|
+
const middle = start + Math.floor((end - start) / 2)
|
|
9
|
+
if (upper ? xAt(middle) <= value : xAt(middle) < value) start = middle + 1
|
|
10
|
+
else end = middle
|
|
11
|
+
}
|
|
12
|
+
return start
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createCompactSamplePointSource(
|
|
16
|
+
y: WaveformTypedValues,
|
|
17
|
+
sampleRate: number,
|
|
18
|
+
startTime: number,
|
|
19
|
+
sourceIndexes?: Uint32Array,
|
|
20
|
+
): WaveformPointSource {
|
|
21
|
+
const xAt = (index: number) => startTime + (sourceIndexes?.[index] ?? index) / sampleRate
|
|
22
|
+
const pointAt = (index: number) =>
|
|
23
|
+
index < 0 || index >= y.length ? undefined : { x: xAt(index), y: y[index]! }
|
|
24
|
+
return {
|
|
25
|
+
isCompact: true,
|
|
26
|
+
length: y.length,
|
|
27
|
+
pointAt,
|
|
28
|
+
pointsInRange(start = 0, end = y.length) {
|
|
29
|
+
const rangeStart = Math.max(0, Math.min(y.length, Math.floor(start)))
|
|
30
|
+
const rangeEnd = Math.max(rangeStart, Math.min(y.length, Math.ceil(end)))
|
|
31
|
+
return Array.from({ length: rangeEnd - rangeStart }, (_, offset) =>
|
|
32
|
+
pointAt(rangeStart + offset),
|
|
33
|
+
).filter((point): point is WaveformPoint => point !== undefined)
|
|
34
|
+
},
|
|
35
|
+
visibleRange(domain: [number, number]) {
|
|
36
|
+
const start = Math.min(domain[0], domain[1])
|
|
37
|
+
const end = Math.max(domain[0], domain[1])
|
|
38
|
+
return {
|
|
39
|
+
start: bound(y.length, xAt, start, false),
|
|
40
|
+
end: bound(y.length, xAt, end, true),
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
nearestPoint(x: number) {
|
|
44
|
+
if (!Number.isFinite(x) || !y.length) return undefined
|
|
45
|
+
if (x <= xAt(0)) return pointAt(0)
|
|
46
|
+
if (x >= xAt(y.length - 1)) return pointAt(y.length - 1)
|
|
47
|
+
const right = bound(y.length, xAt, x, false)
|
|
48
|
+
const left = Math.max(0, right - 1)
|
|
49
|
+
return Math.abs(x - xAt(left)) < Math.abs(xAt(right) - x) ? pointAt(left) : pointAt(right)
|
|
50
|
+
},
|
|
51
|
+
metrics() {
|
|
52
|
+
let yMinimum = Number.POSITIVE_INFINITY
|
|
53
|
+
let yMaximum = Number.NEGATIVE_INFINITY
|
|
54
|
+
for (const value of y) {
|
|
55
|
+
yMinimum = Math.min(yMinimum, value)
|
|
56
|
+
yMaximum = Math.max(yMaximum, value)
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
xMinimum: y.length ? xAt(0) : Number.POSITIVE_INFINITY,
|
|
60
|
+
xMaximum: y.length ? xAt(y.length - 1) : Number.NEGATIVE_INFINITY,
|
|
61
|
+
yMinimum,
|
|
62
|
+
yMaximum,
|
|
63
|
+
hasErrorPoints: false,
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
toWorkerDataset(): WorkerSamplingDataset {
|
|
67
|
+
return {
|
|
68
|
+
kind: 'samples',
|
|
69
|
+
values: y.slice(),
|
|
70
|
+
sampleRate,
|
|
71
|
+
startTime,
|
|
72
|
+
...(sourceIndexes ? { sourceIndexes: sourceIndexes.slice() } : {}),
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/core/data.test.ts
CHANGED
|
@@ -17,6 +17,97 @@ describe('waveform data normalization', () => {
|
|
|
17
17
|
])
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
+
it('normalizes typed samples without mutating their values or buffer', () => {
|
|
21
|
+
const values = new Float32Array([1, Number.NaN, 3])
|
|
22
|
+
const snapshot = values.slice()
|
|
23
|
+
|
|
24
|
+
expect(
|
|
25
|
+
normalizeWaveformData({
|
|
26
|
+
kind: 'typed-samples',
|
|
27
|
+
values,
|
|
28
|
+
sampleRate: 2,
|
|
29
|
+
startTime: 1,
|
|
30
|
+
}),
|
|
31
|
+
).toEqual([
|
|
32
|
+
{ x: 1, y: 1 },
|
|
33
|
+
{ x: 2, y: 3 },
|
|
34
|
+
])
|
|
35
|
+
expect(values).toEqual(snapshot)
|
|
36
|
+
expect(values.buffer.byteLength).toBe(snapshot.buffer.byteLength)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('rejects malformed typed sample input without coercing numeric fields', () => {
|
|
40
|
+
expect(
|
|
41
|
+
normalizeWaveformData({
|
|
42
|
+
kind: 'typed-samples',
|
|
43
|
+
values: new Float64Array([1]),
|
|
44
|
+
sampleRate: Number.POSITIVE_INFINITY,
|
|
45
|
+
}),
|
|
46
|
+
).toEqual([])
|
|
47
|
+
expect(
|
|
48
|
+
normalizeWaveformData({
|
|
49
|
+
kind: 'typed-samples',
|
|
50
|
+
values: new Float64Array([1]),
|
|
51
|
+
sampleRate: 1,
|
|
52
|
+
startTime: Number.NaN,
|
|
53
|
+
}),
|
|
54
|
+
).toEqual([])
|
|
55
|
+
expect(
|
|
56
|
+
normalizeWaveformData({
|
|
57
|
+
kind: 'typed-samples',
|
|
58
|
+
values: new Int32Array([1]),
|
|
59
|
+
sampleRate: 1,
|
|
60
|
+
} as unknown as Parameters<typeof normalizeWaveformData>[0]),
|
|
61
|
+
).toEqual([])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('normalizes typed points, errors, and unordered coordinates without mutation', () => {
|
|
65
|
+
const x = new Float64Array([2, 1, 1, Number.NaN])
|
|
66
|
+
const y = new Float32Array([20, 10, 11, 12])
|
|
67
|
+
const error = new Float64Array([1, -1, Number.NaN, 4])
|
|
68
|
+
const lowerError = new Float32Array([2, 3, 4, 5])
|
|
69
|
+
const upperError = new Float64Array([3, 4, 5, 6])
|
|
70
|
+
const snapshots = [x.slice(), y.slice(), error.slice(), lowerError.slice(), upperError.slice()]
|
|
71
|
+
|
|
72
|
+
expect(
|
|
73
|
+
normalizeWaveformData({ kind: 'typed-points', x, y, error, lowerError, upperError }),
|
|
74
|
+
).toEqual([
|
|
75
|
+
{ x: 1, y: 10, lowerError: 3, upperError: 4 },
|
|
76
|
+
{ x: 1, y: 11, lowerError: 4, upperError: 5 },
|
|
77
|
+
{ x: 2, y: 20, error: 1, lowerError: 2, upperError: 3 },
|
|
78
|
+
])
|
|
79
|
+
expect(x).toEqual(snapshots[0])
|
|
80
|
+
expect(y).toEqual(snapshots[1])
|
|
81
|
+
expect(error).toEqual(snapshots[2])
|
|
82
|
+
expect(lowerError).toEqual(snapshots[3])
|
|
83
|
+
expect(upperError).toEqual(snapshots[4])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('rejects typed points with unsupported arrays or mismatched field lengths', () => {
|
|
87
|
+
expect(
|
|
88
|
+
normalizeWaveformData({
|
|
89
|
+
kind: 'typed-points',
|
|
90
|
+
x: new Float64Array([0, 1]),
|
|
91
|
+
y: new Float32Array([1]),
|
|
92
|
+
}),
|
|
93
|
+
).toEqual([])
|
|
94
|
+
expect(
|
|
95
|
+
normalizeWaveformData({
|
|
96
|
+
kind: 'typed-points',
|
|
97
|
+
x: new Float64Array([0]),
|
|
98
|
+
y: new Float64Array([1]),
|
|
99
|
+
upperError: new Float32Array([1, 2]),
|
|
100
|
+
}),
|
|
101
|
+
).toEqual([])
|
|
102
|
+
expect(
|
|
103
|
+
normalizeWaveformData({
|
|
104
|
+
kind: 'typed-points',
|
|
105
|
+
x: new Float32Array([0]),
|
|
106
|
+
y: new Float32Array([1]),
|
|
107
|
+
} as unknown as Parameters<typeof normalizeWaveformData>[0]),
|
|
108
|
+
).toEqual([])
|
|
109
|
+
})
|
|
110
|
+
|
|
20
111
|
it('skips sorting already ordered points and normalizes errors', () => {
|
|
21
112
|
const sortSpy = vi.spyOn(Array.prototype, 'sort')
|
|
22
113
|
try {
|
|
@@ -124,4 +215,27 @@ describe('waveform data normalization', () => {
|
|
|
124
215
|
|
|
125
216
|
expect(result.map((series) => series.shotNo)).toEqual(['13300', undefined])
|
|
126
217
|
})
|
|
218
|
+
|
|
219
|
+
it('accepts compact inputs inside regular series containers', () => {
|
|
220
|
+
const result = normalizeWaveformSeries({
|
|
221
|
+
kind: 'series',
|
|
222
|
+
series: [
|
|
223
|
+
{
|
|
224
|
+
id: 'typed',
|
|
225
|
+
name: 'Typed',
|
|
226
|
+
data: {
|
|
227
|
+
kind: 'typed-points',
|
|
228
|
+
x: new Float64Array([0, 1]),
|
|
229
|
+
y: new Float32Array([2, 3]),
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
expect(result).toHaveLength(1)
|
|
236
|
+
expect(result[0]?.points).toEqual([
|
|
237
|
+
{ x: 0, y: 2 },
|
|
238
|
+
{ x: 1, y: 3 },
|
|
239
|
+
])
|
|
240
|
+
})
|
|
127
241
|
})
|