3 Commits

Author SHA1 Message Date
李启源
dcc27a88be perf(chart): isolate hover rendering
All checks were successful
Package component / package (push) Successful in 3m59s
2026-07-24 16:10:02 +08:00
李启源
faa9695bc0 chore(deps): 调整组件运行时依赖
All checks were successful
Package component / package (push) Successful in 4m15s
2026-07-23 13:21:04 +08:00
李启源
f102e9c661 fix(tooltip): 保持数值与单位单行显示
All checks were successful
Package component / package (push) Successful in 2m52s
2026-07-23 11:59:27 +08:00
12 changed files with 239 additions and 109 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "waveform-analysis", "name": "waveform-analysis",
"version": "0.1.18", "version": "0.1.21",
"main": "./dist/index.cjs", "main": "./dist/index.cjs",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/types/index.d.ts", "types": "./dist/types/index.d.ts",
@@ -34,11 +34,14 @@
"test": "vitest run", "test": "vitest run",
"test:coverage": "vitest run --coverage" "test:coverage": "vitest run --coverage"
}, },
"dependencies": {
"d3": "^7.9.0",
"vue3-colorpicker": "^2.3.0",
"ant-design-vue": "4.2.6"
},
"peerDependencies": { "peerDependencies": {
"ant-design-vue": ">=3.2.20 <4", "ant-design-vue": ">=3.2.20 <4",
"d3": ">=7.9.0 <8", "vue": ">=3.2.33 <4"
"vue": ">=3.2.33 <4",
"vue3-colorpicker": ">=2.3.0 <3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "10.0.1", "@eslint/js": "10.0.1",
@@ -47,8 +50,6 @@
"@vitejs/plugin-vue": "6.0.8", "@vitejs/plugin-vue": "6.0.8",
"@vitest/coverage-v8": "4.1.10", "@vitest/coverage-v8": "4.1.10",
"@vue/test-utils": "2.4.11", "@vue/test-utils": "2.4.11",
"ant-design-vue": "4.2.6",
"d3": "7.9.0",
"eslint": "10.7.0", "eslint": "10.7.0",
"eslint-config-prettier": "10.1.8", "eslint-config-prettier": "10.1.8",
"eslint-plugin-vue": "10.9.2", "eslint-plugin-vue": "10.9.2",
@@ -60,7 +61,6 @@
"vite": "8.1.5", "vite": "8.1.5",
"vitest": "4.1.10", "vitest": "4.1.10",
"vue": "3.5.40", "vue": "3.5.40",
"vue-tsc": "3.3.7", "vue-tsc": "3.3.7"
"vue3-colorpicker": "2.3.0"
} }
} }

19
pnpm-lock.yaml generated
View File

@@ -7,6 +7,16 @@ settings:
importers: importers:
.: .:
dependencies:
ant-design-vue:
specifier: 4.2.6
version: 4.2.6(vue@3.5.40(typescript@6.0.3))
d3:
specifier: ^7.9.0
version: 7.9.0
vue3-colorpicker:
specifier: ^2.3.0
version: 2.3.0(@aesoper/normal-utils@0.1.5)(@popperjs/core@2.11.8)(@vueuse/core@10.11.1(vue@3.5.40(typescript@6.0.3)))(gradient-parser@1.2.0)(lodash-es@4.18.1)(tinycolor2@1.6.0)(vue-types@4.2.1(vue@3.5.40(typescript@6.0.3)))(vue@3.5.40(typescript@6.0.3))
devDependencies: devDependencies:
'@eslint/js': '@eslint/js':
specifier: 10.0.1 specifier: 10.0.1
@@ -26,12 +36,6 @@ importers:
'@vue/test-utils': '@vue/test-utils':
specifier: 2.4.11 specifier: 2.4.11
version: 2.4.11(@vue/compiler-dom@3.5.40)(@vue/server-renderer@3.5.40)(vue@3.5.40(typescript@6.0.3)) version: 2.4.11(@vue/compiler-dom@3.5.40)(@vue/server-renderer@3.5.40)(vue@3.5.40(typescript@6.0.3))
ant-design-vue:
specifier: 4.2.6
version: 4.2.6(vue@3.5.40(typescript@6.0.3))
d3:
specifier: 7.9.0
version: 7.9.0
eslint: eslint:
specifier: 10.7.0 specifier: 10.7.0
version: 10.7.0 version: 10.7.0
@@ -68,9 +72,6 @@ importers:
vue-tsc: vue-tsc:
specifier: 3.3.7 specifier: 3.3.7
version: 3.3.7(typescript@6.0.3) version: 3.3.7(typescript@6.0.3)
vue3-colorpicker:
specifier: 2.3.0
version: 2.3.0(@aesoper/normal-utils@0.1.5)(@popperjs/core@2.11.8)(@vueuse/core@10.11.1(vue@3.5.40(typescript@6.0.3)))(gradient-parser@1.2.0)(lodash-es@4.18.1)(tinycolor2@1.6.0)(vue-types@4.2.1(vue@3.5.40(typescript@6.0.3)))(vue@3.5.40(typescript@6.0.3))
packages: packages:

View File

@@ -2332,6 +2332,44 @@ describe('WaveformChart', () => {
expect(pendingAnimationFrameCount()).toBe(0) expect(pendingAnimationFrameCount()).toBe(0)
}) })
it('isolates hover rendering from the chart, track, and waveform path subtrees', async () => {
const wrapper = await mountSizedChart(
{
kind: 'points',
points: [
{ x: 0, y: 0 },
{ x: 1, y: 5 },
],
},
{ grid: { rowCount: 1, columnCount: 1 } },
)
const overlay = wrapper.get('.waveform-chart__overlay')
const overlayWidth = Number(overlay.attributes('width'))
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
})
const track = wrapper.getComponent({ name: 'WaveformTrack' })
const seriesLayer = wrapper.getComponent({ name: 'WaveformSeriesLayer' })
const chartUpdate = vi.spyOn(wrapper.vm.$, 'update')
const trackUpdate = vi.spyOn(track.vm.$, 'update')
const seriesLayerUpdate = vi.spyOn(seriesLayer.vm.$, 'update')
const pathBeforeHover = wrapper.get('.waveform-chart__line').element
overlay.element.dispatchEvent(
new MouseEvent('pointermove', { clientX: overlayWidth, clientY: 120, bubbles: true }),
)
flushAnimationFrames()
await flushPromises()
expect(wrapper.get('.waveform-chart__tooltip').text()).toContain('ms: 1,000.0000')
expect(wrapper.findAll('.waveform-chart__crosshair line')).toHaveLength(1)
expect(wrapper.get('.waveform-chart__line').element).toBe(pathBeforeHover)
expect(chartUpdate).not.toHaveBeenCalled()
expect(trackUpdate).not.toHaveBeenCalled()
expect(seriesLayerUpdate).not.toHaveBeenCalled()
})
it('renders reference grid styling and an optional frame watermark', async () => { it('renders reference grid styling and an optional frame watermark', async () => {
const wrapper = await mountSizedChart( const wrapper = await mountSizedChart(
{ {

View File

@@ -21,6 +21,7 @@ import {
onBeforeUnmount, onBeforeUnmount,
onMounted, onMounted,
ref, ref,
shallowReactive,
shallowRef, shallowRef,
watch, watch,
type CSSProperties, type CSSProperties,
@@ -58,8 +59,8 @@ import {
type AnnotationSeriesInfo, type AnnotationSeriesInfo,
type AnnotationTrackLayout, type AnnotationTrackLayout,
} from './annotation' } from './annotation'
import { WaveformTooltip } from './interaction' import { WaveformHoverHost } from './interaction'
import { WaveformLegend, WaveformTrack } from './rendering' import { WaveformHoverLayer, WaveformLegend, WaveformTrack } from './rendering'
import { import {
channelColors, channelColors,
margin as chartMargin, margin as chartMargin,
@@ -88,7 +89,13 @@ import {
X_AXIS_BAND, X_AXIS_BAND,
type WaveformGridOptions, type WaveformGridOptions,
} from './core/grid' } from './core/grid'
import type { DisplaySeries, DisplayTrack, HoveredSeriesPoint, TrackLayout } from './core/types' import type {
DisplaySeries,
DisplayTrack,
HoveredSeriesPoint,
TrackLayout,
WaveformHoverState,
} from './core/types'
import { import {
buildTrackLayouts, buildTrackLayouts,
findClosestTrackAtPointer, findClosestTrackAtPointer,
@@ -192,9 +199,11 @@ const sharedTransform = shallowRef<ZoomTransform>(zoomIdentity)
const independentTransforms = shallowRef<ZoomTransform[]>([]) const independentTransforms = shallowRef<ZoomTransform[]>([])
const sharedYDomains = ref<Record<string, [number, number]>>({}) const sharedYDomains = ref<Record<string, [number, number]>>({})
const independentYDomains = ref<Record<number, [number, number]>>({}) const independentYDomains = ref<Record<number, [number, number]>>({})
const hoveredSeriesPoints = ref<HoveredSeriesPoint[]>([]) const hoverState = shallowReactive<WaveformHoverState>({
const hoveredTrackIndex = ref<number | null>(null) points: [],
const hoverPosition = ref({ x: 0, y: 0 }) trackIndex: null,
position: { x: 0, y: 0 },
})
const suppressHoverUntilMove = ref(false) const suppressHoverUntilMove = ref(false)
const currentPage = ref(1) const currentPage = ref(1)
const resizeObserver = shallowRef<ResizeObserver>() const resizeObserver = shallowRef<ResizeObserver>()
@@ -269,15 +278,6 @@ function handleInteractionKeyUp(event: KeyboardEvent) {
} }
} }
// 用于传递给 WaveformTooltip 的接口
interface TooltipSeriesPoint {
trackIndex: number
name: string
color: string
unit?: string
point: WaveformPoint
}
const fixedWidth = computed(() => const fixedWidth = computed(() =>
Number.isFinite(props.width) ? Math.max(0, props.width ?? 0) : undefined, Number.isFinite(props.width) ? Math.max(0, props.width ?? 0) : undefined,
) )
@@ -569,7 +569,6 @@ const yAxisLayout = computed(() => {
} }
}) })
const hasWaveformData = computed(() => chartSeries.value.length > 0) const hasWaveformData = computed(() => chartSeries.value.length > 0)
const hoveredPoint = computed(() => hoveredSeriesPoints.value[0]?.point ?? null)
const hasChartArea = computed(() => innerWidth.value > 0 && innerHeight.value > 0) const hasChartArea = computed(() => innerWidth.value > 0 && innerHeight.value > 0)
const resolvedXLabel = computed(() => props.xLabel ?? `时间(${props.timeUnit}`) const resolvedXLabel = computed(() => props.xLabel ?? `时间(${props.timeUnit}`)
const activeInteractionMode = computed(() => props.interactionMode) const activeInteractionMode = computed(() => props.interactionMode)
@@ -578,17 +577,6 @@ const isZoomMode = computed(
() => activeInteractionMode.value === 'zoom' || activeInteractionMode.value === undefined, () => activeInteractionMode.value === 'zoom' || activeInteractionMode.value === undefined,
) )
// 转换为 Tooltip 组件需要的格式
const tooltipSeriesPoints = computed<TooltipSeriesPoint[]>(() => {
return hoveredSeriesPoints.value.map((item) => ({
trackIndex: item.trackIndex,
name: item.name,
color: item.color,
unit: item.unit,
point: item.point,
}))
})
const sharedXDomain = computed(() => { const sharedXDomain = computed(() => {
const values: number[] = [] const values: number[] = []
chartTracks.value.forEach((track) => { chartTracks.value.forEach((track) => {
@@ -986,9 +974,9 @@ function scheduleHover(update: () => void) {
function hoveredPointsMatch(nextPoints: HoveredSeriesPoint[]): boolean { function hoveredPointsMatch(nextPoints: HoveredSeriesPoint[]): boolean {
return ( return (
hoveredSeriesPoints.value.length === nextPoints.length && hoverState.points.length === nextPoints.length &&
nextPoints.every((point, index) => { nextPoints.every((point, index) => {
const current = hoveredSeriesPoints.value[index] const current = hoverState.points[index]
return ( return (
current?.id === point.id && current?.id === point.id &&
current.trackIndex === point.trackIndex && current.trackIndex === point.trackIndex &&
@@ -1003,20 +991,34 @@ function commitHover(
trackIndex: number | null, trackIndex: number | null,
position: { x: number; y: number }, position: { x: number; y: number },
) { ) {
if (!hoveredPointsMatch(nextPoints)) hoveredSeriesPoints.value = nextPoints if (!hoveredPointsMatch(nextPoints)) hoverState.points = nextPoints
hoveredTrackIndex.value = trackIndex hoverState.trackIndex = trackIndex
hoverPosition.value = position hoverState.position = position
// Emit using the updated hoveredSeriesPoints to avoid race condition emit('point-hover', hoverState.points[0]?.point ?? null)
emit('point-hover', hoveredSeriesPoints.value[0]?.point ?? null)
} }
function clearHover() { function clearHover() {
cancelPendingHover() cancelPendingHover()
hoveredSeriesPoints.value = [] hoverState.points = []
hoveredTrackIndex.value = null hoverState.trackIndex = null
emit('point-hover', null) emit('point-hover', null)
} }
function createHoveredSeriesPoint(
series: DisplaySeries,
trackIndex: number,
point: WaveformPoint,
): HoveredSeriesPoint {
return {
id: series.id,
name: series.name,
color: series.color,
unit: series.unit,
trackIndex,
point,
}
}
function beginAnnotationDrag() { function beginAnnotationDrag() {
suppressHoverUntilMove.value = true suppressHoverUntilMove.value = true
clearHover() clearHover()
@@ -1334,7 +1336,7 @@ function handleIndependentPointerMove(event: PointerEvent, trackIndex: number) {
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)
return point ? [{ ...series, trackIndex, point }] : [] return point ? [createHoveredSeriesPoint(series, trackIndex, point)] : []
}) })
commitHover(nextPoints, trackIndex, { commitHover(nextPoints, trackIndex, {
x: resolvedChartLeftMargin.value + track.left + pointerX, x: resolvedChartLeftMargin.value + track.left + pointerX,
@@ -1364,7 +1366,7 @@ function handleSharedPointerMove(event: PointerEvent) {
const nextPoints = trackLayouts.value.flatMap((track) => const nextPoints = trackLayouts.value.flatMap((track) =>
track.seriesList.flatMap((series) => { track.seriesList.flatMap((series) => {
const point = nearestPoint(series, xValue) const point = nearestPoint(series, xValue)
return point ? [{ ...series, trackIndex: track.index, point }] : [] return point ? [createHoveredSeriesPoint(series, track.index, point)] : []
}), }),
) )
commitHover(nextPoints, null, { commitHover(nextPoints, null, {
@@ -1939,7 +1941,6 @@ onBeforeUnmount(() => {
:track="track" :track="track"
:clip-path-id="clipPathId" :clip-path-id="clipPathId"
:inner-width="innerWidth" :inner-width="innerWidth"
:show-tooltip="showTooltip"
:zoomable="zoomable" :zoomable="zoomable"
:display-mode="displayMode" :display-mode="displayMode"
:interaction-mode="activeInteractionMode" :interaction-mode="activeInteractionMode"
@@ -1950,7 +1951,6 @@ onBeforeUnmount(() => {
:zero-line="resolvedZeroLine" :zero-line="resolvedZeroLine"
:time-unit="timeUnit" :time-unit="timeUnit"
:y-label="yLabel" :y-label="yLabel"
:hovered-point="hoveredSeriesPoints.find((p) => p.trackIndex === track.index)"
@pointer-move="handleIndependentPointerMove($event, track.index)" @pointer-move="handleIndependentPointerMove($event, track.index)"
@pointer-down="beginViewportDrag($event, track.index, true)" @pointer-down="beginViewportDrag($event, track.index, true)"
@pointer-up="finishViewportDrag" @pointer-up="finishViewportDrag"
@@ -1960,6 +1960,13 @@ onBeforeUnmount(() => {
@contextmenu="handleAnnotationContextMenu($event, track.index)" @contextmenu="handleAnnotationContextMenu($event, track.index)"
/> />
<WaveformHoverLayer
:state="hoverState"
:tracks="trackLayouts"
:clip-path-id="clipPathId"
:visible="showTooltip"
/>
<rect <rect
v-if="selectionBox && selection?.mode === 'box'" v-if="selectionBox && selection?.mode === 'box'"
class="waveform-chart__zoom-selection" class="waveform-chart__zoom-selection"
@@ -2061,13 +2068,10 @@ onBeforeUnmount(() => {
@close="annotationInteraction.closeContextMenu" @close="annotationInteraction.closeContextMenu"
/> />
<!-- Tooltip --> <WaveformHoverHost
<WaveformTooltip :state="hoverState"
:visible="showTooltip && hoveredPoint !== null" :visible="showTooltip"
:position="hoverPosition"
:time-unit="timeUnit" :time-unit="timeUnit"
:hovered-point="hoveredPoint"
:series-points="tooltipSeriesPoints"
:container-width="chartWidth" :container-width="chartWidth"
:container-height="chartHeight" :container-height="chartHeight"
/> />

View File

@@ -63,11 +63,21 @@ export interface WaveformYAxisLayout {
/** /**
* 悬浮的系列点 * 悬浮的系列点
*/ */
export interface HoveredSeriesPoint extends DisplaySeries { export interface HoveredSeriesPoint {
id: string
name: string
unit?: string
color: string
trackIndex: number trackIndex: number
point: WaveformPoint point: WaveformPoint
} }
export interface WaveformHoverState {
points: HoveredSeriesPoint[]
trackIndex: number | null
position: { x: number; y: number }
}
/** /**
* 轨道布局 * 轨道布局
*/ */

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { WaveformHoverState } from '../core/types'
import WaveformTooltip from './WaveformTooltip.vue'
const props = defineProps<{
state: WaveformHoverState
visible: boolean
timeUnit: 's' | 'ms'
containerWidth: number
containerHeight: number
}>()
const hoveredPoint = computed(() => props.state.points[0]?.point ?? null)
</script>
<template>
<WaveformTooltip
:visible="visible && hoveredPoint !== null"
:position="state.position"
:time-unit="timeUnit"
:hovered-point="hoveredPoint"
:series-points="state.points"
:container-width="containerWidth"
:container-height="containerHeight"
/>
</template>

View File

@@ -66,6 +66,31 @@ describe('WaveformTooltip', () => {
expect(wrapper.get('.waveform-tooltip__series small').text()).toBe('(+2 / -1)') expect(wrapper.get('.waveform-tooltip__series small').text()).toBe('(+2 / -1)')
}) })
it('keeps a formatted value and its unit in one value container', () => {
const hoveredPoint = { x: 1, y: -1405.4932 }
const wrapper = mount(WaveformTooltip, {
props: {
visible: true,
position: { x: 10, y: 10 },
timeUnit: 'ms',
hoveredPoint,
seriesPoints: [
{
trackIndex: 0,
name: 'ENG8KJXAc(10001)',
color: '#ffb43b',
unit: 'A',
point: hoveredPoint,
},
],
containerWidth: 400,
containerHeight: 300,
},
})
expect(wrapper.get('.waveform-tooltip__value').text()).toBe('-1,405.4932 A')
})
it('omits the error label when both resolved errors are zero', () => { it('omits the error label when both resolved errors are zero', () => {
const point = { x: 1, y: 12 } const point = { x: 1, y: 12 }
const wrapper = mount(WaveformTooltip, { const wrapper = mount(WaveformTooltip, {

View File

@@ -77,7 +77,7 @@ function formatError(point: WaveformPoint): string | null {
> >
<i :style="{ backgroundColor: seriesPoint.color }" /> <i :style="{ backgroundColor: seriesPoint.color }" />
<strong v-if="seriesPoint.name">{{ seriesPoint.name }}:</strong> <strong v-if="seriesPoint.name">{{ seriesPoint.name }}:</strong>
<span> <span class="waveform-tooltip__value">
{{ formatTooltipNumber(seriesPoint.point.y) {{ formatTooltipNumber(seriesPoint.point.y)
}}{{ seriesPoint.unit ? ` ${seriesPoint.unit}` : '' }} }}{{ seriesPoint.unit ? ` ${seriesPoint.unit}` : '' }}
<small v-if="formatError(seriesPoint.point)">{{ formatError(seriesPoint.point) }}</small> <small v-if="formatError(seriesPoint.point)">{{ formatError(seriesPoint.point) }}</small>
@@ -116,7 +116,7 @@ function formatError(point: WaveformPoint): string | null {
.waveform-tooltip__series { .waveform-tooltip__series {
display: grid; display: grid;
grid-template-columns: 8px auto minmax(0, 1fr); grid-template-columns: 8px minmax(0, 1fr) auto;
gap: 6px; gap: 6px;
align-items: center; align-items: center;
} }
@@ -134,6 +134,10 @@ function formatError(point: WaveformPoint): string | null {
white-space: nowrap; white-space: nowrap;
} }
.waveform-tooltip__value {
white-space: nowrap;
}
.waveform-tooltip__series small { .waveform-tooltip__series small {
color: #667085; color: #667085;
white-space: nowrap; white-space: nowrap;

View File

@@ -1 +1,2 @@
export { default as WaveformTooltip } from './WaveformTooltip.vue' export { default as WaveformTooltip } from './WaveformTooltip.vue'
export { default as WaveformHoverHost } from './WaveformHoverHost.vue'

View File

@@ -0,0 +1,60 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { HoveredSeriesPoint, TrackLayout, WaveformHoverState } from '../core/types'
const props = defineProps<{
state: WaveformHoverState
tracks: TrackLayout[]
clipPathId: string
visible: boolean
}>()
interface Crosshair {
point: HoveredSeriesPoint
track: TrackLayout
x: number
}
const crosshairs = computed<Crosshair[]>(() => {
if (!props.visible) return []
const pointByTrack = new Map<number, HoveredSeriesPoint>()
props.state.points.forEach((point) => {
if (!pointByTrack.has(point.trackIndex)) pointByTrack.set(point.trackIndex, point)
})
return props.tracks.flatMap((track) => {
const point = pointByTrack.get(track.index)
return point && !track.isEmpty && track.hasVisibleSeries
? [{ point, track, x: track.xScale(point.point.x) }]
: []
})
})
</script>
<template>
<g class="waveform-chart__hover-layer" pointer-events="none" aria-hidden="true">
<g
v-for="crosshair in crosshairs"
:key="crosshair.track.index"
class="waveform-track__crosshair waveform-chart__crosshair"
:clip-path="`url(#${clipPathId}-${crosshair.track.index})`"
:transform="`translate(${crosshair.track.left ?? 0}, ${crosshair.track.top})`"
>
<line :x1="crosshair.x" :x2="crosshair.x" y1="0" :y2="crosshair.track.height" />
</g>
</g>
</template>
<style scoped>
.waveform-track__crosshair {
pointer-events: none;
}
.waveform-track__crosshair line {
stroke: #57617b;
stroke-width: 1;
stroke-dasharray: 4 3;
}
</style>

View File

@@ -4,12 +4,7 @@ import { axisBottom, axisLeft, axisRight, select } from 'd3'
import { formatAxisTime, formatScientificAxisLabel } from '../../utils' import { formatAxisTime, formatScientificAxisLabel } from '../../utils'
import type { WaveformAxesOptions, WaveformFrameStyle, WaveformZeroLineOptions } from '../../types' import type { WaveformAxesOptions, WaveformFrameStyle, WaveformZeroLineOptions } from '../../types'
import type { WaveformDisplayMode, WaveformInteractionMode } from '../data/types' import type { WaveformDisplayMode, WaveformInteractionMode } from '../data/types'
import type { import type { DisplaySeries, TrackLayout, WaveformYAxisLayout } from '../core/types'
DisplaySeries,
HoveredSeriesPoint,
TrackLayout,
WaveformYAxisLayout,
} from '../core/types'
import WaveformSeriesLayer from './WaveformSeriesLayer.vue' import WaveformSeriesLayer from './WaveformSeriesLayer.vue'
interface Props { interface Props {
@@ -19,8 +14,6 @@ interface Props {
clipPathId: string clipPathId: string
/** 内部宽度 */ /** 内部宽度 */
innerWidth: number innerWidth: number
/** 是否显示 tooltip */
showTooltip: boolean
/** 是否可缩放 */ /** 是否可缩放 */
zoomable: boolean zoomable: boolean
/** 显示模式 */ /** 显示模式 */
@@ -35,8 +28,6 @@ interface Props {
axes?: WaveformAxesOptions axes?: WaveformAxesOptions
/** 时间单位 */ /** 时间单位 */
timeUnit: 's' | 'ms' timeUnit: 's' | 'ms'
/** 悬浮点(用于显示十字线) */
hoveredPoint?: HoveredSeriesPoint
/** Y 轴标签回退值 */ /** Y 轴标签回退值 */
yLabel?: string yLabel?: string
/** Hide visual aids while keeping chart interaction active. */ /** Hide visual aids while keeping chart interaction active. */
@@ -110,20 +101,6 @@ function shouldShowYAxisLabel(trackHeight: number, trackIndex: number): boolean
return trackIndex % labelSpacing === 0 return trackIndex % labelSpacing === 0
} }
function crosshairX(): number {
return props.hoveredPoint && props.hoveredPoint.trackIndex === props.track.index
? props.track.xScale(props.hoveredPoint.point.x)
: 0
}
function hasCrosshair(): boolean {
return (
props.showTooltip &&
props.hoveredPoint !== undefined &&
props.hoveredPoint.trackIndex === props.track.index
)
}
function zeroLineY(axis: WaveformYAxisLayout): number | null { function zeroLineY(axis: WaveformYAxisLayout): number | null {
const [minimum, maximum] = axis.scale.domain() const [minimum, maximum] = axis.scale.domain()
if (!props.zeroLine.visible || minimum > 0 || maximum < 0) return null if (!props.zeroLine.visible || minimum > 0 || maximum < 0) return null
@@ -472,15 +449,6 @@ watch(
<!-- 波形系列隔离在静态子组件中,避免 hover 更新遍历大量 SVG 节点。 --> <!-- 波形系列隔离在静态子组件中,避免 hover 更新遍历大量 SVG 节点。 -->
<WaveformSeriesLayer :track="track" :clip-path-id="clipPathId" /> <WaveformSeriesLayer :track="track" :clip-path-id="clipPathId" />
<!-- 十字线 -->
<g
v-if="!track.isEmpty && track.hasVisibleSeries && hasCrosshair()"
class="waveform-track__crosshair waveform-chart__crosshair"
:clip-path="`url(#${clipPathId}-${track.index})`"
>
<line :x1="crosshairX()" :x2="crosshairX()" y1="0" :y2="track.height" />
</g>
<!-- 交互覆盖层(仅在独立模式下) --> <!-- 交互覆盖层(仅在独立模式下) -->
<rect <rect
v-if="!track.isEmpty && track.hasVisibleSeries && displayMode === 'independent'" v-if="!track.isEmpty && track.hasVisibleSeries && displayMode === 'independent'"
@@ -587,16 +555,6 @@ watch(
cursor: crosshair; cursor: crosshair;
} }
.waveform-track__crosshair {
pointer-events: none;
}
.waveform-track__crosshair line {
stroke: #57617b;
stroke-width: 1;
stroke-dasharray: 4 3;
}
.waveform-track__zero-line { .waveform-track__zero-line {
fill: none; fill: none;
pointer-events: none; pointer-events: none;

View File

@@ -1,3 +1,4 @@
export { default as WaveformTrack } from './WaveformTrack.vue' export { default as WaveformTrack } from './WaveformTrack.vue'
export { default as WaveformLegend } from './WaveformLegend.vue' export { default as WaveformLegend } from './WaveformLegend.vue'
export { default as WaveformHoverLayer } from './WaveformHoverLayer.vue'
export { waveformPointSymbolPath } from './seriesStyle' export { waveformPointSymbolPath } from './seriesStyle'