Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cbb1e7668 | ||
|
|
f97ff24cdf |
@@ -114,6 +114,7 @@ const data = ref<WaveformData>({
|
||||
| `frameNumber` | `string \| number` | 未设置 | 图框水印内容 |
|
||||
| `zeroLine` | `WaveformZeroLineOptions` | `{ visible: false }` | 零值参考线显隐与样式 |
|
||||
| `cleanView` | `boolean` | `false` | 仅保留波形的净图模式 |
|
||||
| `presentationMode` | `boolean` | `false` | 禁用绘图区交互的展示模式 |
|
||||
| `annotations` | `WaveformAnnotation[]` | `[]` | 受控标注数据 |
|
||||
| `annotationsVisible` | `boolean` | `true` | 标注图层显隐 |
|
||||
| `interactionMode` | `'zoom' \| 'annotation'` | `'zoom'` | 左键交互模式 |
|
||||
@@ -471,6 +472,14 @@ tooltip 仍然可用,切换回普通模式后原有配置和标注不会丢失
|
||||
`interactionMode` 可选 `zoom` 或 `annotation`,默认使用缩放模式。右键绘图区可直接打开
|
||||
标注编辑器,无需切换交互模式。`zoomable`、`pannable` 和 `showTooltip` 可分别控制缩放、
|
||||
空格拖拽平移和 tooltip;平移默认关闭。
|
||||
|
||||
展示场景可启用 `presentationMode`,统一禁用绘图区的 tooltip、缩放、平移、双击复位和
|
||||
标注交互。该模式不会隐藏任何图形内容,也不会禁用图例切换或分页;关闭后恢复原交互配置。
|
||||
|
||||
```vue
|
||||
<WaveformChart :data="chartData" :presentation-mode="true" />
|
||||
```
|
||||
|
||||
空数据或过滤后没有有效点时,组件会保留图框布局并显示“暂无有效波形数据”。
|
||||
|
||||
## 大数据渲染
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "waveform-analysis",
|
||||
"version": "0.1.22",
|
||||
"version": "0.1.24",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
@@ -14,6 +14,7 @@
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"README.md"
|
||||
],
|
||||
"sideEffects": [
|
||||
|
||||
@@ -92,19 +92,22 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
|
||||
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)
|
||||
await flushPromises()
|
||||
const chart = wrapper.getComponent(WaveformChart)
|
||||
|
||||
expect(chart.props('cleanView')).toBe(false)
|
||||
expect(chart.props('presentationMode')).toBe(false)
|
||||
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 flushPromises()
|
||||
|
||||
expect(chart.props('cleanView')).toBe(true)
|
||||
expect(chart.props('presentationMode')).toBe(true)
|
||||
expect(chart.props('zeroLine')).toMatchObject({ visible: true, color: '#98a2b3', width: 1 })
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
@@ -43,6 +43,7 @@ const yAxisLineVisible = ref(false)
|
||||
const annotations = ref<WaveformAnnotation[]>([])
|
||||
const annotationsVisible = ref(true)
|
||||
const cleanView = ref(false)
|
||||
const presentationMode = ref(false)
|
||||
const showTooltip = ref(true)
|
||||
const zeroLineVisible = ref(false)
|
||||
const zeroLineColor = ref('#98a2b3')
|
||||
@@ -289,6 +290,7 @@ const controlPanelModel = reactive({
|
||||
overlayMode,
|
||||
showTooltip,
|
||||
cleanView,
|
||||
presentationMode,
|
||||
selectedSeriesId,
|
||||
selectedLineStyle,
|
||||
lineStyleOptions,
|
||||
@@ -350,6 +352,7 @@ const chartModel = reactive({
|
||||
frameStyle,
|
||||
axes,
|
||||
cleanView,
|
||||
presentationMode,
|
||||
showTooltip,
|
||||
zeroLine,
|
||||
frameWatermarkVisible,
|
||||
|
||||
@@ -104,6 +104,11 @@
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.waveform-chart--presentation .waveform-chart__overlay {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.waveform-chart__label,
|
||||
.waveform-chart__empty {
|
||||
fill: #595959;
|
||||
|
||||
@@ -26,6 +26,7 @@ const props = withDefaults(defineProps<WaveformChartProps>(), {
|
||||
legend: () => ({ position: 'top-right', orientation: 'auto' }),
|
||||
defaultHiddenSeriesIds: () => [],
|
||||
cleanView: false,
|
||||
presentationMode: false,
|
||||
zeroLine: () => ({ visible: false }),
|
||||
})
|
||||
const emit = defineEmits<WaveformChartEmit>()
|
||||
|
||||
@@ -13,6 +13,7 @@ const {
|
||||
displayMode,
|
||||
activeInteractionMode,
|
||||
isCleanView,
|
||||
isPresentationMode,
|
||||
selection,
|
||||
containerStyle,
|
||||
overlayMode,
|
||||
@@ -45,7 +46,7 @@ const {
|
||||
beginSharedViewportDrag,
|
||||
finishViewportDrag,
|
||||
cancelViewportDrag,
|
||||
clearHover,
|
||||
handlePointerLeave,
|
||||
handleAnnotationClick,
|
||||
handleAnnotationContextMenu,
|
||||
resolveFrameNumber,
|
||||
@@ -107,12 +108,14 @@ const {
|
||||
`waveform-chart--interaction-${activeInteractionMode}`,
|
||||
{
|
||||
'waveform-chart--clean': isCleanView,
|
||||
'waveform-chart--presentation': isPresentationMode,
|
||||
'waveform-chart--panning': selection?.mode === 'pan',
|
||||
},
|
||||
]"
|
||||
:style="containerStyle"
|
||||
:data-display-mode="displayMode"
|
||||
:data-interaction-mode="activeInteractionMode"
|
||||
:data-presentation-mode="isPresentationMode"
|
||||
:data-overlay-mode="overlayMode"
|
||||
:data-chart-left-margin="resolvedChartLeftMargin"
|
||||
:data-title-area-height="titleAreaHeight"
|
||||
@@ -192,8 +195,8 @@ const {
|
||||
:ref="setSharedOverlayElement"
|
||||
class="waveform-chart__overlay waveform-chart__overlay--shared"
|
||||
:class="{
|
||||
'is-zoomable': zoomable && isZoomMode,
|
||||
'is-annotating': activeInteractionMode === 'annotation',
|
||||
'is-zoomable': !isPresentationMode && zoomable && isZoomMode,
|
||||
'is-annotating': !isPresentationMode && activeInteractionMode === 'annotation',
|
||||
}"
|
||||
:width="innerWidth"
|
||||
:height="innerHeight"
|
||||
@@ -201,7 +204,7 @@ const {
|
||||
@pointerdown="beginSharedViewportDrag"
|
||||
@pointerup="finishViewportDrag"
|
||||
@pointercancel="cancelViewportDrag"
|
||||
@pointerleave="clearHover"
|
||||
@pointerleave="handlePointerLeave"
|
||||
@click="handleAnnotationClick"
|
||||
@contextmenu="handleAnnotationContextMenu"
|
||||
/>
|
||||
@@ -214,6 +217,7 @@ const {
|
||||
:clip-path-id="clipPathId"
|
||||
:inner-width="innerWidth"
|
||||
:zoomable="zoomable"
|
||||
:interactive="!isPresentationMode"
|
||||
:display-mode="displayMode"
|
||||
:interaction-mode="activeInteractionMode"
|
||||
:frame-number="resolveFrameNumber(track.index)"
|
||||
@@ -227,7 +231,7 @@ const {
|
||||
@pointer-down="beginViewportDrag($event, track.index, true)"
|
||||
@pointer-up="finishViewportDrag"
|
||||
@pointer-cancel="cancelViewportDrag"
|
||||
@pointer-leave="clearHover"
|
||||
@pointer-leave="handlePointerLeave"
|
||||
@click="handleAnnotationClick($event, track.index)"
|
||||
@contextmenu="handleAnnotationContextMenu($event, track.index)"
|
||||
/>
|
||||
@@ -236,7 +240,7 @@ const {
|
||||
:state="hoverState"
|
||||
:tracks="trackLayouts"
|
||||
:clip-path-id="clipPathId"
|
||||
:visible="showTooltip"
|
||||
:visible="showTooltip && !isPresentationMode"
|
||||
/>
|
||||
|
||||
<rect
|
||||
@@ -253,6 +257,7 @@ const {
|
||||
v-if="!isCleanView"
|
||||
:annotations="renderedAnnotations"
|
||||
:visible="annotationsVisible"
|
||||
:interactive="!isPresentationMode"
|
||||
@contextmenu="handleExistingAnnotationContextMenu"
|
||||
@drag-start="beginAnnotationDrag"
|
||||
@move="handleAnnotationMove"
|
||||
@@ -318,7 +323,7 @@ const {
|
||||
/>
|
||||
|
||||
<WaveformAnnotationEditor
|
||||
v-if="editorDraft && !isCleanView"
|
||||
v-if="editorDraft && !isCleanView && !isPresentationMode"
|
||||
:annotation="editorDraft.annotation"
|
||||
:mode="editorDraft.mode"
|
||||
:series="editorSeries"
|
||||
@@ -330,7 +335,7 @@ const {
|
||||
/>
|
||||
|
||||
<WaveformAnnotationContextMenu
|
||||
v-if="!isCleanView"
|
||||
v-if="!isCleanView && !isPresentationMode"
|
||||
:visible="contextMenu !== null"
|
||||
:x="contextMenu?.x || 0"
|
||||
:y="contextMenu?.y || 0"
|
||||
@@ -342,7 +347,7 @@ const {
|
||||
|
||||
<WaveformHoverHost
|
||||
:state="hoverState"
|
||||
:visible="showTooltip"
|
||||
:visible="showTooltip && !isPresentationMode"
|
||||
:time-unit="timeUnit"
|
||||
:container-width="chartWidth"
|
||||
:container-height="chartHeight"
|
||||
|
||||
@@ -7,9 +7,12 @@ import { ANNOTATION_TEXT_FONT, ANNOTATION_TEXT_LINE_HEIGHT } from './markup'
|
||||
interface Props {
|
||||
annotations: RenderedAnnotation[]
|
||||
visible: boolean
|
||||
interactive?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
interactive: true,
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(event: 'contextmenu', annotationId: string, mouseEvent: MouseEvent): void
|
||||
(event: 'drag-start'): void
|
||||
@@ -79,7 +82,7 @@ function scheduleDragFrame() {
|
||||
}
|
||||
|
||||
function handlePointerDown(rendered: RenderedAnnotation, event: PointerEvent) {
|
||||
if (event.button !== 0 || dragState) return
|
||||
if (!props.interactive || event.button !== 0 || dragState) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const target = event.currentTarget as SVGGElement | null
|
||||
@@ -108,6 +111,7 @@ function handlePointerDown(rendered: RenderedAnnotation, event: PointerEvent) {
|
||||
}
|
||||
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
if (!props.interactive) return
|
||||
event.stopPropagation()
|
||||
if (!dragState || event.pointerId !== dragState.pointerId) return
|
||||
const deltaX = event.clientX - dragState.startX
|
||||
@@ -119,6 +123,7 @@ function handlePointerMove(event: PointerEvent) {
|
||||
}
|
||||
|
||||
function finishPointerDrag(event: PointerEvent) {
|
||||
if (!props.interactive) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (!dragState || event.pointerId !== dragState.pointerId) return
|
||||
@@ -181,6 +186,11 @@ function handlePointerCancel(event: PointerEvent) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (!dragState || event.pointerId !== dragState.pointerId) return
|
||||
cancelActiveDrag()
|
||||
}
|
||||
|
||||
function cancelActiveDrag() {
|
||||
if (!dragState) return
|
||||
const state = dragState
|
||||
dragState = null
|
||||
dragOffsets.value = new Map(dragOffsets.value).set(state.annotationId, { x: 0, y: 0 })
|
||||
@@ -188,10 +198,21 @@ function handlePointerCancel(event: PointerEvent) {
|
||||
cancelAnimationFrame(dragFrame)
|
||||
dragFrame = null
|
||||
}
|
||||
if (state.target.hasPointerCapture(state.pointerId)) {
|
||||
state.target.releasePointerCapture(state.pointerId)
|
||||
}
|
||||
emit('drag-end', true)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.interactive,
|
||||
(interactive) => {
|
||||
if (!interactive) cancelActiveDrag()
|
||||
},
|
||||
)
|
||||
|
||||
function handleContextMenu(annotationId: string, event: MouseEvent) {
|
||||
if (!props.interactive) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
// 使用时间戳比较:如果 contextmenu 在拖动结束后 100ms 内触发,则抑制
|
||||
@@ -206,7 +227,11 @@ function markerId(annotationId: string) {
|
||||
</script>
|
||||
|
||||
<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
|
||||
v-for="rendered in props.annotations"
|
||||
:key="rendered.annotation.id"
|
||||
@@ -281,13 +306,13 @@ function markerId(annotationId: string) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.waveform-annotation {
|
||||
.waveform-annotation-layer--interactive .waveform-annotation {
|
||||
pointer-events: auto;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.waveform-annotation:active {
|
||||
.waveform-annotation-layer--interactive .waveform-annotation:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
@@ -300,6 +325,9 @@ function markerId(annotationId: string) {
|
||||
.waveform-annotation__box {
|
||||
stroke-width: 1;
|
||||
rx: 3;
|
||||
}
|
||||
|
||||
.waveform-annotation-layer--interactive .waveform-annotation__box {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ interface AnnotationContext {
|
||||
titleAreaHeight: ComputedRef<number>
|
||||
chartTopMargin: ComputedRef<number>
|
||||
activeInteractionMode: ComputedRef<WaveformInteractionMode | undefined>
|
||||
isPresentationMode: ComputedRef<boolean>
|
||||
}
|
||||
|
||||
interface AnnotationCandidateContext {
|
||||
@@ -75,6 +76,7 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
|
||||
titleAreaHeight,
|
||||
chartTopMargin,
|
||||
activeInteractionMode,
|
||||
isPresentationMode,
|
||||
} = context
|
||||
|
||||
function toggleSeriesVisibility(seriesId: string) {
|
||||
@@ -140,6 +142,7 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
|
||||
}
|
||||
|
||||
function changeDraftSeries(seriesId: string) {
|
||||
if (isPresentationMode.value) return
|
||||
const draft = annotationInteraction.editorDraft.value
|
||||
const candidate = editorSeriesOptions.value.find((item) => item.seriesId === seriesId)
|
||||
const track = trackLayouts.value.find((item) =>
|
||||
@@ -216,7 +219,13 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
|
||||
}
|
||||
|
||||
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)
|
||||
if (!candidateContext) return
|
||||
const nearby = nearbyCandidates(candidateContext.candidates)
|
||||
@@ -238,7 +247,7 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
|
||||
}
|
||||
|
||||
function handleAnnotationContextMenu(event: MouseEvent, trackIndex?: number) {
|
||||
if (!props.annotationsVisible) return
|
||||
if (isPresentationMode.value || !props.annotationsVisible) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const candidateContext = resolveAnnotationCandidates(event, trackIndex)
|
||||
@@ -252,7 +261,7 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
|
||||
}
|
||||
|
||||
function handleExistingAnnotationContextMenu(annotationId: string, event: MouseEvent) {
|
||||
if (!props.annotationsVisible || !container.value) return
|
||||
if (isPresentationMode.value || !props.annotationsVisible || !container.value) return
|
||||
editorSeriesOptions.value = []
|
||||
const annotation = props.annotations.find((item) => item.id === annotationId)
|
||||
if (!annotation) return
|
||||
@@ -270,6 +279,7 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
|
||||
}
|
||||
|
||||
function handleAnnotationMove(annotationId: string, offsetX: number, offsetY: number) {
|
||||
if (isPresentationMode.value) return
|
||||
const annotation = props.annotations.find((item) => item.id === annotationId)
|
||||
if (!annotation || !Number.isFinite(offsetX) || !Number.isFinite(offsetY)) return
|
||||
emit(
|
||||
@@ -281,6 +291,7 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
|
||||
}
|
||||
|
||||
function editContextAnnotation() {
|
||||
if (isPresentationMode.value) return
|
||||
const menu = annotationInteraction.contextMenu.value
|
||||
const annotation = props.annotations.find((item) => item.id === menu?.annotationId)
|
||||
if (!annotation) return
|
||||
@@ -302,6 +313,7 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
|
||||
}
|
||||
|
||||
function deleteContextAnnotation() {
|
||||
if (isPresentationMode.value) return
|
||||
const annotationId = annotationInteraction.contextMenu.value?.annotationId
|
||||
const annotation = props.annotations.find((item) => item.id === annotationId)
|
||||
if (!annotation) return
|
||||
@@ -314,6 +326,7 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
|
||||
}
|
||||
|
||||
function confirmAnnotation(annotation: WaveformAnnotation) {
|
||||
if (isPresentationMode.value) return
|
||||
const draft = annotationInteraction.editorDraft.value
|
||||
if (!draft) return
|
||||
if (draft.mode === 'add') {
|
||||
|
||||
@@ -90,6 +90,7 @@ export function useWaveformChartController(
|
||||
chartWidth,
|
||||
chartHeight,
|
||||
isCleanView,
|
||||
isPresentationMode,
|
||||
hiddenSeriesIdSet,
|
||||
resolvedTitleText,
|
||||
titleAreaReserved,
|
||||
@@ -151,6 +152,7 @@ export function useWaveformChartController(
|
||||
innerHeight,
|
||||
hasChartArea,
|
||||
isZoomMode,
|
||||
isPresentationMode,
|
||||
resolveInitialTrackDomain,
|
||||
cancelPendingHover: () => hover.cancelPendingHover(),
|
||||
})
|
||||
@@ -174,6 +176,7 @@ export function useWaveformChartController(
|
||||
titleAreaHeight,
|
||||
chartTopMargin,
|
||||
activeInteractionMode,
|
||||
isPresentationMode,
|
||||
})
|
||||
|
||||
const viewport = useWaveformViewport({
|
||||
@@ -192,6 +195,7 @@ export function useWaveformChartController(
|
||||
sharedYDomains,
|
||||
independentYDomains,
|
||||
isZoomMode,
|
||||
isPresentationMode,
|
||||
editorSeriesOptions,
|
||||
resolveInitialTrackDomain,
|
||||
canZoomTrack: zoom.canZoomTrack,
|
||||
@@ -214,6 +218,7 @@ export function useWaveformChartController(
|
||||
titleAreaHeight,
|
||||
chartTopMargin,
|
||||
sharedOverlayElement,
|
||||
isPresentationMode,
|
||||
updateViewportDrag: viewport.updateViewportDrag,
|
||||
resolveTrackAtPointer: annotations.resolveTrackAtPointer,
|
||||
})
|
||||
@@ -248,10 +253,12 @@ export function useWaveformChartController(
|
||||
independentYDomains,
|
||||
annotationInteraction,
|
||||
editorSeriesOptions,
|
||||
isPresentationMode,
|
||||
clearHover: hover.clearHover,
|
||||
cancelAnnotation: annotations.cancelAnnotation,
|
||||
configureZoom: zoom.configureZoom,
|
||||
resetViewport: viewport.resetViewport,
|
||||
cancelViewportDrag: viewport.cancelViewportDrag,
|
||||
cancelPendingHover: hover.cancelPendingHover,
|
||||
clearZoomBindings: zoom.clearZoomBindings,
|
||||
})
|
||||
|
||||
@@ -44,10 +44,12 @@ interface LifecycleContext {
|
||||
independentYDomains: Ref<Record<number, [number, number]>>
|
||||
annotationInteraction: ReturnType<typeof useWaveformAnnotationInteraction>
|
||||
editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>
|
||||
isPresentationMode: ComputedRef<boolean>
|
||||
clearHover: () => void
|
||||
cancelAnnotation: () => void
|
||||
configureZoom: () => void
|
||||
resetViewport: () => void
|
||||
cancelViewportDrag: () => void
|
||||
cancelPendingHover: () => void
|
||||
clearZoomBindings: () => void
|
||||
}
|
||||
@@ -94,16 +96,25 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
||||
independentYDomains,
|
||||
annotationInteraction,
|
||||
editorSeriesOptions,
|
||||
isPresentationMode,
|
||||
clearHover,
|
||||
cancelAnnotation,
|
||||
configureZoom,
|
||||
resetViewport,
|
||||
cancelViewportDrag,
|
||||
cancelPendingHover,
|
||||
clearZoomBindings,
|
||||
} = context
|
||||
|
||||
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
|
||||
spacePressed.value = true
|
||||
event.preventDefault()
|
||||
@@ -145,6 +156,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
||||
innerWidth,
|
||||
innerHeight,
|
||||
() => props.zoomable,
|
||||
isPresentationMode,
|
||||
() => props.minZoomSpan,
|
||||
() => props.initialXDomain,
|
||||
() => props.initialXDomains,
|
||||
@@ -191,6 +203,16 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
||||
editorSeriesOptions.value = []
|
||||
})
|
||||
|
||||
watch(isPresentationMode, (enabled) => {
|
||||
spacePressed.value = false
|
||||
if (!enabled) return
|
||||
cancelViewportDrag()
|
||||
clearZoomBindings()
|
||||
clearHover()
|
||||
annotationInteraction.closeContextMenu()
|
||||
cancelAnnotation()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => chartSeries.value.map((series) => series.id).join('\u0000'),
|
||||
() => {
|
||||
|
||||
@@ -49,6 +49,7 @@ export function useWaveformPresentation(context: PresentationContext) {
|
||||
height: fixedHeight.value === undefined ? '100%' : `${fixedHeight.value}px`,
|
||||
}))
|
||||
const isCleanView = computed(() => props.cleanView === true)
|
||||
const isPresentationMode = computed(() => props.presentationMode === true)
|
||||
const resolvedZeroLine = computed(() => {
|
||||
const width = props.zeroLine.width
|
||||
return {
|
||||
@@ -178,6 +179,7 @@ export function useWaveformPresentation(context: PresentationContext) {
|
||||
chartHeight,
|
||||
containerStyle,
|
||||
isCleanView,
|
||||
isPresentationMode,
|
||||
resolvedZeroLine,
|
||||
legendBackgroundColor,
|
||||
legendInteractive,
|
||||
|
||||
@@ -45,6 +45,7 @@ export interface WaveformChartProps {
|
||||
hiddenSeriesIds?: string[]
|
||||
defaultHiddenSeriesIds?: string[]
|
||||
cleanView?: boolean
|
||||
presentationMode?: boolean
|
||||
zeroLine?: WaveformZeroLineOptions
|
||||
}
|
||||
|
||||
@@ -65,6 +66,7 @@ type DefaultedProp =
|
||||
| 'legend'
|
||||
| 'defaultHiddenSeriesIds'
|
||||
| 'cleanView'
|
||||
| 'presentationMode'
|
||||
| 'zeroLine'
|
||||
|
||||
export type ResolvedWaveformChartProps = Readonly<
|
||||
|
||||
@@ -23,6 +23,7 @@ interface HoverContext {
|
||||
titleAreaHeight: ComputedRef<number>
|
||||
chartTopMargin: ComputedRef<number>
|
||||
sharedOverlayElement: ShallowRef<SVGRectElement | undefined>
|
||||
isPresentationMode: ComputedRef<boolean>
|
||||
updateViewportDrag: (event: PointerEvent) => void
|
||||
resolveTrackAtPointer: (pointerX: number, pointerY: number) => TrackLayout | undefined
|
||||
}
|
||||
@@ -42,6 +43,7 @@ export function useWaveformHover(context: HoverContext) {
|
||||
titleAreaHeight,
|
||||
chartTopMargin,
|
||||
sharedOverlayElement,
|
||||
isPresentationMode,
|
||||
updateViewportDrag,
|
||||
resolveTrackAtPointer,
|
||||
} = context
|
||||
@@ -62,6 +64,7 @@ export function useWaveformHover(context: HoverContext) {
|
||||
trackIndex: number | null,
|
||||
position: { x: number; y: number },
|
||||
) => {
|
||||
if (isPresentationMode.value) return
|
||||
if (!hoveredPointsMatch(nextPoints)) hoverState.points = nextPoints
|
||||
hoverState.trackIndex = trackIndex
|
||||
hoverState.position = position
|
||||
@@ -73,6 +76,9 @@ export function useWaveformHover(context: HoverContext) {
|
||||
hoverState.trackIndex = null
|
||||
emit('point-hover', null)
|
||||
}
|
||||
const handlePointerLeave = () => {
|
||||
if (!isPresentationMode.value) clearHover()
|
||||
}
|
||||
const createHoveredSeriesPoint = (
|
||||
series: DisplaySeries,
|
||||
trackIndex: number,
|
||||
@@ -103,6 +109,7 @@ export function useWaveformHover(context: HoverContext) {
|
||||
series.points[pointBisector.center(series.points, xValue)]
|
||||
|
||||
const handleIndependentPointerMove = (event: PointerEvent, trackIndex: number) => {
|
||||
if (isPresentationMode.value) return
|
||||
if (selection.value?.trackIndex === trackIndex && selection.value.independent) {
|
||||
updateViewportDrag(event)
|
||||
return
|
||||
@@ -128,6 +135,7 @@ export function useWaveformHover(context: HoverContext) {
|
||||
})
|
||||
}
|
||||
const handleSharedPointerMove = (event: PointerEvent) => {
|
||||
if (isPresentationMode.value) return
|
||||
if (selection.value?.overlay === event.currentTarget) {
|
||||
updateViewportDrag(event)
|
||||
return
|
||||
@@ -161,6 +169,7 @@ export function useWaveformHover(context: HoverContext) {
|
||||
return {
|
||||
cancelPendingHover,
|
||||
clearHover,
|
||||
handlePointerLeave,
|
||||
beginAnnotationDrag,
|
||||
endAnnotationDrag,
|
||||
handleIndependentPointerMove,
|
||||
|
||||
@@ -26,6 +26,7 @@ interface ViewportContext {
|
||||
sharedYDomains: Ref<Record<string, [number, number]>>
|
||||
independentYDomains: Ref<Record<number, [number, number]>>
|
||||
isZoomMode: ComputedRef<boolean>
|
||||
isPresentationMode: ComputedRef<boolean>
|
||||
editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>
|
||||
resolveInitialTrackDomain: (track: TrackLayout) => [number, number]
|
||||
canZoomTrack: (track: TrackLayout) => boolean
|
||||
@@ -53,6 +54,7 @@ export function useWaveformViewport(context: ViewportContext) {
|
||||
sharedYDomains,
|
||||
independentYDomains,
|
||||
isZoomMode,
|
||||
isPresentationMode,
|
||||
editorSeriesOptions,
|
||||
resolveInitialTrackDomain,
|
||||
canZoomTrack,
|
||||
@@ -131,6 +133,7 @@ export function useWaveformViewport(context: ViewportContext) {
|
||||
]),
|
||||
)
|
||||
const beginViewportDrag = (event: PointerEvent, trackIndex: number, independent: boolean) => {
|
||||
if (isPresentationMode.value) return
|
||||
const panRequested = props.pannable && spacePressed.value
|
||||
if ((!props.zoomable && !panRequested) || !isZoomMode.value || event.button !== 0) return
|
||||
const overlay = event.currentTarget as SVGRectElement
|
||||
@@ -203,6 +206,7 @@ export function useWaveformViewport(context: ViewportContext) {
|
||||
emit('zoom-change', nextX)
|
||||
}
|
||||
const updateViewportDrag = (event: PointerEvent) => {
|
||||
if (isPresentationMode.value) return
|
||||
const active = selection.value
|
||||
if (!active || event.pointerId !== active.pointerId) return
|
||||
const track = trackLayouts.value.find((item) => item.index === active.trackIndex)
|
||||
@@ -285,6 +289,10 @@ export function useWaveformViewport(context: ViewportContext) {
|
||||
void nextTick(configureZoom)
|
||||
}
|
||||
const finishViewportDrag = (event: PointerEvent) => {
|
||||
if (isPresentationMode.value) {
|
||||
cancelViewportDrag()
|
||||
return
|
||||
}
|
||||
const active = selection.value
|
||||
if (!active || event.pointerId !== active.pointerId) return
|
||||
updateViewportDrag(event)
|
||||
@@ -319,7 +327,7 @@ export function useWaveformViewport(context: ViewportContext) {
|
||||
void nextTick(configureZoom)
|
||||
}
|
||||
const requestViewportReset = (event: MouseEvent) => {
|
||||
if (!props.zoomable || !isZoomMode.value) return
|
||||
if (isPresentationMode.value || !props.zoomable || !isZoomMode.value) return
|
||||
event.preventDefault()
|
||||
resetViewport()
|
||||
emit('zoom-reset')
|
||||
|
||||
@@ -29,6 +29,7 @@ interface ZoomContext {
|
||||
innerHeight: ComputedRef<number>
|
||||
hasChartArea: ComputedRef<boolean>
|
||||
isZoomMode: ComputedRef<boolean>
|
||||
isPresentationMode: ComputedRef<boolean>
|
||||
resolveInitialTrackDomain: (track: TrackLayout) => [number, number]
|
||||
cancelPendingHover: () => void
|
||||
}
|
||||
@@ -50,6 +51,7 @@ export function useWaveformZoom(context: ZoomContext) {
|
||||
innerHeight,
|
||||
hasChartArea,
|
||||
isZoomMode,
|
||||
isPresentationMode,
|
||||
resolveInitialTrackDomain,
|
||||
cancelPendingHover,
|
||||
} = context
|
||||
@@ -67,7 +69,7 @@ export function useWaveformZoom(context: ZoomContext) {
|
||||
|
||||
const scheduleZoomCommit = () => zoomThrottle.schedule(commitPendingZoom)
|
||||
const handleSharedZoom = (event: D3ZoomEvent<SVGRectElement, unknown>) => {
|
||||
if (synchronizingZoomTransform) return
|
||||
if (synchronizingZoomTransform || isPresentationMode.value) return
|
||||
cancelPendingHover()
|
||||
pendingSharedZoomTransform = event.transform
|
||||
pendingSharedZoomGesture = 'wheel'
|
||||
@@ -78,7 +80,7 @@ export function useWaveformZoom(context: ZoomContext) {
|
||||
event: D3ZoomEvent<SVGRectElement, unknown>,
|
||||
trackIndex: number,
|
||||
) => {
|
||||
if (synchronizingZoomTransform) return
|
||||
if (synchronizingZoomTransform || isPresentationMode.value) return
|
||||
cancelPendingHover()
|
||||
pendingIndependentZoomTransforms.set(trackIndex, event.transform)
|
||||
pendingIndependentZoomGestures.set(trackIndex, 'wheel')
|
||||
@@ -86,6 +88,10 @@ export function useWaveformZoom(context: ZoomContext) {
|
||||
scheduleWheelZoomEnd()
|
||||
}
|
||||
function commitPendingZoom() {
|
||||
if (isPresentationMode.value) {
|
||||
cancelPendingZoom()
|
||||
return
|
||||
}
|
||||
if (pendingSharedZoomTransform) {
|
||||
const transform = pendingSharedZoomTransform
|
||||
pendingSharedZoomTransform = null
|
||||
@@ -233,7 +239,13 @@ export function useWaveformZoom(context: ZoomContext) {
|
||||
}
|
||||
const configureZoom = () => {
|
||||
clearZoomBindings()
|
||||
if (!props.zoomable || !isZoomMode.value || !hasChartArea.value || !trackLayouts.value.length) {
|
||||
if (
|
||||
isPresentationMode.value ||
|
||||
!props.zoomable ||
|
||||
!isZoomMode.value ||
|
||||
!hasChartArea.value ||
|
||||
!trackLayouts.value.length
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (props.displayMode === 'independent') {
|
||||
|
||||
@@ -70,6 +70,11 @@
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.waveform-track__overlay.is-presentation {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.waveform-track__zero-line {
|
||||
fill: none;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -13,6 +13,7 @@ interface Props {
|
||||
clipPathId: string
|
||||
innerWidth: number
|
||||
zoomable: boolean
|
||||
interactive?: boolean
|
||||
displayMode: WaveformDisplayMode
|
||||
interactionMode?: WaveformInteractionMode
|
||||
frameNumber?: string | number
|
||||
@@ -38,6 +39,7 @@ interface Emits {
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
interactionMode: 'zoom',
|
||||
interactive: true,
|
||||
cleanView: false,
|
||||
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'"
|
||||
class="waveform-track__overlay waveform-track__overlay--independent waveform-chart__overlay waveform-chart__overlay--independent"
|
||||
:class="{
|
||||
'is-zoomable': zoomable && interactionMode === 'zoom',
|
||||
'is-annotating': interactionMode === 'annotation',
|
||||
'is-zoomable': interactive && zoomable && interactionMode === 'zoom',
|
||||
'is-annotating': interactive && interactionMode === 'annotation',
|
||||
'is-presentation': !interactive,
|
||||
}"
|
||||
:data-interactive="interactive"
|
||||
:data-independent-overlay-index="track.index"
|
||||
:width="track.width ?? innerWidth"
|
||||
:height="track.height"
|
||||
|
||||
306
src/components/waveformChartCases/presentationMode.test.ts
Normal file
306
src/components/waveformChartCases/presentationMode.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
@@ -43,6 +43,7 @@ defineExpose({ resetViewport })
|
||||
:frame-style="model.frameStyle"
|
||||
:axes="model.axes"
|
||||
:clean-view="model.cleanView"
|
||||
:presentation-mode="model.presentationMode"
|
||||
:show-tooltip="model.showTooltip"
|
||||
:zero-line="model.zeroLine"
|
||||
:frame-number="model.frameWatermarkVisible ? 1 : undefined"
|
||||
|
||||
@@ -46,6 +46,10 @@ const model = defineModel<DemoControlPanelModel>('model', { required: true })
|
||||
<span>净图</span>
|
||||
<Switch v-model:checked="model.cleanView" size="small" aria-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>
|
||||
</section>
|
||||
<section class="control-section">
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface DemoControlPanelModel {
|
||||
overlayMode: WaveformOverlayMode
|
||||
showTooltip: boolean
|
||||
cleanView: boolean
|
||||
presentationMode: boolean
|
||||
selectedSeriesId: string
|
||||
selectedLineStyle: WaveformLineStyle
|
||||
lineStyleOptions: Array<SelectOption<WaveformLineStyle>>
|
||||
@@ -87,6 +88,7 @@ export interface DemoChartModel {
|
||||
frameStyle: WaveformFrameStyle
|
||||
axes: WaveformAxesOptions
|
||||
cleanView: boolean
|
||||
presentationMode: boolean
|
||||
showTooltip: boolean
|
||||
zeroLine: WaveformZeroLineOptions
|
||||
frameWatermarkVisible: boolean
|
||||
|
||||
Reference in New Issue
Block a user