feat(waveform): 添加趋势图动态线宽和平移功能
- 实现趋势图按可见点数动态计算线宽,避免大数据量线条过粗 - 新增平移工具支持图表区域拖拽浏览 - 优化数据缩放事件处理,提升图表交互体验 - 添加线宽分档规则配置,支持不同数据量级的显示优化 - 移除原有的光标测量和峰值定位功能 - 更新图表点击事件为数据缩放事件处理
This commit is contained in:
@@ -85,3 +85,11 @@ PR 应包含:
|
||||
- 验证多图趋势图时,至少检查单通道拆分图和全部通道列表图两种场景;判断标准是多张图左侧坐标轴竖线形成同一条垂直线,底部横坐标标签不遮挡、不贴线。
|
||||
|
||||
|
||||
## 趋势图线宽显示规则
|
||||
涉及 waveform 或其他线形趋势图时,线宽应按当前可见点数动态计算,避免初始化大数据量时线条糊成一片,也避免放大后线条过细。
|
||||
|
||||
- 当前可见点数按横向缩放范围计算:`ceil(seriesDataLength * ((dataZoom.end - dataZoom.start) / 100))`。
|
||||
- 初始全量展示时,点数越多线越细;横向放大后可见点数减少,线宽可逐步变粗;横向缩小或重置后线宽恢复到对应细线档位。
|
||||
- Y 轴缩放、测量模式、峰值显示不改变主线线宽,避免状态切换造成额外视觉跳动。
|
||||
- 主线最大线宽不得超过 `1.6`。
|
||||
- 线宽分档统一为:`>= 200000` 使用 `0.35`,`100000 - 199999` 使用 `0.45`,`50000 - 99999` 使用 `0.55`,`20000 - 49999` 使用 `0.65`,`10000 - 19999` 使用 `0.75`,`5000 - 9999` 使用 `0.9`,`2000 - 4999` 使用 `1`,`800 - 1999` 使用 `1.2`,`200 - 799` 使用 `1.4`,`< 200` 使用 `1.6`。
|
||||
|
||||
@@ -12,6 +12,10 @@ import * as echarts from 'echarts' // 全引入
|
||||
// import 'echarts-liquidfill'
|
||||
// import 'echarts/lib/component/dataZoom'
|
||||
|
||||
defineOptions({
|
||||
name: 'LineChart'
|
||||
})
|
||||
|
||||
const color = [
|
||||
'var(--el-color-primary)',
|
||||
'#07CCCA',
|
||||
@@ -30,15 +34,72 @@ const chartRef = ref<HTMLDivElement>()
|
||||
|
||||
const props = defineProps(['options', 'isInterVal', 'pieInterVal', 'group'])
|
||||
const emit = defineEmits<{
|
||||
'chart-click': [
|
||||
'chart-data-zoom': [
|
||||
value: {
|
||||
timeLabel: string
|
||||
value: number
|
||||
seriesName: string
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
]
|
||||
}>()
|
||||
let chart: echarts.ECharts | any = null
|
||||
let isPanPointerDown = false
|
||||
|
||||
const getChartViewportRoot = () => chart?.getZr()?.painter?.getViewportRoot?.() as HTMLElement | undefined
|
||||
|
||||
const resetChartCursor = () => {
|
||||
const viewportRoot = getChartViewportRoot()
|
||||
if (viewportRoot) viewportRoot.style.cursor = ''
|
||||
isPanPointerDown = false
|
||||
}
|
||||
|
||||
const updatePanCursor = (event: { offsetX: number; offsetY: number }) => {
|
||||
const viewportRoot = getChartViewportRoot()
|
||||
|
||||
if (!viewportRoot || props.options?.activeTool !== 'pan') {
|
||||
resetChartCursor()
|
||||
return
|
||||
}
|
||||
|
||||
// 平移只在图形绘图区内生效,鼠标样式同步限制到同一范围,避免坐标轴和空白区误导操作。
|
||||
const isInGrid = chart?.containPixel?.({ gridIndex: 0 }, [event.offsetX, event.offsetY])
|
||||
viewportRoot.style.cursor = isInGrid ? (isPanPointerDown ? 'grabbing' : 'grab') : ''
|
||||
}
|
||||
|
||||
const bindPanCursorEvents = () => {
|
||||
const zr = chart?.getZr?.()
|
||||
if (!zr) return
|
||||
|
||||
zr.off('mousemove', updatePanCursor)
|
||||
zr.off('mousedown', handlePanCursorMouseDown)
|
||||
zr.off('mouseup', handlePanCursorMouseUp)
|
||||
zr.off('globalout', resetChartCursor)
|
||||
zr.on('mousemove', updatePanCursor)
|
||||
zr.on('mousedown', handlePanCursorMouseDown)
|
||||
zr.on('mouseup', handlePanCursorMouseUp)
|
||||
zr.on('globalout', resetChartCursor)
|
||||
}
|
||||
|
||||
const unbindPanCursorEvents = () => {
|
||||
const zr = chart?.getZr?.()
|
||||
if (!zr) return
|
||||
|
||||
zr.off('mousemove', updatePanCursor)
|
||||
zr.off('mousedown', handlePanCursorMouseDown)
|
||||
zr.off('mouseup', handlePanCursorMouseUp)
|
||||
zr.off('globalout', resetChartCursor)
|
||||
resetChartCursor()
|
||||
}
|
||||
|
||||
function handlePanCursorMouseDown(event: { offsetX: number; offsetY: number }) {
|
||||
isPanPointerDown = true
|
||||
updatePanCursor(event)
|
||||
}
|
||||
|
||||
function handlePanCursorMouseUp(event: { offsetX: number; offsetY: number }) {
|
||||
isPanPointerDown = false
|
||||
updatePanCursor(event)
|
||||
}
|
||||
|
||||
const resizeHandler = () => {
|
||||
// 不在视野中的时候不进行resize
|
||||
if (!chartRef.value) return
|
||||
@@ -51,6 +112,7 @@ const resizeHandler = () => {
|
||||
}
|
||||
const initChart = () => {
|
||||
if (!props.isInterVal && !props.pieInterVal) {
|
||||
unbindPanCursorEvents()
|
||||
chart?.dispose()
|
||||
}
|
||||
// chart?.dispose()
|
||||
@@ -128,6 +190,15 @@ const initChart = () => {
|
||||
end: 100
|
||||
}
|
||||
],
|
||||
toolbox: {
|
||||
show: false,
|
||||
feature: {
|
||||
dataZoom: {
|
||||
yAxisIndex: 'none'
|
||||
}
|
||||
},
|
||||
...(props.options?.toolbox || null)
|
||||
},
|
||||
color: props.options?.color || color,
|
||||
series: props.options?.series,
|
||||
...props.options?.options
|
||||
@@ -136,16 +207,17 @@ const initChart = () => {
|
||||
handlerBar(options)
|
||||
|
||||
chart.setOption(options, true)
|
||||
chart.off('click')
|
||||
chart.on('click', (params: any) => {
|
||||
const value = Array.isArray(params.value) ? params.value[1] : params.value
|
||||
chart.off('datazoom')
|
||||
chart.on('datazoom', (params: any) => {
|
||||
const zoomPayload = Array.isArray(params.batch) ? params.batch[0] : params
|
||||
const start = Number(zoomPayload?.start)
|
||||
const end = Number(zoomPayload?.end)
|
||||
|
||||
if (params.componentType !== 'series' || !Number.isFinite(Number(value))) return
|
||||
if (!Number.isFinite(start) || !Number.isFinite(end)) return
|
||||
|
||||
emit('chart-click', {
|
||||
timeLabel: `${params.name ?? params.dataIndex ?? ''}`,
|
||||
value: Number(value),
|
||||
seriesName: `${params.seriesName || ''}`
|
||||
emit('chart-data-zoom', {
|
||||
start,
|
||||
end
|
||||
})
|
||||
})
|
||||
chart.dispatchAction({
|
||||
@@ -153,6 +225,7 @@ const initChart = () => {
|
||||
key: 'dataZoomSelect',
|
||||
dataZoomSelectActive: props.options?.activeTool === 'box-zoom'
|
||||
})
|
||||
bindPanCursorEvents()
|
||||
|
||||
setTimeout(() => {
|
||||
chart.resize()
|
||||
@@ -268,14 +341,14 @@ const handlerXAxis = () => {
|
||||
let throttle: ReturnType<typeof setTimeout>
|
||||
// 动态计算table高度
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
if (!entries.length) return
|
||||
|
||||
if (throttle) {
|
||||
clearTimeout(throttle)
|
||||
}
|
||||
throttle = setTimeout(() => {
|
||||
resizeHandler()
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
onMounted(() => {
|
||||
initChart()
|
||||
@@ -284,11 +357,12 @@ onMounted(() => {
|
||||
defineExpose({ initChart })
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver.unobserve(chartRef.value!)
|
||||
unbindPanCursorEvents()
|
||||
chart?.dispose()
|
||||
})
|
||||
watch(
|
||||
() => props.options,
|
||||
(newVal, oldVal) => {
|
||||
() => {
|
||||
initChart()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<LineChart
|
||||
:options="group.multiChannelOptions"
|
||||
:group="group.group"
|
||||
@chart-click="handleChartClick"
|
||||
@chart-data-zoom="handleChartDataZoom"
|
||||
/>
|
||||
</div>
|
||||
<template v-else>
|
||||
@@ -21,14 +21,21 @@
|
||||
:class="{ 'trend-chart-block--with-axis': item.isLastChart }"
|
||||
>
|
||||
<div class="single-channel-chart">
|
||||
<LineChart :options="item.options" :group="item.group" @chart-click="handleChartClick" />
|
||||
<LineChart
|
||||
:options="item.options"
|
||||
:group="item.group"
|
||||
@chart-data-zoom="handleChartDataZoom"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="hasWaveformData && activeDisplayMode === 'multi-channel'" class="chart-container">
|
||||
<LineChart :options="activeTrendOptions" @chart-click="handleChartClick" />
|
||||
<LineChart
|
||||
:options="activeTrendOptions"
|
||||
@chart-data-zoom="handleChartDataZoom"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="hasWaveformData" class="single-channel-list">
|
||||
<div
|
||||
@@ -38,7 +45,11 @@
|
||||
:class="{ 'trend-chart-block--with-axis': item.isLastChart }"
|
||||
>
|
||||
<div class="single-channel-chart">
|
||||
<LineChart :options="item.options" :group="item.group" @chart-click="handleChartClick" />
|
||||
<LineChart
|
||||
:options="item.options"
|
||||
:group="item.group"
|
||||
@chart-data-zoom="handleChartDataZoom"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,7 +65,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import LineChart from '@/components/echarts/line/index.vue'
|
||||
import type { AllChannelTrendGroup, DisplayMode, SingleChannelTrendOption, TrendChartClickPayload } from './types'
|
||||
import type {
|
||||
AllChannelTrendGroup,
|
||||
DisplayMode,
|
||||
SingleChannelTrendOption,
|
||||
TrendChartZoomPayload
|
||||
} from './types'
|
||||
|
||||
defineProps<{
|
||||
hasWaveformData: boolean
|
||||
@@ -67,11 +83,11 @@ defineProps<{
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'chart-click': [value: TrendChartClickPayload]
|
||||
'chart-data-zoom': [value: TrendChartZoomPayload]
|
||||
}>()
|
||||
|
||||
const handleChartClick = (value: TrendChartClickPayload) => {
|
||||
emit('chart-click', value)
|
||||
const handleChartDataZoom = (value: TrendChartZoomPayload) => {
|
||||
emit('chart-data-zoom', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -7,11 +7,16 @@
|
||||
|
||||
<div class="trend-tool-groups">
|
||||
<div v-for="group in trendToolGroups" :key="group.key" class="trend-tool-group">
|
||||
<el-tooltip v-for="item in group.items" :key="item.action" :content="item.label" placement="top">
|
||||
<el-tooltip
|
||||
v-for="item in group.items"
|
||||
:key="item.action"
|
||||
:content="getTrendToolTooltip(item)"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
:type="isTrendToolActive(item.action) ? 'primary' : 'default'"
|
||||
:icon="item.icon"
|
||||
:disabled="!hasWaveformData"
|
||||
:disabled="isTrendToolDisabled(item.action)"
|
||||
circle
|
||||
@click="handleTrendToolClick(item.action)"
|
||||
/>
|
||||
@@ -29,7 +34,7 @@
|
||||
:all-channel-trend-groups="allChannelTrendGroups"
|
||||
:is-all-channels-active="isAllChannelsActive"
|
||||
:last-parse-error-message="lastParseErrorMessage"
|
||||
@chart-click="handleChartClick"
|
||||
@chart-data-zoom="handleChartDataZoom"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
@@ -49,7 +54,7 @@
|
||||
:all-channel-trend-groups="allChannelTrendGroups"
|
||||
:is-all-channels-active="isAllChannelsActive"
|
||||
:last-parse-error-message="lastParseErrorMessage"
|
||||
@chart-click="handleChartClick"
|
||||
@chart-data-zoom="handleChartDataZoom"
|
||||
/>
|
||||
</el-dialog>
|
||||
</section>
|
||||
@@ -57,14 +62,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Aim,
|
||||
ArrowDownBold,
|
||||
ArrowUpBold,
|
||||
Crop,
|
||||
Download,
|
||||
FullScreen,
|
||||
Picture,
|
||||
Pointer,
|
||||
Rank,
|
||||
RefreshLeft,
|
||||
ZoomIn,
|
||||
@@ -77,7 +80,7 @@ import type {
|
||||
DisplayMode,
|
||||
LabelValueOption,
|
||||
SingleChannelTrendOption,
|
||||
TrendChartClickPayload,
|
||||
TrendChartZoomPayload,
|
||||
TrendToolAction,
|
||||
TrendTabValue
|
||||
} from './types'
|
||||
@@ -96,12 +99,13 @@ const props = defineProps<{
|
||||
allChannelTrendGroups: AllChannelTrendGroup[]
|
||||
lastParseErrorMessage: string
|
||||
activeTrendToolStates: Partial<Record<TrendToolAction, boolean>>
|
||||
disabledTrendToolStates?: Partial<Record<TrendToolAction, boolean>>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:activeTrendTab': [value: TrendTabValue]
|
||||
'trend-tool-action': [value: TrendToolAction]
|
||||
'chart-click': [value: TrendChartClickPayload]
|
||||
'chart-data-zoom': [value: TrendChartZoomPayload]
|
||||
}>()
|
||||
|
||||
const fullscreenVisible = ref(false)
|
||||
@@ -126,13 +130,6 @@ const trendToolGroups: Array<{
|
||||
{ action: 'reset', label: '恢复', icon: RefreshLeft }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'analysis',
|
||||
items: [
|
||||
{ action: 'measure', label: '光标测量', icon: Pointer },
|
||||
{ action: 'peak', label: '峰值定位', icon: Aim }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'export',
|
||||
items: [
|
||||
@@ -153,7 +150,24 @@ const isTrendToolActive = (action: TrendPanelToolAction) => {
|
||||
return props.activeTrendToolStates[action]
|
||||
}
|
||||
|
||||
const isTrendToolDisabled = (action: TrendPanelToolAction) => {
|
||||
if (!props.hasWaveformData) return true
|
||||
if (action === 'fullscreen') return false
|
||||
|
||||
return !!props.disabledTrendToolStates?.[action]
|
||||
}
|
||||
|
||||
const getTrendToolTooltip = (item: { action: TrendPanelToolAction; label: string }) => {
|
||||
if (item.action === 'pan' && isTrendToolDisabled(item.action) && props.hasWaveformData) {
|
||||
return '请先放大 X 轴或框选局部区域后再平移'
|
||||
}
|
||||
|
||||
return item.label
|
||||
}
|
||||
|
||||
const handleTrendToolClick = (action: TrendPanelToolAction) => {
|
||||
if (isTrendToolDisabled(action)) return
|
||||
|
||||
if (action === 'fullscreen') {
|
||||
fullscreenVisible.value = true
|
||||
return
|
||||
@@ -162,8 +176,8 @@ const handleTrendToolClick = (action: TrendPanelToolAction) => {
|
||||
emit('trend-tool-action', action)
|
||||
}
|
||||
|
||||
const handleChartClick = (value: TrendChartClickPayload) => {
|
||||
emit('chart-click', value)
|
||||
const handleChartDataZoom = (value: TrendChartZoomPayload) => {
|
||||
emit('chart-data-zoom', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@ export type TrendToolAction =
|
||||
| 'box-zoom'
|
||||
| 'pan'
|
||||
| 'reset'
|
||||
| 'measure'
|
||||
| 'peak'
|
||||
| 'download-image'
|
||||
| 'download-data'
|
||||
|
||||
@@ -54,8 +52,7 @@ export interface FeatureCardItem {
|
||||
}>
|
||||
}
|
||||
|
||||
export interface TrendChartClickPayload {
|
||||
timeLabel: string
|
||||
value: number
|
||||
seriesName: string
|
||||
export interface TrendChartZoomPayload {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
lineStyle: {
|
||||
width: resolveTrendLineWidth(visiblePointCount)
|
||||
},
|
||||
label: {
|
||||
color: '#fff',
|
||||
fontSize: 10
|
||||
},
|
||||
data: markPointData
|
||||
}
|
||||
}
|
||||
: null),
|
||||
...(measureMarkLine && index === 0 ? { markLine: measureMarkLine } : null)
|
||||
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]], {
|
||||
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