waveform-analysis 0.1.48 → 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 (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
@@ -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
- for (const point of series.points) {
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: { kind: 'points', points: [{ x: 0, y: -0.8 }, { x: 1, y: 0.1 }] },
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: { kind: 'points', points: [{ x: 0, y: -0.7 }, { x: 1, y: 0 }] },
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
- const path = wrapper.get('.waveform-chart__line').attributes('d') ?? ''
103
- const renderedPointCount = path.match(/[ML]/g)?.length ?? 0
104
-
105
- expect(renderedPointCount).toBeGreaterThan(0)
106
- expect(renderedPointCount).toBeLessThanOrEqual(Math.floor(overlayWidth * 4) + 2)
107
- expect(path).toContain(',0')
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: { kind: 'points', points: [{ x: 0, y: 0 }, { x: 10, y: 1 }] },
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: { kind: 'points', points: [{ x: 100, y: 0 }, { x: 110, y: 1 }] },
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
+ }
@@ -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
  })