feat(chart): add series styling and visibility controls
This commit is contained in:
85
src/components/interaction/WaveformTooltip.test.ts
Normal file
85
src/components/interaction/WaveformTooltip.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import WaveformTooltip from './WaveformTooltip.vue'
|
||||
|
||||
describe('WaveformTooltip', () => {
|
||||
const point = { x: 1, y: 12 }
|
||||
|
||||
function mountTooltip(positionX: number, containerWidth = 400) {
|
||||
return mount(WaveformTooltip, {
|
||||
props: {
|
||||
visible: true,
|
||||
position: { x: positionX, y: 100 },
|
||||
timeUnit: 's',
|
||||
hoveredPoint: point,
|
||||
seriesPoints: [{ trackIndex: 0, name: 'Temperature', color: '#f00', point }],
|
||||
containerWidth,
|
||||
containerHeight: 300,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
it('positions the tooltip to the right when enough space remains', () => {
|
||||
const tooltip = mountTooltip(100).get('.waveform-tooltip')
|
||||
|
||||
expect(tooltip.attributes('style')).toContain('left: 112px')
|
||||
expect(tooltip.attributes('style')).not.toContain('right:')
|
||||
})
|
||||
|
||||
it('flips the tooltip to the left near the right boundary', () => {
|
||||
const tooltip = mountTooltip(370).get('.waveform-tooltip')
|
||||
|
||||
expect(tooltip.attributes('style')).toContain('right: 42px')
|
||||
expect(tooltip.attributes('style')).not.toContain('left:')
|
||||
})
|
||||
|
||||
it('keeps the tooltip inside the left boundary when neither side has enough space', () => {
|
||||
const tooltip = mountTooltip(100, 200).get('.waveform-tooltip')
|
||||
|
||||
expect(tooltip.attributes('style')).toContain('left: 8px')
|
||||
expect(tooltip.attributes('style')).not.toContain('right:')
|
||||
})
|
||||
|
||||
it('shows resolved asymmetric errors beside the hovered value', () => {
|
||||
const pointWithErrors = { x: 1, y: 12, error: 1, upperError: 2 }
|
||||
const wrapper = mount(WaveformTooltip, {
|
||||
props: {
|
||||
visible: true,
|
||||
position: { x: 10, y: 10 },
|
||||
timeUnit: 's',
|
||||
hoveredPoint: pointWithErrors,
|
||||
seriesPoints: [
|
||||
{
|
||||
trackIndex: 0,
|
||||
name: '温度',
|
||||
color: '#f00',
|
||||
unit: 'C',
|
||||
point: pointWithErrors,
|
||||
},
|
||||
],
|
||||
containerWidth: 400,
|
||||
containerHeight: 300,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.get('.waveform-tooltip__series small').text()).toBe('(+2 / -1)')
|
||||
})
|
||||
|
||||
it('omits the error label when both resolved errors are zero', () => {
|
||||
const point = { x: 1, y: 12 }
|
||||
const wrapper = mount(WaveformTooltip, {
|
||||
props: {
|
||||
visible: true,
|
||||
position: { x: 10, y: 10 },
|
||||
timeUnit: 's',
|
||||
hoveredPoint: point,
|
||||
seriesPoints: [{ trackIndex: 0, name: '温度', color: '#f00', point }],
|
||||
containerWidth: 400,
|
||||
containerHeight: 300,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find('.waveform-tooltip__series small').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { resolveWaveformPointErrors } from '../../core'
|
||||
import { formatTooltipNumber, formatTooltipTime } from '../../utils'
|
||||
import type { WaveformPoint } from '../data/types'
|
||||
|
||||
@@ -30,15 +31,34 @@ interface Props {
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const tooltipGap = 12
|
||||
const containerPadding = 8
|
||||
const tooltipMaxWidth = 238
|
||||
|
||||
const tooltipStyle = computed(() => {
|
||||
if (!props.visible || !props.hoveredPoint) return { display: 'none' }
|
||||
|
||||
const estimatedHeight = 44 + props.seriesPoints.length * 22
|
||||
const rightPlacement = props.position.x + tooltipGap
|
||||
const leftPlacement = props.position.x - tooltipGap - tooltipMaxWidth
|
||||
const horizontalStyle =
|
||||
rightPlacement + tooltipMaxWidth <= props.containerWidth - containerPadding
|
||||
? { left: `${rightPlacement}px` }
|
||||
: leftPlacement >= containerPadding
|
||||
? { right: `${props.containerWidth - props.position.x + tooltipGap}px` }
|
||||
: { left: `${containerPadding}px` }
|
||||
|
||||
return {
|
||||
left: `${Math.min(props.position.x + 12, Math.max(8, props.containerWidth - 250))}px`,
|
||||
...horizontalStyle,
|
||||
top: `${Math.max(8, Math.min(props.position.y - 18, props.containerHeight - estimatedHeight - 8))}px`,
|
||||
}
|
||||
})
|
||||
|
||||
function formatError(point: WaveformPoint): string | null {
|
||||
const { lower, upper } = resolveWaveformPointErrors(point)
|
||||
if (lower === 0 && upper === 0) return null
|
||||
return `(+${formatTooltipNumber(upper)} / -${formatTooltipNumber(lower)})`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -60,6 +80,7 @@ const tooltipStyle = computed(() => {
|
||||
<span>
|
||||
{{ formatTooltipNumber(seriesPoint.point.y)
|
||||
}}{{ seriesPoint.unit ? ` ${seriesPoint.unit}` : '' }}
|
||||
<small v-if="formatError(seriesPoint.point)">{{ formatError(seriesPoint.point) }}</small>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -67,6 +88,7 @@ const tooltipStyle = computed(() => {
|
||||
|
||||
<style scoped>
|
||||
.waveform-tooltip {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
@@ -111,4 +133,9 @@ const tooltipStyle = computed(() => {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.waveform-tooltip__series small {
|
||||
color: #667085;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user