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,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
- })