|
|
|
|
<template>
|
|
|
|
|
<div class="device-data-chart">
|
|
|
|
|
<v-chart
|
|
|
|
|
class="chart"
|
|
|
|
|
ref="chartRef"
|
|
|
|
|
:option="chartOption"
|
|
|
|
|
:autoresize="autoresize"
|
|
|
|
|
:loading-options="loadingOpt"
|
|
|
|
|
:loading="loading"
|
|
|
|
|
@click="onChartClick"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<LineChartDlg ref="LineChartDlgRef" @on-save="onChangeCoefficient" />
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
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'])
|
|
|
|
|
|
|
|
|
|
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[] = []
|
|
|
|
|
for (const legend of props.legends.filter(item => item.addr !== 'ts')) {
|
|
|
|
|
const entry = props.chartDatas.get(legend.addr) ?? []
|
|
|
|
|
const lineData: SeriesOption = {
|
|
|
|
|
name: `${legend.label}|${legend.addr}`,
|
|
|
|
|
type: 'line',
|
|
|
|
|
id: legend.addr,
|
|
|
|
|
// symbol: "none",
|
|
|
|
|
connectNulls: true,
|
|
|
|
|
triggerLineEvent: true,
|
|
|
|
|
data: entry.map(([ts, val]) => {
|
|
|
|
|
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 date = new Date(params[0].value[0])
|
|
|
|
|
const timeStr = dayjs(params[0].value[0]).format('YYYY-MM-DD HH:mm:ss.SSS')
|
|
|
|
|
|
|
|
|
|
let relVal = timeStr
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < params.length; i++) {
|
|
|
|
|
const data = props.legends.find(item => item.addr === params[i].seriesId)
|
|
|
|
|
const coefficient = data?.coefficient ? Number(data.coefficient) : undefined
|
|
|
|
|
const unit = data?.unit || ''
|
|
|
|
|
relVal += `<div style="display: flex; justify-content: space-between; gap: 30px;">
|
|
|
|
|
<div>${params[i].marker}${params[i].seriesName.split('|')[0]}:</div>
|
|
|
|
|
<div>${params[i].value[1]}${
|
|
|
|
|
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 scoped lang="scss">
|
|
|
|
|
.device-data-chart {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
|
|
.chart {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 90%;
|
|
|
|
|
min-height: 100px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|