waveform-analysis 0.1.58 → 0.1.59
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.
- package/README.md +9 -6
- package/dist/index.cjs +1 -1
- package/dist/index.js +705 -644
- package/dist/types/components/WaveformChart.vue.d.ts +2 -0
- package/dist/types/components/core/useWaveformChartLifecycle.d.ts +1 -2
- package/dist/types/components/core/waveformChartTypes.d.ts +2 -1
- package/dist/types/components/data/types.d.ts +1 -1
- package/dist/types/components/index.d.ts +1 -1
- package/dist/types/components/interaction/useWaveformZoom.d.ts +2 -1
- package/dist/types/components/interaction/zoomEventPayload.d.ts +7 -0
- package/dist/types/core/rendering.d.ts +1 -1
- package/dist/types/core/renderingOptions.d.ts +1 -1
- package/dist/types/core/wasmSampling.d.ts +1 -1
- package/dist/types/core/workerSampling/multiresolution.d.ts +1 -1
- package/dist/types/core/workerSampling/protocol.d.ts +1 -1
- package/dist/types/core/workerSampling/repositoryDataset.d.ts +1 -1
- package/dist/types/core/workerSampling/repositorySupport.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/types/chart.d.ts +8 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/utils/geometry.d.ts +1 -1
- package/dist/types/wasm/waveform_sampling_wasm.d.ts +74 -0
- package/dist/types/wasm/waveform_sampling_wasm_bg.wasm.d.ts +19 -0
- package/package.json +5 -4
- package/src/components/core/useWaveformChartController.ts +6 -1
- package/src/components/core/useWaveformChartLifecycle.ts +15 -9
- package/src/components/core/waveformChartTypes.ts +2 -0
- package/src/components/data/types.ts +1 -0
- package/src/components/index.ts +1 -0
- package/src/components/interaction/useWaveformViewport.ts +2 -0
- package/src/components/interaction/useWaveformZoom.ts +58 -61
- package/src/components/interaction/zoomEventPayload.ts +88 -0
- package/src/components/waveformChartCases/zoomIntentAndDataRebind.test.ts +213 -0
- package/src/core/rendering.ts +1 -1
- package/src/core/renderingOptions.ts +1 -1
- package/src/core/workerSampling/multiresolution.ts +1 -1
- package/src/core/workerSampling/protocol.ts +1 -1
- package/src/core/workerSampling/repository.ts +1 -1
- package/src/core/workerSampling/repositoryDataset.ts +1 -1
- package/src/core/workerSampling/repositorySupport.ts +1 -1
- package/src/index.ts +1 -0
- package/src/types/chart.ts +9 -0
- package/src/types/index.ts +1 -0
- package/src/utils/geometry.ts +1 -1
|
@@ -21,6 +21,11 @@ import {
|
|
|
21
21
|
transformForDomain,
|
|
22
22
|
type ZoomSeriesGroup,
|
|
23
23
|
} from './zoomConstraints'
|
|
24
|
+
import {
|
|
25
|
+
emitIndependentWheelZoomEnds,
|
|
26
|
+
emitSharedWheelZoomEnd,
|
|
27
|
+
seriesIdentity,
|
|
28
|
+
} from './zoomEventPayload'
|
|
24
29
|
|
|
25
30
|
interface ZoomContext {
|
|
26
31
|
props: ResolvedWaveformChartProps
|
|
@@ -70,9 +75,10 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
70
75
|
let lastSharedZoomGesture: ZoomGestureKind | null = null
|
|
71
76
|
const pendingIndependentZoomTransforms = new Map<number, ZoomTransform>()
|
|
72
77
|
const pendingIndependentZoomGestures = new Map<number, ZoomGestureKind>()
|
|
73
|
-
const
|
|
74
|
-
const lastZoomedTrackIndexes = new Set<number>()
|
|
78
|
+
const lastIndependentWheelSeriesIds = new Map<string, string[]>()
|
|
75
79
|
let wheelZoomEndTimer: ReturnType<typeof setTimeout> | undefined
|
|
80
|
+
let preservePendingWheelDuringDataRebind = false
|
|
81
|
+
let zoomBindingEpoch = 0
|
|
76
82
|
|
|
77
83
|
const scheduleZoomCommit = () => zoomThrottle.schedule(commitPendingZoom)
|
|
78
84
|
const handleSharedZoom = (event: D3ZoomEvent<SVGRectElement, unknown>) => {
|
|
@@ -80,8 +86,12 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
80
86
|
cancelPendingHover()
|
|
81
87
|
pendingSharedZoomTransform = event.transform
|
|
82
88
|
pendingSharedZoomGesture = 'wheel'
|
|
83
|
-
scheduleZoomCommit()
|
|
84
89
|
scheduleWheelZoomEnd()
|
|
90
|
+
const domain = event.transform
|
|
91
|
+
.rescaleX(scaleLinear(initialXDomain.value, [0, innerWidth.value]))
|
|
92
|
+
.domain()
|
|
93
|
+
emit('zoom-intent', { start: domain[0], end: domain[1], gesture: 'wheel' })
|
|
94
|
+
scheduleZoomCommit()
|
|
85
95
|
}
|
|
86
96
|
const handleIndependentZoom = (
|
|
87
97
|
event: D3ZoomEvent<SVGRectElement, unknown>,
|
|
@@ -91,8 +101,21 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
91
101
|
cancelPendingHover()
|
|
92
102
|
pendingIndependentZoomTransforms.set(trackIndex, event.transform)
|
|
93
103
|
pendingIndependentZoomGestures.set(trackIndex, 'wheel')
|
|
94
|
-
scheduleZoomCommit()
|
|
95
104
|
scheduleWheelZoomEnd()
|
|
105
|
+
const track = trackLayouts.value.find((item) => item.index === trackIndex)
|
|
106
|
+
if (track) {
|
|
107
|
+
const domain = event.transform
|
|
108
|
+
.rescaleX(scaleLinear(resolveInitialTrackDomain(track), [0, track.width]))
|
|
109
|
+
.domain()
|
|
110
|
+
emit('zoom-intent', {
|
|
111
|
+
start: domain[0],
|
|
112
|
+
end: domain[1],
|
|
113
|
+
trackIndex,
|
|
114
|
+
seriesIds: track.seriesList.map((series) => series.id),
|
|
115
|
+
gesture: 'wheel',
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
scheduleZoomCommit()
|
|
96
119
|
}
|
|
97
120
|
function commitPendingZoom() {
|
|
98
121
|
if (isPresentationMode.value) {
|
|
@@ -113,12 +136,7 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
113
136
|
if (!pendingIndependentZoomTransforms.size) return
|
|
114
137
|
const nextTransforms = [...independentTransforms.value]
|
|
115
138
|
const changedTrackIndexes = Array.from(pendingIndependentZoomTransforms.keys())
|
|
116
|
-
|
|
117
|
-
changedTrackIndexes.forEach((trackIndex) => lastZoomedTrackIndexes.add(trackIndex))
|
|
118
|
-
changedTrackIndexes.forEach((trackIndex) => {
|
|
119
|
-
const gesture = pendingIndependentZoomGestures.get(trackIndex)
|
|
120
|
-
if (gesture) lastIndependentZoomGestures.set(trackIndex, gesture)
|
|
121
|
-
})
|
|
139
|
+
const changedGestures = new Map(pendingIndependentZoomGestures)
|
|
122
140
|
pendingIndependentZoomTransforms.forEach((transform, trackIndex) => {
|
|
123
141
|
nextTransforms[trackIndex] = transform
|
|
124
142
|
})
|
|
@@ -129,69 +147,37 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
129
147
|
changedTrackIndexes.forEach((trackIndex) => {
|
|
130
148
|
const track = tracksByIndex.get(trackIndex)
|
|
131
149
|
if (!track) return
|
|
150
|
+
if (changedGestures.get(trackIndex) === 'wheel') {
|
|
151
|
+
const seriesIds = track.seriesList.map((series) => series.id)
|
|
152
|
+
lastIndependentWheelSeriesIds.set(seriesIdentity(seriesIds), seriesIds)
|
|
153
|
+
}
|
|
132
154
|
const domain = track.xScale.domain()
|
|
133
155
|
emit('zoom-change', [domain[0], domain[1]])
|
|
134
156
|
})
|
|
135
157
|
}
|
|
136
158
|
const emitZoomEnd = () => {
|
|
137
159
|
if (props.displayMode === 'independent') {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (lastIndependentZoomGestures.get(trackIndex) !== 'wheel') return
|
|
141
|
-
const track = tracksByIndex.get(trackIndex)
|
|
142
|
-
if (!track) return
|
|
143
|
-
const domain = track.xScale.domain() as [number, number]
|
|
144
|
-
const yDomain = track.yScale.domain()
|
|
145
|
-
emit('zoom-end', {
|
|
146
|
-
start: domain[0],
|
|
147
|
-
end: domain[1],
|
|
148
|
-
yStart: yDomain[0],
|
|
149
|
-
yEnd: yDomain[1],
|
|
150
|
-
trackIndex,
|
|
151
|
-
seriesIds: track.seriesList.map((series) => series.id),
|
|
152
|
-
gesture: 'wheel',
|
|
153
|
-
})
|
|
154
|
-
})
|
|
155
|
-
lastZoomedTrackIndexes.clear()
|
|
156
|
-
lastIndependentZoomGestures.clear()
|
|
160
|
+
emitIndependentWheelZoomEnds(emit, trackLayouts.value, lastIndependentWheelSeriesIds.values())
|
|
161
|
+
lastIndependentWheelSeriesIds.clear()
|
|
157
162
|
return
|
|
158
163
|
}
|
|
159
164
|
if (lastSharedZoomGesture !== 'wheel') {
|
|
160
165
|
lastSharedZoomGesture = null
|
|
161
166
|
return
|
|
162
167
|
}
|
|
163
|
-
|
|
164
|
-
const visibleTracks = trackLayouts.value.filter((track) => track.hasVisibleSeries)
|
|
165
|
-
if (visibleTracks.length === 1) {
|
|
166
|
-
const yDomain = visibleTracks[0]?.yScale.domain()
|
|
167
|
-
emit('zoom-end', {
|
|
168
|
-
start: domain[0],
|
|
169
|
-
end: domain[1],
|
|
170
|
-
yStart: yDomain?.[0],
|
|
171
|
-
yEnd: yDomain?.[1],
|
|
172
|
-
gesture: 'wheel',
|
|
173
|
-
})
|
|
174
|
-
} else {
|
|
175
|
-
emit('zoom-end', {
|
|
176
|
-
start: domain[0],
|
|
177
|
-
end: domain[1],
|
|
178
|
-
yRanges: Object.fromEntries(
|
|
179
|
-
visibleTracks.map((track) => [
|
|
180
|
-
track.series?.trackId ?? track.series?.id ?? track.id,
|
|
181
|
-
track.yScale.domain() as [number, number],
|
|
182
|
-
]),
|
|
183
|
-
),
|
|
184
|
-
gesture: 'wheel',
|
|
185
|
-
})
|
|
186
|
-
}
|
|
168
|
+
emitSharedWheelZoomEnd(emit, sharedZoomDomain.value, trackLayouts.value)
|
|
187
169
|
lastSharedZoomGesture = null
|
|
188
170
|
}
|
|
189
171
|
const flushPendingZoom = () => {
|
|
190
172
|
zoomThrottle.flush()
|
|
191
173
|
commitPendingZoom()
|
|
192
|
-
if (lastSharedZoomGesture === 'wheel' ||
|
|
174
|
+
if (lastSharedZoomGesture === 'wheel' || lastIndependentWheelSeriesIds.size) return
|
|
193
175
|
emitZoomEnd()
|
|
194
176
|
}
|
|
177
|
+
const handleD3ZoomEnd = (bindingEpoch: number) => {
|
|
178
|
+
if (bindingEpoch !== zoomBindingEpoch) return
|
|
179
|
+
flushPendingZoom()
|
|
180
|
+
}
|
|
195
181
|
function scheduleWheelZoomEnd() {
|
|
196
182
|
if (wheelZoomEndTimer !== undefined) clearTimeout(wheelZoomEndTimer)
|
|
197
183
|
wheelZoomEndTimer = setTimeout(() => {
|
|
@@ -199,24 +185,32 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
199
185
|
zoomThrottle.flush()
|
|
200
186
|
commitPendingZoom()
|
|
201
187
|
emitZoomEnd()
|
|
188
|
+
preservePendingWheelDuringDataRebind = false
|
|
202
189
|
}, WHEEL_ZOOM_DEBOUNCE_MS)
|
|
203
190
|
}
|
|
191
|
+
const prepareForDataChange = () => {
|
|
192
|
+
if (wheelZoomEndTimer === undefined) return
|
|
193
|
+
zoomThrottle.flush()
|
|
194
|
+
commitPendingZoom()
|
|
195
|
+
preservePendingWheelDuringDataRebind = true
|
|
196
|
+
}
|
|
204
197
|
const cancelPendingZoom = () => {
|
|
205
198
|
pendingSharedZoomTransform = null
|
|
206
199
|
pendingSharedZoomGesture = null
|
|
207
200
|
lastSharedZoomGesture = null
|
|
208
201
|
pendingIndependentZoomTransforms.clear()
|
|
209
202
|
pendingIndependentZoomGestures.clear()
|
|
210
|
-
|
|
211
|
-
lastIndependentZoomGestures.clear()
|
|
203
|
+
lastIndependentWheelSeriesIds.clear()
|
|
212
204
|
zoomThrottle.cancel()
|
|
205
|
+
preservePendingWheelDuringDataRebind = false
|
|
213
206
|
if (wheelZoomEndTimer !== undefined) {
|
|
214
207
|
clearTimeout(wheelZoomEndTimer)
|
|
215
208
|
wheelZoomEndTimer = undefined
|
|
216
209
|
}
|
|
217
210
|
}
|
|
218
|
-
const clearZoomBindings = () => {
|
|
219
|
-
|
|
211
|
+
const clearZoomBindings = (preservePendingWheel = false) => {
|
|
212
|
+
zoomBindingEpoch += 1
|
|
213
|
+
if (!preservePendingWheel) cancelPendingZoom()
|
|
220
214
|
svgElement.value
|
|
221
215
|
?.querySelectorAll<SVGRectElement>('.waveform-chart__overlay')
|
|
222
216
|
.forEach((overlay) => select(overlay).on('.zoom', null))
|
|
@@ -268,7 +262,9 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
268
262
|
return false
|
|
269
263
|
}
|
|
270
264
|
const configureZoom = () => {
|
|
271
|
-
|
|
265
|
+
const preservePendingWheel = preservePendingWheelDuringDataRebind
|
|
266
|
+
clearZoomBindings(preservePendingWheel)
|
|
267
|
+
const bindingEpoch = zoomBindingEpoch
|
|
272
268
|
if (
|
|
273
269
|
isPresentationMode.value ||
|
|
274
270
|
!props.zoomable ||
|
|
@@ -303,7 +299,7 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
303
299
|
[track.width, track.height],
|
|
304
300
|
])
|
|
305
301
|
.on('zoom', (event) => handleIndependentZoom(event, track.index))
|
|
306
|
-
.on('end',
|
|
302
|
+
.on('end', () => handleD3ZoomEnd(bindingEpoch))
|
|
307
303
|
zoomBehaviors.set(track.index, behavior)
|
|
308
304
|
synchronizingZoomTransform = true
|
|
309
305
|
try {
|
|
@@ -337,7 +333,7 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
337
333
|
[innerWidth.value, innerHeight.value],
|
|
338
334
|
])
|
|
339
335
|
.on('zoom', handleSharedZoom)
|
|
340
|
-
.on('end',
|
|
336
|
+
.on('end', () => handleD3ZoomEnd(bindingEpoch))
|
|
341
337
|
zoomBehaviors.set('shared', behavior)
|
|
342
338
|
synchronizingZoomTransform = true
|
|
343
339
|
try {
|
|
@@ -356,5 +352,6 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
356
352
|
canZoomTrack,
|
|
357
353
|
canZoomSharedTracks,
|
|
358
354
|
configureZoom,
|
|
355
|
+
prepareForDataChange,
|
|
359
356
|
}
|
|
360
357
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { TrackLayout } from '../core/types'
|
|
2
|
+
import type { WaveformChartEmit } from '../core/waveformChartTypes'
|
|
3
|
+
|
|
4
|
+
export function seriesIdentity(seriesIds: readonly string[]) {
|
|
5
|
+
return [...seriesIds].sort().join('\u0000')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function findTrackBySeriesIds(
|
|
9
|
+
trackLayouts: readonly TrackLayout[],
|
|
10
|
+
seriesIds: readonly string[],
|
|
11
|
+
) {
|
|
12
|
+
const identity = seriesIdentity(seriesIds)
|
|
13
|
+
return trackLayouts.find(
|
|
14
|
+
(track) => seriesIdentity(track.seriesList.map((series) => series.id)) === identity,
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function emitBoxZoomIntent(
|
|
19
|
+
emit: WaveformChartEmit,
|
|
20
|
+
domain: [number, number],
|
|
21
|
+
track: TrackLayout,
|
|
22
|
+
independent: boolean,
|
|
23
|
+
) {
|
|
24
|
+
emit(
|
|
25
|
+
'zoom-intent',
|
|
26
|
+
independent
|
|
27
|
+
? {
|
|
28
|
+
start: domain[0],
|
|
29
|
+
end: domain[1],
|
|
30
|
+
trackIndex: track.index,
|
|
31
|
+
seriesIds: track.seriesList.map((series) => series.id),
|
|
32
|
+
gesture: 'box',
|
|
33
|
+
}
|
|
34
|
+
: { start: domain[0], end: domain[1], gesture: 'box' },
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function emitIndependentWheelZoomEnds(
|
|
39
|
+
emit: WaveformChartEmit,
|
|
40
|
+
trackLayouts: readonly TrackLayout[],
|
|
41
|
+
pendingSeriesIds: Iterable<readonly string[]>,
|
|
42
|
+
) {
|
|
43
|
+
for (const seriesIds of pendingSeriesIds) {
|
|
44
|
+
const track = findTrackBySeriesIds(trackLayouts, seriesIds)
|
|
45
|
+
if (!track) continue
|
|
46
|
+
const domain = track.xScale.domain() as [number, number]
|
|
47
|
+
const yDomain = track.yScale.domain()
|
|
48
|
+
emit('zoom-end', {
|
|
49
|
+
start: domain[0],
|
|
50
|
+
end: domain[1],
|
|
51
|
+
yStart: yDomain[0],
|
|
52
|
+
yEnd: yDomain[1],
|
|
53
|
+
trackIndex: track.index,
|
|
54
|
+
seriesIds: [...seriesIds],
|
|
55
|
+
gesture: 'wheel',
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function emitSharedWheelZoomEnd(
|
|
61
|
+
emit: WaveformChartEmit,
|
|
62
|
+
domain: [number, number],
|
|
63
|
+
trackLayouts: readonly TrackLayout[],
|
|
64
|
+
) {
|
|
65
|
+
const visibleTracks = trackLayouts.filter((track) => track.hasVisibleSeries)
|
|
66
|
+
if (visibleTracks.length === 1) {
|
|
67
|
+
const yDomain = visibleTracks[0]?.yScale.domain()
|
|
68
|
+
emit('zoom-end', {
|
|
69
|
+
start: domain[0],
|
|
70
|
+
end: domain[1],
|
|
71
|
+
yStart: yDomain?.[0],
|
|
72
|
+
yEnd: yDomain?.[1],
|
|
73
|
+
gesture: 'wheel',
|
|
74
|
+
})
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
emit('zoom-end', {
|
|
78
|
+
start: domain[0],
|
|
79
|
+
end: domain[1],
|
|
80
|
+
yRanges: Object.fromEntries(
|
|
81
|
+
visibleTracks.map((track) => [
|
|
82
|
+
track.series?.trackId ?? track.series?.id ?? track.id,
|
|
83
|
+
track.yScale.domain() as [number, number],
|
|
84
|
+
]),
|
|
85
|
+
),
|
|
86
|
+
gesture: 'wheel',
|
|
87
|
+
})
|
|
88
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { flushPromises, mount } from '@vue/test-utils'
|
|
2
|
+
import { defineComponent, h, ref } from 'vue'
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import WaveformChart from '../WaveformChart.vue'
|
|
6
|
+
import type { WaveformData } from '../waveform'
|
|
7
|
+
import { resizeObservers } from '../../test/setup'
|
|
8
|
+
import { gridSeries, mountSizedChart } from '../../test/waveformChart'
|
|
9
|
+
|
|
10
|
+
function seriesWindow(ids: string[]) {
|
|
11
|
+
return {
|
|
12
|
+
kind: 'series' as const,
|
|
13
|
+
series: ids.map((id, index) => ({
|
|
14
|
+
id,
|
|
15
|
+
name: id,
|
|
16
|
+
data: {
|
|
17
|
+
kind: 'points' as const,
|
|
18
|
+
points: [
|
|
19
|
+
{ x: 40, y: index },
|
|
20
|
+
{ x: 50, y: index + 1 },
|
|
21
|
+
{ x: 60, y: index + 2 },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
})),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fullSeries(ids: string[]): WaveformData {
|
|
29
|
+
return {
|
|
30
|
+
kind: 'series',
|
|
31
|
+
series: ids.map((id, index) => ({
|
|
32
|
+
id,
|
|
33
|
+
name: id,
|
|
34
|
+
data: {
|
|
35
|
+
kind: 'points',
|
|
36
|
+
points: [
|
|
37
|
+
{ x: 0, y: index },
|
|
38
|
+
{ x: 50, y: index + 1 },
|
|
39
|
+
{ x: 100, y: index + 2 },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
})),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function configureOverlay(overlay: ReturnType<typeof mount>['element']) {
|
|
47
|
+
const width = Number(overlay.getAttribute('width'))
|
|
48
|
+
const height = Number(overlay.getAttribute('height'))
|
|
49
|
+
Object.defineProperty(overlay, 'getBoundingClientRect', {
|
|
50
|
+
value: () => ({ left: 0, top: 0, width, height }),
|
|
51
|
+
})
|
|
52
|
+
return { width, height }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function dispatchWheel(overlay: Element, deltaY: number, width: number, height: number) {
|
|
56
|
+
overlay.dispatchEvent(
|
|
57
|
+
new WheelEvent('wheel', {
|
|
58
|
+
deltaY,
|
|
59
|
+
clientX: width / 2,
|
|
60
|
+
clientY: height / 2,
|
|
61
|
+
bubbles: true,
|
|
62
|
+
cancelable: true,
|
|
63
|
+
}),
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('WaveformChart zoom intent and data rebind', () => {
|
|
68
|
+
it('keeps one shared wheel end after an immediate intent-driven data replacement', async () => {
|
|
69
|
+
vi.useFakeTimers()
|
|
70
|
+
try {
|
|
71
|
+
const data = ref<WaveformData>(fullSeries(['shared']))
|
|
72
|
+
const host = mount(
|
|
73
|
+
defineComponent({
|
|
74
|
+
setup() {
|
|
75
|
+
return () =>
|
|
76
|
+
h(WaveformChart, {
|
|
77
|
+
data: data.value,
|
|
78
|
+
displayMode: 'separated',
|
|
79
|
+
initialXDomain: [0, 100],
|
|
80
|
+
onZoomIntent: () => {
|
|
81
|
+
data.value = seriesWindow(['shared'])
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
)
|
|
87
|
+
resizeObservers.at(-1)?.resize(800, 360)
|
|
88
|
+
await flushPromises()
|
|
89
|
+
|
|
90
|
+
const chart = host.findComponent(WaveformChart)
|
|
91
|
+
const overlay = chart.get('.waveform-chart__overlay')
|
|
92
|
+
const { width, height } = configureOverlay(overlay.element)
|
|
93
|
+
dispatchWheel(overlay.element, -4_000, width, height)
|
|
94
|
+
|
|
95
|
+
const firstIntent = chart.emitted('zoom-intent')?.[0]?.[0] as {
|
|
96
|
+
start: number
|
|
97
|
+
end: number
|
|
98
|
+
}
|
|
99
|
+
expect(firstIntent.end - firstIntent.start).toBeCloseTo(2.5)
|
|
100
|
+
expect(chart.emitted('zoom-change')).toBeUndefined()
|
|
101
|
+
|
|
102
|
+
await flushPromises()
|
|
103
|
+
dispatchWheel(overlay.element, 1_000, width, height)
|
|
104
|
+
await flushPromises()
|
|
105
|
+
await vi.advanceTimersByTimeAsync(199)
|
|
106
|
+
expect(chart.emitted('zoom-end')).toBeUndefined()
|
|
107
|
+
|
|
108
|
+
await vi.advanceTimersByTimeAsync(1)
|
|
109
|
+
await flushPromises()
|
|
110
|
+
const zoomEnd = chart.emitted('zoom-end')?.[0]?.[0] as { start: number; end: number }
|
|
111
|
+
expect(chart.emitted('zoom-intent')).toHaveLength(2)
|
|
112
|
+
expect(chart.emitted('zoom-end')).toHaveLength(1)
|
|
113
|
+
expect(zoomEnd.end - zoomEnd.start).toBeGreaterThan(firstIntent.end - firstIntent.start)
|
|
114
|
+
} finally {
|
|
115
|
+
vi.useRealTimers()
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('retains a pending independent wheel gesture by stable series identity', async () => {
|
|
120
|
+
vi.useFakeTimers()
|
|
121
|
+
try {
|
|
122
|
+
const data = ref<WaveformData>(fullSeries(['channel-a', 'channel-b']))
|
|
123
|
+
const host = mount(
|
|
124
|
+
defineComponent({
|
|
125
|
+
setup() {
|
|
126
|
+
return () =>
|
|
127
|
+
h(WaveformChart, {
|
|
128
|
+
data: data.value,
|
|
129
|
+
displayMode: 'independent',
|
|
130
|
+
grid: { rowCount: 1, columnCount: 2 },
|
|
131
|
+
initialXDomain: [0, 100],
|
|
132
|
+
onZoomIntent: () => {
|
|
133
|
+
data.value = seriesWindow(['channel-b', 'channel-a'])
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
)
|
|
139
|
+
resizeObservers.at(-1)?.resize(800, 360)
|
|
140
|
+
await flushPromises()
|
|
141
|
+
|
|
142
|
+
const chart = host.findComponent(WaveformChart)
|
|
143
|
+
const overlay = chart.findAll('.waveform-chart__overlay--independent')[0]!
|
|
144
|
+
const { width, height } = configureOverlay(overlay.element)
|
|
145
|
+
dispatchWheel(overlay.element, -4_000, width, height)
|
|
146
|
+
await flushPromises()
|
|
147
|
+
await vi.advanceTimersByTimeAsync(200)
|
|
148
|
+
await flushPromises()
|
|
149
|
+
|
|
150
|
+
const zoomEnd = chart.emitted('zoom-end')?.[0]?.[0] as {
|
|
151
|
+
trackIndex: number
|
|
152
|
+
seriesIds: string[]
|
|
153
|
+
}
|
|
154
|
+
expect(chart.emitted('zoom-intent')?.[0]?.[0]).toMatchObject({
|
|
155
|
+
trackIndex: 0,
|
|
156
|
+
seriesIds: ['channel-a'],
|
|
157
|
+
gesture: 'wheel',
|
|
158
|
+
})
|
|
159
|
+
expect(zoomEnd).toMatchObject({ trackIndex: 1, seriesIds: ['channel-a'], gesture: 'wheel' })
|
|
160
|
+
} finally {
|
|
161
|
+
vi.useRealTimers()
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('emits box intent with the selected independent series', async () => {
|
|
166
|
+
const wrapper = await mountSizedChart(fullSeries(['series-a']), { initialXDomain: [0, 100] })
|
|
167
|
+
const overlay = wrapper.get('.waveform-chart__overlay--independent')
|
|
168
|
+
const { width, height } = configureOverlay(overlay.element)
|
|
169
|
+
const dispatchPointer = (type: string, clientX: number, clientY: number) => {
|
|
170
|
+
const event = new MouseEvent(type, { button: 0, clientX, clientY, bubbles: true })
|
|
171
|
+
Object.defineProperty(event, 'pointerId', { value: 11 })
|
|
172
|
+
overlay.element.dispatchEvent(event)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
dispatchPointer('pointerdown', width * 0.25, height / 2)
|
|
176
|
+
dispatchPointer('pointermove', width * 0.75, height / 2)
|
|
177
|
+
dispatchPointer('pointerup', width * 0.75, height / 2)
|
|
178
|
+
await flushPromises()
|
|
179
|
+
|
|
180
|
+
expect(wrapper.emitted('zoom-intent')?.[0]?.[0]).toMatchObject({
|
|
181
|
+
trackIndex: 0,
|
|
182
|
+
seriesIds: ['series-a'],
|
|
183
|
+
gesture: 'box',
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('cancels pending wheel ends when reset or pagination changes the viewport', async () => {
|
|
188
|
+
vi.useFakeTimers()
|
|
189
|
+
try {
|
|
190
|
+
const reset = await mountSizedChart(fullSeries(['reset']), { initialXDomain: [0, 100] })
|
|
191
|
+
const resetOverlay = reset.get('.waveform-chart__overlay--independent')
|
|
192
|
+
const resetBounds = configureOverlay(resetOverlay.element)
|
|
193
|
+
dispatchWheel(resetOverlay.element, -4_000, resetBounds.width, resetBounds.height)
|
|
194
|
+
;(reset.vm as unknown as { resetViewport: () => void }).resetViewport()
|
|
195
|
+
await vi.advanceTimersByTimeAsync(200)
|
|
196
|
+
expect(reset.emitted('zoom-end')).toBeUndefined()
|
|
197
|
+
|
|
198
|
+
const paged = await mountSizedChart(gridSeries(2), {
|
|
199
|
+
displayMode: 'independent',
|
|
200
|
+
grid: { rowCount: 1, columnCount: 1 },
|
|
201
|
+
})
|
|
202
|
+
const pageOverlay = paged.get('.waveform-chart__overlay--independent')
|
|
203
|
+
const pageBounds = configureOverlay(pageOverlay.element)
|
|
204
|
+
dispatchWheel(pageOverlay.element, -4_000, pageBounds.width, pageBounds.height)
|
|
205
|
+
await paged.get('.ant-pagination-next button').trigger('click')
|
|
206
|
+
await flushPromises()
|
|
207
|
+
await vi.advanceTimersByTimeAsync(200)
|
|
208
|
+
expect(paged.emitted('zoom-end')).toBeUndefined()
|
|
209
|
+
} finally {
|
|
210
|
+
vi.useRealTimers()
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
})
|
package/src/core/rendering.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { bisector } from 'd3'
|
|
2
2
|
|
|
3
|
-
import type { WaveformPoint } from '
|
|
3
|
+
import type { WaveformPoint } from '../types'
|
|
4
4
|
import { resolveWaveformPointErrors } from './data'
|
|
5
5
|
import type { ResolvedWaveformRenderingOptions } from './renderingOptions'
|
|
6
6
|
import {
|
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
WaveformSamplingDiagnostics,
|
|
7
7
|
WaveformSamplingMode,
|
|
8
8
|
WaveformSamplingStrategy,
|
|
9
|
-
} from '
|
|
9
|
+
} from '../../types'
|
|
10
10
|
import type { WorkerSamplingDataset } from '../waveformPointSource'
|
|
11
11
|
|
|
12
12
|
export const DEFAULT_WORKER_SAMPLING_AUTO_THRESHOLD = 1_000
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WaveformPoint } from '
|
|
1
|
+
import type { WaveformPoint } from '../../types'
|
|
2
2
|
import type { WorkerSamplingDataset } from '../waveformPointSource'
|
|
3
3
|
import type { WasmDatasetSamplingBackend } from '../wasmSampling'
|
|
4
4
|
import { MultiResolutionSamplingIndex } from './multiresolution'
|
package/src/index.ts
CHANGED
package/src/types/chart.ts
CHANGED
|
@@ -49,6 +49,15 @@ export interface WaveformZoomEndPayload {
|
|
|
49
49
|
gesture?: 'wheel' | 'box'
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** Describes the X-axis viewport targeted by a user zoom gesture before rendering settles. */
|
|
53
|
+
export interface WaveformZoomIntentPayload {
|
|
54
|
+
start: number
|
|
55
|
+
end: number
|
|
56
|
+
gesture: 'wheel' | 'box'
|
|
57
|
+
trackIndex?: number
|
|
58
|
+
seriesIds?: string[]
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
/** Identifies the viewport reset by a double-click gesture. */
|
|
53
62
|
export interface WaveformZoomResetPayload {
|
|
54
63
|
trackIndex?: number
|
package/src/types/index.ts
CHANGED
package/src/utils/geometry.ts
CHANGED