waveform-analysis 0.1.58 → 0.1.60
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 +894 -817
- package/dist/types/components/WaveformChart.vue.d.ts +2 -0
- package/dist/types/components/core/layout.d.ts +9 -2
- package/dist/types/components/core/useWaveformChartLifecycle.d.ts +1 -2
- package/dist/types/components/core/waveformChartTypes.d.ts +2 -1
- package/dist/types/components/core/yAxisLayoutMetrics.d.ts +10 -0
- 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/layout.test.ts +24 -0
- package/src/components/core/layout.ts +75 -13
- package/src/components/core/trackLayoutBuilder.ts +7 -13
- package/src/components/core/useWaveformChartController.ts +6 -1
- package/src/components/core/useWaveformChartLifecycle.ts +15 -9
- package/src/components/core/useWaveformLayout.ts +30 -58
- package/src/components/core/waveformChartTypes.ts +2 -0
- package/src/components/core/yAxisLayoutMetrics.ts +75 -0
- package/src/components/core/yAxisSlotLayout.test.ts +37 -1
- 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/rendering/WaveformTrackAxes.vue +8 -4
- package/src/components/waveformChartCases/layoutAndDomains.test.ts +23 -24
- package/src/components/waveformChartCases/yAxisLayout.test.ts +103 -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
|
@@ -9,12 +9,12 @@ import {
|
|
|
9
9
|
type ShallowRef,
|
|
10
10
|
} from 'vue'
|
|
11
11
|
|
|
12
|
-
import type { AnnotationSeriesCandidate } from '../annotation'
|
|
13
|
-
import type { useWaveformAnnotationInteraction } from '../annotation'
|
|
12
|
+
import type { AnnotationSeriesCandidate, useWaveformAnnotationInteraction } from '../annotation'
|
|
14
13
|
import type { NormalizedWaveformGridOptions } from './grid'
|
|
15
14
|
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
|
|
16
15
|
import type { ResolvedWaveformChartProps, WaveformChartEmit } from './waveformChartTypes'
|
|
17
16
|
import { constrainZoomDomain, transformForDomain } from '../interaction/zoomConstraints'
|
|
17
|
+
import { seriesIdentity } from '../interaction/zoomEventPayload'
|
|
18
18
|
|
|
19
19
|
interface LifecycleContext {
|
|
20
20
|
props: ResolvedWaveformChartProps
|
|
@@ -152,16 +152,20 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
let pendingSharedXDomain: [number, number] | undefined
|
|
155
|
-
let pendingIndependentXDomains:
|
|
155
|
+
let pendingIndependentXDomains: Map<string, [number, number]> | undefined
|
|
156
156
|
|
|
157
157
|
function handleBeforeDataReferenceChange() {
|
|
158
158
|
if (props.displayMode === 'independent') {
|
|
159
159
|
pendingSharedXDomain = undefined
|
|
160
|
-
pendingIndependentXDomains =
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
160
|
+
pendingIndependentXDomains = new Map(
|
|
161
|
+
trackLayouts.value.flatMap((track) => {
|
|
162
|
+
const current = track.xScale.domain() as [number, number]
|
|
163
|
+
const boundary = resolveInitialTrackDomain(track)
|
|
164
|
+
return current[1] - current[0] < boundary[1] - boundary[0] - 1e-12
|
|
165
|
+
? [[seriesIdentity(track.seriesList.map((series) => series.id)), current]]
|
|
166
|
+
: []
|
|
167
|
+
}),
|
|
168
|
+
)
|
|
165
169
|
return
|
|
166
170
|
}
|
|
167
171
|
pendingIndependentXDomains = undefined
|
|
@@ -184,7 +188,9 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
184
188
|
if (props.displayMode === 'independent' && pendingIndependentXDomains) {
|
|
185
189
|
const nextTransforms = chartTracks.value.map(() => zoomIdentity)
|
|
186
190
|
trackLayouts.value.forEach((track) => {
|
|
187
|
-
const previousDomain = pendingIndependentXDomains?.
|
|
191
|
+
const previousDomain = pendingIndependentXDomains?.get(
|
|
192
|
+
seriesIdentity(track.seriesList.map((series) => series.id)),
|
|
193
|
+
)
|
|
188
194
|
if (!previousDomain) return
|
|
189
195
|
const boundary = resolveInitialTrackDomain(track)
|
|
190
196
|
const domain = constrainZoomDomain(previousDomain, boundary, [track.seriesList], props)
|
|
@@ -7,16 +7,7 @@ import {
|
|
|
7
7
|
type AnnotationSeriesInfo,
|
|
8
8
|
type AnnotationTrackLayout,
|
|
9
9
|
} from '../annotation'
|
|
10
|
-
import {
|
|
11
|
-
channelColors,
|
|
12
|
-
margin,
|
|
13
|
-
MINIMUM_PLOT_WIDTH,
|
|
14
|
-
Y_AXIS_CHARACTER_WIDTH,
|
|
15
|
-
Y_AXIS_LABEL_BAND_WIDTH,
|
|
16
|
-
Y_AXIS_LABEL_GAP,
|
|
17
|
-
Y_AXIS_OUTER_PADDING,
|
|
18
|
-
Y_AXIS_TICK_PADDING,
|
|
19
|
-
} from './constants'
|
|
10
|
+
import { channelColors, margin, MINIMUM_PLOT_WIDTH } from './constants'
|
|
20
11
|
import {
|
|
21
12
|
getGridGap,
|
|
22
13
|
getPageCount,
|
|
@@ -24,17 +15,13 @@ import {
|
|
|
24
15
|
paginateSeries,
|
|
25
16
|
resolveGridCellGeometry,
|
|
26
17
|
} from './grid'
|
|
27
|
-
import {
|
|
28
|
-
buildTrackLayouts,
|
|
29
|
-
axisTextMetrics,
|
|
30
|
-
buildYAxisSlots,
|
|
31
|
-
resolveYAxisSeriesGroups,
|
|
32
|
-
} from './layout'
|
|
18
|
+
import { buildTrackLayouts, buildYAxisSlots, resolveYAxisTickCount } from './layout'
|
|
33
19
|
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
|
|
34
20
|
import { createFrameNumberResolver, resolvePageableTracks } from './trackPagination'
|
|
35
21
|
import type { PreparedWaveformSeries } from './useWaveformData'
|
|
36
22
|
import type { ResolvedWaveformChartProps } from './waveformChartTypes'
|
|
37
23
|
import { applyXDomainStrategy } from './xDomain'
|
|
24
|
+
import { resolveYAxisLayoutMetrics, resolveViewportYDomains } from './yAxisLayoutMetrics'
|
|
38
25
|
import type { useWaveformAnnotationInteraction } from '../annotation'
|
|
39
26
|
interface LayoutContext {
|
|
40
27
|
props: ResolvedWaveformChartProps
|
|
@@ -121,38 +108,29 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
121
108
|
? tracksWithSeries
|
|
122
109
|
: pagedTracks.value
|
|
123
110
|
})
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
)
|
|
148
|
-
const fullClearance =
|
|
149
|
-
tickTextWidth +
|
|
150
|
-
Y_AXIS_TICK_PADDING +
|
|
151
|
-
Y_AXIS_LABEL_GAP +
|
|
152
|
-
Y_AXIS_LABEL_BAND_WIDTH +
|
|
153
|
-
Y_AXIS_OUTER_PADDING
|
|
154
|
-
return { tickClearance, fullClearance, labelCenterX }
|
|
155
|
-
})
|
|
111
|
+
const yAxisTickCount = computed(() =>
|
|
112
|
+
resolveYAxisTickCount(innerHeight.value, props.axes?.y?.splitNumber),
|
|
113
|
+
)
|
|
114
|
+
const viewportYDomains = computed(() =>
|
|
115
|
+
resolveViewportYDomains(
|
|
116
|
+
props.displayMode,
|
|
117
|
+
chartTracks.value,
|
|
118
|
+
sharedYDomains.value,
|
|
119
|
+
independentYDomains.value,
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
const yAxisMetrics = computed(() =>
|
|
123
|
+
resolveYAxisLayoutMetrics(
|
|
124
|
+
chartTracks.value,
|
|
125
|
+
props.overlayMode,
|
|
126
|
+
props.yDomain,
|
|
127
|
+
props.yDomains,
|
|
128
|
+
viewportYDomains.value,
|
|
129
|
+
props.axes?.y?.nice !== false,
|
|
130
|
+
yAxisTickCount.value,
|
|
131
|
+
props.displayMode === 'compact',
|
|
132
|
+
),
|
|
133
|
+
)
|
|
156
134
|
const hasYAxisLabels = computed(() =>
|
|
157
135
|
chartTracks.value.some(
|
|
158
136
|
(track) =>
|
|
@@ -179,8 +157,10 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
179
157
|
props.overlayMode,
|
|
180
158
|
props.yDomain,
|
|
181
159
|
props.yDomains,
|
|
182
|
-
|
|
160
|
+
yAxisTickCount.value,
|
|
183
161
|
props.axes?.y?.nice !== false,
|
|
162
|
+
props.displayMode === 'compact',
|
|
163
|
+
viewportYDomains.value,
|
|
184
164
|
),
|
|
185
165
|
)
|
|
186
166
|
const resolvedChartLeftMargin = computed(() =>
|
|
@@ -302,15 +282,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
302
282
|
xDomainStrategy: props.xDomainStrategy,
|
|
303
283
|
fixedYDomain: props.yDomain,
|
|
304
284
|
fixedYDomains: props.yDomains,
|
|
305
|
-
yDomains:
|
|
306
|
-
props.displayMode === 'independent'
|
|
307
|
-
? Object.fromEntries(
|
|
308
|
-
chartTracks.value.flatMap((track, index) => {
|
|
309
|
-
const domain = independentYDomains.value[index]
|
|
310
|
-
return domain ? [[track.id, domain]] : []
|
|
311
|
-
}),
|
|
312
|
-
)
|
|
313
|
-
: sharedYDomains.value,
|
|
285
|
+
yDomains: viewportYDomains.value,
|
|
314
286
|
timeUnit: props.timeUnit,
|
|
315
287
|
xAxisLabelFormatter: props.axes?.x?.labelFormatter,
|
|
316
288
|
yAxisSplitNumber: props.axes?.y?.splitNumber,
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
WaveformTitleOptions,
|
|
17
17
|
WaveformXDomainStrategy,
|
|
18
18
|
WaveformZeroLineOptions,
|
|
19
|
+
WaveformZoomIntentPayload,
|
|
19
20
|
WaveformZoomEndPayload,
|
|
20
21
|
WaveformZoomResetPayload,
|
|
21
22
|
} from '../data/types'
|
|
@@ -90,6 +91,7 @@ export type ResolvedWaveformChartProps = Readonly<
|
|
|
90
91
|
|
|
91
92
|
export interface WaveformChartEmit {
|
|
92
93
|
(event: 'point-hover', point: WaveformPoint | null): void
|
|
94
|
+
(event: 'zoom-intent', payload: WaveformZoomIntentPayload): void
|
|
93
95
|
(event: 'zoom-change', domain: [number, number]): void
|
|
94
96
|
(event: 'zoom-end', payload: WaveformZoomEndPayload): void
|
|
95
97
|
(event: 'zoom-reset', payload: WaveformZoomResetPayload): void
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { WaveformOverlayMode } from '../data/types'
|
|
2
|
+
import {
|
|
3
|
+
Y_AXIS_CHARACTER_WIDTH,
|
|
4
|
+
Y_AXIS_LABEL_BAND_WIDTH,
|
|
5
|
+
Y_AXIS_LABEL_GAP,
|
|
6
|
+
Y_AXIS_OUTER_PADDING,
|
|
7
|
+
Y_AXIS_TICK_PADDING,
|
|
8
|
+
} from './constants'
|
|
9
|
+
import { axisTextMetrics, resolveRenderedYAxisSeriesGroups } from './layout'
|
|
10
|
+
import type { DisplayTrack } from './types'
|
|
11
|
+
import type { WaveformYDomain } from './yDomain'
|
|
12
|
+
|
|
13
|
+
export interface YAxisLayoutMetrics {
|
|
14
|
+
tickClearance: number
|
|
15
|
+
fullClearance: number
|
|
16
|
+
labelCenterX: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveViewportYDomains(
|
|
20
|
+
displayMode: 'independent' | 'separated' | 'compact',
|
|
21
|
+
chartTracks: DisplayTrack[],
|
|
22
|
+
sharedYDomains: Record<string, [number, number]>,
|
|
23
|
+
independentYDomains: Record<number, [number, number]>,
|
|
24
|
+
): Record<string, [number, number]> {
|
|
25
|
+
if (displayMode !== 'independent') return sharedYDomains
|
|
26
|
+
return Object.fromEntries(
|
|
27
|
+
chartTracks.flatMap((track, index) => {
|
|
28
|
+
const domain = independentYDomains[index]
|
|
29
|
+
return domain ? [[track.id, domain]] : []
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveYAxisLayoutMetrics(
|
|
35
|
+
chartTracks: DisplayTrack[],
|
|
36
|
+
overlayMode: WaveformOverlayMode,
|
|
37
|
+
yDomain: WaveformYDomain | undefined,
|
|
38
|
+
yDomains: Record<string, WaveformYDomain> | undefined,
|
|
39
|
+
viewportYDomains: Record<string, [number, number]>,
|
|
40
|
+
nice: boolean,
|
|
41
|
+
tickCount: number,
|
|
42
|
+
compact: boolean,
|
|
43
|
+
): YAxisLayoutMetrics {
|
|
44
|
+
const axisText = chartTracks
|
|
45
|
+
.filter((track) => track.visibleSeries.length > 0)
|
|
46
|
+
.flatMap((track) =>
|
|
47
|
+
resolveRenderedYAxisSeriesGroups(track, overlayMode, yDomain, yDomains, viewportYDomains),
|
|
48
|
+
)
|
|
49
|
+
.map(
|
|
50
|
+
(group) =>
|
|
51
|
+
axisTextMetrics(
|
|
52
|
+
group.domain,
|
|
53
|
+
nice,
|
|
54
|
+
undefined,
|
|
55
|
+
group.seriesList[0]?.unit,
|
|
56
|
+
tickCount,
|
|
57
|
+
compact,
|
|
58
|
+
).tickTextWidth,
|
|
59
|
+
)
|
|
60
|
+
const tickTextWidth = Math.max(Y_AXIS_CHARACTER_WIDTH, ...axisText)
|
|
61
|
+
const tickClearance = tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_OUTER_PADDING
|
|
62
|
+
const labelCenterX = -(
|
|
63
|
+
Y_AXIS_TICK_PADDING +
|
|
64
|
+
tickTextWidth +
|
|
65
|
+
Y_AXIS_LABEL_GAP +
|
|
66
|
+
Y_AXIS_LABEL_BAND_WIDTH / 2
|
|
67
|
+
)
|
|
68
|
+
const fullClearance =
|
|
69
|
+
tickTextWidth +
|
|
70
|
+
Y_AXIS_TICK_PADDING +
|
|
71
|
+
Y_AXIS_LABEL_GAP +
|
|
72
|
+
Y_AXIS_LABEL_BAND_WIDTH +
|
|
73
|
+
Y_AXIS_OUTER_PADDING
|
|
74
|
+
return { tickClearance, fullClearance, labelCenterX }
|
|
75
|
+
}
|
|
@@ -2,7 +2,7 @@ import { zoomIdentity } from 'd3'
|
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
4
|
import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from '../../core'
|
|
5
|
-
import { buildTrackLayouts, buildYAxisSlots } from './layout'
|
|
5
|
+
import { buildTrackLayouts, buildYAxisSlots, resolveRenderedYAxisSeriesGroups } from './layout'
|
|
6
6
|
import type { DisplaySeries, DisplayTrack } from './types'
|
|
7
7
|
import { Y_AXIS_RIGHT_LABEL_OFFSET } from './yAxisConstants'
|
|
8
8
|
|
|
@@ -126,4 +126,40 @@ describe('Y-axis slots', () => {
|
|
|
126
126
|
expect(fourAxisSlots.slots[1]!.axisOffset).toBeLessThan(0)
|
|
127
127
|
expect(fourAxisSlots.slots[3]!.axisOffset).toBeGreaterThan(0)
|
|
128
128
|
})
|
|
129
|
+
|
|
130
|
+
it('uses dynamic Y domains when measuring multi-axis slots', () => {
|
|
131
|
+
const negativeVoltage = series('negative-voltage', 0, 1)
|
|
132
|
+
negativeVoltage.unit = 'V'
|
|
133
|
+
const longUnit = series('long-unit', 0, 1)
|
|
134
|
+
longUnit.unit = 'volt-amperes-per-phase'
|
|
135
|
+
const sourceTrack = track([negativeVoltage, longUnit])
|
|
136
|
+
const viewportYDomains = { [sourceTrack.id]: [-0.7434, -0.3434] as [number, number] }
|
|
137
|
+
|
|
138
|
+
expect(
|
|
139
|
+
resolveRenderedYAxisSeriesGroups(
|
|
140
|
+
sourceTrack,
|
|
141
|
+
'multi-axis',
|
|
142
|
+
undefined,
|
|
143
|
+
undefined,
|
|
144
|
+
viewportYDomains,
|
|
145
|
+
).map((group) => group.domain),
|
|
146
|
+
).toEqual([
|
|
147
|
+
[-0.7434, -0.3434],
|
|
148
|
+
[-0.7434, -0.3434],
|
|
149
|
+
])
|
|
150
|
+
|
|
151
|
+
const slots = buildYAxisSlots(
|
|
152
|
+
[sourceTrack],
|
|
153
|
+
'multi-axis',
|
|
154
|
+
undefined,
|
|
155
|
+
undefined,
|
|
156
|
+
5,
|
|
157
|
+
false,
|
|
158
|
+
false,
|
|
159
|
+
viewportYDomains,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
expect(slots.clearance.left).toBe(100)
|
|
163
|
+
expect(slots.clearance.right).toBeGreaterThan(180)
|
|
164
|
+
})
|
|
129
165
|
})
|
package/src/components/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { tryReleasePointerCapture } from './pointerCapture'
|
|
|
10
10
|
import { transitionViewportInteraction } from './viewportInteractionState'
|
|
11
11
|
import { createViewportDomainSetter } from './viewportDomain'
|
|
12
12
|
import { constrainZoomDomain, transformForDomain } from './zoomConstraints'
|
|
13
|
+
import { emitBoxZoomIntent } from './zoomEventPayload'
|
|
13
14
|
interface ViewportContext {
|
|
14
15
|
props: ResolvedWaveformChartProps
|
|
15
16
|
emit: WaveformChartEmit
|
|
@@ -254,6 +255,7 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
254
255
|
} else {
|
|
255
256
|
sharedTransform.value = transformForDomain(xDomain, baseXDomain, innerWidth.value)
|
|
256
257
|
}
|
|
258
|
+
emitBoxZoomIntent(emit, xDomain, track, active.independent)
|
|
257
259
|
const targets = active.independent
|
|
258
260
|
? [track]
|
|
259
261
|
: trackLayouts.value.filter((target) => target.hasVisibleSeries)
|
|
@@ -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
|
+
}
|