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,373 +0,0 @@
1
- import { zoomIdentity } from 'd3'
2
- import { describe, expect, it } from 'vitest'
3
-
4
- import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from '../../core'
5
- import type { DisplaySeries, DisplayTrack } from './types'
6
- import {
7
- axisTextMetrics,
8
- buildTrackLayouts,
9
- buildYAxisSeriesGroups,
10
- formatYAxisTickLabel,
11
- findClosestTrackAtPointer,
12
- MAX_MULTI_Y_AXIS_COUNT,
13
- measureYAxisGroupClearance,
14
- resolveYAxisTicks,
15
- resolveYAxisTickCount,
16
- } from './layout'
17
-
18
- function series(id: string, minimum: number, maximum: number): DisplaySeries {
19
- return {
20
- id,
21
- name: id,
22
- color: '#1677ff',
23
- lineType: 'linear',
24
- lineStyle: 'solid',
25
- pointType: 'none',
26
- errorBar: { visible: false, width: 1.5, capWidth: 8 },
27
- points: [
28
- { x: 0, y: minimum },
29
- { x: 1, y: maximum },
30
- ],
31
- xDomain: [0, 1],
32
- yDomain: [minimum, maximum],
33
- hasErrorPoints: false,
34
- }
35
- }
36
-
37
- function track(seriesList: DisplaySeries[]): DisplayTrack {
38
- return {
39
- id: 'track',
40
- series: seriesList,
41
- visibleSeries: seriesList,
42
- xDomain: [0, 1],
43
- yDomain: [0, 50],
44
- }
45
- }
46
-
47
- function layoutForSeries(
48
- sourceSeries: DisplaySeries,
49
- rendering = DEFAULT_WAVEFORM_RENDERING_OPTIONS,
50
- transform = zoomIdentity,
51
- ) {
52
- const sourceTrack = track([sourceSeries])
53
- sourceTrack.xDomain = sourceSeries.xDomain
54
- sourceTrack.yDomain = sourceSeries.yDomain
55
- return buildTrackLayouts({
56
- cells: [
57
- {
58
- slotIndex: 0,
59
- row: 0,
60
- column: 0,
61
- left: 0,
62
- top: 0,
63
- width: 120,
64
- height: 100,
65
- plotHeight: 100,
66
- cellHeight: 130,
67
- xAxisBand: 30,
68
- series: sourceTrack,
69
- },
70
- ],
71
- grid: {
72
- rowCount: 1,
73
- columnCount: 1,
74
- showPagination: false,
75
- fillIncompleteLastRow: false,
76
- trackLines: {},
77
- },
78
- displayMode: 'independent',
79
- overlayMode: 'single-axis',
80
- independentTransforms: [transform],
81
- sharedZoomDomain: sourceSeries.xDomain,
82
- yDomains: undefined,
83
- timeUnit: 'ms',
84
- rendering,
85
- hideSecondaryLabels: false,
86
- yAxisLabelX: -50,
87
- showCompactEmptyTracks: false,
88
- })[0]!.seriesPaths[0]!
89
- }
90
-
91
- describe('multi-value Y-axis grouping', () => {
92
- it('rebuilds Y scales when the same track ID receives a different domain', () => {
93
- const first = layoutForSeries(series('shared', 0, 1))
94
- const second = layoutForSeries(series('shared', 10_000, 20_000))
95
-
96
- expect(first.yScale.domain()).toEqual([0, 1])
97
- expect(second.yScale.domain()).toEqual([10_000, 20_000])
98
- })
99
-
100
- it('uses a configured visible Y domain for axis and series scales', () => {
101
- const source = series('a', 0, 100)
102
- const sourceTrack = track([source])
103
- const result = buildTrackLayouts({
104
- cells: [
105
- {
106
- slotIndex: 0,
107
- row: 0,
108
- column: 0,
109
- left: 0,
110
- top: 0,
111
- width: 120,
112
- height: 100,
113
- plotHeight: 100,
114
- cellHeight: 130,
115
- xAxisBand: 30,
116
- series: sourceTrack,
117
- },
118
- ],
119
- grid: {
120
- rowCount: 1,
121
- columnCount: 1,
122
- showPagination: false,
123
- fillIncompleteLastRow: false,
124
- trackLines: {},
125
- },
126
- displayMode: 'independent',
127
- overlayMode: 'single-axis',
128
- independentTransforms: [zoomIdentity],
129
- sharedZoomDomain: [0, 1],
130
- yDomains: { track: [25, 75] },
131
- timeUnit: 'ms',
132
- rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
133
- hideSecondaryLabels: false,
134
- yAxisLabelX: -50,
135
- showCompactEmptyTracks: false,
136
- })[0]
137
-
138
- expect(result?.yScale.domain()).toEqual([20, 80])
139
- expect(result?.seriesPaths[0]?.yScale.domain()).toEqual([20, 80])
140
- })
141
-
142
- it('normalizes configured Y-axis split counts', () => {
143
- expect(resolveYAxisTickCount(100, 0)).toBe(2)
144
- expect(resolveYAxisTickCount(100, 4.8)).toBe(4)
145
- expect(resolveYAxisTickCount(100, Number.NaN)).toBe(5)
146
- expect(resolveYAxisTickCount(220)).toBe(5)
147
- })
148
-
149
- it('measures the same final endpoint label that Y-axis rendering uses', () => {
150
- const ticks = resolveYAxisTicks([-0.7434, -0.3434], 5, false)
151
- const labels = ticks.values.map((value) =>
152
- formatYAxisTickLabel(value, ticks.domain, ticks.values, 'V'),
153
- )
154
-
155
- expect(labels).toEqual(['-0.7434', '-0.6434', '-0.5434', '-0.4434', '(V) -0.3434'])
156
- expect(axisTextMetrics(ticks.domain, false, undefined, 'V', 5).tickTextWidth).toBe(77)
157
- })
158
-
159
- it('includes the compact-axis unit label after its top tick is hidden', () => {
160
- const ticks = resolveYAxisTicks([0.12345, 100], 5, false)
161
- const compactTicks = ticks.values.slice(0, -1)
162
-
163
- expect(formatYAxisTickLabel(compactTicks.at(-1)!, ticks.domain, compactTicks, 'V')).toBe(
164
- '(V) 75.031',
165
- )
166
- expect(axisTextMetrics(ticks.domain, false, undefined, 'V', 5).tickTextWidth).toBe(49)
167
- expect(axisTextMetrics(ticks.domain, false, undefined, 'V', 5, true).tickTextWidth).toBe(70)
168
- })
169
-
170
- it('keeps every overlaid series on one axis in single-axis mode', () => {
171
- const groups = buildYAxisSeriesGroups(
172
- track([series('a', 0, 1), series('b', 10, 20)]),
173
- 'single-axis',
174
- )
175
-
176
- expect(groups).toHaveLength(1)
177
- expect(groups[0]?.seriesList.map((item) => item.id)).toEqual(['a', 'b'])
178
- expect(groups[0]?.domain).toEqual([0, 50])
179
- })
180
-
181
- it('uses the reference left-right axis order and merges overflow into axis four', () => {
182
- const groups = buildYAxisSeriesGroups(
183
- track([
184
- series('a', 0, 1),
185
- series('b', 10, 11),
186
- series('c', 20, 21),
187
- series('d', 30, 31),
188
- series('e', 40, 50),
189
- ]),
190
- 'multi-axis',
191
- )
192
-
193
- expect(groups).toHaveLength(MAX_MULTI_Y_AXIS_COUNT)
194
- expect(groups.map((group) => group.side)).toEqual(['left', 'left', 'right', 'right'])
195
- expect(groups.map((group) => group.seriesList.map((item) => item.id))).toEqual([
196
- ['a'],
197
- ['b'],
198
- ['c'],
199
- ['d', 'e'],
200
- ])
201
- expect(groups[3]?.domain[0]).toBeLessThanOrEqual(30)
202
- expect(groups[3]?.domain[1]).toBeGreaterThanOrEqual(50)
203
- })
204
-
205
- it('derives merged multi-axis domains from precomputed series domains', () => {
206
- const first = series('a', -20, -10)
207
- const second = series('b', 40, 60)
208
- first.points = [{ x: 0, y: -15 }]
209
- second.points = [{ x: 0, y: 50 }]
210
-
211
- const groups = buildYAxisSeriesGroups(
212
- track([
213
- series('left', 0, 1),
214
- series('middle', 10, 11),
215
- series('right', 20, 21),
216
- first,
217
- second,
218
- ]),
219
- 'multi-axis',
220
- )
221
-
222
- expect(groups[3]?.domain[0]).toBeLessThanOrEqual(-20)
223
- expect(groups[3]?.domain[1]).toBeGreaterThanOrEqual(60)
224
- })
225
-
226
- it('places two and three axes on the expected sides', () => {
227
- const source = [series('a', 0, 1), series('b', 10, 11), series('c', 20, 21)]
228
-
229
- expect(
230
- buildYAxisSeriesGroups(track(source.slice(0, 2)), 'multi-axis').map((g) => g.side),
231
- ).toEqual(['left', 'right'])
232
- expect(buildYAxisSeriesGroups(track(source), 'multi-axis').map((g) => g.side)).toEqual([
233
- 'left',
234
- 'right',
235
- 'right',
236
- ])
237
- })
238
-
239
- it('reserves label clearance for exponent-prefixed ticks on both axis sides', () => {
240
- const layout = buildTrackLayouts({
241
- cells: [
242
- {
243
- slotIndex: 0,
244
- row: 0,
245
- column: 0,
246
- left: 0,
247
- top: 0,
248
- width: 600,
249
- height: 300,
250
- plotHeight: 300,
251
- cellHeight: 330,
252
- xAxisBand: 30,
253
- series: track([series('left', 1000, 3000), series('right', 1000, 3000)]),
254
- },
255
- ],
256
- grid: {
257
- rowCount: 1,
258
- columnCount: 1,
259
- showPagination: false,
260
- fillIncompleteLastRow: false,
261
- trackLines: {},
262
- },
263
- displayMode: 'independent',
264
- overlayMode: 'multi-axis',
265
- independentTransforms: [zoomIdentity],
266
- sharedZoomDomain: [0, 1],
267
- timeUnit: 'ms',
268
- rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
269
- hideSecondaryLabels: false,
270
- yAxisLabelX: -50,
271
- showCompactEmptyTracks: false,
272
- })[0]
273
-
274
- expect(
275
- layout?.yAxes.map(({ side, x, labelX }) => ({
276
- side,
277
- labelOffset: Math.abs(labelX - x),
278
- })),
279
- ).toEqual([
280
- { side: 'left', labelOffset: 55 },
281
- { side: 'right', labelOffset: 45 },
282
- ])
283
- })
284
-
285
- it('retains enough outer clearance for long scientific exponents', () => {
286
- const [group] = buildYAxisSeriesGroups(track([series('long', -1e120, 1e120)]), 'multi-axis')
287
-
288
- expect(group).toBeDefined()
289
- expect(measureYAxisGroupClearance(group!)).toBe(72)
290
- })
291
- })
292
-
293
- describe('track hit testing', () => {
294
- const tracks = [
295
- { id: 'first', left: 0, top: 0, width: 100, height: 40 },
296
- { id: 'second', left: 0, top: 50, width: 100, height: 40 },
297
- ]
298
-
299
- it('switches tracks immediately across a boundary less than five pixels apart', () => {
300
- expect(findClosestTrackAtPointer(tracks, 50, 44)?.id).toBe('first')
301
- expect(findClosestTrackAtPointer(tracks, 50, 46)?.id).toBe('second')
302
- })
303
- })
304
-
305
- describe('decoration sampling', () => {
306
- const denseSeries = (): DisplaySeries => ({
307
- ...series('dense', -1, 1),
308
- pointType: 'circle',
309
- errorBar: { visible: true, width: 1.5, capWidth: 8 },
310
- points: Array.from({ length: 1_000 }, (_, index) => ({
311
- x: index,
312
- y: Math.sin(index / 20),
313
- error: index % 200 === 1 ? 0.1 : 0,
314
- })),
315
- xDomain: [0, 999],
316
- hasErrorPoints: true,
317
- })
318
-
319
- it('shares prioritized source points between dense symbols and error bars', () => {
320
- const sourceSeries = denseSeries()
321
- const path = layoutForSeries(sourceSeries, {
322
- ...DEFAULT_WAVEFORM_RENDERING_OPTIONS,
323
- pointMinSpacing: 10,
324
- errorBarMinSpacing: 12,
325
- })
326
- const sourceErrorPoints = sourceSeries.points.filter((point) => point.error !== 0)
327
-
328
- expect(path.errorBarRenderPoints).toEqual(sourceErrorPoints)
329
- expect(path.errorBarRenderPoints.every((point) => path.pointRenderPoints.includes(point))).toBe(
330
- true,
331
- )
332
- expect(path.pointRenderPoints.length).toBeLessThanOrEqual(Math.ceil(120 / 12) + 2)
333
- })
334
-
335
- it('keeps standalone, zero-error, and non-downsampled decoration behavior', () => {
336
- const noErrors = denseSeries()
337
- noErrors.points = noErrors.points.map((point) => ({ x: point.x, y: point.y }))
338
- noErrors.hasErrorPoints = false
339
- const zeroErrorPath = layoutForSeries(noErrors)
340
- expect(zeroErrorPath.errorBarRenderPoints).toEqual([])
341
- expect(zeroErrorPath.pointRenderPoints.length).toBeLessThanOrEqual(Math.ceil(120 / 10) + 2)
342
-
343
- const errorsOnly = denseSeries()
344
- errorsOnly.pointType = 'none'
345
- const errorsOnlyPath = layoutForSeries(errorsOnly)
346
- expect(errorsOnlyPath.pointRenderPoints).toEqual([])
347
- expect(errorsOnlyPath.errorBarRenderPoints.length).toBeLessThanOrEqual(Math.ceil(120 / 12) + 2)
348
-
349
- const pointsOnly = denseSeries()
350
- pointsOnly.errorBar.visible = false
351
- const pointsOnlyPath = layoutForSeries(pointsOnly)
352
- expect(pointsOnlyPath.errorBarRenderPoints).toEqual([])
353
- expect(pointsOnlyPath.pointRenderPoints.length).toBeLessThanOrEqual(Math.ceil(120 / 10) + 2)
354
-
355
- const completePath = layoutForSeries(denseSeries(), {
356
- ...DEFAULT_WAVEFORM_RENDERING_OPTIONS,
357
- downsample: false,
358
- })
359
- expect(completePath.pointRenderPoints).toHaveLength(1_000)
360
- expect(completePath.errorBarRenderPoints).toHaveLength(5)
361
- })
362
-
363
- it('restores every visible source decoration after zooming to sparse spacing', () => {
364
- const path = layoutForSeries(
365
- denseSeries(),
366
- DEFAULT_WAVEFORM_RENDERING_OPTIONS,
367
- zoomIdentity.scale(200),
368
- )
369
-
370
- expect(path.pointRenderPoints.map((point) => point.x)).toEqual([0, 1, 2, 3, 4])
371
- expect(path.errorBarRenderPoints.map((point) => point.x)).toEqual([1])
372
- })
373
- })
@@ -1,76 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { calculateRotatedTitleLayout, TITLE_AREA_MAX_HEIGHT, TITLE_AREA_MIN_HEIGHT } from './title'
4
-
5
- describe('calculateRotatedTitleLayout', () => {
6
- it('reserves the available width for an unrotated title that fits', () => {
7
- expect(
8
- calculateRotatedTitleLayout({
9
- naturalWidth: 200,
10
- naturalHeight: 20,
11
- availableWidth: 300,
12
- rotation: 0,
13
- }),
14
- ).toEqual({
15
- textWidth: 300,
16
- textHeight: 20,
17
- visualWidth: 300,
18
- visualHeight: 20,
19
- areaHeight: TITLE_AREA_MIN_HEIGHT,
20
- scale: 1,
21
- wrapped: false,
22
- })
23
- })
24
-
25
- it.each([45, 90, -90, 180])('keeps a %s degree title inside the maximum area', (rotation) => {
26
- const layout = calculateRotatedTitleLayout({
27
- naturalWidth: 400,
28
- naturalHeight: 20,
29
- availableWidth: 600,
30
- rotation,
31
- })
32
-
33
- expect(layout.areaHeight).toBeGreaterThanOrEqual(TITLE_AREA_MIN_HEIGHT)
34
- expect(layout.areaHeight).toBeLessThanOrEqual(TITLE_AREA_MAX_HEIGHT)
35
- expect(layout.visualHeight).toBeLessThanOrEqual(TITLE_AREA_MAX_HEIGHT)
36
- expect(layout.textWidth).toBe(400)
37
- expect(layout.scale).toBeLessThanOrEqual(1)
38
- expect(layout.wrapped).toBe(false)
39
- })
40
-
41
- it('wraps an unrotated title to the available width without scaling', () => {
42
- const layout = calculateRotatedTitleLayout({
43
- naturalWidth: 1_000,
44
- naturalHeight: 20,
45
- availableWidth: 120,
46
- rotation: 0,
47
- })
48
-
49
- expect(layout.textWidth).toBe(120)
50
- expect(layout.textHeight).toBe(180)
51
- expect(layout.visualWidth).toBe(120)
52
- expect(layout.visualHeight).toBe(180)
53
- expect(layout.areaHeight).toBe(198)
54
- expect(layout.scale).toBe(1)
55
- expect(layout.wrapped).toBe(true)
56
- })
57
-
58
- it('falls back safely for non-finite dimensions and rotation', () => {
59
- const layout = calculateRotatedTitleLayout({
60
- naturalWidth: Number.NaN,
61
- naturalHeight: Number.POSITIVE_INFINITY,
62
- availableWidth: Number.NaN,
63
- rotation: Number.NaN,
64
- })
65
-
66
- expect(layout).toEqual({
67
- textWidth: 1,
68
- textHeight: 1,
69
- visualWidth: 1,
70
- visualHeight: 1,
71
- areaHeight: TITLE_AREA_MIN_HEIGHT,
72
- scale: 1,
73
- wrapped: false,
74
- })
75
- })
76
- })
@@ -1,95 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { prepareWaveformSeries } from './useWaveformData'
4
-
5
- describe('prepareWaveformSeries compact inputs', () => {
6
- it('keeps dense typed samples as numeric columns until individual points are requested', () => {
7
- const values = Float32Array.from({ length: 100_001 }, (_, index) => index / 10)
8
- const snapshot = values.slice()
9
- const [series] = prepareWaveformSeries({
10
- kind: 'typed-samples',
11
- values,
12
- sampleRate: 1_000,
13
- startTime: 2,
14
- })
15
-
16
- expect(series?.source.isCompact).toBe(true)
17
- expect(series?.points).toHaveLength(values.length)
18
- expect(series?.xDomain[0]).toBeLessThanOrEqual(2)
19
- expect(series?.xDomain[1]).toBeGreaterThanOrEqual(102)
20
- expect(series?.source.pointAt(50_000)).toEqual({ x: 52, y: 5_000 })
21
- expect(values).toEqual(snapshot)
22
-
23
- const workerDataset = series?.source.toWorkerDataset()
24
- expect(workerDataset).toMatchObject({
25
- kind: 'samples',
26
- sampleRate: 1_000,
27
- startTime: 2,
28
- })
29
- if (workerDataset?.kind === 'samples') {
30
- expect(workerDataset).not.toHaveProperty('sourceIndexes')
31
- expect(workerDataset.values.buffer).not.toBe(values.buffer)
32
- expect(workerDataset.values).toEqual(values)
33
- }
34
- })
35
-
36
- it('preserves invalid-sample time gaps without allocating an X column', () => {
37
- const [series] = prepareWaveformSeries({
38
- kind: 'typed-samples',
39
- values: new Float64Array([1, Number.NaN, 3]),
40
- sampleRate: 2,
41
- startTime: 10,
42
- })
43
-
44
- expect(series?.source.pointsInRange()).toEqual([
45
- { x: 10, y: 1 },
46
- { x: 11, y: 3 },
47
- ])
48
- expect(series?.source.toWorkerDataset()).toMatchObject({
49
- kind: 'samples',
50
- values: new Float64Array([1, 3]),
51
- sourceIndexes: new Uint32Array([0, 2]),
52
- })
53
- })
54
-
55
- it('filters and stably orders compact typed points without materializing source point objects', () => {
56
- const [series] = prepareWaveformSeries({
57
- kind: 'typed-points',
58
- x: new Float64Array([2, 1, 1, Number.NaN]),
59
- y: new Float32Array([20, 10, 11, 12]),
60
- error: new Float32Array([1, -1, Number.NaN, 4]),
61
- })
62
-
63
- expect(series?.source.isCompact).toBe(true)
64
- expect(series?.source.pointsInRange()).toEqual([
65
- { x: 1, y: 10 },
66
- { x: 1, y: 11 },
67
- { x: 2, y: 20, error: 1 },
68
- ])
69
- expect(series?.source.visibleRange([1, 1])).toEqual({ start: 0, end: 2 })
70
- expect(series?.source.nearestPoint(-10)).toEqual({ x: 1, y: 10 })
71
- expect(series?.source.nearestPoint(10)).toEqual({ x: 2, y: 20, error: 1 })
72
- })
73
-
74
- it('retains normal array access, iteration, slicing, mapping, and enumeration semantics', () => {
75
- const [series] = prepareWaveformSeries({
76
- kind: 'typed-samples',
77
- values: new Float32Array([2, 4, 6]),
78
- sampleRate: 2,
79
- })
80
-
81
- expect(series?.points[1]).toEqual({ x: 0.5, y: 4 })
82
- expect(series?.points.at(-1)).toEqual({ x: 1, y: 6 })
83
- expect([...(series?.points ?? [])]).toEqual([
84
- { x: 0, y: 2 },
85
- { x: 0.5, y: 4 },
86
- { x: 1, y: 6 },
87
- ])
88
- expect(series?.points.slice(1)).toEqual([
89
- { x: 0.5, y: 4 },
90
- { x: 1, y: 6 },
91
- ])
92
- expect(series?.points.map((point) => point.y)).toEqual([2, 4, 6])
93
- expect(Object.keys(series?.points ?? [])).toEqual(['0', '1', '2'])
94
- })
95
- })
@@ -1,17 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { resolveAutoSelectedMode } from './latestTaskScheduler'
4
-
5
- describe('resolveAutoSelectedMode', () => {
6
- it('uses the exact threshold when no interaction selection is supplied', () => {
7
- expect(resolveAutoSelectedMode(1_000, 1_000, 200)).toBe('raw')
8
- expect(resolveAutoSelectedMode(1_001, 1_000, 200)).toBe('sampled')
9
- })
10
-
11
- it('keeps the previous mode inside the configured interaction hysteresis band', () => {
12
- expect(resolveAutoSelectedMode(1_100, 1_000, 200, 'raw')).toBe('raw')
13
- expect(resolveAutoSelectedMode(1_201, 1_000, 200, 'raw')).toBe('sampled')
14
- expect(resolveAutoSelectedMode(900, 1_000, 200, 'sampled')).toBe('sampled')
15
- expect(resolveAutoSelectedMode(800, 1_000, 200, 'sampled')).toBe('raw')
16
- })
17
- })
@@ -1,28 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { applyXDomainStrategy } from './xDomain'
4
-
5
- describe('applyXDomainStrategy', () => {
6
- it('keeps the exact data domain by default', () => {
7
- expect(applyXDomainStrategy([0, 4999.999], { type: 'data' })).toEqual([0, 4999.999])
8
- })
9
-
10
- it('expands both bounds to stable nice values', () => {
11
- expect(applyXDomainStrategy([123, 456], { type: 'nice' })).toEqual([100, 500])
12
- })
13
-
14
- it('can expand only the end bound', () => {
15
- expect(applyXDomainStrategy([123, 456], { type: 'nice', bounds: 'end' })).toEqual([123, 500])
16
- })
17
-
18
- it('keeps explicit domains exact unless they are included', () => {
19
- expect(applyXDomainStrategy([123, 456], { type: 'nice' }, true)).toEqual([123, 456])
20
- expect(applyXDomainStrategy([123, 456], { type: 'nice', includeExplicit: true }, true)).toEqual(
21
- [100, 500],
22
- )
23
- })
24
-
25
- it('falls back to the default tick count for invalid values', () => {
26
- expect(applyXDomainStrategy([0, 4999.999], { type: 'nice', tickCount: 0 })).toEqual([0, 5000])
27
- })
28
- })