feat(chart): add presentation mode
Some checks failed
Package component / package (push) Failing after 50s

This commit is contained in:
李启源
2026-07-29 12:12:03 +08:00
parent 312e7fde52
commit f97ff24cdf
22 changed files with 478 additions and 26 deletions

View File

@@ -114,6 +114,7 @@ const data = ref<WaveformData>({
| `frameNumber` | `string \| number` | 未设置 | 图框水印内容 | | `frameNumber` | `string \| number` | 未设置 | 图框水印内容 |
| `zeroLine` | `WaveformZeroLineOptions` | `{ visible: false }` | 零值参考线显隐与样式 | | `zeroLine` | `WaveformZeroLineOptions` | `{ visible: false }` | 零值参考线显隐与样式 |
| `cleanView` | `boolean` | `false` | 仅保留波形的净图模式 | | `cleanView` | `boolean` | `false` | 仅保留波形的净图模式 |
| `presentationMode` | `boolean` | `false` | 禁用绘图区交互的展示模式 |
| `annotations` | `WaveformAnnotation[]` | `[]` | 受控标注数据 | | `annotations` | `WaveformAnnotation[]` | `[]` | 受控标注数据 |
| `annotationsVisible` | `boolean` | `true` | 标注图层显隐 | | `annotationsVisible` | `boolean` | `true` | 标注图层显隐 |
| `interactionMode` | `'zoom' \| 'annotation'` | `'zoom'` | 左键交互模式 | | `interactionMode` | `'zoom' \| 'annotation'` | `'zoom'` | 左键交互模式 |
@@ -471,6 +472,14 @@ tooltip 仍然可用,切换回普通模式后原有配置和标注不会丢失
`interactionMode` 可选 `zoom``annotation`,默认使用缩放模式。右键绘图区可直接打开 `interactionMode` 可选 `zoom``annotation`,默认使用缩放模式。右键绘图区可直接打开
标注编辑器,无需切换交互模式。`zoomable``pannable``showTooltip` 可分别控制缩放、 标注编辑器,无需切换交互模式。`zoomable``pannable``showTooltip` 可分别控制缩放、
空格拖拽平移和 tooltip平移默认关闭。 空格拖拽平移和 tooltip平移默认关闭。
展示场景可启用 `presentationMode`,统一禁用绘图区的 tooltip、缩放、平移、双击复位和
标注交互。该模式不会隐藏任何图形内容,也不会禁用图例切换或分页;关闭后恢复原交互配置。
```vue
<WaveformChart :data="chartData" :presentation-mode="true" />
```
空数据或过滤后没有有效点时,组件会保留图框布局并显示“暂无有效波形数据”。 空数据或过滤后没有有效点时,组件会保留图框布局并显示“暂无有效波形数据”。
## 大数据渲染 ## 大数据渲染

View File

@@ -1,6 +1,6 @@
{ {
"name": "waveform-analysis", "name": "waveform-analysis",
"version": "0.1.22", "version": "0.1.23",
"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",
@@ -14,6 +14,7 @@
}, },
"files": [ "files": [
"dist", "dist",
"src",
"README.md" "README.md"
], ],
"sideEffects": [ "sideEffects": [

View File

@@ -92,19 +92,22 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
wrapper.unmount() wrapper.unmount()
}) })
it('passes clean view and zero-line controls to the chart', async () => { it('passes clean view, presentation mode, and zero-line controls to the chart', async () => {
const wrapper = mount(App) const wrapper = mount(App)
await flushPromises() await flushPromises()
const chart = wrapper.getComponent(WaveformChart) const chart = wrapper.getComponent(WaveformChart)
expect(chart.props('cleanView')).toBe(false) expect(chart.props('cleanView')).toBe(false)
expect(chart.props('presentationMode')).toBe(false)
expect(chart.props('zeroLine')).toMatchObject({ visible: false, color: '#98a2b3', width: 1 }) expect(chart.props('zeroLine')).toMatchObject({ visible: false, color: '#98a2b3', width: 1 })
await wrapper.get('[aria-label="净图模式"]').trigger('click') await wrapper.get('[aria-label="净图模式"]').trigger('click')
await wrapper.get('[aria-label="展示模式"]').trigger('click')
await wrapper.get('[aria-label="显示零值参考线"]').trigger('click') await wrapper.get('[aria-label="显示零值参考线"]').trigger('click')
await flushPromises() await flushPromises()
expect(chart.props('cleanView')).toBe(true) expect(chart.props('cleanView')).toBe(true)
expect(chart.props('presentationMode')).toBe(true)
expect(chart.props('zeroLine')).toMatchObject({ visible: true, color: '#98a2b3', width: 1 }) expect(chart.props('zeroLine')).toMatchObject({ visible: true, color: '#98a2b3', width: 1 })
wrapper.unmount() wrapper.unmount()
}) })

View File

@@ -43,6 +43,7 @@ const yAxisLineVisible = ref(false)
const annotations = ref<WaveformAnnotation[]>([]) const annotations = ref<WaveformAnnotation[]>([])
const annotationsVisible = ref(true) const annotationsVisible = ref(true)
const cleanView = ref(false) const cleanView = ref(false)
const presentationMode = ref(false)
const showTooltip = ref(true) const showTooltip = ref(true)
const zeroLineVisible = ref(false) const zeroLineVisible = ref(false)
const zeroLineColor = ref('#98a2b3') const zeroLineColor = ref('#98a2b3')
@@ -289,6 +290,7 @@ const controlPanelModel = reactive({
overlayMode, overlayMode,
showTooltip, showTooltip,
cleanView, cleanView,
presentationMode,
selectedSeriesId, selectedSeriesId,
selectedLineStyle, selectedLineStyle,
lineStyleOptions, lineStyleOptions,
@@ -350,6 +352,7 @@ const chartModel = reactive({
frameStyle, frameStyle,
axes, axes,
cleanView, cleanView,
presentationMode,
showTooltip, showTooltip,
zeroLine, zeroLine,
frameWatermarkVisible, frameWatermarkVisible,

View File

@@ -104,6 +104,11 @@
cursor: crosshair; cursor: crosshair;
} }
.waveform-chart--presentation .waveform-chart__overlay {
cursor: default;
pointer-events: none;
}
.waveform-chart__label, .waveform-chart__label,
.waveform-chart__empty { .waveform-chart__empty {
fill: #595959; fill: #595959;

View File

@@ -26,6 +26,7 @@ const props = withDefaults(defineProps<WaveformChartProps>(), {
legend: () => ({ position: 'top-right', orientation: 'auto' }), legend: () => ({ position: 'top-right', orientation: 'auto' }),
defaultHiddenSeriesIds: () => [], defaultHiddenSeriesIds: () => [],
cleanView: false, cleanView: false,
presentationMode: false,
zeroLine: () => ({ visible: false }), zeroLine: () => ({ visible: false }),
}) })
const emit = defineEmits<WaveformChartEmit>() const emit = defineEmits<WaveformChartEmit>()

View File

@@ -13,6 +13,7 @@ const {
displayMode, displayMode,
activeInteractionMode, activeInteractionMode,
isCleanView, isCleanView,
isPresentationMode,
selection, selection,
containerStyle, containerStyle,
overlayMode, overlayMode,
@@ -45,7 +46,7 @@ const {
beginSharedViewportDrag, beginSharedViewportDrag,
finishViewportDrag, finishViewportDrag,
cancelViewportDrag, cancelViewportDrag,
clearHover, handlePointerLeave,
handleAnnotationClick, handleAnnotationClick,
handleAnnotationContextMenu, handleAnnotationContextMenu,
resolveFrameNumber, resolveFrameNumber,
@@ -107,12 +108,14 @@ const {
`waveform-chart--interaction-${activeInteractionMode}`, `waveform-chart--interaction-${activeInteractionMode}`,
{ {
'waveform-chart--clean': isCleanView, 'waveform-chart--clean': isCleanView,
'waveform-chart--presentation': isPresentationMode,
'waveform-chart--panning': selection?.mode === 'pan', 'waveform-chart--panning': selection?.mode === 'pan',
}, },
]" ]"
:style="containerStyle" :style="containerStyle"
:data-display-mode="displayMode" :data-display-mode="displayMode"
:data-interaction-mode="activeInteractionMode" :data-interaction-mode="activeInteractionMode"
:data-presentation-mode="isPresentationMode"
:data-overlay-mode="overlayMode" :data-overlay-mode="overlayMode"
:data-chart-left-margin="resolvedChartLeftMargin" :data-chart-left-margin="resolvedChartLeftMargin"
:data-title-area-height="titleAreaHeight" :data-title-area-height="titleAreaHeight"
@@ -192,8 +195,8 @@ const {
:ref="setSharedOverlayElement" :ref="setSharedOverlayElement"
class="waveform-chart__overlay waveform-chart__overlay--shared" class="waveform-chart__overlay waveform-chart__overlay--shared"
:class="{ :class="{
'is-zoomable': zoomable && isZoomMode, 'is-zoomable': !isPresentationMode && zoomable && isZoomMode,
'is-annotating': activeInteractionMode === 'annotation', 'is-annotating': !isPresentationMode && activeInteractionMode === 'annotation',
}" }"
:width="innerWidth" :width="innerWidth"
:height="innerHeight" :height="innerHeight"
@@ -201,7 +204,7 @@ const {
@pointerdown="beginSharedViewportDrag" @pointerdown="beginSharedViewportDrag"
@pointerup="finishViewportDrag" @pointerup="finishViewportDrag"
@pointercancel="cancelViewportDrag" @pointercancel="cancelViewportDrag"
@pointerleave="clearHover" @pointerleave="handlePointerLeave"
@click="handleAnnotationClick" @click="handleAnnotationClick"
@contextmenu="handleAnnotationContextMenu" @contextmenu="handleAnnotationContextMenu"
/> />
@@ -214,6 +217,7 @@ const {
:clip-path-id="clipPathId" :clip-path-id="clipPathId"
:inner-width="innerWidth" :inner-width="innerWidth"
:zoomable="zoomable" :zoomable="zoomable"
:interactive="!isPresentationMode"
:display-mode="displayMode" :display-mode="displayMode"
:interaction-mode="activeInteractionMode" :interaction-mode="activeInteractionMode"
:frame-number="resolveFrameNumber(track.index)" :frame-number="resolveFrameNumber(track.index)"
@@ -227,7 +231,7 @@ const {
@pointer-down="beginViewportDrag($event, track.index, true)" @pointer-down="beginViewportDrag($event, track.index, true)"
@pointer-up="finishViewportDrag" @pointer-up="finishViewportDrag"
@pointer-cancel="cancelViewportDrag" @pointer-cancel="cancelViewportDrag"
@pointer-leave="clearHover" @pointer-leave="handlePointerLeave"
@click="handleAnnotationClick($event, track.index)" @click="handleAnnotationClick($event, track.index)"
@contextmenu="handleAnnotationContextMenu($event, track.index)" @contextmenu="handleAnnotationContextMenu($event, track.index)"
/> />
@@ -236,7 +240,7 @@ const {
:state="hoverState" :state="hoverState"
:tracks="trackLayouts" :tracks="trackLayouts"
:clip-path-id="clipPathId" :clip-path-id="clipPathId"
:visible="showTooltip" :visible="showTooltip && !isPresentationMode"
/> />
<rect <rect
@@ -253,6 +257,7 @@ const {
v-if="!isCleanView" v-if="!isCleanView"
:annotations="renderedAnnotations" :annotations="renderedAnnotations"
:visible="annotationsVisible" :visible="annotationsVisible"
:interactive="!isPresentationMode"
@contextmenu="handleExistingAnnotationContextMenu" @contextmenu="handleExistingAnnotationContextMenu"
@drag-start="beginAnnotationDrag" @drag-start="beginAnnotationDrag"
@move="handleAnnotationMove" @move="handleAnnotationMove"
@@ -318,7 +323,7 @@ const {
/> />
<WaveformAnnotationEditor <WaveformAnnotationEditor
v-if="editorDraft && !isCleanView" v-if="editorDraft && !isCleanView && !isPresentationMode"
:annotation="editorDraft.annotation" :annotation="editorDraft.annotation"
:mode="editorDraft.mode" :mode="editorDraft.mode"
:series="editorSeries" :series="editorSeries"
@@ -330,7 +335,7 @@ const {
/> />
<WaveformAnnotationContextMenu <WaveformAnnotationContextMenu
v-if="!isCleanView" v-if="!isCleanView && !isPresentationMode"
:visible="contextMenu !== null" :visible="contextMenu !== null"
:x="contextMenu?.x || 0" :x="contextMenu?.x || 0"
:y="contextMenu?.y || 0" :y="contextMenu?.y || 0"
@@ -342,7 +347,7 @@ const {
<WaveformHoverHost <WaveformHoverHost
:state="hoverState" :state="hoverState"
:visible="showTooltip" :visible="showTooltip && !isPresentationMode"
:time-unit="timeUnit" :time-unit="timeUnit"
:container-width="chartWidth" :container-width="chartWidth"
:container-height="chartHeight" :container-height="chartHeight"

View File

@@ -7,9 +7,12 @@ import { ANNOTATION_TEXT_FONT, ANNOTATION_TEXT_LINE_HEIGHT } from './markup'
interface Props { interface Props {
annotations: RenderedAnnotation[] annotations: RenderedAnnotation[]
visible: boolean visible: boolean
interactive?: boolean
} }
const props = defineProps<Props>() const props = withDefaults(defineProps<Props>(), {
interactive: true,
})
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'contextmenu', annotationId: string, mouseEvent: MouseEvent): void (event: 'contextmenu', annotationId: string, mouseEvent: MouseEvent): void
(event: 'drag-start'): void (event: 'drag-start'): void
@@ -79,7 +82,7 @@ function scheduleDragFrame() {
} }
function handlePointerDown(rendered: RenderedAnnotation, event: PointerEvent) { function handlePointerDown(rendered: RenderedAnnotation, event: PointerEvent) {
if (event.button !== 0 || dragState) return if (!props.interactive || event.button !== 0 || dragState) return
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
const target = event.currentTarget as SVGGElement | null const target = event.currentTarget as SVGGElement | null
@@ -108,6 +111,7 @@ function handlePointerDown(rendered: RenderedAnnotation, event: PointerEvent) {
} }
function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
if (!props.interactive) return
event.stopPropagation() event.stopPropagation()
if (!dragState || event.pointerId !== dragState.pointerId) return if (!dragState || event.pointerId !== dragState.pointerId) return
const deltaX = event.clientX - dragState.startX const deltaX = event.clientX - dragState.startX
@@ -119,6 +123,7 @@ function handlePointerMove(event: PointerEvent) {
} }
function finishPointerDrag(event: PointerEvent) { function finishPointerDrag(event: PointerEvent) {
if (!props.interactive) return
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
if (!dragState || event.pointerId !== dragState.pointerId) return if (!dragState || event.pointerId !== dragState.pointerId) return
@@ -181,6 +186,11 @@ function handlePointerCancel(event: PointerEvent) {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
if (!dragState || event.pointerId !== dragState.pointerId) return if (!dragState || event.pointerId !== dragState.pointerId) return
cancelActiveDrag()
}
function cancelActiveDrag() {
if (!dragState) return
const state = dragState const state = dragState
dragState = null dragState = null
dragOffsets.value = new Map(dragOffsets.value).set(state.annotationId, { x: 0, y: 0 }) dragOffsets.value = new Map(dragOffsets.value).set(state.annotationId, { x: 0, y: 0 })
@@ -188,10 +198,21 @@ function handlePointerCancel(event: PointerEvent) {
cancelAnimationFrame(dragFrame) cancelAnimationFrame(dragFrame)
dragFrame = null dragFrame = null
} }
if (state.target.hasPointerCapture(state.pointerId)) {
state.target.releasePointerCapture(state.pointerId)
}
emit('drag-end', true) emit('drag-end', true)
} }
watch(
() => props.interactive,
(interactive) => {
if (!interactive) cancelActiveDrag()
},
)
function handleContextMenu(annotationId: string, event: MouseEvent) { function handleContextMenu(annotationId: string, event: MouseEvent) {
if (!props.interactive) return
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
// 使用时间戳比较:如果 contextmenu 在拖动结束后 100ms 内触发,则抑制 // 使用时间戳比较:如果 contextmenu 在拖动结束后 100ms 内触发,则抑制
@@ -206,7 +227,11 @@ function markerId(annotationId: string) {
</script> </script>
<template> <template>
<g v-if="props.visible" class="waveform-annotation-layer"> <g
v-if="props.visible"
class="waveform-annotation-layer"
:class="{ 'waveform-annotation-layer--interactive': props.interactive }"
>
<g <g
v-for="rendered in props.annotations" v-for="rendered in props.annotations"
:key="rendered.annotation.id" :key="rendered.annotation.id"
@@ -281,13 +306,13 @@ function markerId(annotationId: string) {
pointer-events: none; pointer-events: none;
} }
.waveform-annotation { .waveform-annotation-layer--interactive .waveform-annotation {
pointer-events: auto; pointer-events: auto;
cursor: grab; cursor: grab;
touch-action: none; touch-action: none;
} }
.waveform-annotation:active { .waveform-annotation-layer--interactive .waveform-annotation:active {
cursor: grabbing; cursor: grabbing;
} }
@@ -300,6 +325,9 @@ function markerId(annotationId: string) {
.waveform-annotation__box { .waveform-annotation__box {
stroke-width: 1; stroke-width: 1;
rx: 3; rx: 3;
}
.waveform-annotation-layer--interactive .waveform-annotation__box {
pointer-events: auto; pointer-events: auto;
} }

View File

@@ -38,6 +38,7 @@ interface AnnotationContext {
titleAreaHeight: ComputedRef<number> titleAreaHeight: ComputedRef<number>
chartTopMargin: ComputedRef<number> chartTopMargin: ComputedRef<number>
activeInteractionMode: ComputedRef<WaveformInteractionMode | undefined> activeInteractionMode: ComputedRef<WaveformInteractionMode | undefined>
isPresentationMode: ComputedRef<boolean>
} }
interface AnnotationCandidateContext { interface AnnotationCandidateContext {
@@ -75,6 +76,7 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
titleAreaHeight, titleAreaHeight,
chartTopMargin, chartTopMargin,
activeInteractionMode, activeInteractionMode,
isPresentationMode,
} = context } = context
function toggleSeriesVisibility(seriesId: string) { function toggleSeriesVisibility(seriesId: string) {
@@ -140,6 +142,7 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
} }
function changeDraftSeries(seriesId: string) { function changeDraftSeries(seriesId: string) {
if (isPresentationMode.value) return
const draft = annotationInteraction.editorDraft.value const draft = annotationInteraction.editorDraft.value
const candidate = editorSeriesOptions.value.find((item) => item.seriesId === seriesId) const candidate = editorSeriesOptions.value.find((item) => item.seriesId === seriesId)
const track = trackLayouts.value.find((item) => const track = trackLayouts.value.find((item) =>
@@ -216,7 +219,13 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
} }
function handleAnnotationClick(event: MouseEvent, trackIndex?: number) { function handleAnnotationClick(event: MouseEvent, trackIndex?: number) {
if (!props.annotationsVisible || activeInteractionMode.value !== 'annotation') return if (
isPresentationMode.value ||
!props.annotationsVisible ||
activeInteractionMode.value !== 'annotation'
) {
return
}
const candidateContext = resolveAnnotationCandidates(event, trackIndex) const candidateContext = resolveAnnotationCandidates(event, trackIndex)
if (!candidateContext) return if (!candidateContext) return
const nearby = nearbyCandidates(candidateContext.candidates) const nearby = nearbyCandidates(candidateContext.candidates)
@@ -238,7 +247,7 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
} }
function handleAnnotationContextMenu(event: MouseEvent, trackIndex?: number) { function handleAnnotationContextMenu(event: MouseEvent, trackIndex?: number) {
if (!props.annotationsVisible) return if (isPresentationMode.value || !props.annotationsVisible) return
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
const candidateContext = resolveAnnotationCandidates(event, trackIndex) const candidateContext = resolveAnnotationCandidates(event, trackIndex)
@@ -252,7 +261,7 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
} }
function handleExistingAnnotationContextMenu(annotationId: string, event: MouseEvent) { function handleExistingAnnotationContextMenu(annotationId: string, event: MouseEvent) {
if (!props.annotationsVisible || !container.value) return if (isPresentationMode.value || !props.annotationsVisible || !container.value) return
editorSeriesOptions.value = [] editorSeriesOptions.value = []
const annotation = props.annotations.find((item) => item.id === annotationId) const annotation = props.annotations.find((item) => item.id === annotationId)
if (!annotation) return if (!annotation) return
@@ -270,6 +279,7 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
} }
function handleAnnotationMove(annotationId: string, offsetX: number, offsetY: number) { function handleAnnotationMove(annotationId: string, offsetX: number, offsetY: number) {
if (isPresentationMode.value) return
const annotation = props.annotations.find((item) => item.id === annotationId) const annotation = props.annotations.find((item) => item.id === annotationId)
if (!annotation || !Number.isFinite(offsetX) || !Number.isFinite(offsetY)) return if (!annotation || !Number.isFinite(offsetX) || !Number.isFinite(offsetY)) return
emit( emit(
@@ -281,6 +291,7 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
} }
function editContextAnnotation() { function editContextAnnotation() {
if (isPresentationMode.value) return
const menu = annotationInteraction.contextMenu.value const menu = annotationInteraction.contextMenu.value
const annotation = props.annotations.find((item) => item.id === menu?.annotationId) const annotation = props.annotations.find((item) => item.id === menu?.annotationId)
if (!annotation) return if (!annotation) return
@@ -302,6 +313,7 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
} }
function deleteContextAnnotation() { function deleteContextAnnotation() {
if (isPresentationMode.value) return
const annotationId = annotationInteraction.contextMenu.value?.annotationId const annotationId = annotationInteraction.contextMenu.value?.annotationId
const annotation = props.annotations.find((item) => item.id === annotationId) const annotation = props.annotations.find((item) => item.id === annotationId)
if (!annotation) return if (!annotation) return
@@ -314,6 +326,7 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
} }
function confirmAnnotation(annotation: WaveformAnnotation) { function confirmAnnotation(annotation: WaveformAnnotation) {
if (isPresentationMode.value) return
const draft = annotationInteraction.editorDraft.value const draft = annotationInteraction.editorDraft.value
if (!draft) return if (!draft) return
if (draft.mode === 'add') { if (draft.mode === 'add') {

View File

@@ -90,6 +90,7 @@ export function useWaveformChartController(
chartWidth, chartWidth,
chartHeight, chartHeight,
isCleanView, isCleanView,
isPresentationMode,
hiddenSeriesIdSet, hiddenSeriesIdSet,
resolvedTitleText, resolvedTitleText,
titleAreaReserved, titleAreaReserved,
@@ -151,6 +152,7 @@ export function useWaveformChartController(
innerHeight, innerHeight,
hasChartArea, hasChartArea,
isZoomMode, isZoomMode,
isPresentationMode,
resolveInitialTrackDomain, resolveInitialTrackDomain,
cancelPendingHover: () => hover.cancelPendingHover(), cancelPendingHover: () => hover.cancelPendingHover(),
}) })
@@ -174,6 +176,7 @@ export function useWaveformChartController(
titleAreaHeight, titleAreaHeight,
chartTopMargin, chartTopMargin,
activeInteractionMode, activeInteractionMode,
isPresentationMode,
}) })
const viewport = useWaveformViewport({ const viewport = useWaveformViewport({
@@ -192,6 +195,7 @@ export function useWaveformChartController(
sharedYDomains, sharedYDomains,
independentYDomains, independentYDomains,
isZoomMode, isZoomMode,
isPresentationMode,
editorSeriesOptions, editorSeriesOptions,
resolveInitialTrackDomain, resolveInitialTrackDomain,
canZoomTrack: zoom.canZoomTrack, canZoomTrack: zoom.canZoomTrack,
@@ -214,6 +218,7 @@ export function useWaveformChartController(
titleAreaHeight, titleAreaHeight,
chartTopMargin, chartTopMargin,
sharedOverlayElement, sharedOverlayElement,
isPresentationMode,
updateViewportDrag: viewport.updateViewportDrag, updateViewportDrag: viewport.updateViewportDrag,
resolveTrackAtPointer: annotations.resolveTrackAtPointer, resolveTrackAtPointer: annotations.resolveTrackAtPointer,
}) })
@@ -248,10 +253,12 @@ export function useWaveformChartController(
independentYDomains, independentYDomains,
annotationInteraction, annotationInteraction,
editorSeriesOptions, editorSeriesOptions,
isPresentationMode,
clearHover: hover.clearHover, clearHover: hover.clearHover,
cancelAnnotation: annotations.cancelAnnotation, cancelAnnotation: annotations.cancelAnnotation,
configureZoom: zoom.configureZoom, configureZoom: zoom.configureZoom,
resetViewport: viewport.resetViewport, resetViewport: viewport.resetViewport,
cancelViewportDrag: viewport.cancelViewportDrag,
cancelPendingHover: hover.cancelPendingHover, cancelPendingHover: hover.cancelPendingHover,
clearZoomBindings: zoom.clearZoomBindings, clearZoomBindings: zoom.clearZoomBindings,
}) })

View File

@@ -44,10 +44,12 @@ interface LifecycleContext {
independentYDomains: Ref<Record<number, [number, number]>> independentYDomains: Ref<Record<number, [number, number]>>
annotationInteraction: ReturnType<typeof useWaveformAnnotationInteraction> annotationInteraction: ReturnType<typeof useWaveformAnnotationInteraction>
editorSeriesOptions: Ref<AnnotationSeriesCandidate[]> editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>
isPresentationMode: ComputedRef<boolean>
clearHover: () => void clearHover: () => void
cancelAnnotation: () => void cancelAnnotation: () => void
configureZoom: () => void configureZoom: () => void
resetViewport: () => void resetViewport: () => void
cancelViewportDrag: () => void
cancelPendingHover: () => void cancelPendingHover: () => void
clearZoomBindings: () => void clearZoomBindings: () => void
} }
@@ -94,16 +96,25 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
independentYDomains, independentYDomains,
annotationInteraction, annotationInteraction,
editorSeriesOptions, editorSeriesOptions,
isPresentationMode,
clearHover, clearHover,
cancelAnnotation, cancelAnnotation,
configureZoom, configureZoom,
resetViewport, resetViewport,
cancelViewportDrag,
cancelPendingHover, cancelPendingHover,
clearZoomBindings, clearZoomBindings,
} = context } = context
function handleInteractionKeyDown(event: KeyboardEvent) { function handleInteractionKeyDown(event: KeyboardEvent) {
if (event.code !== 'Space' || !props.pannable || !pointerInsideChart.value) return if (
isPresentationMode.value ||
event.code !== 'Space' ||
!props.pannable ||
!pointerInsideChart.value
) {
return
}
if (isEditableTarget(event.target)) return if (isEditableTarget(event.target)) return
spacePressed.value = true spacePressed.value = true
event.preventDefault() event.preventDefault()
@@ -145,6 +156,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
innerWidth, innerWidth,
innerHeight, innerHeight,
() => props.zoomable, () => props.zoomable,
isPresentationMode,
() => props.minZoomSpan, () => props.minZoomSpan,
() => props.initialXDomain, () => props.initialXDomain,
() => props.initialXDomains, () => props.initialXDomains,
@@ -191,6 +203,16 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
editorSeriesOptions.value = [] editorSeriesOptions.value = []
}) })
watch(isPresentationMode, (enabled) => {
spacePressed.value = false
if (!enabled) return
cancelViewportDrag()
clearZoomBindings()
clearHover()
annotationInteraction.closeContextMenu()
cancelAnnotation()
})
watch( watch(
() => chartSeries.value.map((series) => series.id).join('\u0000'), () => chartSeries.value.map((series) => series.id).join('\u0000'),
() => { () => {

View File

@@ -49,6 +49,7 @@ export function useWaveformPresentation(context: PresentationContext) {
height: fixedHeight.value === undefined ? '100%' : `${fixedHeight.value}px`, height: fixedHeight.value === undefined ? '100%' : `${fixedHeight.value}px`,
})) }))
const isCleanView = computed(() => props.cleanView === true) const isCleanView = computed(() => props.cleanView === true)
const isPresentationMode = computed(() => props.presentationMode === true)
const resolvedZeroLine = computed(() => { const resolvedZeroLine = computed(() => {
const width = props.zeroLine.width const width = props.zeroLine.width
return { return {
@@ -178,6 +179,7 @@ export function useWaveformPresentation(context: PresentationContext) {
chartHeight, chartHeight,
containerStyle, containerStyle,
isCleanView, isCleanView,
isPresentationMode,
resolvedZeroLine, resolvedZeroLine,
legendBackgroundColor, legendBackgroundColor,
legendInteractive, legendInteractive,

View File

@@ -45,6 +45,7 @@ export interface WaveformChartProps {
hiddenSeriesIds?: string[] hiddenSeriesIds?: string[]
defaultHiddenSeriesIds?: string[] defaultHiddenSeriesIds?: string[]
cleanView?: boolean cleanView?: boolean
presentationMode?: boolean
zeroLine?: WaveformZeroLineOptions zeroLine?: WaveformZeroLineOptions
} }
@@ -65,6 +66,7 @@ type DefaultedProp =
| 'legend' | 'legend'
| 'defaultHiddenSeriesIds' | 'defaultHiddenSeriesIds'
| 'cleanView' | 'cleanView'
| 'presentationMode'
| 'zeroLine' | 'zeroLine'
export type ResolvedWaveformChartProps = Readonly< export type ResolvedWaveformChartProps = Readonly<

View File

@@ -23,6 +23,7 @@ interface HoverContext {
titleAreaHeight: ComputedRef<number> titleAreaHeight: ComputedRef<number>
chartTopMargin: ComputedRef<number> chartTopMargin: ComputedRef<number>
sharedOverlayElement: ShallowRef<SVGRectElement | undefined> sharedOverlayElement: ShallowRef<SVGRectElement | undefined>
isPresentationMode: ComputedRef<boolean>
updateViewportDrag: (event: PointerEvent) => void updateViewportDrag: (event: PointerEvent) => void
resolveTrackAtPointer: (pointerX: number, pointerY: number) => TrackLayout | undefined resolveTrackAtPointer: (pointerX: number, pointerY: number) => TrackLayout | undefined
} }
@@ -42,6 +43,7 @@ export function useWaveformHover(context: HoverContext) {
titleAreaHeight, titleAreaHeight,
chartTopMargin, chartTopMargin,
sharedOverlayElement, sharedOverlayElement,
isPresentationMode,
updateViewportDrag, updateViewportDrag,
resolveTrackAtPointer, resolveTrackAtPointer,
} = context } = context
@@ -62,6 +64,7 @@ export function useWaveformHover(context: HoverContext) {
trackIndex: number | null, trackIndex: number | null,
position: { x: number; y: number }, position: { x: number; y: number },
) => { ) => {
if (isPresentationMode.value) return
if (!hoveredPointsMatch(nextPoints)) hoverState.points = nextPoints if (!hoveredPointsMatch(nextPoints)) hoverState.points = nextPoints
hoverState.trackIndex = trackIndex hoverState.trackIndex = trackIndex
hoverState.position = position hoverState.position = position
@@ -73,6 +76,9 @@ export function useWaveformHover(context: HoverContext) {
hoverState.trackIndex = null hoverState.trackIndex = null
emit('point-hover', null) emit('point-hover', null)
} }
const handlePointerLeave = () => {
if (!isPresentationMode.value) clearHover()
}
const createHoveredSeriesPoint = ( const createHoveredSeriesPoint = (
series: DisplaySeries, series: DisplaySeries,
trackIndex: number, trackIndex: number,
@@ -103,6 +109,7 @@ export function useWaveformHover(context: HoverContext) {
series.points[pointBisector.center(series.points, xValue)] series.points[pointBisector.center(series.points, xValue)]
const handleIndependentPointerMove = (event: PointerEvent, trackIndex: number) => { const handleIndependentPointerMove = (event: PointerEvent, trackIndex: number) => {
if (isPresentationMode.value) return
if (selection.value?.trackIndex === trackIndex && selection.value.independent) { if (selection.value?.trackIndex === trackIndex && selection.value.independent) {
updateViewportDrag(event) updateViewportDrag(event)
return return
@@ -128,6 +135,7 @@ export function useWaveformHover(context: HoverContext) {
}) })
} }
const handleSharedPointerMove = (event: PointerEvent) => { const handleSharedPointerMove = (event: PointerEvent) => {
if (isPresentationMode.value) return
if (selection.value?.overlay === event.currentTarget) { if (selection.value?.overlay === event.currentTarget) {
updateViewportDrag(event) updateViewportDrag(event)
return return
@@ -161,6 +169,7 @@ export function useWaveformHover(context: HoverContext) {
return { return {
cancelPendingHover, cancelPendingHover,
clearHover, clearHover,
handlePointerLeave,
beginAnnotationDrag, beginAnnotationDrag,
endAnnotationDrag, endAnnotationDrag,
handleIndependentPointerMove, handleIndependentPointerMove,

View File

@@ -26,6 +26,7 @@ interface ViewportContext {
sharedYDomains: Ref<Record<string, [number, number]>> sharedYDomains: Ref<Record<string, [number, number]>>
independentYDomains: Ref<Record<number, [number, number]>> independentYDomains: Ref<Record<number, [number, number]>>
isZoomMode: ComputedRef<boolean> isZoomMode: ComputedRef<boolean>
isPresentationMode: ComputedRef<boolean>
editorSeriesOptions: Ref<AnnotationSeriesCandidate[]> editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>
resolveInitialTrackDomain: (track: TrackLayout) => [number, number] resolveInitialTrackDomain: (track: TrackLayout) => [number, number]
canZoomTrack: (track: TrackLayout) => boolean canZoomTrack: (track: TrackLayout) => boolean
@@ -53,6 +54,7 @@ export function useWaveformViewport(context: ViewportContext) {
sharedYDomains, sharedYDomains,
independentYDomains, independentYDomains,
isZoomMode, isZoomMode,
isPresentationMode,
editorSeriesOptions, editorSeriesOptions,
resolveInitialTrackDomain, resolveInitialTrackDomain,
canZoomTrack, canZoomTrack,
@@ -131,6 +133,7 @@ export function useWaveformViewport(context: ViewportContext) {
]), ]),
) )
const beginViewportDrag = (event: PointerEvent, trackIndex: number, independent: boolean) => { const beginViewportDrag = (event: PointerEvent, trackIndex: number, independent: boolean) => {
if (isPresentationMode.value) return
const panRequested = props.pannable && spacePressed.value const panRequested = props.pannable && spacePressed.value
if ((!props.zoomable && !panRequested) || !isZoomMode.value || event.button !== 0) return if ((!props.zoomable && !panRequested) || !isZoomMode.value || event.button !== 0) return
const overlay = event.currentTarget as SVGRectElement const overlay = event.currentTarget as SVGRectElement
@@ -203,6 +206,7 @@ export function useWaveformViewport(context: ViewportContext) {
emit('zoom-change', nextX) emit('zoom-change', nextX)
} }
const updateViewportDrag = (event: PointerEvent) => { const updateViewportDrag = (event: PointerEvent) => {
if (isPresentationMode.value) return
const active = selection.value const active = selection.value
if (!active || event.pointerId !== active.pointerId) return if (!active || event.pointerId !== active.pointerId) return
const track = trackLayouts.value.find((item) => item.index === active.trackIndex) const track = trackLayouts.value.find((item) => item.index === active.trackIndex)
@@ -285,6 +289,10 @@ export function useWaveformViewport(context: ViewportContext) {
void nextTick(configureZoom) void nextTick(configureZoom)
} }
const finishViewportDrag = (event: PointerEvent) => { const finishViewportDrag = (event: PointerEvent) => {
if (isPresentationMode.value) {
cancelViewportDrag()
return
}
const active = selection.value const active = selection.value
if (!active || event.pointerId !== active.pointerId) return if (!active || event.pointerId !== active.pointerId) return
updateViewportDrag(event) updateViewportDrag(event)
@@ -319,7 +327,7 @@ export function useWaveformViewport(context: ViewportContext) {
void nextTick(configureZoom) void nextTick(configureZoom)
} }
const requestViewportReset = (event: MouseEvent) => { const requestViewportReset = (event: MouseEvent) => {
if (!props.zoomable || !isZoomMode.value) return if (isPresentationMode.value || !props.zoomable || !isZoomMode.value) return
event.preventDefault() event.preventDefault()
resetViewport() resetViewport()
emit('zoom-reset') emit('zoom-reset')

View File

@@ -29,6 +29,7 @@ interface ZoomContext {
innerHeight: ComputedRef<number> innerHeight: ComputedRef<number>
hasChartArea: ComputedRef<boolean> hasChartArea: ComputedRef<boolean>
isZoomMode: ComputedRef<boolean> isZoomMode: ComputedRef<boolean>
isPresentationMode: ComputedRef<boolean>
resolveInitialTrackDomain: (track: TrackLayout) => [number, number] resolveInitialTrackDomain: (track: TrackLayout) => [number, number]
cancelPendingHover: () => void cancelPendingHover: () => void
} }
@@ -50,6 +51,7 @@ export function useWaveformZoom(context: ZoomContext) {
innerHeight, innerHeight,
hasChartArea, hasChartArea,
isZoomMode, isZoomMode,
isPresentationMode,
resolveInitialTrackDomain, resolveInitialTrackDomain,
cancelPendingHover, cancelPendingHover,
} = context } = context
@@ -67,7 +69,7 @@ export function useWaveformZoom(context: ZoomContext) {
const scheduleZoomCommit = () => zoomThrottle.schedule(commitPendingZoom) const scheduleZoomCommit = () => zoomThrottle.schedule(commitPendingZoom)
const handleSharedZoom = (event: D3ZoomEvent<SVGRectElement, unknown>) => { const handleSharedZoom = (event: D3ZoomEvent<SVGRectElement, unknown>) => {
if (synchronizingZoomTransform) return if (synchronizingZoomTransform || isPresentationMode.value) return
cancelPendingHover() cancelPendingHover()
pendingSharedZoomTransform = event.transform pendingSharedZoomTransform = event.transform
pendingSharedZoomGesture = 'wheel' pendingSharedZoomGesture = 'wheel'
@@ -78,7 +80,7 @@ export function useWaveformZoom(context: ZoomContext) {
event: D3ZoomEvent<SVGRectElement, unknown>, event: D3ZoomEvent<SVGRectElement, unknown>,
trackIndex: number, trackIndex: number,
) => { ) => {
if (synchronizingZoomTransform) return if (synchronizingZoomTransform || isPresentationMode.value) return
cancelPendingHover() cancelPendingHover()
pendingIndependentZoomTransforms.set(trackIndex, event.transform) pendingIndependentZoomTransforms.set(trackIndex, event.transform)
pendingIndependentZoomGestures.set(trackIndex, 'wheel') pendingIndependentZoomGestures.set(trackIndex, 'wheel')
@@ -86,6 +88,10 @@ export function useWaveformZoom(context: ZoomContext) {
scheduleWheelZoomEnd() scheduleWheelZoomEnd()
} }
function commitPendingZoom() { function commitPendingZoom() {
if (isPresentationMode.value) {
cancelPendingZoom()
return
}
if (pendingSharedZoomTransform) { if (pendingSharedZoomTransform) {
const transform = pendingSharedZoomTransform const transform = pendingSharedZoomTransform
pendingSharedZoomTransform = null pendingSharedZoomTransform = null
@@ -233,7 +239,13 @@ export function useWaveformZoom(context: ZoomContext) {
} }
const configureZoom = () => { const configureZoom = () => {
clearZoomBindings() clearZoomBindings()
if (!props.zoomable || !isZoomMode.value || !hasChartArea.value || !trackLayouts.value.length) { if (
isPresentationMode.value ||
!props.zoomable ||
!isZoomMode.value ||
!hasChartArea.value ||
!trackLayouts.value.length
) {
return return
} }
if (props.displayMode === 'independent') { if (props.displayMode === 'independent') {

View File

@@ -70,6 +70,11 @@
cursor: crosshair; cursor: crosshair;
} }
.waveform-track__overlay.is-presentation {
cursor: default;
pointer-events: none;
}
.waveform-track__zero-line { .waveform-track__zero-line {
fill: none; fill: none;
pointer-events: none; pointer-events: none;

View File

@@ -13,6 +13,7 @@ interface Props {
clipPathId: string clipPathId: string
innerWidth: number innerWidth: number
zoomable: boolean zoomable: boolean
interactive?: boolean
displayMode: WaveformDisplayMode displayMode: WaveformDisplayMode
interactionMode?: WaveformInteractionMode interactionMode?: WaveformInteractionMode
frameNumber?: string | number frameNumber?: string | number
@@ -38,6 +39,7 @@ interface Emits {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
interactionMode: 'zoom', interactionMode: 'zoom',
interactive: true,
cleanView: false, cleanView: false,
zeroLine: () => ({ visible: false, color: '#98a2b3', width: 1, dash: '6 4' }), zeroLine: () => ({ visible: false, color: '#98a2b3', width: 1, dash: '6 4' }),
}) })
@@ -116,9 +118,11 @@ const resolvedFrameStyle = computed(() => {
v-if="!track.isEmpty && track.hasVisibleSeries && displayMode === 'independent'" v-if="!track.isEmpty && track.hasVisibleSeries && displayMode === 'independent'"
class="waveform-track__overlay waveform-track__overlay--independent waveform-chart__overlay waveform-chart__overlay--independent" class="waveform-track__overlay waveform-track__overlay--independent waveform-chart__overlay waveform-chart__overlay--independent"
:class="{ :class="{
'is-zoomable': zoomable && interactionMode === 'zoom', 'is-zoomable': interactive && zoomable && interactionMode === 'zoom',
'is-annotating': interactionMode === 'annotation', 'is-annotating': interactive && interactionMode === 'annotation',
'is-presentation': !interactive,
}" }"
:data-interactive="interactive"
:data-independent-overlay-index="track.index" :data-independent-overlay-index="track.index"
:width="track.width ?? innerWidth" :width="track.width ?? innerWidth"
:height="track.height" :height="track.height"

View File

@@ -0,0 +1,306 @@
import { flushPromises } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { flushAnimationFrames } from '../../test/setup'
import { mountSizedChart } from '../../test/waveformChart'
import type { WaveformData, WaveformDisplayMode } from '../data/types'
const presentationData: WaveformData = {
kind: 'series',
series: [
{
id: 'low',
trackId: 'shared',
name: '低量程',
data: {
kind: 'points',
points: [
{ x: 0, y: 0 },
{ x: 1, y: 10 },
{ x: 2, y: 5 },
],
},
},
{
id: 'high',
trackId: 'shared',
name: '高量程',
data: {
kind: 'points',
points: [
{ x: 0, y: 100 },
{ x: 1, y: 200 },
{ x: 2, y: 150 },
],
},
},
{
id: 'other',
name: '其他通道',
data: {
kind: 'points',
points: [
{ x: 0, y: -1 },
{ x: 1, y: 1 },
{ x: 2, y: 0 },
],
},
},
],
}
function setOverlayBounds(overlay: ReturnType<Awaited<ReturnType<typeof mountSizedChart>>['get']>) {
const width = Number(overlay.attributes('width'))
const height = Number(overlay.attributes('height'))
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
value: () => ({ left: 0, top: 0, width, height }),
})
return { width, height }
}
function dispatchPointer(
element: Element,
type: string,
options: MouseEventInit & { pointerId: number },
) {
const event = new MouseEvent(type, { bubbles: true, cancelable: true, ...options })
Object.defineProperty(event, 'pointerId', { value: options.pointerId })
element.dispatchEvent(event)
}
describe('WaveformChart presentation mode', () => {
it('keeps existing plot interaction enabled by default', async () => {
const wrapper = await mountSizedChart(presentationData, {
grid: { rowCount: 1, columnCount: 1 },
})
const overlay = wrapper.get('.waveform-chart__overlay--independent')
const { width, height } = setOverlayBounds(overlay)
expect(wrapper.attributes('data-presentation-mode')).toBe('false')
expect(overlay.classes()).toContain('is-zoomable')
overlay.element.dispatchEvent(
new MouseEvent('pointermove', {
clientX: width / 2,
clientY: height / 2,
bubbles: true,
}),
)
flushAnimationFrames()
await flushPromises()
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
expect(wrapper.emitted('point-hover')?.length).toBeGreaterThan(0)
})
it.each<WaveformDisplayMode>(['independent', 'separated'])(
'blocks plot mouse events in %s layout',
async (displayMode) => {
const wrapper = await mountSizedChart(presentationData, {
annotations: [{ id: 'note', seriesId: 'low', x: 1, y: 10, text: '只读标注' }],
displayMode,
grid: { rowCount: 1, columnCount: 1 },
interactionMode: 'annotation',
presentationMode: true,
})
const overlay = wrapper.get('.waveform-chart__overlay')
const { width, height } = setOverlayBounds(overlay)
expect(wrapper.attributes('data-presentation-mode')).toBe('true')
expect(overlay.classes()).not.toContain('is-zoomable')
expect(overlay.classes()).not.toContain('is-annotating')
overlay.element.dispatchEvent(
new MouseEvent('pointermove', {
clientX: width / 2,
clientY: height / 2,
bubbles: true,
}),
)
overlay.element.dispatchEvent(
new WheelEvent('wheel', {
deltaY: -4000,
clientX: width / 2,
clientY: height / 2,
bubbles: true,
cancelable: true,
}),
)
overlay.element.dispatchEvent(
new MouseEvent('click', {
clientX: width / 2,
clientY: height / 2,
bubbles: true,
cancelable: true,
}),
)
overlay.element.dispatchEvent(
new MouseEvent('contextmenu', {
clientX: width / 2,
clientY: height / 2,
bubbles: true,
cancelable: true,
}),
)
wrapper.get('.waveform-chart__svg').element.dispatchEvent(
new MouseEvent('dblclick', { bubbles: true, cancelable: true }),
)
await wrapper.get('[data-annotation-id="note"]').trigger('contextmenu', {
clientX: width / 2,
clientY: height / 2,
})
const annotation = wrapper.get('[data-annotation-id="note"]').element
dispatchPointer(annotation, 'pointerdown', {
button: 0,
clientX: width / 2,
clientY: height / 2,
pointerId: 42,
})
dispatchPointer(annotation, 'pointermove', {
clientX: width / 2 + 30,
clientY: height / 2 + 20,
pointerId: 42,
})
dispatchPointer(annotation, 'pointerup', {
clientX: width / 2 + 30,
clientY: height / 2 + 20,
pointerId: 42,
})
flushAnimationFrames()
await flushPromises()
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(false)
expect(wrapper.find('.waveform-annotation-editor').exists()).toBe(false)
expect(wrapper.find('.waveform-annotation-context-menu').exists()).toBe(false)
expect(wrapper.emitted('point-hover')).toBeUndefined()
expect(wrapper.emitted('zoom-change')).toBeUndefined()
expect(wrapper.emitted('zoom-end')).toBeUndefined()
expect(wrapper.emitted('zoom-reset')).toBeUndefined()
expect(wrapper.emitted('update:annotations')).toBeUndefined()
},
)
it('keeps the legend and pagination interactive', async () => {
const wrapper = await mountSizedChart(presentationData, {
grid: { rowCount: 1, columnCount: 1, showPagination: true },
legend: { interactive: true },
presentationMode: true,
})
await wrapper.findAll('.waveform-chart__legend-item')[1]!.trigger('click')
await flushPromises()
expect(wrapper.emitted('update:hidden-series-ids')?.at(-1)).toEqual([['high']])
await wrapper.get('.ant-pagination-next button').trigger('click')
await flushPromises()
expect(wrapper.emitted('page-change')?.at(-1)).toEqual([2, 2])
})
it('cleans active UI state and restores interaction without resetting the viewport', async () => {
const wrapper = await mountSizedChart(presentationData, {
grid: { rowCount: 1, columnCount: 1 },
})
const overlay = wrapper.get('.waveform-chart__overlay--independent')
const { width, height } = setOverlayBounds(overlay)
overlay.element.dispatchEvent(
new WheelEvent('wheel', {
deltaY: -4000,
clientX: width / 2,
clientY: height / 2,
bubbles: true,
cancelable: true,
}),
)
flushAnimationFrames()
await flushPromises()
const zoomedEndpoint = wrapper.get('.waveform-chart__axis-endpoint--end').text()
dispatchPointer(overlay.element, 'pointerdown', {
button: 0,
clientX: width * 0.25,
clientY: height / 2,
pointerId: 41,
})
dispatchPointer(overlay.element, 'pointermove', {
clientX: width * 0.75,
clientY: height / 2,
pointerId: 41,
})
await flushPromises()
expect(wrapper.find('.waveform-chart__zoom-selection').exists()).toBe(true)
await wrapper.setProps({ presentationMode: true })
await flushPromises()
expect(wrapper.find('.waveform-chart__zoom-selection').exists()).toBe(false)
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe(zoomedEndpoint)
const zoomEventCount = wrapper.emitted('zoom-change')?.length ?? 0
await wrapper.setProps({ presentationMode: false })
await flushPromises()
overlay.element.dispatchEvent(
new WheelEvent('wheel', {
deltaY: -200,
clientX: width / 2,
clientY: height / 2,
bubbles: true,
cancelable: true,
}),
)
flushAnimationFrames()
await flushPromises()
expect(wrapper.emitted('zoom-change')?.length).toBeGreaterThan(zoomEventCount)
})
it('closes hover and annotation UI when enabled at runtime', async () => {
const wrapper = await mountSizedChart(presentationData, {
grid: { rowCount: 1, columnCount: 1 },
interactionMode: 'annotation',
})
const overlay = wrapper.get('.waveform-chart__overlay--independent')
const { width, height } = setOverlayBounds(overlay)
overlay.element.dispatchEvent(
new MouseEvent('pointermove', {
clientX: width / 2,
clientY: height / 2,
bubbles: true,
}),
)
flushAnimationFrames()
await flushPromises()
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
overlay.element.dispatchEvent(
new MouseEvent('contextmenu', {
clientX: width / 2,
clientY: height / 2,
bubbles: true,
cancelable: true,
}),
)
await flushPromises()
expect(wrapper.find('.waveform-annotation-editor').exists()).toBe(true)
await wrapper.setProps({ presentationMode: true })
await flushPromises()
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(false)
expect(wrapper.find('.waveform-annotation-editor').exists()).toBe(false)
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([null])
await wrapper.setProps({ presentationMode: false })
overlay.element.dispatchEvent(
new MouseEvent('pointermove', {
clientX: width / 2,
clientY: height / 2,
bubbles: true,
}),
)
flushAnimationFrames()
await flushPromises()
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
})
})

View File

@@ -43,6 +43,7 @@ defineExpose({ resetViewport })
:frame-style="model.frameStyle" :frame-style="model.frameStyle"
:axes="model.axes" :axes="model.axes"
:clean-view="model.cleanView" :clean-view="model.cleanView"
:presentation-mode="model.presentationMode"
:show-tooltip="model.showTooltip" :show-tooltip="model.showTooltip"
:zero-line="model.zeroLine" :zero-line="model.zeroLine"
:frame-number="model.frameWatermarkVisible ? 1 : undefined" :frame-number="model.frameWatermarkVisible ? 1 : undefined"

View File

@@ -46,6 +46,10 @@ const model = defineModel<DemoControlPanelModel>('model', { required: true })
<span>净图</span> <span>净图</span>
<Switch v-model:checked="model.cleanView" size="small" aria-label="净图模式" /> <Switch v-model:checked="model.cleanView" size="small" aria-label="净图模式" />
</label> </label>
<label class="frame-style-control frame-style-control--switch">
<span>展示模式</span>
<Switch v-model:checked="model.presentationMode" size="small" aria-label="展示模式" />
</label>
</div> </div>
</section> </section>
<section class="control-section"> <section class="control-section">

View File

@@ -26,6 +26,7 @@ export interface DemoControlPanelModel {
overlayMode: WaveformOverlayMode overlayMode: WaveformOverlayMode
showTooltip: boolean showTooltip: boolean
cleanView: boolean cleanView: boolean
presentationMode: boolean
selectedSeriesId: string selectedSeriesId: string
selectedLineStyle: WaveformLineStyle selectedLineStyle: WaveformLineStyle
lineStyleOptions: Array<SelectOption<WaveformLineStyle>> lineStyleOptions: Array<SelectOption<WaveformLineStyle>>
@@ -87,6 +88,7 @@ export interface DemoChartModel {
frameStyle: WaveformFrameStyle frameStyle: WaveformFrameStyle
axes: WaveformAxesOptions axes: WaveformAxesOptions
cleanView: boolean cleanView: boolean
presentationMode: boolean
showTooltip: boolean showTooltip: boolean
zeroLine: WaveformZeroLineOptions zeroLine: WaveformZeroLineOptions
frameWatermarkVisible: boolean frameWatermarkVisible: boolean