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.
Files changed (83) hide show
  1. package/README.md +9 -2
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +953 -946
  4. package/dist/style.css +1 -1
  5. package/dist/types/types/chart.d.ts +2 -1
  6. package/package.json +1 -1
  7. package/src/components/WaveformChart.vue +1 -1
  8. package/src/components/core/xDomain.ts +26 -2
  9. package/src/components/rendering/WaveformLegend.vue +1 -1
  10. package/src/components/rendering/WaveformSeriesLayer.vue +1 -1
  11. package/src/demo/DemoChartHost.vue +1 -0
  12. package/src/types/chart.ts +2 -1
  13. package/src/App.test.ts +0 -400
  14. package/src/components/annotation/components.test.ts +0 -372
  15. package/src/components/annotation/markup.test.ts +0 -371
  16. package/src/components/annotation/serialization.test.ts +0 -114
  17. package/src/components/core/constants.test.ts +0 -29
  18. package/src/components/core/fixedYDomainLayout.test.ts +0 -204
  19. package/src/components/core/grid.test.ts +0 -197
  20. package/src/components/core/latestTaskScheduler.test.ts +0 -51
  21. package/src/components/core/layout.test.ts +0 -373
  22. package/src/components/core/title.test.ts +0 -76
  23. package/src/components/core/useWaveformData.test.ts +0 -95
  24. package/src/components/core/useWaveformRenderSampling.test.ts +0 -17
  25. package/src/components/core/xDomain.test.ts +0 -28
  26. package/src/components/core/yAxisNice.test.ts +0 -174
  27. package/src/components/core/yAxisSlotLayout.test.ts +0 -165
  28. package/src/components/core/yDomain.test.ts +0 -56
  29. package/src/components/interaction/WaveformTooltip.test.ts +0 -276
  30. package/src/components/interaction/viewportInteractionState.test.ts +0 -101
  31. package/src/components/utils/useAnimationFrameThrottle.test.ts +0 -53
  32. package/src/components/waveformChartCases/annotationEditingAndReprojection.test.ts +0 -257
  33. package/src/components/waveformChartCases/compactAndSharedHover.test.ts +0 -358
  34. package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +0 -358
  35. package/src/components/waveformChartCases/dataLifecycle.test.ts +0 -310
  36. package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +0 -389
  37. package/src/components/waveformChartCases/gridFill.test.ts +0 -95
  38. package/src/components/waveformChartCases/incompleteDataHover.test.ts +0 -155
  39. package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +0 -328
  40. package/src/components/waveformChartCases/layoutAndDomains.test.ts +0 -371
  41. package/src/components/waveformChartCases/legendVisibility.test.ts +0 -350
  42. package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +0 -400
  43. package/src/components/waveformChartCases/multiAxisLabelAlignment.test.ts +0 -61
  44. package/src/components/waveformChartCases/presentationAndAxes.test.ts +0 -345
  45. package/src/components/waveformChartCases/presentationMode.test.ts +0 -327
  46. package/src/components/waveformChartCases/renderingStyles.test.ts +0 -283
  47. package/src/components/waveformChartCases/sizingAndTitles.test.ts +0 -370
  48. package/src/components/waveformChartCases/trackOrder.test.ts +0 -323
  49. package/src/components/waveformChartCases/viewportBoxAndPan.test.ts +0 -365
  50. package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +0 -311
  51. package/src/components/waveformChartCases/viewportLifecycle.test.ts +0 -132
  52. package/src/components/waveformChartCases/viewportReset.test.ts +0 -251
  53. package/src/components/waveformChartCases/wheelZoomOut.test.ts +0 -114
  54. package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +0 -117
  55. package/src/components/waveformChartCases/xDomainStrategy.test.ts +0 -168
  56. package/src/components/waveformChartCases/yAxisLayout.test.ts +0 -103
  57. package/src/components/waveformChartCases/zoomIntentAndDataRebind.test.ts +0 -213
  58. package/src/components/waveformChartCases/zoomPointConstraints.test.ts +0 -308
  59. package/src/components/waveformLegendGeometry.test.ts +0 -21
  60. package/src/components/waveformNormalization.test.ts +0 -170
  61. package/src/core/data.test.ts +0 -241
  62. package/src/core/rendering.test.ts +0 -267
  63. package/src/core/renderingStrategies.test.ts +0 -90
  64. package/src/core/samplingReference.test.ts +0 -124
  65. package/src/core/wasmSampling.test.ts +0 -205
  66. package/src/core/workerSampling/multiresolution.test.ts +0 -69
  67. package/src/core/workerSampling/protocol.test.ts +0 -34
  68. package/src/core/workerSampling/repository.test.ts +0 -400
  69. package/src/core/workerSampling/repositoryCache.test.ts +0 -163
  70. package/src/core/workerSampling/repositorySampling.test.ts +0 -106
  71. package/src/core/workerSampling/repositoryTypedDataset.test.ts +0 -89
  72. package/src/core/workerSampling/repositoryWasmDataset.test.ts +0 -70
  73. package/src/data/simulatedWaveforms.test.ts +0 -60
  74. package/src/demo/plotMarginControls.test.ts +0 -29
  75. package/src/demo/xAxisLabelControls.test.ts +0 -86
  76. package/src/test/setup.ts +0 -82
  77. package/src/test/waveformChart.ts +0 -75
  78. package/src/utils/domain.test.ts +0 -28
  79. package/src/utils/formatters.test.ts +0 -131
  80. package/src/utils/sampling.test.ts +0 -226
  81. package/src/views/FixedYDomainDemo.test.ts +0 -44
  82. package/src/views/WasmSamplingDemo.test.ts +0 -13
  83. package/src/views/wasmSamplingDemoSampling.test.ts +0 -27
@@ -1,170 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { prepareWaveformSeries } from './core/useWaveformData'
3
- import { normalizeWaveformData, normalizeWaveformSeries } from './waveform'
4
- describe('normalizeWaveformData', () => {
5
- it('converts samples into time-based points', () => {
6
- expect(
7
- normalizeWaveformData({
8
- kind: 'samples',
9
- values: [2, 4, 6],
10
- sampleRate: 2,
11
- startTime: 1,
12
- }),
13
- ).toEqual([
14
- { x: 1, y: 2 },
15
- { x: 1.5, y: 4 },
16
- { x: 2, y: 6 },
17
- ])
18
- })
19
-
20
- it('sorts explicit points and filters non-finite values', () => {
21
- expect(
22
- normalizeWaveformData({
23
- kind: 'points',
24
- points: [
25
- { x: 2, y: 4 },
26
- { x: Number.NaN, y: 3 },
27
- { x: 1, y: 2 },
28
- ],
29
- }),
30
- ).toEqual([
31
- { x: 1, y: 2 },
32
- { x: 2, y: 4 },
33
- ])
34
- })
35
-
36
- it('returns no points for an invalid sample rate', () => {
37
- expect(normalizeWaveformData({ kind: 'samples', values: [1, 2], sampleRate: 0 })).toEqual([])
38
- })
39
-
40
- it('keeps large-data error extrema in the prepared Y domain', () => {
41
- const points = Array.from({ length: 10_001 }, (_, index) => ({
42
- x: index,
43
- y: 0,
44
- ...(index === 5_555 ? { error: 10_000 } : {}),
45
- }))
46
- const [series] = prepareWaveformSeries({
47
- kind: 'series',
48
- series: [
49
- {
50
- name: 'errors',
51
- errorBar: { visible: true },
52
- data: { kind: 'points', points },
53
- },
54
- ],
55
- })
56
-
57
- expect(series?.points).toHaveLength(points.length)
58
- expect(series?.hasErrorPoints).toBe(true)
59
- expect(series?.yDomain[0]).toBeLessThanOrEqual(-10_000)
60
- expect(series?.yDomain[1]).toBeGreaterThanOrEqual(10_000)
61
- })
62
-
63
- it('normalizes errors and preserves a pure error-bar series', () => {
64
- const [series] = normalizeWaveformSeries({
65
- kind: 'series',
66
- series: [
67
- {
68
- name: 'styled',
69
- lineType: 'none',
70
- pointType: 'none',
71
- errorBar: { visible: true, width: -1, capWidth: Number.NaN },
72
- data: {
73
- kind: 'points',
74
- points: [
75
- { x: 0, y: 2, error: 1, lowerError: -1, upperError: 2 },
76
- { x: 1, y: 3, error: Number.NaN },
77
- ],
78
- },
79
- },
80
- ],
81
- })
82
-
83
- expect(series).toMatchObject({
84
- lineType: 'none',
85
- pointType: 'none',
86
- errorBar: { visible: true, width: 1.5, capWidth: 8 },
87
- points: [
88
- { x: 0, y: 2, error: 1, upperError: 2 },
89
- { x: 1, y: 3 },
90
- ],
91
- })
92
- })
93
-
94
- it('falls back to a line only when every series visual is disabled', () => {
95
- const [series] = normalizeWaveformSeries({
96
- kind: 'series',
97
- series: [
98
- {
99
- name: 'invisible',
100
- lineType: 'none',
101
- pointType: 'none',
102
- errorBar: { visible: false },
103
- data: { kind: 'points', points: [{ x: 0, y: 1 }] },
104
- },
105
- ],
106
- })
107
-
108
- expect(series).toMatchObject({
109
- lineType: 'linear',
110
- pointType: 'none',
111
- errorBar: { visible: false },
112
- })
113
- })
114
-
115
- it('includes visible error bounds in the prepared Y domain', () => {
116
- const [series] = prepareWaveformSeries({
117
- kind: 'series',
118
- series: [
119
- {
120
- name: 'errors',
121
- errorBar: { visible: true },
122
- data: {
123
- kind: 'points',
124
- points: [
125
- { x: 0, y: 2, lowerError: 3, upperError: 4 },
126
- { x: 1, y: 3 },
127
- ],
128
- },
129
- },
130
- ],
131
- })
132
-
133
- expect(series?.yDomain[0]).toBeLessThanOrEqual(-1)
134
- expect(series?.yDomain[1]).toBeGreaterThanOrEqual(6)
135
- expect(series?.hasErrorPoints).toBe(true)
136
- })
137
-
138
- it('normalizes multiple named series and removes empty series', () => {
139
- expect(
140
- normalizeWaveformSeries({
141
- kind: 'series',
142
- series: [
143
- {
144
- trackId: 'comparison-track',
145
- name: 'BT2_2M',
146
- unit: 'T',
147
- data: { kind: 'points', points: [{ x: 1, y: 2 }] },
148
- },
149
- {
150
- name: 'empty',
151
- data: { kind: 'samples', values: [1], sampleRate: 0 },
152
- },
153
- ],
154
- }),
155
- ).toEqual([
156
- {
157
- id: 'series-0',
158
- trackId: 'comparison-track',
159
- name: 'BT2_2M',
160
- unit: 'T',
161
- color: undefined,
162
- lineType: 'linear',
163
- lineStyle: 'solid',
164
- pointType: 'none',
165
- errorBar: { visible: false, width: 1.5, capWidth: 8 },
166
- points: [{ x: 1, y: 2 }],
167
- },
168
- ])
169
- })
170
- })
@@ -1,241 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest'
2
-
3
- import { normalizeWaveformData, normalizeWaveformSeries } from './data'
4
-
5
- describe('waveform data normalization', () => {
6
- it('builds sample points in one pass while preserving source indexes', () => {
7
- expect(
8
- normalizeWaveformData({
9
- kind: 'samples',
10
- values: [1, Number.NaN, 3],
11
- sampleRate: 2,
12
- startTime: 1,
13
- }),
14
- ).toEqual([
15
- { x: 1, y: 1 },
16
- { x: 2, y: 3 },
17
- ])
18
- })
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
-
111
- it('skips sorting already ordered points and normalizes errors', () => {
112
- const sortSpy = vi.spyOn(Array.prototype, 'sort')
113
- try {
114
- const result = normalizeWaveformData({
115
- kind: 'points',
116
- points: [
117
- { x: 0, y: 1, error: -1, upperError: 2 },
118
- { x: 1, y: 2, lowerError: 3 },
119
- ],
120
- })
121
-
122
- expect(sortSpy).not.toHaveBeenCalled()
123
- expect(result).toEqual([
124
- { x: 0, y: 1, upperError: 2 },
125
- { x: 1, y: 2, lowerError: 3 },
126
- ])
127
- } finally {
128
- sortSpy.mockRestore()
129
- }
130
- })
131
-
132
- it('sorts only unordered points and preserves duplicate-x order', () => {
133
- expect(
134
- normalizeWaveformData({
135
- kind: 'points',
136
- points: [
137
- { x: 2, y: 20 },
138
- { x: 1, y: 10 },
139
- { x: 1, y: 11 },
140
- { x: Number.NaN, y: 12 },
141
- ],
142
- }),
143
- ).toEqual([
144
- { x: 1, y: 10 },
145
- { x: 1, y: 11 },
146
- { x: 2, y: 20 },
147
- ])
148
- })
149
-
150
- it('preserves every valid point in large data sets', () => {
151
- const points = Array.from({ length: 10_001 }, (_, index) => ({
152
- x: index,
153
- y: index === 5_555 ? 1 : 0,
154
- ...(index === 5_555 ? { error: 10_000 } : {}),
155
- }))
156
-
157
- const result = normalizeWaveformData({ kind: 'points', points })
158
-
159
- expect(result).toHaveLength(points.length)
160
- expect(result[5_555]).toEqual({ x: 5_555, y: 1, error: 10_000 })
161
- })
162
-
163
- it('defaults and normalizes per-series line styles', () => {
164
- const data = {
165
- kind: 'series' as const,
166
- series: [
167
- { id: 'solid', name: 'Solid', data: { kind: 'points' as const, points: [{ x: 0, y: 1 }] } },
168
- {
169
- id: 'dashed',
170
- name: 'Dashed',
171
- lineStyle: 'dashed' as const,
172
- data: { kind: 'points' as const, points: [{ x: 0, y: 1 }] },
173
- },
174
- {
175
- id: 'dash-dot',
176
- name: 'Dash dot',
177
- lineStyle: 'dash-dot' as const,
178
- data: { kind: 'points' as const, points: [{ x: 0, y: 1 }] },
179
- },
180
- {
181
- id: 'invalid',
182
- name: 'Invalid',
183
- lineStyle: 'zigzag' as never,
184
- data: { kind: 'points' as const, points: [{ x: 0, y: 1 }] },
185
- },
186
- ],
187
- }
188
-
189
- expect(normalizeWaveformSeries(data).map((series) => series.lineStyle)).toEqual([
190
- 'solid',
191
- 'dashed',
192
- 'dash-dot',
193
- 'solid',
194
- ])
195
- })
196
-
197
- it('preserves trimmed shot numbers and omits blank values', () => {
198
- const result = normalizeWaveformSeries({
199
- kind: 'series',
200
- series: [
201
- {
202
- id: 'with-shot',
203
- shotNo: ' 13300 ',
204
- name: '通道 A',
205
- data: { kind: 'points', points: [{ x: 0, y: 1 }] },
206
- },
207
- {
208
- id: 'without-shot',
209
- shotNo: ' ',
210
- name: '通道 B',
211
- data: { kind: 'points', points: [{ x: 0, y: 2 }] },
212
- },
213
- ],
214
- })
215
-
216
- expect(result.map((series) => series.shotNo)).toEqual(['13300', undefined])
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
- })
241
- })
@@ -1,267 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import type { WaveformPoint, WaveformRenderingOptions } from '../types'
4
- import {
5
- DEFAULT_WAVEFORM_RENDERING_OPTIONS,
6
- hasMinimumVisibleXValues,
7
- resolveWaveformRenderingOptions,
8
- selectDecorationPoints,
9
- selectRenderablePoints,
10
- selectSeriesRenderPoints,
11
- } from './rendering'
12
-
13
- describe('waveform rendering selection', () => {
14
- const points = Array.from({ length: 10_000 }, (_, index): WaveformPoint => ({
15
- x: index,
16
- y: index === 5_001 ? 10_000 : Math.sin(index / 20),
17
- }))
18
-
19
- it('clips to the visible domain and retains one continuity point on each side', () => {
20
- const selected = selectRenderablePoints(
21
- points,
22
- [4_000, 4_100],
23
- 500,
24
- resolveWaveformRenderingOptions({ downsample: false }),
25
- )
26
-
27
- expect(selected[0].x).toBe(3_999)
28
- expect(selected.at(-1)?.x).toBe(4_101)
29
- })
30
-
31
- it('bounds path density while preserving narrow extrema', () => {
32
- const selected = selectRenderablePoints(
33
- points,
34
- [0, 9_999],
35
- 100,
36
- resolveWaveformRenderingOptions({ downsampleThreshold: 100, maxPointsPerPixel: 4 }),
37
- )
38
-
39
- expect(selected.length).toBeLessThanOrEqual(402)
40
- expect(selected).toContain(points[5_001])
41
- expect(selected[0]).toBe(points[0])
42
- expect(selected.at(-1)).toBe(points.at(-1))
43
- })
44
-
45
- it('normalizes invalid rendering options to stable defaults', () => {
46
- expect(
47
- resolveWaveformRenderingOptions({
48
- downsampleThreshold: -1,
49
- maxPointsPerPixel: 0,
50
- pointMinSpacing: -1,
51
- errorBarMinSpacing: Number.POSITIVE_INFINITY,
52
- }),
53
- ).toEqual(DEFAULT_WAVEFORM_RENDERING_OPTIONS)
54
- })
55
-
56
- it('accepts custom decoration spacing and uses zero to disable it', () => {
57
- expect(resolveWaveformRenderingOptions({ pointMinSpacing: 6, errorBarMinSpacing: 0 })).toEqual({
58
- ...DEFAULT_WAVEFORM_RENDERING_OPTIONS,
59
- pointMinSpacing: 6,
60
- errorBarMinSpacing: 0,
61
- })
62
- })
63
-
64
- it('normalizes sampling options with strict runtime validation', () => {
65
- const options: WaveformRenderingOptions = {
66
- sampling: {
67
- mode: 'wasm',
68
- autoThreshold: 1_001.9,
69
- autoHysteresis: 20.8,
70
- strategy: 'minmax',
71
- maxPointsPerPixel: 2.5,
72
- maxPointCount: 1_000.9,
73
- rawPointLimit: 50_000.2,
74
- wasmFailureFallback: 'javascript',
75
- },
76
- }
77
-
78
- expect(resolveWaveformRenderingOptions(options)).toMatchObject({
79
- downsample: true,
80
- maxPointsPerPixel: 2.5,
81
- sampling: {
82
- mode: 'wasm',
83
- autoThreshold: 1_001,
84
- autoHysteresis: 20,
85
- strategy: 'minmax',
86
- maxPointsPerPixel: 2.5,
87
- maxPointCount: 1_000,
88
- rawPointLimit: 50_000,
89
- wasmFailureFallback: 'javascript',
90
- },
91
- })
92
-
93
- expect(
94
- resolveWaveformRenderingOptions({
95
- sampling: {
96
- mode: 'invalid',
97
- autoThreshold: 0,
98
- autoHysteresis: -1,
99
- strategy: 'invalid',
100
- maxPointsPerPixel: Number.POSITIVE_INFINITY,
101
- maxPointCount: 0,
102
- rawPointLimit: 0,
103
- wasmFailureFallback: 'fallback',
104
- },
105
- } as unknown as WaveformRenderingOptions),
106
- ).toMatchObject({ sampling: DEFAULT_WAVEFORM_RENDERING_OPTIONS.sampling })
107
- })
108
-
109
- it('maps legacy downsample flags and gives explicit sampling mode precedence', () => {
110
- expect(resolveWaveformRenderingOptions({ downsample: false }).sampling.mode).toBe('raw')
111
- expect(resolveWaveformRenderingOptions({ downsample: true }).sampling.mode).toBe('auto')
112
- expect(
113
- resolveWaveformRenderingOptions({ downsample: false, sampling: { mode: 'wasm' } }),
114
- ).toMatchObject({ downsample: true, sampling: { mode: 'wasm' } })
115
- })
116
-
117
- it('prefers nested sampling density over the legacy rendering field', () => {
118
- const legacy = resolveWaveformRenderingOptions({ maxPointsPerPixel: 2 })
119
- const nested = resolveWaveformRenderingOptions({
120
- maxPointsPerPixel: 2,
121
- sampling: { maxPointsPerPixel: 3 },
122
- })
123
-
124
- expect(legacy.maxPointsPerPixel).toBe(2)
125
- expect(legacy.sampling.maxPointsPerPixel).toBe(2)
126
- expect(nested.maxPointsPerPixel).toBe(3)
127
- expect(nested.sampling.maxPointsPerPixel).toBe(3)
128
- })
129
-
130
- it('selects evenly distributed source points for dense decorations', () => {
131
- const selected = selectDecorationPoints(points, [0, 9_999], 100, 10, true)
132
-
133
- expect(selected.length).toBeLessThanOrEqual(12)
134
- expect(selected[0]).toBe(points[0])
135
- expect(selected.at(-1)).toBe(points.at(-1))
136
- expect(selected.every((point) => points.includes(point))).toBe(true)
137
- })
138
-
139
- it('clips decorations exactly to the visible domain and preserves sparse points', () => {
140
- const sparsePoints = [
141
- { x: 0, y: 0 },
142
- { x: 40, y: 1 },
143
- { x: 80, y: 2 },
144
- { x: 120, y: 3 },
145
- ]
146
-
147
- expect(selectDecorationPoints(sparsePoints, [40, 80], 100, 10, true)).toEqual([
148
- sparsePoints[1],
149
- sparsePoints[2],
150
- ])
151
- })
152
-
153
- it('supports filtering decoration candidates and disabling sampling', () => {
154
- const errorPoints = Array.from({ length: 100 }, (_, index) => ({
155
- x: index,
156
- y: index,
157
- error: index % 10 === 0 ? 1 : 0,
158
- }))
159
- const hasError = (point: WaveformPoint) => (point.error ?? 0) > 0
160
-
161
- expect(selectDecorationPoints(errorPoints, [0, 99], 100, 12, true, hasError)).toHaveLength(10)
162
- expect(selectDecorationPoints(errorPoints, [0, 99], 100, 12, false)).toHaveLength(100)
163
- expect(selectDecorationPoints(errorPoints, [0, 99], 100, 0, true)).toHaveLength(100)
164
- })
165
-
166
- it('prefers priority candidates within dense decoration buckets', () => {
167
- const priorityPoints = Array.from({ length: 100 }, (_, index) => ({
168
- x: index,
169
- y: index,
170
- error: index % 20 === 1 ? 1 : 0,
171
- }))
172
- const hasError = (point: WaveformPoint) => (point.error ?? 0) > 0
173
- const selected = selectDecorationPoints(
174
- priorityPoints,
175
- [0, 99],
176
- 100,
177
- 20,
178
- true,
179
- undefined,
180
- hasError,
181
- )
182
-
183
- expect(selected.filter(hasError)).toEqual(priorityPoints.filter(hasError))
184
- expect(selected.length).toBeLessThanOrEqual(Math.ceil(100 / 20) + 2)
185
- })
186
-
187
- it('shares visible-range selection for a dense 100k-point series', () => {
188
- const densePoints = Array.from({ length: 100_000 }, (_, index): WaveformPoint => ({
189
- x: index,
190
- y: index === 50_001 ? 10_000 : Math.sin(index / 20),
191
- error: index % 1_000 === 0 ? 1 : undefined,
192
- }))
193
- const selected = selectSeriesRenderPoints(
194
- densePoints,
195
- [99_999, 0],
196
- 500,
197
- resolveWaveformRenderingOptions({
198
- downsampleThreshold: 100,
199
- sampling: { autoThreshold: 200_000 },
200
- }),
201
- {
202
- lineVisible: true,
203
- pointVisible: true,
204
- errorBarVisible: true,
205
- hasErrorPoints: true,
206
- },
207
- )
208
-
209
- expect(selected.linePoints.length).toBeLessThanOrEqual(2_002)
210
- expect(selected.linePoints).toContain(densePoints[50_001])
211
- expect(selected.linePoints[0]).toBe(densePoints[0])
212
- expect(selected.linePoints.at(-1)).toBe(densePoints.at(-1))
213
- expect(selected.pointRenderPoints.length).toBeLessThanOrEqual(52)
214
- expect(selected.errorBarRenderPoints.every((point) => (point.error ?? 0) > 0)).toBe(true)
215
- })
216
-
217
- it('uses a width-bounded placeholder while large-series Worker sampling is pending', () => {
218
- const rendering = resolveWaveformRenderingOptions({
219
- sampling: { mode: 'auto', autoThreshold: 1_000, maxPointsPerPixel: 2 },
220
- })
221
- const selected = selectSeriesRenderPoints(points, [0, 9_999], 100, rendering, {
222
- lineVisible: true,
223
- pointVisible: false,
224
- errorBarVisible: false,
225
- hasErrorPoints: false,
226
- })
227
-
228
- expect(selected.linePoints).toHaveLength(200)
229
- expect(selected.linePoints[0]).toBe(points[0])
230
- expect(selected.linePoints.at(-1)).toBe(points.at(-1))
231
- })
232
-
233
- it('counts unique visible x values across series and reversed domains', () => {
234
- const first = {
235
- points: [
236
- { x: 0, y: 0 },
237
- { x: 1, y: 1 },
238
- ],
239
- }
240
- const second = {
241
- points: [
242
- { x: 0, y: 2 },
243
- { x: 1, y: 3 },
244
- { x: 2, y: 4 },
245
- ],
246
- }
247
-
248
- expect(hasMinimumVisibleXValues([first, second], [2, 0], 3)).toBe(true)
249
- expect(hasMinimumVisibleXValues([first, second], [0, 2], 4)).toBe(false)
250
- expect(hasMinimumVisibleXValues([first, second], [1, 1], 1)).toBe(true)
251
- expect(hasMinimumVisibleXValues([first, second], [3, 4], 1)).toBe(false)
252
- })
253
-
254
- it('stops scanning a 100k-point series after reaching the minimum', () => {
255
- const source = Array.from({ length: 100_000 }, (_, index) => ({ x: index, y: index }))
256
- let pointReads = 0
257
- const points = new Proxy(source, {
258
- get(target, property, receiver) {
259
- if (typeof property === 'string' && /^\d+$/.test(property)) pointReads += 1
260
- return Reflect.get(target, property, receiver)
261
- },
262
- })
263
-
264
- expect(hasMinimumVisibleXValues([{ points }], [0, 99_999], 5)).toBe(true)
265
- expect(pointReads).toBeLessThan(100)
266
- })
267
- })