You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
385 lines
9.5 KiB
385 lines
9.5 KiB
<template> |
|
<div class="device-data-chart"> |
|
<v-chart |
|
ref="chartRef" |
|
:autoresize="autoresize" |
|
:loading="loading" |
|
:loading-options="loadingOpt" |
|
:option="chartOption" |
|
class="chart" |
|
@click="onChartClick" |
|
@legendselectchanged="changeLegend" |
|
/> |
|
</div> |
|
<LineChartDlg ref="LineChartDlgRef" @on-save="onChangeCoefficient"/> |
|
</template> |
|
|
|
<script lang="ts" setup> |
|
import Big from 'big.js' |
|
import dayjs from 'dayjs' |
|
import VChart from 'vue-echarts' |
|
import { use } from 'echarts/core' |
|
import { CanvasRenderer } from 'echarts/renderers' |
|
import type { EChartsOption, SeriesOption } from 'echarts/types/dist/shared.js' |
|
import { LineChart } from 'echarts/charts' |
|
import LineChartDlg from './line-dlg.vue' |
|
import { |
|
DataZoomComponent, |
|
GridComponent, |
|
LegendComponent, |
|
TooltipComponent, |
|
} from 'echarts/components' |
|
|
|
use([ |
|
CanvasRenderer, |
|
TooltipComponent, |
|
LineChart, |
|
LegendComponent, |
|
GridComponent, |
|
DataZoomComponent, |
|
]) |
|
const ZOOM_HEIGHT = 30 |
|
const ZOOM_BOTTOM = 10 |
|
const emits = defineEmits(['on-change-coefficient']) |
|
|
|
const EXTENDED_PALETTE = [ |
|
'#5470c6', |
|
'#91cc75', |
|
'#fac858', |
|
'#ee6666', |
|
'#73c0de', |
|
'#3ba272', |
|
'#fc8452', |
|
'#9a60b4', |
|
'#ea7ccc', |
|
'#2f4554', |
|
'#61a0a8', |
|
'#d48265', |
|
'#91c7ae', |
|
'#749f83', |
|
'#ca8622', |
|
'#bda29a', |
|
'#6e7074', |
|
'#546570', |
|
'#c4ccd3', |
|
'#f05b72', |
|
'#ef5b9c', |
|
'#f47920', |
|
'#905a3d', |
|
'#fab27b', |
|
'#2a5caa', |
|
'#444693', |
|
'#726930', |
|
'#b2d235', |
|
'#6d8346', |
|
'#ac6767', |
|
'#1d953f', |
|
'#6950a1', |
|
'#918597', |
|
] |
|
|
|
type Legend = { |
|
addr: string |
|
label: string |
|
unit?: string |
|
coefficient?: string | number |
|
} |
|
|
|
const autoresize = { |
|
throttle: 0, |
|
} |
|
|
|
const LineChartDlgRef = ref() |
|
|
|
function onChangeCoefficient(data: { id: string; coefficient: string }) { |
|
emits('on-change-coefficient', data) |
|
} |
|
|
|
const onChartClick = (params: any) => { |
|
// 判断是否点到折线 |
|
const [seriesName, id] = params?.seriesName?.split('|') |
|
if ( |
|
params.componentType === 'series' && |
|
params.seriesType === 'line' && |
|
seriesName && |
|
id |
|
) { |
|
const find = props.legends.find(item => item.addr === id) as any |
|
if (find) { |
|
LineChartDlgRef.value?.open(seriesName, id, find?.coefficient) |
|
} |
|
} |
|
} |
|
|
|
const props = defineProps({ |
|
title: String, |
|
smooth: Boolean, //当前是曲线图还是阶梯图 |
|
legends: { |
|
type: Array as PropType<Legend[]>, |
|
default: () => [], |
|
}, |
|
axisData: { |
|
type: Array as PropType<string[]>, |
|
default: () => [], |
|
}, |
|
chartDatas: { |
|
type: Object as PropType<Map<string, any[]>>, |
|
default: () => new Map(), |
|
}, |
|
}) |
|
|
|
const loading = ref(true) |
|
const loadingOpt = { |
|
type: 'default', |
|
text: '暂无数据', |
|
color: '#c23531', |
|
textColor: '#666', |
|
maskColor: 'rgba(0, 0, 0, 0)', |
|
showSpinner: false, |
|
} |
|
|
|
const chartRef = ref() |
|
|
|
const chartOption = computed<EChartsOption>(() => { |
|
if (props.chartDatas.size === 0) { |
|
return {} |
|
} |
|
loading.value = false |
|
const tmpSeries: SeriesOption[] = [] |
|
const legends = props.legends.filter(item => item.addr !== 'ts') |
|
for (let i = 0; i < legends.length; i++) { |
|
const legend = legends[i] |
|
const entry = props.chartDatas.get(legend.addr) ?? [] |
|
const color = EXTENDED_PALETTE[i % EXTENDED_PALETTE.length] |
|
|
|
const lineData: SeriesOption = { |
|
name: `${legend.label}|${legend.addr}`, |
|
type: 'line', |
|
id: legend.addr, |
|
// symbol: "none", |
|
connectNulls: true, |
|
triggerLineEvent: true, |
|
itemStyle: { |
|
color: color, |
|
}, |
|
data: entry.map(([ts, val]) => { |
|
console.log(ts) |
|
const coef = legend?.coefficient ? new Big(legend.coefficient) : new Big(1) |
|
let valBig = new Big(val ?? 0) |
|
valBig = valBig.div(coef) |
|
return [ts, valBig.toNumber()] |
|
}), |
|
sampling: 'lttb', |
|
animation: false, |
|
} |
|
if (props.smooth) { |
|
lineData.smooth = true |
|
} else { |
|
lineData.step = 'middle' |
|
} |
|
tmpSeries.push(lineData) |
|
} |
|
const dataLength = tmpSeries.length * tmpSeries[0]?.data?.length || 0 |
|
const visibleCount = Math.min(tmpSeries.length * 1000, dataLength) |
|
|
|
const startPercent = |
|
dataLength === 0 ? 0 : ((dataLength - visibleCount) / dataLength) * 100 |
|
const endPercent = 100 |
|
const maxSpanPercent = dataLength === 0 ? 100 : (visibleCount / dataLength) * 100 |
|
|
|
const option: EChartsOption = { |
|
grid: { |
|
left: 60, |
|
right: 198, |
|
top: '5%', |
|
bottom: `25%`, |
|
}, |
|
tooltip: { |
|
trigger: 'axis', |
|
confine: true, |
|
appendToBody: true, |
|
formatter: function (params: any) { |
|
// 时间格式化 |
|
const targetTime = params[0].value[0] |
|
const timeStr = dayjs(targetTime).format('YYYY-MM-DD HH:mm:ss.SSS') |
|
|
|
let relVal = timeStr |
|
const findNearest = (data: any[], target: number) => { |
|
if (!data || data.length === 0) return null |
|
let left = 0 |
|
let right = data.length - 1 |
|
let nearest = data[0] |
|
let minDiff = Math.abs(data[0][0] - target) |
|
|
|
while (left <= right) { |
|
const mid = Math.floor((left + right) / 2) |
|
const current = data[mid] |
|
const diff = Math.abs(current[0] - target) |
|
|
|
if (diff < minDiff) { |
|
minDiff = diff |
|
nearest = current |
|
} |
|
|
|
if (current[0] < target) { |
|
left = mid + 1 |
|
} else if (current[0] > target) { |
|
right = mid - 1 |
|
} else { |
|
return current |
|
} |
|
} |
|
return nearest |
|
} |
|
|
|
chartOption.value.series |
|
?.filter((series: any) => !unCheckArr.value.includes(series.name)) |
|
.forEach((series: any, index: number) => { |
|
// 尝试从 params 中找到当前系列的数据(如果是 hover 点) |
|
const paramItem = params.find((p: any) => p.seriesId === series.id) |
|
|
|
let value |
|
let marker |
|
|
|
if (paramItem) { |
|
value = paramItem.value[1] |
|
marker = paramItem.marker |
|
} else { |
|
// 如果不在 params 中,则在原始数据中查找最近的点 |
|
const nearestPoint = findNearest(series.data, targetTime) |
|
value = nearestPoint ? nearestPoint[1] : '--' |
|
// 手动生成 marker |
|
const color = series.itemStyle?.color || '#000' |
|
marker = `<span style="display:inline-block;margin-right:4px;color:${color};font-weight:bold;font-size: 20px;text-align:center;vertical-align:middle;line-height:10px;">*</span>` |
|
} |
|
|
|
const legendData = props.legends.find(item => item.addr === series.id) |
|
const coefficient = legendData?.coefficient |
|
? Number(legendData.coefficient) |
|
: undefined |
|
const unit = legendData?.unit || '' |
|
const seriesName = series.name.split('|')[0] |
|
|
|
relVal += `<div style="display: flex; justify-content: space-between; gap: 30px;"> |
|
<div style="display:flex;align-items:center;">${marker}${seriesName}:</div> |
|
<div>${value}${coefficient ? `【系数为${coefficient}】` : ''}${unit}</div> |
|
</div>` |
|
}) |
|
|
|
return relVal |
|
}, |
|
}, |
|
legend: { |
|
type: 'scroll', |
|
orient: 'vertical', |
|
formatter: name => { |
|
const nameStr = name.split('|')[0] |
|
return nameStr.length > 15 ? nameStr.substring(0, 15) + '...' : nameStr |
|
}, |
|
tooltip: { |
|
show: true, |
|
formatter: function (params) { |
|
return params.name.split('|')[0] |
|
}, |
|
}, |
|
right: 0, |
|
top: 20, |
|
itemWidth: 10, |
|
itemHeight: 10, |
|
pageIconSize: 12, |
|
data: props.legends.map(item => `${item.label}|${item.addr}`), |
|
// selected: unCheckArr.value.reduce((acc, cur) => { |
|
// acc[cur] = false |
|
// return acc |
|
// }, {} as Record<string, boolean>), |
|
}, |
|
xAxis: { |
|
type: 'time', |
|
axisLine: { |
|
//y轴线的颜色以及宽度 |
|
show: true, |
|
lineStyle: { |
|
width: 1, |
|
type: 'solid', |
|
}, |
|
}, |
|
axisLabel: { |
|
show: false, |
|
}, |
|
}, |
|
yAxis: { |
|
type: 'value', |
|
splitNumber: 6, |
|
axisLine: { |
|
//y轴线的颜色以及宽度 |
|
show: true, |
|
lineStyle: { |
|
width: 1, |
|
type: 'solid', |
|
}, |
|
}, |
|
axisLabel: { |
|
margin: 12, |
|
fontSize: 12, |
|
}, |
|
splitLine: { |
|
show: true, |
|
lineStyle: {}, |
|
}, |
|
}, |
|
dataZoom: [ |
|
{ |
|
type: 'inside', |
|
xAxisIndex: 0, |
|
filterMode: 'filter', |
|
start: startPercent, |
|
end: endPercent, |
|
// maxSpan: maxSpanPercent, |
|
}, |
|
{ |
|
type: 'slider', |
|
xAxisIndex: 0, |
|
start: startPercent, |
|
end: endPercent, |
|
height: ZOOM_HEIGHT, |
|
bottom: '20%', |
|
borderColor: '#88abf5', |
|
dataBackground: { lineStyle: { color: '#d2dbee' } }, |
|
selectedDataBackground: { lineStyle: { color: '#2c6cf7' } }, |
|
moveHandleStyle: { color: '#2c6cf7' }, |
|
// maxSpan: maxSpanPercent, |
|
}, |
|
], |
|
series: tmpSeries, |
|
} |
|
|
|
return option |
|
}) |
|
|
|
const unCheckArr = ref<string[]>([]) |
|
|
|
function changeLegend(data: { name: string; selected: Record<string, boolean> }) { |
|
const { name, selected } = data |
|
if (!selected[name]) { |
|
unCheckArr.value.push(name) |
|
} else { |
|
unCheckArr.value = unCheckArr.value.filter(item => item !== name) |
|
} |
|
} |
|
</script> |
|
|
|
<style lang="scss" scoped> |
|
.device-data-chart { |
|
width: 100%; |
|
height: 100%; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
|
|
.chart { |
|
width: 100%; |
|
height: 90%; |
|
min-height: 100px; |
|
} |
|
} |
|
</style>
|
|
|