main
vue 159 lines 4.13 KB
Raw
1 <template>
2 <VChart class="w-full" :style="{ height, width: '100%' }" autoresize :option="chartOption" @click="onChartClick" />
3 </template>
4
5 <script setup lang="ts">
6 import type { PieSeriesOption } from "echarts/charts"
7 import type { LegendComponentOption, TitleComponentOption, TooltipComponentOption } from "echarts/components"
8 import type { ComposeOption, ECElementEvent } from "echarts/core"
9 import { PieChart } from "echarts/charts"
10 import { GraphicComponent, LegendComponent, TitleComponent, TooltipComponent } from "echarts/components"
11 import { use } from "echarts/core"
12 import { CanvasRenderer } from "echarts/renderers"
13 import { computed } from "vue"
14 import VChart from "vue-echarts"
15 import { useThemeStore } from "@/stores/theme"
16 import { buildChartTooltipGlassBase, CHART_COLORS, chartTooltipThemeFromStyle, formatChartTooltipPieItem } from "."
17
18 const props = withDefaults(
19 defineProps<{
20 labels?: string[]
21 data?: number[]
22 height?: string
23 monochrome?: boolean
24 }>(),
25 {
26 labels: () => [],
27 data: () => [],
28 height: "100%"
29 }
30 )
31
32 const emit = defineEmits<{
33 itemClick: [item: { name: string }]
34 }>()
35
36 use([CanvasRenderer, PieChart, LegendComponent, TooltipComponent, TitleComponent, GraphicComponent])
37
38 type ChartOption = ComposeOption<
39 TitleComponentOption | TooltipComponentOption | LegendComponentOption | PieSeriesOption
40 >
41
42 const themeStore = useThemeStore()
43
44 const pieData = computed(() =>
45 (props.labels || []).map((label, i) => ({
46 name: label,
47 value: Number(props.data[i] ?? 0)
48 }))
49 )
50
51 const totalValue = computed(() => pieData.value.reduce((sum, item) => sum + item.value, 0))
52
53 const chartOption = computed((): ChartOption => {
54 const style = themeStore.style
55 const fg = style["fg-default-color"]
56 const ff = style["font-family"]
57 const palette = props.monochrome ? [CHART_COLORS[0]] : [...CHART_COLORS]
58 const hasData = props.labels?.length > 0
59
60 if (!hasData) {
61 return {
62 backgroundColor: "transparent",
63 title: {
64 text: "No data",
65 left: "center",
66 top: "center",
67 textStyle: { color: fg, fontSize: 16, fontFamily: ff }
68 }
69 }
70 }
71
72 return {
73 backgroundColor: "transparent",
74 color: palette,
75 tooltip: {
76 ...buildChartTooltipGlassBase(chartTooltipThemeFromStyle(style)),
77 formatter: params =>
78 formatChartTooltipPieItem(params, {
79 resolveColor: p => {
80 const idx = p.dataIndex ?? 0
81 return palette[idx % palette.length]
82 }
83 })
84 },
85 graphic: [
86 {
87 type: "text",
88 left: "center",
89 top: "33%",
90 style: {
91 text: `Total\n\n${totalValue.value}`,
92 fill: fg,
93 fontSize: 14,
94 fontFamily: ff,
95 textAlign: "center",
96 textVerticalAlign: "middle"
97 }
98 }
99 ],
100 legend: {
101 show: true,
102 selectedMode: false,
103 type: "scroll",
104 orient: "horizontal",
105 bottom: 4,
106 left: "center",
107 width: "92%",
108 textStyle: { color: fg, fontSize: 11, fontFamily: ff },
109 pageTextStyle: { color: fg },
110 pageIconColor: fg,
111 pageIconInactiveColor: style["fg-secondary-color"],
112 itemWidth: 7,
113 itemHeight: 7,
114 itemGap: 16,
115 formatter: (name: string) => {
116 const item = pieData.value.find(d => d.name === name)
117 const val = item?.value ?? 0
118 const pct = totalValue.value > 0 ? (val / totalValue.value) * 100 : 0
119 return `${name} - ${val} (${pct.toFixed(1)}%)`
120 }
121 },
122 series: [
123 {
124 name: "value",
125 type: "pie",
126 radius: ["40%", "55%"],
127 center: ["50%", "40%"],
128 avoidLabelOverlap: true,
129 itemStyle: { borderWidth: 0 },
130 label: {
131 show: true,
132 position: "outside",
133 color: fg,
134 fontSize: 11,
135 fontFamily: ff,
136 formatter: params => {
137 const pct = typeof params.percent === "number" ? params.percent : 0
138 return `${pct.toFixed(1)}%`
139 }
140 },
141 labelLine: { show: true, length: 10, length2: 6 },
142 data: pieData.value
143 }
144 ]
145 }
146 })
147
148 function resolveClickedItemName(params: ECElementEvent): string | undefined {
149 if (params.componentType === "legend" || params.componentType === "series") {
150 return params.name ?? (props.labels || [])[params.dataIndex ?? -1]
151 }
152 return undefined
153 }
154
155 function onChartClick(params: ECElementEvent) {
156 const name = resolveClickedItemName(params)
157 if (name) emit("itemClick", { name })
158 }
159 </script>