feat(waveform): 添加趋势图动态线宽和平移功能

- 实现趋势图按可见点数动态计算线宽,避免大数据量线条过粗
- 新增平移工具支持图表区域拖拽浏览
- 优化数据缩放事件处理,提升图表交互体验
- 添加线宽分档规则配置,支持不同数据量级的显示优化
- 移除原有的光标测量和峰值定位功能
- 更新图表点击事件为数据缩放事件处理
This commit is contained in:
2026-05-07 11:16:51 +08:00
parent 2babe9d99d
commit 32f324909d
6 changed files with 235 additions and 168 deletions

View File

@@ -28,9 +28,10 @@
:is-all-channels-active="isAllChannelsActive"
:last-parse-error-message="lastParseErrorMessage"
:active-trend-tool-states="activeTrendToolStates"
:disabled-trend-tool-states="disabledTrendToolStates"
@update:active-trend-tab="activeTrendTab = $event"
@trend-tool-action="handleTrendToolAction"
@chart-click="handleTrendChartClick"
@chart-data-zoom="handleTrendChartDataZoom"
/>
<WaveformInfoPanel
@@ -62,7 +63,7 @@ import type {
LabelValueOption,
SingleChannelTrendOption,
SummaryItem,
TrendChartClickPayload,
TrendChartZoomPayload,
TrendToolAction,
TrendTabValue,
ValueMode,
@@ -96,12 +97,6 @@ interface TrendZoomRange {
end: number
}
interface TrendMeasureCursor {
timeLabel: string
value: number
seriesName: string
}
const activeTrendTab = ref<TrendTabValue>('instant')
const activeValueMode = ref<ValueMode>('primary')
const activeDisplayMode = ref<DisplayMode>('single-channel')
@@ -119,9 +114,6 @@ const waveformFileAccept = '.cfg,.dat'
const trendXZoomRange = ref<TrendZoomRange>({ start: 0, end: 100 })
const trendYZoomScale = ref(1)
const activeTrendInteractionMode = ref<'none' | 'box-zoom' | 'pan'>('none')
const isMeasureModeActive = ref(false)
const isPeakVisible = ref(false)
const measureCursors = ref<TrendMeasureCursor[]>([])
const trendTabs: LabelValueOption<TrendTabValue>[] = [
{ value: 'instant', label: '瞬时波形' },
@@ -353,11 +345,19 @@ const hasWaveformData = computed(() => {
return activeTrendPayload.value.series.length > 0
})
const canPanTrendChart = computed(() => {
const { start, end } = trendXZoomRange.value
return hasWaveformData.value && (start > 0 || end < 100)
})
const activeTrendToolStates = computed<Partial<Record<TrendToolAction, boolean>>>(() => ({
'box-zoom': activeTrendInteractionMode.value === 'box-zoom',
pan: activeTrendInteractionMode.value === 'pan',
measure: isMeasureModeActive.value,
peak: isPeakVisible.value
pan: activeTrendInteractionMode.value === 'pan'
}))
const disabledTrendToolStates = computed<Partial<Record<TrendToolAction, boolean>>>(() => ({
pan: !canPanTrendChart.value
}))
const TREND_AXIS_EXPAND_RATIO = 1.2
@@ -375,6 +375,7 @@ const TREND_GRID_BOTTOM = {
withTimeAxis: '30px',
withoutTimeAxis: '6px'
}
const TREND_LINE_MAX_WIDTH = 1.6
const getAxisPrecision = (step: number) => {
const absStep = Math.abs(step)
@@ -430,13 +431,32 @@ const formatAxisLabel = (value: number, precision: number) => {
const clampPercent = (value: number) => Math.min(Math.max(value, 0), 100)
// 趋势图按当前可见点数调整线宽,避免大数据初始展示时线条过粗。
const resolveTrendVisiblePointCount = (pointCount: number) => {
const { start, end } = trendXZoomRange.value
const visibleRatio = Math.max((end - start) / 100, 0)
return Math.ceil(pointCount * visibleRatio)
}
const resolveTrendLineWidth = (pointCount: number) => {
if (pointCount >= 200000) return 0.35
if (pointCount >= 100000) return 0.45
if (pointCount >= 50000) return 0.55
if (pointCount >= 20000) return 0.65
if (pointCount >= 10000) return 0.75
if (pointCount >= 5000) return 0.9
if (pointCount >= 2000) return 1
if (pointCount >= 800) return 1.2
if (pointCount >= 200) return 1.4
return TREND_LINE_MAX_WIDTH
}
const resetTrendToolState = () => {
trendXZoomRange.value = { start: 0, end: 100 }
trendYZoomScale.value = 1
activeTrendInteractionMode.value = 'none'
isMeasureModeActive.value = false
isPeakVisible.value = false
measureCursors.value = []
}
const zoomTrendXAxis = (ratio: number) => {
@@ -594,7 +614,8 @@ const buildTrendAxisConfig = (trendPayload: WaveformTrendPayload, splitCount = T
let axisMax = resolveExpandedAxisBoundary(max, false)
if (shouldUseBalancedAxisBoundary(min, max)) {
const axisBoundary = roundAxisValueUp(Math.max(Math.abs(min), Math.abs(max)))
// 正负幅值接近时,对称边界必须基于已向外扩展后的范围,避免把峰值贴到坐标边界。
const axisBoundary = roundAxisValueUp(Math.max(Math.abs(axisMin), Math.abs(axisMax)))
axisMin = -axisBoundary
axisMax = axisBoundary
}
@@ -643,91 +664,20 @@ const buildTrendAxisConfig = (trendPayload: WaveformTrendPayload, splitCount = T
}
}
const buildPeakMarkPointData = (trendPayload: WaveformTrendPayload, item: WaveformSeriesItem) => {
if (!isPeakVisible.value || !item.data.length) return []
const maxValue = Math.max(...item.data)
const minValue = Math.min(...item.data)
const maxIndex = item.data.findIndex(value => value === maxValue)
const minIndex = item.data.findIndex(value => value === minValue)
const buildPeakItem = (name: string, index: number, value: number) => ({
name,
coord: [trendPayload.timeLabels[index], value],
value: formatNumber(value),
symbol: 'pin',
symbolSize: 36,
label: {
formatter: name
}
})
return [
...(maxIndex >= 0 ? [buildPeakItem('最大值', maxIndex, maxValue)] : []),
...(minIndex >= 0 && minIndex !== maxIndex ? [buildPeakItem('最小值', minIndex, minValue)] : [])
]
}
const buildMeasureMarkLine = (unit: string) => {
if (measureCursors.value.length < 2) return null
const [firstCursor, secondCursor] = measureCursors.value
const deltaTime = Number(secondCursor.timeLabel) - Number(firstCursor.timeLabel)
const deltaValue = secondCursor.value - firstCursor.value
const valueText = unit ? `${formatNumber(deltaValue)} ${unit}` : formatNumber(deltaValue)
return {
silent: true,
symbol: ['none', 'none'],
lineStyle: {
color: '#e6a23c',
type: 'dashed',
width: 1
},
label: {
show: true,
formatter: `Δt ${formatNumber(deltaTime, 2)} ms / ΔY ${valueText}`,
color: '#e6a23c',
position: 'insideEndTop'
},
data: [{ xAxis: firstCursor.timeLabel }, { xAxis: secondCursor.timeLabel }]
}
}
const buildTrendSeries = (
trendPayload: WaveformTrendPayload,
seriesList: WaveformSeriesItem[],
yAxisConfig: Record<string, unknown>,
showTimeAxis: boolean
) => {
const measureMarkLine = showTimeAxis ? buildMeasureMarkLine(trendPayload.unit) : null
return seriesList.map((item, index) => {
const markPointData = buildPeakMarkPointData(trendPayload, item)
const buildTrendSeries = (seriesList: WaveformSeriesItem[]) => {
return seriesList.map(item => {
const visiblePointCount = resolveTrendVisiblePointCount(item.data.length)
return {
name: item.name,
type: 'line',
smooth: true,
symbol: isMeasureModeActive.value || isPeakVisible.value ? 'circle' : 'none',
symbol: 'none',
symbolSize: 3,
data: item.data,
...(markPointData.length
? {
markPoint: {
symbolSize: 12,
itemStyle: {
color: '#f56c6c',
borderColor: '#f56c6c'
},
label: {
color: '#fff',
fontSize: 10
},
data: markPointData
}
}
: null),
...(measureMarkLine && index === 0 ? { markLine: measureMarkLine } : null)
lineStyle: {
width: resolveTrendLineWidth(visiblePointCount)
},
data: item.data
}
})
}
@@ -853,7 +803,7 @@ const buildTrendChartOptions = (
}
],
color: chartColors,
series: buildTrendSeries(trendPayload, seriesList, yAxisConfig, showTimeAxis)
series: buildTrendSeries(seriesList)
}
}
@@ -875,15 +825,26 @@ const buildSingleChannelTrendOptionsList = (
// 单通道下每张图只保留一个 series需要单独指定对应相色。
return trendPayload.series.map((item, index) => {
const showTimeAxis = resolveShowTimeAxis(index, trendPayload.series.length)
const itemTrendPayload = {
...trendPayload,
min: item.data.length ? Math.min(...item.data) : undefined,
max: item.data.length ? Math.max(...item.data) : undefined,
series: [item]
}
return {
key: `${detailIndex}-${item.name}`,
group: chartGroup,
isLastChart: showTimeAxis,
options: buildTrendChartOptions(trendPayload, [item], [trendColors[index] || defaultPhaseColors[index]], {
showTimeAxis,
yAxisSplitCount: TREND_AXIS_COMPACT_SPLIT_COUNT
})
options: buildTrendChartOptions(
itemTrendPayload,
itemTrendPayload.series,
[trendColors[index] || defaultPhaseColors[index]],
{
showTimeAxis,
yAxisSplitCount: TREND_AXIS_COMPACT_SPLIT_COUNT
}
)
}
})
}
@@ -977,18 +938,16 @@ const handleTrendToolAction = async (action: TrendToolAction) => {
activeTrendInteractionMode.value = activeTrendInteractionMode.value === 'box-zoom' ? 'none' : 'box-zoom'
break
case 'pan':
if (!canPanTrendChart.value) {
ElMessage.info('请先放大 X 轴或框选局部区域后再平移')
activeTrendInteractionMode.value = 'none'
break
}
activeTrendInteractionMode.value = activeTrendInteractionMode.value === 'pan' ? 'none' : 'pan'
break
case 'reset':
resetTrendToolState()
break
case 'measure':
isMeasureModeActive.value = !isMeasureModeActive.value
measureCursors.value = []
break
case 'peak':
isPeakVisible.value = !isPeakVisible.value
break
case 'download-image':
await downloadTrendImage()
break
@@ -1000,16 +959,15 @@ const handleTrendToolAction = async (action: TrendToolAction) => {
}
}
const handleTrendChartClick = (value: TrendChartClickPayload) => {
if (!isMeasureModeActive.value) return
const nextCursor = {
timeLabel: value.timeLabel,
value: value.value,
seriesName: value.seriesName
const handleTrendChartDataZoom = (value: TrendChartZoomPayload) => {
trendXZoomRange.value = {
start: clampPercent(value.start),
end: clampPercent(value.end)
}
measureCursors.value = measureCursors.value.length >= 2 ? [nextCursor] : [...measureCursors.value, nextCursor]
if (!canPanTrendChart.value && activeTrendInteractionMode.value === 'pan') {
activeTrendInteractionMode.value = 'none'
}
}
watch([activeTrendTab, activeValueMode, activeDisplayMode, activeChannelIndex], () => {