| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- <script setup lang="ts">
- import * as echarts from 'echarts'
- import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
- import type { ChartSeries } from '../types'
- interface RefLine {
- value: number
- label?: string
- }
- const props = withDefaults(
- defineProps<{
- series: ChartSeries[]
- faultStart: number
- faultEnd: number
- height?: number
- yMin?: number
- yMax?: number
- unit?: string
- refLines?: RefLine[]
- showYAxis?: boolean
- dark?: boolean
- zoom?: boolean
- xMin?: number
- xMax?: number
- showLegend?: boolean
- showAxisUnit?: boolean
- lineColor?: string
- insideZoom?: boolean
- perSeriesLegend?: boolean
- gridHeight?: number
- gridDivider?: boolean
- }>(),
- {
- height: 180,
- unit: '',
- refLines: () => [],
- showYAxis: true,
- dark: true,
- zoom: false,
- showLegend: false,
- showAxisUnit: true,
- lineColor: '',
- insideZoom: false,
- perSeriesLegend: false,
- gridDivider: false,
- },
- )
- function layout() {
- const n = props.series.length
- const perSeries = props.perSeriesLegend
- const hasLegend = props.showLegend || n > 1
- const xLabelH = 18
- const sliderH = 16
- const topPad = 6
- const band = perSeries ? 14 : hasLegend ? 18 : 0
- const gap = perSeries ? 18 : 8
- const bottom = props.zoom ? sliderH + xLabelH + 10 : xLabelH + 8
- const totalH = props.gridHeight
- ? topPad + (perSeries ? band * n : band) + gap * (n - 1) + props.gridHeight * n + bottom
- : props.height
- const usable = perSeries
- ? totalH - topPad - bottom - band * n - gap * (n - 1)
- : totalH - topPad - band - bottom - gap * (n - 1)
- const gridH = props.gridHeight ?? Math.max(20, usable / n)
- return { n, totalH, perSeries, hasLegend, topPad, band, gap, bottom, gridH, xLabelH, sliderH }
- }
- const containerHeight = computed(() => layout().totalH)
- const el = ref<HTMLElement | null>(null)
- let chart: echarts.ECharts | null = null
- let observer: ResizeObserver | null = null
- const BLUE = '#4da3ff'
- const RED = '#E53935'
- function pad2(value: number): string {
- return String(value).padStart(2, '0')
- }
- /** 实际数据的时间范围(用于坐标轴/dataZoom 的范围;与故障红区无关)。 */
- function seriesExtent(): [number, number] {
- let lo = Infinity
- let hi = -Infinity
- props.series.forEach((s) => {
- s.data.forEach(([t]) => {
- if (t < lo) lo = t
- if (t > hi) hi = t
- })
- })
- if (!Number.isFinite(lo) || !Number.isFinite(hi)) return [props.faultStart, props.faultEnd]
- return [lo, hi]
- }
- function buildDataZoom(
- indexes: number[],
- sliderH: number,
- axisColor: string,
- startValue: number,
- endValue: number,
- ): Record<string, unknown>[] {
- return [
- { type: 'inside', xAxisIndex: indexes, startValue, endValue },
- {
- type: 'slider',
- xAxisIndex: indexes,
- startValue,
- endValue,
- bottom: 4,
- height: sliderH,
- borderColor: props.dark ? '#333a45' : '#dddddd',
- fillerColor: 'rgba(77,163,255,0.2)',
- handleStyle: { color: BLUE },
- textStyle: { color: axisColor, fontSize: 9 },
- showDetail: false,
- },
- ]
- }
- function buildOption(): Record<string, unknown> {
- const { n, perSeries, hasLegend, gridH, sliderH, topPad, band, gap } = layout()
- const topOf = (i: number): number =>
- perSeries ? topPad + i * (band + gridH + gap) : topPad + band + i * (gridH + gap)
- const gridTop = (i: number): number => (perSeries ? topOf(i) + band : topOf(i))
- const indexes = props.series.map((_, i) => i)
- const [tMin, tMax] =
- props.xMin != null && props.xMax != null ? [props.xMin, props.xMax] : seriesExtent()
- const axisColor = props.dark ? '#aaaaaa' : '#666666'
- const lineColor = props.dark ? '#555555' : '#cccccc'
- const legendColor = props.dark ? '#d6dbe2' : '#333333'
- const refColor = props.dark ? 'rgba(170,170,170,0.7)' : 'rgba(120,120,120,0.8)'
- const refText = props.dark ? '#aaaaaa' : '#666666'
- const tooltipBg = props.dark ? 'rgba(0,0,0,0.8)' : 'rgba(255,255,255,0.95)'
- const tooltipText = props.dark ? '#ffffff' : '#333333'
- const faultLine = props.dark ? '#ff5252' : RED
- const faultFill = props.dark ? 'rgba(229,57,53,0.22)' : 'rgba(229,57,53,0.10)'
- const faultBorder = props.dark ? 'rgba(229,57,53,0.55)' : 'transparent'
- const baseColor = props.lineColor || BLUE
- const customColors = props.series.some((s) => s.color)
- const markLines = props.refLines.map((line) => ({
- yAxis: line.value,
- lineStyle: { color: refColor, type: 'dashed', width: 1 },
- label: line.label
- ? { formatter: line.label, position: 'insideEndTop', color: refText, fontSize: 9 }
- : { show: false },
- }))
- const grids: Record<string, unknown>[] = []
- const xAxis: Record<string, unknown>[] = []
- const yAxis: Record<string, unknown>[] = []
- const series: Record<string, unknown>[] = []
- props.series.forEach((s, i) => {
- const last = i === n - 1
- grids.push({
- left: 4,
- right: 8,
- top: gridTop(i),
- height: gridH,
- containLabel: props.showYAxis,
- })
- xAxis.push({
- type: 'time',
- gridIndex: i,
- min: tMin,
- max: tMax,
- axisLine: { show: last, lineStyle: { color: lineColor } },
- axisTick: { show: last },
- axisLabel: { show: last, fontSize: 9, color: axisColor, hideOverlap: false, interval: 'auto' },
- splitLine: { show: false },
- axisPointer: { label: { show: false } },
- })
- yAxis.push({
- type: 'value',
- gridIndex: i,
- show: props.showYAxis,
- scale: true,
- min: s.yMin ?? props.yMin,
- max: s.yMax ?? props.yMax,
- splitNumber: 4,
- axisLine: { show: false },
- axisTick: { show: false },
- axisLabel: {
- fontSize: 9,
- color: axisColor,
- formatter: (value: number) =>
- props.showAxisUnit ? `${Number(value).toFixed(2)}${props.unit}` : Number(value).toFixed(2),
- },
- splitLine: { show: false },
- })
- series.push({
- name: s.name,
- type: 'line',
- xAxisIndex: i,
- yAxisIndex: i,
- data: s.data,
- showSymbol: false,
- symbol: 'none',
- animation: false,
- sampling: 'none',
- lineStyle: { width: props.dark ? 1.5 : 1, ...(s.color ? { color: s.color } : {}) },
- itemStyle: s.color ? { color: s.color } : undefined,
- emphasis: { disabled: true },
- markLine: { silent: true, symbol: 'none', data: markLines },
- markArea: {
- silent: true,
- itemStyle: { color: faultFill, borderColor: faultBorder, borderWidth: 1 },
- data: [[{ xAxis: props.faultStart }, { xAxis: props.faultEnd }]],
- },
- })
- })
- const width = chart ? chart.getWidth() : el.value?.clientWidth ?? 0
- const graphic: Record<string, unknown>[] = []
- if (props.gridDivider && width > 0) {
- for (let i = 0; i < n - 1; i += 1) {
- const y = gridTop(i) + gridH + gap / 2
- graphic.push({
- type: 'line',
- z: 10,
- silent: true,
- shape: { x1: 4, y1: y, x2: width - 8, y2: y },
- style: { stroke: '#ffffff', lineWidth: 1 },
- })
- }
- }
- return {
- animation: false,
- backgroundColor: 'transparent',
- graphic: graphic.length ? graphic : undefined,
- textStyle: {
- fontFamily: '-apple-system, "PingFang SC", "Microsoft YaHei", sans-serif',
- fontWeight: 'normal',
- },
- legend: perSeries
- ? props.series.map((s, i) => ({
- top: topOf(i) + 1,
- left: 'center',
- itemWidth: 14,
- itemHeight: 8,
- textStyle: { fontSize: 10, color: legendColor },
- data: [s.name],
- }))
- : hasLegend
- ? {
- top: 0,
- left: 'center',
- itemWidth: 12,
- itemHeight: 8,
- textStyle: { fontSize: 10, color: legendColor },
- data: props.series.map((s) => s.name),
- }
- : undefined,
- grid: grids,
- xAxis,
- yAxis,
- series,
- visualMap: customColors
- ? undefined
- : {
- show: false,
- dimension: 0,
- seriesIndex: indexes,
- pieces: [
- { lt: props.faultStart, color: baseColor },
- { gte: props.faultStart, lte: props.faultEnd, color: faultLine },
- { gt: props.faultEnd, color: baseColor },
- ],
- outOfRange: { color: baseColor },
- },
- axisPointer: { link: [{ xAxisIndex: 'all' }] },
- dataZoom: props.insideZoom
- ? [{ type: 'inside', xAxisIndex: indexes, startValue: tMin, endValue: tMax }]
- : props.zoom
- ? buildDataZoom(indexes, sliderH, axisColor, tMin, tMax)
- : undefined,
- tooltip: {
- trigger: 'axis',
- axisPointer: { type: 'line' },
- backgroundColor: tooltipBg,
- textStyle: { fontSize: 11, color: tooltipText },
- formatter: (params: unknown) => {
- const items = Array.isArray(params) ? params : [params]
- if (!items.length) return ''
- const first = items[0] as { axisValue: number }
- const date = new Date(first.axisValue)
- const head =
- `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ` +
- `${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`
- const lines = items.map((raw) => {
- const item = raw as { marker: string; seriesName: string; value: [number, number | null] }
- const value = item.value?.[1]
- const text = value === null || value === undefined ? '-' : Number(value).toFixed(2)
- return `${item.marker}${item.seriesName}: ${text}${props.unit}`
- })
- return [head, ...lines].join('<br/>')
- },
- },
- }
- }
- function render(): void {
- if (!chart) return
- chart.setOption(buildOption() as unknown as echarts.EChartsOption, { notMerge: true })
- }
- onMounted(() => {
- if (!el.value) return
- chart = echarts.init(el.value)
- render()
- observer = new ResizeObserver(() => {
- chart?.resize()
- render()
- })
- observer.observe(el.value)
- })
- onBeforeUnmount(() => {
- observer?.disconnect()
- observer = null
- chart?.dispose()
- chart = null
- })
- watch(
- () => [props.series, props.yMin, props.yMax, props.refLines, props.xMin, props.xMax],
- render,
- { deep: true },
- )
- </script>
- <template>
- <div ref="el" class="fault-chart" :style="{ height: `${containerHeight}px` }" />
- </template>
- <style scoped>
- .fault-chart {
- width: 100%;
- }
- </style>
|