feat(视口): 支持恢复受控缩放范围
All checks were successful
Package component / package (push) Successful in 4m38s
All checks were successful
Package component / package (push) Successful in 4m38s
This commit is contained in:
@@ -36,6 +36,7 @@ const controller = useWaveformChartController(props as ResolvedWaveformChartProp
|
||||
|
||||
defineExpose({
|
||||
resetViewport: controller.resetViewport,
|
||||
setViewportDomain: controller.setViewportDomain,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -259,6 +259,9 @@ export function useWaveformChartController(
|
||||
trackLayouts,
|
||||
innerWidth,
|
||||
innerHeight,
|
||||
sharedZoomDomain,
|
||||
initialXDomain,
|
||||
sharedTransform,
|
||||
activeInteractionMode,
|
||||
hiddenSeriesIdSet,
|
||||
internalHiddenSeriesIds,
|
||||
|
||||
@@ -39,6 +39,9 @@ interface LifecycleContext {
|
||||
trackLayouts: ComputedRef<TrackLayout[]>
|
||||
innerWidth: ComputedRef<number>
|
||||
innerHeight: ComputedRef<number>
|
||||
sharedZoomDomain: ComputedRef<[number, number]>
|
||||
initialXDomain: ComputedRef<[number, number]>
|
||||
sharedTransform: ShallowRef<ZoomTransform>
|
||||
activeInteractionMode: ComputedRef<string | undefined>
|
||||
hiddenSeriesIdSet: ComputedRef<Set<string>>
|
||||
internalHiddenSeriesIds: Ref<Set<string>>
|
||||
@@ -93,6 +96,9 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
||||
trackLayouts,
|
||||
innerWidth,
|
||||
innerHeight,
|
||||
sharedZoomDomain,
|
||||
initialXDomain,
|
||||
sharedTransform,
|
||||
activeInteractionMode,
|
||||
hiddenSeriesIdSet,
|
||||
internalHiddenSeriesIds,
|
||||
@@ -144,25 +150,24 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
||||
emit('page-change', nextPage, pageCount.value)
|
||||
}
|
||||
|
||||
let pendingSharedXDomain: [number, number] | undefined
|
||||
let pendingIndependentXDomains: Array<[number, number] | undefined> | undefined
|
||||
|
||||
function handleBeforeDataReferenceChange() {
|
||||
if (props.displayMode !== 'independent') return
|
||||
pendingIndependentXDomains = trackLayouts.value.map((track) => {
|
||||
const configuredDomain =
|
||||
props.initialXDomains?.[track.series.trackId ?? track.series.id] ??
|
||||
props.initialXDomains?.[track.series.id] ??
|
||||
props.initialXDomain
|
||||
if (
|
||||
!configuredDomain ||
|
||||
!Number.isFinite(configuredDomain[0]) ||
|
||||
!Number.isFinite(configuredDomain[1]) ||
|
||||
configuredDomain[0] === configuredDomain[1]
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
return track.xScale.domain() as [number, number]
|
||||
})
|
||||
if (props.displayMode === 'independent') {
|
||||
pendingSharedXDomain = undefined
|
||||
pendingIndependentXDomains = trackLayouts.value.map((track) => {
|
||||
const current = track.xScale.domain() as [number, number]
|
||||
const boundary = resolveInitialTrackDomain(track)
|
||||
return current[1] - current[0] < boundary[1] - boundary[0] - 1e-12 ? current : undefined
|
||||
})
|
||||
return
|
||||
}
|
||||
pendingIndependentXDomains = undefined
|
||||
const current = sharedZoomDomain.value
|
||||
const boundary = initialXDomain.value
|
||||
pendingSharedXDomain =
|
||||
current[1] - current[0] < boundary[1] - boundary[0] - 1e-12 ? [...current] : undefined
|
||||
}
|
||||
|
||||
function handleDataReferenceChange() {
|
||||
@@ -186,10 +191,15 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
||||
})
|
||||
independentTransforms.value = nextTransforms
|
||||
pendingIndependentXDomains = undefined
|
||||
void nextTick(configureZoom)
|
||||
return
|
||||
} else if (props.displayMode !== 'independent' && pendingSharedXDomain) {
|
||||
const boundary = initialXDomain.value
|
||||
const groups = trackLayouts.value
|
||||
.filter((track) => track.hasVisibleSeries)
|
||||
.map((track) => track.seriesList)
|
||||
const domain = constrainZoomDomain(pendingSharedXDomain, boundary, groups, props)
|
||||
sharedTransform.value = transformForDomain(domain, boundary, innerWidth.value)
|
||||
pendingSharedXDomain = undefined
|
||||
}
|
||||
pendingIndependentXDomains = undefined
|
||||
configureZoom()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
import type { AnnotationSeriesCandidate } from '../annotation'
|
||||
import { tryReleasePointerCapture } from './pointerCapture'
|
||||
import { transitionViewportInteraction } from './viewportInteractionState'
|
||||
import { createViewportDomainSetter } from './viewportDomain'
|
||||
import { constrainZoomDomain, transformForDomain } from './zoomConstraints'
|
||||
interface ViewportContext {
|
||||
props: ResolvedWaveformChartProps
|
||||
@@ -344,6 +345,20 @@ export function useWaveformViewport(context: ViewportContext) {
|
||||
editorSeriesOptions.value = []
|
||||
void nextTick(configureZoom)
|
||||
}
|
||||
const setViewportDomain = createViewportDomainSetter({
|
||||
props,
|
||||
trackLayouts,
|
||||
initialXDomain,
|
||||
resolveInitialTrackDomain,
|
||||
innerWidth,
|
||||
sharedTransform,
|
||||
independentTransforms,
|
||||
cancelPendingZoom,
|
||||
cancelViewportDrag,
|
||||
clearHover,
|
||||
editorSeriesOptions,
|
||||
configureZoom,
|
||||
})
|
||||
const requestViewportReset = (event: MouseEvent) => {
|
||||
if (isPresentationMode.value || !props.zoomable || !isZoomMode.value) return
|
||||
if (props.displayMode === 'independent') {
|
||||
@@ -372,6 +387,7 @@ export function useWaveformViewport(context: ViewportContext) {
|
||||
finishViewportDrag,
|
||||
cancelViewportDrag,
|
||||
resetViewport,
|
||||
setViewportDomain,
|
||||
requestViewportReset,
|
||||
}
|
||||
}
|
||||
|
||||
60
src/components/interaction/viewportDomain.ts
Normal file
60
src/components/interaction/viewportDomain.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { nextTick, type ComputedRef, type Ref, type ShallowRef } from 'vue'
|
||||
import type { ZoomTransform } from 'd3'
|
||||
|
||||
import type { TrackLayout } from '../core/types'
|
||||
import type { ResolvedWaveformChartProps } from '../core/waveformChartTypes'
|
||||
import type { AnnotationSeriesCandidate } from '../annotation'
|
||||
import { constrainZoomDomain, transformForDomain } from './zoomConstraints'
|
||||
|
||||
interface ViewportDomainContext {
|
||||
props: ResolvedWaveformChartProps
|
||||
trackLayouts: ComputedRef<TrackLayout[]>
|
||||
initialXDomain: ComputedRef<[number, number]>
|
||||
resolveInitialTrackDomain: (track: TrackLayout) => [number, number]
|
||||
innerWidth: ComputedRef<number>
|
||||
sharedTransform: ShallowRef<ZoomTransform>
|
||||
independentTransforms: ShallowRef<ZoomTransform[]>
|
||||
cancelPendingZoom: () => void
|
||||
cancelViewportDrag: () => void
|
||||
clearHover: () => void
|
||||
editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>
|
||||
configureZoom: () => void
|
||||
}
|
||||
|
||||
export function createViewportDomainSetter(context: ViewportDomainContext) {
|
||||
return (domain: [number, number], trackIndex?: number) => {
|
||||
if (!Number.isFinite(domain[0]) || !Number.isFinite(domain[1]) || domain[0] === domain[1])
|
||||
return
|
||||
context.cancelPendingZoom()
|
||||
context.cancelViewportDrag()
|
||||
if (context.props.displayMode === 'independent') {
|
||||
const indexes =
|
||||
trackIndex === undefined
|
||||
? context.trackLayouts.value.map((track) => track.index)
|
||||
: [trackIndex]
|
||||
const nextTransforms = [...context.independentTransforms.value]
|
||||
indexes.forEach((index) => {
|
||||
const track = context.trackLayouts.value.find((item) => item.index === index)
|
||||
if (!track) return
|
||||
const boundary = context.resolveInitialTrackDomain(track)
|
||||
const constrained = constrainZoomDomain(domain, boundary, [track.seriesList], context.props)
|
||||
nextTransforms[index] = transformForDomain(constrained, boundary, track.width)
|
||||
})
|
||||
context.independentTransforms.value = nextTransforms
|
||||
} else {
|
||||
const boundary = context.initialXDomain.value
|
||||
const groups = context.trackLayouts.value
|
||||
.filter((track) => track.hasVisibleSeries)
|
||||
.map((track) => track.seriesList)
|
||||
const constrained = constrainZoomDomain(domain, boundary, groups, context.props)
|
||||
context.sharedTransform.value = transformForDomain(
|
||||
constrained,
|
||||
boundary,
|
||||
context.innerWidth.value,
|
||||
)
|
||||
}
|
||||
context.clearHover()
|
||||
context.editorSeriesOptions.value = []
|
||||
void nextTick(context.configureZoom)
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,108 @@ describe('WaveformChart viewport reset', () => {
|
||||
expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('1000')
|
||||
})
|
||||
|
||||
it('keeps a controlled shared viewport when the data reference changes', async () => {
|
||||
const wrapper = await mountSizedChart(
|
||||
{
|
||||
kind: 'points',
|
||||
points: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1, y: 1 },
|
||||
],
|
||||
},
|
||||
{ displayMode: 'separated' },
|
||||
)
|
||||
const chart = wrapper.vm as unknown as {
|
||||
setViewportDomain: (domain: [number, number], trackIndex?: number) => void
|
||||
}
|
||||
chart.setViewportDomain([0.25, 0.75])
|
||||
await flushPromises()
|
||||
|
||||
const startBefore = Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())
|
||||
const endBefore = Number(wrapper.get('.waveform-chart__axis-endpoint--end').text())
|
||||
expect(startBefore).toBeGreaterThan(0)
|
||||
expect(endBefore).toBeLessThan(1000)
|
||||
|
||||
await wrapper.setProps({
|
||||
data: {
|
||||
kind: 'points',
|
||||
points: [
|
||||
{ x: 0, y: 10 },
|
||||
{ x: 1, y: 11 },
|
||||
],
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
expect(Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())).toBeCloseTo(
|
||||
startBefore,
|
||||
10,
|
||||
)
|
||||
expect(Number(wrapper.get('.waveform-chart__axis-endpoint--end').text())).toBeCloseTo(
|
||||
endBefore,
|
||||
10,
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps independently controlled track viewports when the data reference changes', async () => {
|
||||
const wrapper = await mountSizedChart(gridSeries(2), {
|
||||
displayMode: 'independent',
|
||||
grid: { rowCount: 1, columnCount: 2 },
|
||||
})
|
||||
const chart = wrapper.vm as unknown as {
|
||||
setViewportDomain: (domain: [number, number], trackIndex?: number) => void
|
||||
}
|
||||
chart.setViewportDomain([0.2, 0.8], 0)
|
||||
chart.setViewportDomain([0.1, 0.9], 1)
|
||||
await flushPromises()
|
||||
|
||||
const endpoints = () =>
|
||||
wrapper.findAll('.waveform-chart__track').map((track) => ({
|
||||
start: track.get('.waveform-chart__axis-endpoint--start').text(),
|
||||
end: track.get('.waveform-chart__axis-endpoint--end').text(),
|
||||
}))
|
||||
const endpointsBefore = endpoints()
|
||||
expect(Number(endpointsBefore[0].start)).toBeGreaterThan(0)
|
||||
expect(Number(endpointsBefore[0].end)).toBeLessThan(1000)
|
||||
expect(Number(endpointsBefore[1].start)).toBeGreaterThan(0)
|
||||
expect(Number(endpointsBefore[1].end)).toBeLessThan(1000)
|
||||
|
||||
await wrapper.setProps({ data: gridSeries(2) })
|
||||
await flushPromises()
|
||||
|
||||
const endpointsAfter = endpoints()
|
||||
endpointsAfter.forEach((endpoint, index) => {
|
||||
expect(Number(endpoint.start)).toBeCloseTo(Number(endpointsBefore[index].start), 10)
|
||||
expect(Number(endpoint.end)).toBeCloseTo(Number(endpointsBefore[index].end), 10)
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores invalid domains and applies viewport constraints', async () => {
|
||||
const wrapper = await mountSizedChart(
|
||||
{
|
||||
kind: 'points',
|
||||
points: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1, y: 1 },
|
||||
],
|
||||
},
|
||||
{ displayMode: 'separated', minZoomSpan: 0.2 },
|
||||
)
|
||||
const chart = wrapper.vm as unknown as {
|
||||
setViewportDomain: (domain: [number, number], trackIndex?: number) => void
|
||||
}
|
||||
chart.setViewportDomain([Number.NaN, 0.5])
|
||||
await flushPromises()
|
||||
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
|
||||
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
||||
|
||||
chart.setViewportDomain([0.49, 0.51])
|
||||
await flushPromises()
|
||||
const start = Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())
|
||||
const end = Number(wrapper.get('.waveform-chart__axis-endpoint--end').text())
|
||||
expect(end - start).toBeGreaterThanOrEqual(200)
|
||||
})
|
||||
|
||||
it('resets shared and independent explicit domains to their included nice bounds', async () => {
|
||||
const shared = await mountSizedChart(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user