291 lines
8.1 KiB
Vue
291 lines
8.1 KiB
Vue
<template>
|
|
<v-chart autoresize class="pie-chart-container" :class="{ 'transition-in': skeleton }" :option="chartOptions"
|
|
@click="clickItem" @legendselectchanged="onLegendSelectChanged" />
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed } from 'vue';
|
|
import { useTheme } from 'vuetify';
|
|
|
|
import type { ECElementEvent } from 'echarts/core';
|
|
import type { CallbackDataParams } from 'echarts/types/dist/shared';
|
|
|
|
import { useI18n } from '@/locales/helpers.ts';
|
|
import { type CommonPieChartDataItem, type CommonPieChartProps, usePieChartBase } from '@/components/base/PieChartBase.ts'
|
|
|
|
import type { ColorValue } from '@/core/color.ts';
|
|
import { ThemeType } from '@/core/theme.ts';
|
|
import { DEFAULT_ICON_COLOR } from '@/consts/color.ts';
|
|
|
|
interface DesktopPieChartDataItem extends CommonPieChartDataItem {
|
|
itemStyle: {
|
|
color: ColorValue;
|
|
};
|
|
selected: boolean;
|
|
}
|
|
|
|
const props = defineProps<CommonPieChartProps>();
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'click', value: Record<string, unknown>): void;
|
|
}>();
|
|
|
|
const theme = useTheme();
|
|
|
|
const { formatAmountWithCurrency } = useI18n();
|
|
const { selectedIndex, validItems } = usePieChartBase(props);
|
|
|
|
const selectedLegends = ref<Record<string, boolean> | null>(null);
|
|
|
|
const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark);
|
|
|
|
const itemsMap = computed<Record<string, Record<string, unknown>>>(() => {
|
|
const map: Record<string, Record<string, unknown>> = {};
|
|
|
|
for (let i = 0; i < props.items.length; i++) {
|
|
const item = props.items[i];
|
|
let id = '';
|
|
|
|
if (props.idField && item[props.idField]) {
|
|
id = item[props.idField] as string;
|
|
} else {
|
|
id = item[props.nameField] as string;;
|
|
}
|
|
|
|
map[id] = item;
|
|
}
|
|
|
|
return map;
|
|
});
|
|
|
|
const seriesData = computed<DesktopPieChartDataItem[]>(() => {
|
|
const ret: DesktopPieChartDataItem[] = [];
|
|
|
|
for (let i = 0; i < validItems.value.length; i++) {
|
|
const item = validItems.value[i];
|
|
ret.push({
|
|
...item,
|
|
itemStyle: {
|
|
color: getColor(item.color),
|
|
},
|
|
selected: true
|
|
});
|
|
}
|
|
|
|
return ret;
|
|
});
|
|
|
|
const hasUnselectedItem = computed<boolean>(() => {
|
|
for (let i = 0; i < validItems.value.length; i++) {
|
|
const item = validItems.value[i];
|
|
|
|
if (selectedLegends.value && !selectedLegends.value[item.id]) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
const firstItemAndHalfCurrentItemTotalPercent = computed<number>(() => {
|
|
let totalValue = 0;
|
|
let firstValue = null;
|
|
let firstToCurrentTotalValue = 0;
|
|
|
|
for (let i = 0; i < validItems.value.length; i++) {
|
|
const item = validItems.value[i];
|
|
|
|
if (selectedLegends.value && !selectedLegends.value[item.id]) {
|
|
continue;
|
|
}
|
|
|
|
if (firstValue === null) {
|
|
firstValue = item.value;
|
|
}
|
|
|
|
if (firstValue !== null) {
|
|
if (i < selectedIndex.value) {
|
|
firstToCurrentTotalValue += item.value;
|
|
} else if (i === selectedIndex.value) {
|
|
firstToCurrentTotalValue += item.value / 2;
|
|
}
|
|
}
|
|
|
|
totalValue += item.value;
|
|
}
|
|
|
|
if (firstToCurrentTotalValue && totalValue > 0) {
|
|
return firstToCurrentTotalValue / totalValue;
|
|
} else {
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
const chartOptions = computed<object>(() => {
|
|
return {
|
|
tooltip: {
|
|
trigger: 'item',
|
|
backgroundColor: isDarkMode.value ? '#333' : '#fff',
|
|
borderColor: isDarkMode.value ? '#333' : '#fff',
|
|
textStyle: {
|
|
color: isDarkMode.value ? '#eee' : '#333'
|
|
},
|
|
formatter: (params: CallbackDataParams) => {
|
|
const dataItem = params.data as DesktopPieChartDataItem;
|
|
const name = dataItem ? dataItem.displayName : '';
|
|
const value = dataItem ? dataItem.displayValue : formatAmountWithCurrency(params.value as number);
|
|
let percent = dataItem ? dataItem.displayPercent : (params.percent + '%');
|
|
|
|
if (hasUnselectedItem.value) {
|
|
percent = params.percent + '%';
|
|
}
|
|
|
|
let tooltip = `<div><span class="chart-pointer" style="background-color: ${params.color}"></span>`;
|
|
|
|
if (name) {
|
|
tooltip += `<span>${name}</span><br/><span>${value} (${percent})</span>`;
|
|
} else {
|
|
tooltip += `<span>${value} (${percent})</span>`;
|
|
}
|
|
|
|
tooltip += '</div>';
|
|
|
|
return tooltip;
|
|
}
|
|
},
|
|
legend: {
|
|
orient: 'horizontal',
|
|
data: validItems.value.map(item => item.name),
|
|
selected: selectedLegends.value,
|
|
textStyle: {
|
|
color: isDarkMode.value ? '#eee' : '#333'
|
|
},
|
|
formatter: (id: string) => {
|
|
const item = itemsMap.value[id];
|
|
return item && props.nameField && item[props.nameField] ? item[props.nameField] as string : id;
|
|
}
|
|
},
|
|
series: [
|
|
{
|
|
type: 'pie',
|
|
data: seriesData.value,
|
|
top: 50,
|
|
startAngle: -90 + firstItemAndHalfCurrentItemTotalPercent.value * 360,
|
|
emphasis: {
|
|
itemStyle: {
|
|
shadowBlur: 10,
|
|
shadowOffsetX: 0,
|
|
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
|
}
|
|
},
|
|
label: {
|
|
color: isDarkMode.value ? '#eee' : '#333',
|
|
formatter: (params: CallbackDataParams) => {
|
|
const dataItem = params.data as DesktopPieChartDataItem;
|
|
return dataItem ? dataItem.displayName : '';
|
|
}
|
|
},
|
|
animation: !props.skeleton
|
|
}
|
|
],
|
|
media: [
|
|
{
|
|
query: {
|
|
minWidth: 600,
|
|
},
|
|
option: {
|
|
legend: {
|
|
orient: 'vertical',
|
|
left: 'left'
|
|
},
|
|
series: [
|
|
{
|
|
type: 'pie',
|
|
top: 0
|
|
}
|
|
]
|
|
},
|
|
}
|
|
]
|
|
};
|
|
});
|
|
|
|
function getColor(color: string): ColorValue {
|
|
if (color && color !== DEFAULT_ICON_COLOR) {
|
|
color = '#' + color;
|
|
}
|
|
|
|
return color;
|
|
}
|
|
|
|
function clickItem(e: ECElementEvent): void {
|
|
if (!props.enableClickItem || e.componentType !== 'series' || e.seriesType !=='pie') {
|
|
return;
|
|
}
|
|
|
|
if (e.event && e.event.target && e.event.target.currentStates && e.event.target.currentStates[0] && e.event.target.currentStates[0] === 'emphasis') {
|
|
selectedIndex.value = e.dataIndex;
|
|
return;
|
|
}
|
|
|
|
if (!e.data) {
|
|
return;
|
|
}
|
|
|
|
const data = e.data as object;
|
|
|
|
if ('sourceItem' in data) {
|
|
emit('click', data.sourceItem as Record<string, unknown>);
|
|
}
|
|
}
|
|
|
|
function onLegendSelectChanged(e: { selected: Record<string, boolean> }): void {
|
|
selectedLegends.value = e.selected;
|
|
const selectedItem = validItems.value[selectedIndex.value];
|
|
|
|
if (!selectedItem || !selectedLegends.value[selectedItem.id]) {
|
|
let newSelectedIndex = 0;
|
|
|
|
for (let i = 0; i < validItems.value.length; i++) {
|
|
const item = validItems.value[i];
|
|
|
|
if (selectedLegends.value[item.id]) {
|
|
newSelectedIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
selectedIndex.value = newSelectedIndex;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.pie-chart-container {
|
|
width: 100%;
|
|
height: 400px;
|
|
}
|
|
|
|
@media (min-width: 600px) {
|
|
.pie-chart-container {
|
|
height: 500px;
|
|
}
|
|
}
|
|
|
|
.pie-chart-container.transition-in {
|
|
animation: pie-chart-skeleton-fade-in 2s 1;
|
|
}
|
|
|
|
@keyframes pie-chart-skeleton-fade-in {
|
|
0% {
|
|
opacity: 0;
|
|
}
|
|
20% {
|
|
opacity: 0;
|
|
}
|
|
100% {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
</style>
|