main
vue 188 lines 5.05 KB
Raw
1 <template>
2 <VChart
3 ref="chartRef"
4 class="w-full"
5 :autoresize="{ onResize: updatePlotWidth }"
6 :option="chartOption"
7 :style="{ height: `${height}px` }"
8 @finished="updatePlotWidth"
9 />
10 </template>
11
12 <script setup lang="ts">
13 import type { LineSeriesOption } from "echarts/charts"
14 import type {
15 GridComponentOption,
16 LegendComponentOption,
17 TitleComponentOption,
18 TooltipComponentOption
19 } from "echarts/components"
20 import type { ComposeOption } from "echarts/core"
21 import type { CallbackDataParams } from "echarts/types/dist/shared"
22 import type { TimeSeriesData } from "@/types/metrics.d"
23 import { LineChart } from "echarts/charts"
24 import { GridComponent, LegendComponent, TitleComponent, TooltipComponent } from "echarts/components"
25 import { use } from "echarts/core"
26 import { CanvasRenderer } from "echarts/renderers"
27 import { computed, ref, toRefs } from "vue"
28 import VChart from "vue-echarts"
29 import { useThemeStore } from "@/stores/theme"
30 import { formatBytes } from "@/utils/format"
31 import {
32 buildChartTooltipGlassBase,
33 CHART_COLORS,
34 chartTooltipThemeFromStyle,
35 formatChartTooltipAxisMultiSeriesFromParams
36 } from "../common/charts"
37
38 const props = withDefaults(
39 defineProps<{
40 title: string
41 series: TimeSeriesData
42 height?: number
43 yAxisName?: string
44 useFormatBytes?: boolean
45 }>(),
46 {
47 height: 250,
48 yAxisName: "",
49 useFormatBytes: false
50 }
51 )
52
53 use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
54
55 type ChartOption = ComposeOption<
56 TitleComponentOption | TooltipComponentOption | LegendComponentOption | GridComponentOption | LineSeriesOption
57 >
58
59 const GRID_HORIZONTAL_PADDING = 80
60
61 const { title, series, height, yAxisName, useFormatBytes } = toRefs(props)
62 const style = computed(() => useThemeStore().style)
63 const chartRef = ref<InstanceType<typeof VChart> | null>(null)
64 const plotWidth = ref(0)
65
66 function updatePlotWidth() {
67 const chartWidth = chartRef.value?.getWidth() ?? 0
68 plotWidth.value = Math.max(0, chartWidth - GRID_HORIZONTAL_PADDING)
69 }
70
71 const xAxisLabelFormatter = computed(() => {
72 const compact = plotWidth.value > 0 && plotWidth.value < 360
73 return (value: string | number) => {
74 const ts = typeof value === "number" ? value : Number(value)
75 const d = new Date(ts)
76 if (Number.isNaN(d.getTime())) return String(value)
77 if (compact) {
78 return d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
79 }
80 return d.toLocaleString(undefined, {
81 month: "short",
82 day: "numeric",
83 hour: "2-digit",
84 minute: "2-digit"
85 })
86 }
87 })
88
89 const chartOption = computed((): ChartOption => {
90 const seriesData = series.value || {}
91 const seriesNames = Object.keys(seriesData)
92 const fg = style.value["fg-default-color"]
93 const fgSecondary = style.value["fg-secondary-color"]
94 const border = style.value["border-color"]
95
96 const eSeries: LineSeriesOption[] = seriesNames.map((name, i) => ({
97 name,
98 type: "line",
99 smooth: true,
100 symbol: "none",
101 lineStyle: { width: 1.5 },
102 areaStyle: { opacity: 0.08 },
103 itemStyle: { color: CHART_COLORS[i % CHART_COLORS.length] },
104 data: (seriesData[name] || []).map(d => [new Date(d.time).getTime(), d.value])
105 }))
106
107 return {
108 backgroundColor: "transparent",
109 title: {
110 text: title.value,
111 textStyle: {
112 color: fg,
113 fontSize: 13,
114 fontWeight: 500,
115 fontFamily: style.value["font-family"]
116 },
117 left: 10,
118 top: 0
119 },
120 tooltip: {
121 ...buildChartTooltipGlassBase(
122 chartTooltipThemeFromStyle({
123 ...style.value,
124 "font-family": style.value["font-family-mono"]
125 }),
126 { trigger: "axis" }
127 ),
128 formatter: params =>
129 formatChartTooltipAxisMultiSeriesFromParams(params, {
130 titleMutedColor: fgSecondary,
131 formatTitle: (first: CallbackDataParams) => {
132 const time = Array.isArray(first.value) ? first.value[0] : null
133 return time != null ? new Date(time).toLocaleString() : ""
134 },
135 formatRow: (p: CallbackDataParams) => {
136 const raw = Array.isArray(p.value) ? p.value[1] : p.value
137 const valueHtml = useFormatBytes.value
138 ? `${formatBytes(`${raw}`)}`
139 : typeof raw === "number"
140 ? raw.toFixed(2)
141 : String(raw ?? "")
142 return { label: p.seriesName ?? "", valueHtml }
143 }
144 })
145 },
146 legend: {
147 bottom: 0,
148 textStyle: { color: fgSecondary, fontSize: 11 },
149 icon: "roundRect",
150 itemWidth: 12,
151 itemHeight: 3
152 },
153 grid: {
154 left: 60,
155 right: 20,
156 top: 55,
157 bottom: 40
158 },
159 xAxis: {
160 type: "category",
161 axisLine: { lineStyle: { color: border } },
162 axisLabel: {
163 color: fgSecondary,
164 fontSize: 10,
165 interval: "auto",
166 hideOverlap: true,
167 showMinLabel: true,
168 showMaxLabel: true,
169 formatter: xAxisLabelFormatter.value
170 },
171 splitLine: { show: false }
172 },
173 yAxis: {
174 type: "value",
175 name: yAxisName.value,
176 nameTextStyle: { color: fgSecondary, fontSize: 10 },
177 axisLine: { lineStyle: { color: border } },
178 axisLabel: {
179 color: fgSecondary,
180 fontSize: 10,
181 formatter: useFormatBytes.value ? (v: number) => `${formatBytes(`${v}`)}` : undefined
182 },
183 splitLine: { lineStyle: { color: border, opacity: 0.3 } }
184 },
185 series: eSeries
186 }
187 })
188 </script>