fix(chart): guard deferred pointer updates
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
|||||||
} from 'd3'
|
} from 'd3'
|
||||||
import { resolveWaveformRenderingOptions } from '../core'
|
import { resolveWaveformRenderingOptions } from '../core'
|
||||||
import { formatScientificAxisExponent, formatScientificAxisLabel, paddedDomain } from '../utils'
|
import { formatScientificAxisExponent, formatScientificAxisLabel, paddedDomain } from '../utils'
|
||||||
|
import { useAnimationFrameThrottle } from './utils/useAnimationFrameThrottle'
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
nextTick,
|
nextTick,
|
||||||
@@ -173,11 +174,10 @@ const annotationInteraction = useWaveformAnnotationInteraction()
|
|||||||
const editorSeriesOptions = ref<AnnotationSeriesCandidate[]>([])
|
const editorSeriesOptions = ref<AnnotationSeriesCandidate[]>([])
|
||||||
let generatedAnnotationId = 0
|
let generatedAnnotationId = 0
|
||||||
let synchronizingZoomTransform = false
|
let synchronizingZoomTransform = false
|
||||||
let zoomAnimationFrame: number | null = null
|
|
||||||
let pendingSharedZoomTransform: ZoomTransform | null = null
|
let pendingSharedZoomTransform: ZoomTransform | null = null
|
||||||
const pendingIndependentZoomTransforms = new Map<number, ZoomTransform>()
|
const pendingIndependentZoomTransforms = new Map<number, ZoomTransform>()
|
||||||
let hoverAnimationFrame: number | null = null
|
const zoomThrottle = useAnimationFrameThrottle()
|
||||||
let pendingHoverUpdate: (() => void) | null = null
|
const hoverThrottle = useAnimationFrameThrottle()
|
||||||
const preparedSeries = usePreparedWaveformSeries(() => props.data, handleDataReferenceChange)
|
const preparedSeries = usePreparedWaveformSeries(() => props.data, handleDataReferenceChange)
|
||||||
|
|
||||||
// 用于传递给 WaveformTooltip 的接口
|
// 用于传递给 WaveformTooltip 的接口
|
||||||
@@ -608,27 +608,18 @@ function commitPendingZoom() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleZoomCommit() {
|
function scheduleZoomCommit() {
|
||||||
if (zoomAnimationFrame !== null) return
|
zoomThrottle.schedule(() => commitPendingZoom())
|
||||||
zoomAnimationFrame = requestAnimationFrame(() => {
|
|
||||||
zoomAnimationFrame = null
|
|
||||||
commitPendingZoom()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function flushPendingZoom() {
|
function flushPendingZoom() {
|
||||||
if (zoomAnimationFrame !== null) {
|
zoomThrottle.flush()
|
||||||
cancelAnimationFrame(zoomAnimationFrame)
|
|
||||||
zoomAnimationFrame = null
|
|
||||||
}
|
|
||||||
commitPendingZoom()
|
commitPendingZoom()
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelPendingZoom() {
|
function cancelPendingZoom() {
|
||||||
pendingSharedZoomTransform = null
|
pendingSharedZoomTransform = null
|
||||||
pendingIndependentZoomTransforms.clear()
|
pendingIndependentZoomTransforms.clear()
|
||||||
if (zoomAnimationFrame === null) return
|
zoomThrottle.cancel()
|
||||||
cancelAnimationFrame(zoomAnimationFrame)
|
|
||||||
zoomAnimationFrame = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearZoomBindings() {
|
function clearZoomBindings() {
|
||||||
@@ -705,21 +696,11 @@ function configureZoom() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cancelPendingHover() {
|
function cancelPendingHover() {
|
||||||
pendingHoverUpdate = null
|
hoverThrottle.cancel()
|
||||||
if (hoverAnimationFrame === null) return
|
|
||||||
cancelAnimationFrame(hoverAnimationFrame)
|
|
||||||
hoverAnimationFrame = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleHover(update: () => void) {
|
function scheduleHover(update: () => void) {
|
||||||
pendingHoverUpdate = update
|
hoverThrottle.schedule(update)
|
||||||
if (hoverAnimationFrame !== null) return
|
|
||||||
hoverAnimationFrame = requestAnimationFrame(() => {
|
|
||||||
hoverAnimationFrame = null
|
|
||||||
const nextUpdate = pendingHoverUpdate
|
|
||||||
pendingHoverUpdate = null
|
|
||||||
nextUpdate?.()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hoveredPointsMatch(nextPoints: HoveredSeriesPoint[]): boolean {
|
function hoveredPointsMatch(nextPoints: HoveredSeriesPoint[]): boolean {
|
||||||
@@ -1051,9 +1032,15 @@ function handleIndependentPointerMove(event: PointerEvent, trackIndex: number) {
|
|||||||
const overlay = event.currentTarget as SVGRectElement | null
|
const overlay = event.currentTarget as SVGRectElement | null
|
||||||
if (!overlay) return
|
if (!overlay) return
|
||||||
const [pointerX, pointerY] = pointer(event, overlay)
|
const [pointerX, pointerY] = pointer(event, overlay)
|
||||||
scheduleHover(() => {
|
// 捕获轨道对象以避免竞态条件
|
||||||
const track = trackLayouts.value[trackIndex]
|
const track = trackLayouts.value[trackIndex]
|
||||||
if (!track) return
|
if (!track || !track.hasVisibleSeries) return
|
||||||
|
|
||||||
|
scheduleHover(() => {
|
||||||
|
// 重新验证轨道仍然有效且有可见系列
|
||||||
|
const currentTrack = trackLayouts.value[trackIndex]
|
||||||
|
if (!currentTrack || !currentTrack.hasVisibleSeries || currentTrack !== track) return
|
||||||
|
|
||||||
const xValue = track.xScale.invert(Math.max(0, Math.min(innerWidth.value, pointerX)))
|
const xValue = track.xScale.invert(Math.max(0, Math.min(innerWidth.value, pointerX)))
|
||||||
const nextPoints = track.seriesList.flatMap((series) => {
|
const nextPoints = track.seriesList.flatMap((series) => {
|
||||||
const point = nearestPoint(series, xValue)
|
const point = nearestPoint(series, xValue)
|
||||||
@@ -1177,7 +1164,7 @@ watch(
|
|||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
retainedIds.size !== internalHiddenSeriesIds.value.size ||
|
retainedIds.size !== internalHiddenSeriesIds.value.size ||
|
||||||
Array.from(retainedIds).some((seriesId) => !internalHiddenSeriesIds.value.has(seriesId))
|
Array.from(internalHiddenSeriesIds.value).some((seriesId) => !retainedIds.has(seriesId))
|
||||||
) {
|
) {
|
||||||
internalHiddenSeriesIds.value = retainedIds
|
internalHiddenSeriesIds.value = retainedIds
|
||||||
}
|
}
|
||||||
|
|||||||
56
src/components/utils/useAnimationFrameThrottle.ts
Normal file
56
src/components/utils/useAnimationFrameThrottle.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* 创建一个基于 requestAnimationFrame 的节流工具
|
||||||
|
* 用于合并多个快速连续的调用到单个动画帧中
|
||||||
|
*/
|
||||||
|
export function useAnimationFrameThrottle<T = void>() {
|
||||||
|
let frameHandle: number | null = null
|
||||||
|
let pendingCallback: (() => T) | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度一个回调在下一个动画帧中执行
|
||||||
|
* 如果已经有待处理的帧,则替换待处理的回调
|
||||||
|
*/
|
||||||
|
function schedule(callback: () => T): void {
|
||||||
|
pendingCallback = callback
|
||||||
|
if (frameHandle !== null) return
|
||||||
|
|
||||||
|
frameHandle = requestAnimationFrame(() => {
|
||||||
|
frameHandle = null
|
||||||
|
const cb = pendingCallback
|
||||||
|
pendingCallback = null
|
||||||
|
cb?.()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消待处理的动画帧调度
|
||||||
|
*/
|
||||||
|
function cancel(): void {
|
||||||
|
pendingCallback = null
|
||||||
|
if (frameHandle === null) return
|
||||||
|
cancelAnimationFrame(frameHandle)
|
||||||
|
frameHandle = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 立即执行待处理的回调(如果有)
|
||||||
|
*/
|
||||||
|
function flush(): void {
|
||||||
|
if (frameHandle !== null) {
|
||||||
|
cancelAnimationFrame(frameHandle)
|
||||||
|
frameHandle = null
|
||||||
|
}
|
||||||
|
const cb = pendingCallback
|
||||||
|
pendingCallback = null
|
||||||
|
cb?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有待处理的调度
|
||||||
|
*/
|
||||||
|
function isPending(): boolean {
|
||||||
|
return frameHandle !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
return { schedule, cancel, flush, isPending }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user