feat(waveform): 添加趋势图动态线宽和平移功能
- 实现趋势图按可见点数动态计算线宽,避免大数据量线条过粗 - 新增平移工具支持图表区域拖拽浏览 - 优化数据缩放事件处理,提升图表交互体验 - 添加线宽分档规则配置,支持不同数据量级的显示优化 - 移除原有的光标测量和峰值定位功能 - 更新图表点击事件为数据缩放事件处理
This commit is contained in:
@@ -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], () => {
|
||||
|
||||
Reference in New Issue
Block a user