waveform-analysis 0.1.63 → 0.1.65

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 (97) hide show
  1. package/README.md +21 -5
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +986 -964
  4. package/dist/style.css +1 -1
  5. package/dist/types/components/core/constants.d.ts +1 -1
  6. package/dist/types/components/core/trackLayoutBuilder.d.ts +1 -0
  7. package/dist/types/components/core/yAxisConstants.d.ts +1 -1
  8. package/dist/types/types/chart.d.ts +6 -1
  9. package/package.json +1 -1
  10. package/src/App.vue +2 -2
  11. package/src/components/WaveformChart.css +4 -0
  12. package/src/components/WaveformChart.vue +2 -2
  13. package/src/components/core/constants.ts +3 -3
  14. package/src/components/core/layout.ts +1 -1
  15. package/src/components/core/trackLayoutBuilder.ts +42 -1
  16. package/src/components/core/useWaveformLayout.ts +2 -1
  17. package/src/components/core/useWaveformPresentation.ts +1 -1
  18. package/src/components/core/xDomain.ts +26 -2
  19. package/src/components/core/yAxisConstants.ts +1 -1
  20. package/src/components/rendering/WaveformLegend.vue +1 -1
  21. package/src/components/rendering/WaveformSeriesLayer.vue +1 -1
  22. package/src/components/rendering/WaveformTrack.css +5 -2
  23. package/src/components/rendering/WaveformTrack.vue +7 -1
  24. package/src/components/rendering/WaveformTrackAxes.vue +8 -2
  25. package/src/demo/DemoChartHost.vue +1 -0
  26. package/src/types/chart.ts +6 -1
  27. package/src/App.test.ts +0 -400
  28. package/src/components/annotation/components.test.ts +0 -372
  29. package/src/components/annotation/markup.test.ts +0 -371
  30. package/src/components/annotation/serialization.test.ts +0 -114
  31. package/src/components/core/constants.test.ts +0 -29
  32. package/src/components/core/fixedYDomainLayout.test.ts +0 -204
  33. package/src/components/core/grid.test.ts +0 -197
  34. package/src/components/core/latestTaskScheduler.test.ts +0 -51
  35. package/src/components/core/layout.test.ts +0 -373
  36. package/src/components/core/title.test.ts +0 -76
  37. package/src/components/core/useWaveformData.test.ts +0 -95
  38. package/src/components/core/useWaveformRenderSampling.test.ts +0 -17
  39. package/src/components/core/xDomain.test.ts +0 -28
  40. package/src/components/core/yAxisNice.test.ts +0 -174
  41. package/src/components/core/yAxisSlotLayout.test.ts +0 -165
  42. package/src/components/core/yDomain.test.ts +0 -56
  43. package/src/components/interaction/WaveformTooltip.test.ts +0 -276
  44. package/src/components/interaction/viewportInteractionState.test.ts +0 -101
  45. package/src/components/utils/useAnimationFrameThrottle.test.ts +0 -53
  46. package/src/components/waveformChartCases/annotationEditingAndReprojection.test.ts +0 -257
  47. package/src/components/waveformChartCases/compactAndSharedHover.test.ts +0 -358
  48. package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +0 -358
  49. package/src/components/waveformChartCases/dataLifecycle.test.ts +0 -310
  50. package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +0 -389
  51. package/src/components/waveformChartCases/gridFill.test.ts +0 -95
  52. package/src/components/waveformChartCases/incompleteDataHover.test.ts +0 -155
  53. package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +0 -328
  54. package/src/components/waveformChartCases/layoutAndDomains.test.ts +0 -371
  55. package/src/components/waveformChartCases/legendVisibility.test.ts +0 -350
  56. package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +0 -400
  57. package/src/components/waveformChartCases/multiAxisLabelAlignment.test.ts +0 -61
  58. package/src/components/waveformChartCases/presentationAndAxes.test.ts +0 -345
  59. package/src/components/waveformChartCases/presentationMode.test.ts +0 -327
  60. package/src/components/waveformChartCases/renderingStyles.test.ts +0 -283
  61. package/src/components/waveformChartCases/sizingAndTitles.test.ts +0 -370
  62. package/src/components/waveformChartCases/trackOrder.test.ts +0 -323
  63. package/src/components/waveformChartCases/viewportBoxAndPan.test.ts +0 -365
  64. package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +0 -311
  65. package/src/components/waveformChartCases/viewportLifecycle.test.ts +0 -132
  66. package/src/components/waveformChartCases/viewportReset.test.ts +0 -251
  67. package/src/components/waveformChartCases/wheelZoomOut.test.ts +0 -114
  68. package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +0 -117
  69. package/src/components/waveformChartCases/xDomainStrategy.test.ts +0 -168
  70. package/src/components/waveformChartCases/yAxisLayout.test.ts +0 -103
  71. package/src/components/waveformChartCases/zoomIntentAndDataRebind.test.ts +0 -213
  72. package/src/components/waveformChartCases/zoomPointConstraints.test.ts +0 -308
  73. package/src/components/waveformLegendGeometry.test.ts +0 -21
  74. package/src/components/waveformNormalization.test.ts +0 -170
  75. package/src/core/data.test.ts +0 -241
  76. package/src/core/rendering.test.ts +0 -267
  77. package/src/core/renderingStrategies.test.ts +0 -90
  78. package/src/core/samplingReference.test.ts +0 -124
  79. package/src/core/wasmSampling.test.ts +0 -205
  80. package/src/core/workerSampling/multiresolution.test.ts +0 -69
  81. package/src/core/workerSampling/protocol.test.ts +0 -34
  82. package/src/core/workerSampling/repository.test.ts +0 -400
  83. package/src/core/workerSampling/repositoryCache.test.ts +0 -163
  84. package/src/core/workerSampling/repositorySampling.test.ts +0 -106
  85. package/src/core/workerSampling/repositoryTypedDataset.test.ts +0 -89
  86. package/src/core/workerSampling/repositoryWasmDataset.test.ts +0 -70
  87. package/src/data/simulatedWaveforms.test.ts +0 -60
  88. package/src/demo/plotMarginControls.test.ts +0 -29
  89. package/src/demo/xAxisLabelControls.test.ts +0 -86
  90. package/src/test/setup.ts +0 -82
  91. package/src/test/waveformChart.ts +0 -75
  92. package/src/utils/domain.test.ts +0 -28
  93. package/src/utils/formatters.test.ts +0 -131
  94. package/src/utils/sampling.test.ts +0 -226
  95. package/src/views/FixedYDomainDemo.test.ts +0 -44
  96. package/src/views/WasmSamplingDemo.test.ts +0 -13
  97. 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
- })