waveform-analysis 0.1.30 → 0.1.32

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 (52) hide show
  1. package/README.md +12 -2
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +827 -602
  4. package/dist/style.css +1 -1
  5. package/dist/types/components/WaveformChart.vue.d.ts +2 -2
  6. package/dist/types/components/core/useWaveformChartController.d.ts +2 -14
  7. package/dist/types/components/core/useWaveformChartLifecycle.d.ts +4 -1
  8. package/dist/types/components/core/useWaveformData.d.ts +1 -1
  9. package/dist/types/components/core/waveformChartTypes.d.ts +8 -5
  10. package/dist/types/components/data/types.d.ts +1 -1
  11. package/dist/types/components/interaction/pointerCapture.d.ts +1 -0
  12. package/dist/types/components/interaction/viewportInteractionState.d.ts +39 -0
  13. package/dist/types/components/interaction/zoomConstraints.d.ts +14 -0
  14. package/dist/types/core/rendering.d.ts +1 -4
  15. package/dist/types/core/renderingStrategies.d.ts +22 -0
  16. package/dist/types/demo/types.d.ts +0 -1
  17. package/dist/types/index.d.ts +1 -1
  18. package/dist/types/types/chart.d.ts +5 -0
  19. package/dist/types/types/index.d.ts +1 -1
  20. package/package.json +5 -1
  21. package/src/App.vue +0 -4
  22. package/src/components/WaveformChartView.vue +2 -2
  23. package/src/components/core/useWaveformChartController.ts +7 -2
  24. package/src/components/core/useWaveformChartLifecycle.ts +47 -3
  25. package/src/components/core/useWaveformData.ts +6 -1
  26. package/src/components/core/waveformChartTypes.ts +6 -4
  27. package/src/components/data/types.ts +1 -0
  28. package/src/components/interaction/WaveformTooltip.test.ts +39 -0
  29. package/src/components/interaction/WaveformTooltip.vue +72 -22
  30. package/src/components/interaction/pointerCapture.ts +10 -0
  31. package/src/components/interaction/useWaveformHover.ts +1 -1
  32. package/src/components/interaction/useWaveformViewport.ts +101 -75
  33. package/src/components/interaction/useWaveformZoom.ts +46 -14
  34. package/src/components/interaction/viewportInteractionState.test.ts +101 -0
  35. package/src/components/interaction/viewportInteractionState.ts +89 -0
  36. package/src/components/interaction/zoomConstraints.ts +167 -0
  37. package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +0 -65
  38. package/src/components/waveformChartCases/viewportLifecycle.test.ts +132 -0
  39. package/src/components/waveformChartCases/viewportReset.test.ts +114 -0
  40. package/src/components/waveformChartCases/zoomPointConstraints.test.ts +248 -0
  41. package/src/core/rendering.ts +11 -81
  42. package/src/core/renderingStrategies.test.ts +90 -0
  43. package/src/core/renderingStrategies.ts +131 -0
  44. package/src/data/simulatedWaveforms.ts +9 -10
  45. package/src/demo/DemoChartHost.vue +1 -2
  46. package/src/demo/types.ts +0 -1
  47. package/src/index.ts +1 -0
  48. package/src/types/chart.ts +6 -0
  49. package/src/types/index.ts +1 -0
  50. package/src/utils/formatters.test.ts +4 -3
  51. package/src/utils/formatters.ts +1 -2
  52. package/src/utils/sampling.ts +2 -1
@@ -0,0 +1,89 @@
1
+ import type { ViewportSelectionState } from '../core/waveformChartTypes'
2
+
3
+ export interface ViewportGestureStart {
4
+ trackIndex: number
5
+ independent: boolean
6
+ startX: number
7
+ startY: number
8
+ pointerId: number
9
+ kind: 'box' | 'pan'
10
+ xDomain: [number, number]
11
+ yDomains: Record<string, [number, number]>
12
+ }
13
+
14
+ export interface ViewportGesturePosition {
15
+ currentX: number
16
+ currentY: number
17
+ }
18
+
19
+ export type ViewportInteractionEvent =
20
+ | { type: 'begin'; gesture: ViewportGestureStart }
21
+ | { type: 'move'; pointerId: number; position: ViewportGesturePosition }
22
+ | { type: 'finish'; pointerId: number; position: ViewportGesturePosition }
23
+ | { type: 'cancel'; pointerId?: number }
24
+ | { type: 'reset' }
25
+
26
+ export interface ViewportInteractionTransition {
27
+ state: ViewportSelectionState | null
28
+ accepted: boolean
29
+ completed: ViewportSelectionState | null
30
+ }
31
+
32
+ function createSelectionState(input: ViewportGestureStart): ViewportSelectionState {
33
+ return {
34
+ ...input,
35
+ currentX: input.startX,
36
+ currentY: input.startY,
37
+ xDomain: [...input.xDomain],
38
+ yDomains: Object.fromEntries(
39
+ Object.entries(input.yDomains).map(([key, domain]) => [key, [...domain] as [number, number]]),
40
+ ),
41
+ }
42
+ }
43
+
44
+ function withPosition(
45
+ state: ViewportSelectionState,
46
+ position: ViewportGesturePosition,
47
+ ): ViewportSelectionState {
48
+ return { ...state, ...position }
49
+ }
50
+
51
+ function rejectedTransition(state: ViewportSelectionState | null): ViewportInteractionTransition {
52
+ return { state, accepted: false, completed: null }
53
+ }
54
+
55
+ export function transitionViewportInteraction(
56
+ state: ViewportSelectionState | null,
57
+ event: ViewportInteractionEvent,
58
+ ): ViewportInteractionTransition {
59
+ switch (event.type) {
60
+ case 'begin':
61
+ return state
62
+ ? rejectedTransition(state)
63
+ : { state: createSelectionState(event.gesture), accepted: true, completed: null }
64
+ case 'move':
65
+ if (!state || state.pointerId !== event.pointerId) return rejectedTransition(state)
66
+ return { state: withPosition(state, event.position), accepted: true, completed: null }
67
+ case 'finish':
68
+ if (!state || state.pointerId !== event.pointerId) return rejectedTransition(state)
69
+ return {
70
+ state: null,
71
+ accepted: true,
72
+ completed: withPosition(state, event.position),
73
+ }
74
+ case 'cancel':
75
+ if (!state || (event.pointerId !== undefined && state.pointerId !== event.pointerId)) {
76
+ return rejectedTransition(state)
77
+ }
78
+ return { state: null, accepted: true, completed: null }
79
+ case 'reset':
80
+ return { state: null, accepted: true, completed: null }
81
+ }
82
+ }
83
+
84
+ export function reduceViewportInteraction(
85
+ state: ViewportSelectionState | null,
86
+ event: ViewportInteractionEvent,
87
+ ): ViewportSelectionState | null {
88
+ return transitionViewportInteraction(state, event).state
89
+ }
@@ -0,0 +1,167 @@
1
+ import { scaleLinear, zoomIdentity, type ZoomTransform } from 'd3'
2
+
3
+ import type { WaveformPoint } from '../../types'
4
+ import { ZOOM_CONSTRAINTS } from '../core/constants'
5
+
6
+ interface PointSeriesSource {
7
+ points: WaveformPoint[]
8
+ }
9
+
10
+ export type ZoomSeriesGroup = readonly PointSeriesSource[]
11
+
12
+ interface ZoomConstraintOptions {
13
+ minZoomSpan?: number
14
+ minVisiblePoints?: number
15
+ }
16
+
17
+ const xValueCache = new WeakMap<object, Map<string, number[]>>()
18
+
19
+ function normalizedBoundary(boundary: [number, number]): [number, number] {
20
+ return boundary[0] <= boundary[1] ? [...boundary] : [boundary[1], boundary[0]]
21
+ }
22
+
23
+ function resolveRequiredPointCount(minimum: number | undefined): number {
24
+ return Number.isFinite(minimum) && (minimum ?? 0) > 0 ? Math.ceil(minimum as number) : 0
25
+ }
26
+
27
+ function uniqueXValues(group: ZoomSeriesGroup, boundary: [number, number]): number[] {
28
+ const boundaryKey = `${boundary[0]}\u0000${boundary[1]}`
29
+ const cached = xValueCache.get(group)?.get(boundaryKey)
30
+ if (cached) return cached
31
+ const values = new Set<number>()
32
+ for (const series of group) {
33
+ for (const point of series.points) {
34
+ if (point.x >= boundary[0] && point.x <= boundary[1]) values.add(point.x)
35
+ }
36
+ }
37
+ const sorted = Array.from(values).sort((left, right) => left - right)
38
+ const groupCache = xValueCache.get(group) ?? new Map<string, number[]>()
39
+ groupCache.set(boundaryKey, sorted)
40
+ xValueCache.set(group, groupCache)
41
+ return sorted
42
+ }
43
+
44
+ function minimumPointSpan(
45
+ groups: readonly ZoomSeriesGroup[],
46
+ boundary: [number, number],
47
+ required: number,
48
+ ): number {
49
+ if (required <= 1) return 0
50
+ let minimumSpan = 0
51
+ for (const group of groups) {
52
+ const values = uniqueXValues(group, boundary)
53
+ if (values.length < required) return boundary[1] - boundary[0]
54
+ let groupSpan = Number.POSITIVE_INFINITY
55
+ for (let index = 0; index + required <= values.length; index += 1) {
56
+ groupSpan = Math.min(groupSpan, values[index + required - 1] - values[index])
57
+ }
58
+ const endpointTolerance =
59
+ Number.EPSILON * Math.max(1, Math.abs(boundary[0]), Math.abs(boundary[1])) * 16
60
+ minimumSpan = Math.max(minimumSpan, groupSpan + endpointTolerance)
61
+ }
62
+ return minimumSpan
63
+ }
64
+
65
+ export function resolveMinimumZoomSpan(
66
+ boundary: [number, number],
67
+ groups: readonly ZoomSeriesGroup[],
68
+ options: ZoomConstraintOptions,
69
+ ): number {
70
+ const normalized = normalizedBoundary(boundary)
71
+ const boundarySpan = normalized[1] - normalized[0]
72
+ if (!Number.isFinite(boundarySpan) || boundarySpan <= 0) return 0
73
+ const configuredSpan =
74
+ Number.isFinite(options.minZoomSpan) && (options.minZoomSpan ?? 0) > 0
75
+ ? Math.min(boundarySpan, options.minZoomSpan as number)
76
+ : 0
77
+ const required = resolveRequiredPointCount(options.minVisiblePoints)
78
+ const pointSpan = minimumPointSpan(groups, normalized, required)
79
+ if (configuredSpan > 0 || required > 0) {
80
+ return Math.max(configuredSpan, pointSpan)
81
+ }
82
+ return boundarySpan / ZOOM_CONSTRAINTS.DEFAULT_MAX_SCALE
83
+ }
84
+
85
+ function expandToMinimumPoints(
86
+ domain: [number, number],
87
+ boundary: [number, number],
88
+ groups: readonly ZoomSeriesGroup[],
89
+ required: number,
90
+ ): [number, number] {
91
+ if (required <= 0) return domain
92
+ const endpointTolerance =
93
+ Number.EPSILON * Math.max(1, Math.abs(boundary[0]), Math.abs(boundary[1])) * 16
94
+ let expanded = domain
95
+ for (const group of groups) {
96
+ const values = uniqueXValues(group, boundary)
97
+ if (values.length < required) return [...boundary]
98
+ const visibleCount = values.filter(
99
+ (value) => value >= expanded[0] && value <= expanded[1],
100
+ ).length
101
+ if (visibleCount >= required) continue
102
+ let best: [number, number] | undefined
103
+ let bestSpan = Number.POSITIVE_INFINITY
104
+ let bestCenterDistance = Number.POSITIVE_INFINITY
105
+ const requestedCenter = (expanded[0] + expanded[1]) / 2
106
+ for (let index = 0; index + required <= values.length; index += 1) {
107
+ const candidate: [number, number] = [
108
+ Math.max(boundary[0], Math.min(expanded[0], values[index] - endpointTolerance)),
109
+ Math.min(
110
+ boundary[1],
111
+ Math.max(expanded[1], values[index + required - 1] + endpointTolerance),
112
+ ),
113
+ ]
114
+ const span = candidate[1] - candidate[0]
115
+ const centerDistance = Math.abs((candidate[0] + candidate[1]) / 2 - requestedCenter)
116
+ if (span < bestSpan || (span === bestSpan && centerDistance < bestCenterDistance)) {
117
+ best = candidate
118
+ bestSpan = span
119
+ bestCenterDistance = centerDistance
120
+ }
121
+ }
122
+ expanded = best ?? [...boundary]
123
+ }
124
+ return expanded
125
+ }
126
+
127
+ export function constrainZoomDomain(
128
+ domain: [number, number],
129
+ boundary: [number, number],
130
+ groups: readonly ZoomSeriesGroup[],
131
+ options: ZoomConstraintOptions,
132
+ ): [number, number] {
133
+ const normalized = normalizedBoundary(boundary)
134
+ const boundarySpan = normalized[1] - normalized[0]
135
+ if (!Number.isFinite(boundarySpan) || boundarySpan <= 0) return normalized
136
+ if (Math.abs(domain[1] - domain[0]) >= boundarySpan * (1 - 1e-12)) return normalized
137
+ const requested: [number, number] = [
138
+ Math.max(normalized[0], Math.min(domain[0], domain[1])),
139
+ Math.min(normalized[1], Math.max(domain[0], domain[1])),
140
+ ]
141
+ const expanded = expandToMinimumPoints(
142
+ requested,
143
+ normalized,
144
+ groups,
145
+ resolveRequiredPointCount(options.minVisiblePoints),
146
+ )
147
+ const minimumSpan = resolveMinimumZoomSpan(normalized, groups, options)
148
+ const span = Math.max(minimumSpan, Math.min(boundarySpan, expanded[1] - expanded[0]))
149
+ const center = (expanded[0] + expanded[1]) / 2
150
+ const start = Math.max(normalized[0], Math.min(center - span / 2, normalized[1] - span))
151
+ return [start, start + span]
152
+ }
153
+
154
+ export function transformForDomain(
155
+ domain: [number, number],
156
+ baseDomain: [number, number],
157
+ width: number,
158
+ ): ZoomTransform {
159
+ const baseSpan = baseDomain[1] - baseDomain[0]
160
+ const span = domain[1] - domain[0]
161
+ if (!Number.isFinite(baseSpan) || !Number.isFinite(span) || baseSpan <= 0 || span <= 0) {
162
+ return zoomIdentity
163
+ }
164
+ const scale = baseSpan / span
165
+ const baseScale = scaleLinear(baseDomain, [0, width])
166
+ return zoomIdentity.translate(-scale * baseScale(domain[0]), 0).scale(scale)
167
+ }
@@ -308,69 +308,4 @@ describe('WaveformChart', () => {
308
308
  expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(eventCount + 1)
309
309
  expect(zoomedOutDomain).toEqual([0, 2])
310
310
  })
311
-
312
- it('resets a shared viewport on double-click and emits zoom-reset', async () => {
313
- const wrapper = await mountSizedChart({
314
- kind: 'points',
315
- points: [
316
- { x: 0, y: 0 },
317
- { x: 2, y: 1 },
318
- ],
319
- })
320
- const overlay = wrapper.get('.waveform-chart__overlay')
321
- const width = Number(overlay.attributes('width'))
322
- Object.defineProperty(overlay.element, 'getBoundingClientRect', {
323
- value: () => ({ left: 0, top: 0, width, height: 290 }),
324
- })
325
- overlay.element.dispatchEvent(
326
- new WheelEvent('wheel', {
327
- deltaY: -4000,
328
- clientX: width / 2,
329
- clientY: 145,
330
- bubbles: true,
331
- cancelable: true,
332
- }),
333
- )
334
- flushAnimationFrames()
335
- await flushPromises()
336
- expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe('0')
337
-
338
- overlay.element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }))
339
- await flushPromises()
340
-
341
- expect(wrapper.emitted('zoom-reset')).toHaveLength(1)
342
- expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
343
- expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
344
- })
345
-
346
- it('exposes resetViewport for independent tracks', async () => {
347
- const wrapper = await mountSizedChart(gridSeries(2), {
348
- displayMode: 'independent',
349
- grid: { rowCount: 1, columnCount: 2 },
350
- })
351
- const overlay = wrapper.findAll('.waveform-chart__overlay--independent')[0]
352
- const width = Number(overlay.attributes('width'))
353
- Object.defineProperty(overlay.element, 'getBoundingClientRect', {
354
- value: () => ({ left: 0, top: 0, width, height: 260 }),
355
- })
356
- overlay.element.dispatchEvent(
357
- new WheelEvent('wheel', {
358
- deltaY: -4000,
359
- clientX: width / 2,
360
- clientY: 130,
361
- bubbles: true,
362
- cancelable: true,
363
- }),
364
- )
365
- flushAnimationFrames()
366
- await flushPromises()
367
- expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).not.toBe('0')
368
-
369
- const chart = wrapper.vm as unknown as { resetViewport: () => void }
370
- chart.resetViewport()
371
- await flushPromises()
372
-
373
- expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).toBe('0')
374
- expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('1000')
375
- })
376
311
  })
@@ -0,0 +1,132 @@
1
+ import { flushPromises } from '@vue/test-utils'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+
4
+ import { gridSeries, mountSizedChart } from '../../test/waveformChart'
5
+ import WaveformChartView from '../WaveformChartView.vue'
6
+
7
+ describe('WaveformChart viewport lifecycle', () => {
8
+ it('cleans an active shared gesture when its track is removed', async () => {
9
+ const wrapper = await mountSizedChart(gridSeries(2), {
10
+ displayMode: 'separated',
11
+ grid: { rowCount: 2, columnCount: 1 },
12
+ })
13
+ const overlay = wrapper.get('.waveform-chart__overlay--shared')
14
+ const width = Number(overlay.attributes('width'))
15
+ const height = Number(overlay.attributes('height'))
16
+ const releasePointerCapture = vi.fn()
17
+ Object.defineProperty(overlay.element, 'getBoundingClientRect', {
18
+ value: () => ({ left: 0, top: 0, width, height }),
19
+ })
20
+ Object.defineProperty(overlay.element, 'setPointerCapture', { value: vi.fn() })
21
+ Object.defineProperty(overlay.element, 'releasePointerCapture', {
22
+ value: releasePointerCapture,
23
+ })
24
+
25
+ const dispatchPointer = (type: string, pointerId: number, clientY: number) => {
26
+ const event = new MouseEvent(type, {
27
+ button: 0,
28
+ clientX: width / 2,
29
+ clientY,
30
+ bubbles: true,
31
+ })
32
+ Object.defineProperty(event, 'pointerId', { value: pointerId })
33
+ overlay.element.dispatchEvent(event)
34
+ }
35
+
36
+ dispatchPointer('pointerdown', 41, height * 0.75)
37
+ await flushPromises()
38
+ expect(wrapper.find('.waveform-chart__zoom-selection').exists()).toBe(true)
39
+
40
+ await wrapper.setProps({ data: gridSeries(1) })
41
+ await flushPromises()
42
+ dispatchPointer('pointerup', 41, height * 0.75)
43
+ await flushPromises()
44
+
45
+ expect(releasePointerCapture).toHaveBeenCalledWith(41)
46
+ expect(wrapper.find('.waveform-chart__zoom-selection').exists()).toBe(false)
47
+
48
+ dispatchPointer('pointerdown', 42, height / 2)
49
+ dispatchPointer('pointermove', 42, height / 2 + 20)
50
+ await flushPromises()
51
+ expect(wrapper.find('.waveform-chart__zoom-selection').exists()).toBe(true)
52
+ })
53
+
54
+ it('cleans an active gesture when its overlay leaves the DOM', async () => {
55
+ const wrapper = await mountSizedChart(
56
+ {
57
+ kind: 'points',
58
+ points: [
59
+ { x: 0, y: 0 },
60
+ { x: 1, y: 1 },
61
+ ],
62
+ },
63
+ { displayMode: 'separated' },
64
+ )
65
+ const overlay = wrapper.get('.waveform-chart__overlay--shared')
66
+ const width = Number(overlay.attributes('width'))
67
+ const height = Number(overlay.attributes('height'))
68
+ const releasePointerCapture = vi.fn(() => {
69
+ throw new DOMException('Pointer capture is no longer available', 'NotFoundError')
70
+ })
71
+ Object.defineProperty(overlay.element, 'getBoundingClientRect', {
72
+ value: () => ({ left: 0, top: 0, width, height }),
73
+ })
74
+ Object.defineProperty(overlay.element, 'setPointerCapture', { value: vi.fn() })
75
+ Object.defineProperty(overlay.element, 'releasePointerCapture', {
76
+ value: releasePointerCapture,
77
+ })
78
+
79
+ const down = new MouseEvent('pointerdown', {
80
+ button: 0,
81
+ clientX: width / 2,
82
+ clientY: height / 2,
83
+ bubbles: true,
84
+ })
85
+ Object.defineProperty(down, 'pointerId', { value: 43 })
86
+ overlay.element.dispatchEvent(down)
87
+ await wrapper.setProps({ hiddenSeriesIds: ['series-0'] })
88
+ await flushPromises()
89
+
90
+ expect(wrapper.find('.waveform-chart__overlay--shared').exists()).toBe(false)
91
+ const view = wrapper.findComponent(WaveformChartView)
92
+ const finishViewportDrag = (
93
+ view.vm as unknown as { finishViewportDrag: (event: PointerEvent) => void }
94
+ ).finishViewportDrag
95
+ const up = new MouseEvent('pointerup', {
96
+ clientX: width / 2,
97
+ clientY: height / 2,
98
+ bubbles: true,
99
+ })
100
+ Object.defineProperty(up, 'pointerId', { value: 43 })
101
+ finishViewportDrag(up as unknown as PointerEvent)
102
+ await flushPromises()
103
+
104
+ expect(releasePointerCapture).toHaveBeenCalledWith(43)
105
+ expect(wrapper.find('.waveform-chart__zoom-selection').exists()).toBe(false)
106
+
107
+ await wrapper.setProps({ hiddenSeriesIds: [] })
108
+ await flushPromises()
109
+ const nextOverlay = wrapper.get('.waveform-chart__overlay--shared')
110
+ Object.defineProperty(nextOverlay.element, 'getBoundingClientRect', {
111
+ value: () => ({ left: 0, top: 0, width, height }),
112
+ })
113
+ const nextDown = new MouseEvent('pointerdown', {
114
+ button: 0,
115
+ clientX: width / 2,
116
+ clientY: height / 2,
117
+ bubbles: true,
118
+ })
119
+ Object.defineProperty(nextDown, 'pointerId', { value: 44 })
120
+ nextOverlay.element.dispatchEvent(nextDown)
121
+ const nextMove = new MouseEvent('pointermove', {
122
+ clientX: width * 0.75,
123
+ clientY: height / 2,
124
+ bubbles: true,
125
+ })
126
+ Object.defineProperty(nextMove, 'pointerId', { value: 44 })
127
+ nextOverlay.element.dispatchEvent(nextMove)
128
+ await flushPromises()
129
+
130
+ expect(wrapper.find('.waveform-chart__zoom-selection').exists()).toBe(true)
131
+ })
132
+ })
@@ -0,0 +1,114 @@
1
+ import { flushPromises } from '@vue/test-utils'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { flushAnimationFrames } from '../../test/setup'
5
+ import { gridSeries, mountSizedChart } from '../../test/waveformChart'
6
+
7
+ describe('WaveformChart viewport reset', () => {
8
+ it('resets a shared viewport on double-click and emits a global payload', async () => {
9
+ const wrapper = await mountSizedChart(
10
+ {
11
+ kind: 'points',
12
+ points: [
13
+ { x: 0, y: 0 },
14
+ { x: 2, y: 1 },
15
+ ],
16
+ },
17
+ { displayMode: 'separated' },
18
+ )
19
+ const overlay = wrapper.get('.waveform-chart__overlay')
20
+ const width = Number(overlay.attributes('width'))
21
+ Object.defineProperty(overlay.element, 'getBoundingClientRect', {
22
+ value: () => ({ left: 0, top: 0, width, height: 290 }),
23
+ })
24
+ overlay.element.dispatchEvent(
25
+ new WheelEvent('wheel', {
26
+ deltaY: -4000,
27
+ clientX: width / 2,
28
+ clientY: 145,
29
+ bubbles: true,
30
+ cancelable: true,
31
+ }),
32
+ )
33
+ flushAnimationFrames()
34
+ await flushPromises()
35
+ expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe('0')
36
+
37
+ overlay.element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }))
38
+ await flushPromises()
39
+
40
+ expect(wrapper.emitted('zoom-reset')).toEqual([[{}]])
41
+ expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
42
+ expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
43
+ })
44
+
45
+ it('resets only the double-clicked independent track and identifies it', async () => {
46
+ const wrapper = await mountSizedChart(gridSeries(2), {
47
+ displayMode: 'independent',
48
+ grid: { rowCount: 1, columnCount: 2 },
49
+ })
50
+ const overlays = wrapper.findAll('.waveform-chart__overlay--independent')
51
+ for (const overlay of overlays) {
52
+ const width = Number(overlay.attributes('width'))
53
+ Object.defineProperty(overlay.element, 'getBoundingClientRect', {
54
+ value: () => ({ left: 0, top: 0, width, height: 260 }),
55
+ })
56
+ overlay.element.dispatchEvent(
57
+ new WheelEvent('wheel', {
58
+ deltaY: -4000,
59
+ clientX: width / 2,
60
+ clientY: 130,
61
+ bubbles: true,
62
+ cancelable: true,
63
+ }),
64
+ )
65
+ flushAnimationFrames()
66
+ await flushPromises()
67
+ }
68
+ const startsBeforeReset = wrapper.findAll('.waveform-chart__axis-endpoint--start')
69
+ expect(startsBeforeReset[0].text()).not.toBe('0')
70
+ expect(startsBeforeReset[1].text()).not.toBe('0')
71
+ const secondTrackStart = startsBeforeReset[1].text()
72
+
73
+ overlays[0].element.dispatchEvent(
74
+ new MouseEvent('dblclick', { bubbles: true, cancelable: true }),
75
+ )
76
+ await flushPromises()
77
+
78
+ const startsAfterReset = wrapper.findAll('.waveform-chart__axis-endpoint--start')
79
+ expect(startsAfterReset[0].text()).toBe('0')
80
+ expect(startsAfterReset[1].text()).toBe(secondTrackStart)
81
+ expect(wrapper.emitted('zoom-reset')).toEqual([[{ trackIndex: 0, seriesIds: ['channel-0'] }]])
82
+ })
83
+
84
+ it('keeps the exposed no-argument resetViewport global', async () => {
85
+ const wrapper = await mountSizedChart(gridSeries(2), {
86
+ displayMode: 'independent',
87
+ grid: { rowCount: 1, columnCount: 2 },
88
+ })
89
+ const overlay = wrapper.findAll('.waveform-chart__overlay--independent')[0]
90
+ const width = Number(overlay.attributes('width'))
91
+ Object.defineProperty(overlay.element, 'getBoundingClientRect', {
92
+ value: () => ({ left: 0, top: 0, width, height: 260 }),
93
+ })
94
+ overlay.element.dispatchEvent(
95
+ new WheelEvent('wheel', {
96
+ deltaY: -4000,
97
+ clientX: width / 2,
98
+ clientY: 130,
99
+ bubbles: true,
100
+ cancelable: true,
101
+ }),
102
+ )
103
+ flushAnimationFrames()
104
+ await flushPromises()
105
+ expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).not.toBe('0')
106
+
107
+ const chart = wrapper.vm as unknown as { resetViewport: () => void }
108
+ chart.resetViewport()
109
+ await flushPromises()
110
+
111
+ expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).toBe('0')
112
+ expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('1000')
113
+ })
114
+ })