migrate desktop pie chart to composition API and typescript

This commit is contained in:
MaysWind
2025-01-23 00:06:05 +08:00
parent 85557c2879
commit 70428b6c96
+288 -268
View File
@@ -3,298 +3,318 @@
@click="clickItem" @legendselectchanged="onLegendSelectChanged" /> @click="clickItem" @legendselectchanged="onLegendSelectChanged" />
</template> </template>
<script> <script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { useTheme } from 'vuetify'; import { useTheme } from 'vuetify';
import { mapStores } from 'pinia'; import type { ECElementEvent } from 'echarts/core';
import { useSettingsStore } from '@/stores/setting.ts'; import type { CallbackDataParams } from 'echarts/types/dist/shared';
import { useUserStore } from '@/stores/user.ts';
import { DEFAULT_ICON_COLOR, DEFAULT_CHART_COLORS } from '@/consts/color.ts'; import { useI18n } from '@/locales/helpers.ts';
import type { ColorValue } from '@/core/color.ts';
import { ThemeType } from '@/core/theme.ts'; import { ThemeType } from '@/core/theme.ts';
import { DEFAULT_ICON_COLOR, DEFAULT_CHART_COLORS } from '@/consts/color.ts';
import { isNumber } from '@/lib/common.ts';
import { formatPercent } from '@/lib/numeral.ts'; import { formatPercent } from '@/lib/numeral.ts';
export default { interface DesktopPieChartDataItem {
props: [ id: string;
'skeleton', name: string;
'items', displayName: string;
'idField', value: number;
'nameField', percent: number;
'valueField', actualPercent: number;
'percentField', itemStyle: {
'colorField', color: ColorValue;
'hiddenField', };
'minValidPercent', selected: boolean;
'defaultCurrency', sourceItem: Record<string, unknown>;
'showValue', displayPercent?: string;
'enableClickItem' displayValue?: string;
], }
emits: [
'click'
],
data() {
return {
selectedLegends: null,
selectedIndex: 0
};
},
computed: {
...mapStores(useSettingsStore, useUserStore),
isDarkMode() {
return this.globalTheme.global.name.value === ThemeType.Dark;
},
itemsMap: function () {
const map = {};
for (let i = 0; i < this.items.length; i++) { const props = defineProps<{
const item = this.items[i]; skeleton?: boolean;
let id = ''; items: Record<string, unknown>[];
idField?: string;
nameField: string;
valueField: string;
percentField?: string;
colorField?: string;
hiddenField?: string;
minValidPercent?: number;
defaultCurrency?: string;
showValue?: boolean;
enableClickItem?: boolean;
}>();
if (this.idField && item[this.idField]) { const emit = defineEmits<{
id = item[this.idField]; (e: 'click', value: Record<string, unknown>): void;
} else { }>();
id = item[this.nameField];
}
map[id] = item; const theme = useTheme();
}
return map; const { formatAmountWithCurrency } = useI18n();
},
validItems: function () {
let totalValidValue = 0;
for (let i = 0; i < this.items.length; i++) { const selectedLegends = ref<Record<string, boolean> | null>(null);
const item = this.items[i]; const selectedIndex = ref<number>(0);
if (item[this.valueField] && item[this.valueField] > 0 && (!this.hiddenField || !item[this.hiddenField])) { const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark);
totalValidValue += item[this.valueField];
}
}
const validItems = []; const itemsMap = computed<Record<string, Record<string, unknown>>>(() => {
const map: Record<string, Record<string, unknown>> = {};
for (let i = 0; i < this.items.length; i++) { for (let i = 0; i < props.items.length; i++) {
const item = this.items[i]; const item = props.items[i];
let id = '';
if (item[this.valueField] && item[this.valueField] > 0 && if (props.idField && item[props.idField]) {
(!this.hiddenField || !item[this.hiddenField]) && id = item[props.idField] as string;
(!this.minValidPercent || item[this.valueField] / totalValidValue > this.minValidPercent)) { } else {
const finalItem = { id = item[props.nameField] as string;;
id: (this.idField && item[this.idField]) ? item[this.idField] : item[this.nameField],
name: (this.idField && item[this.idField]) ? item[this.idField] : item[this.nameField],
displayName: item[this.nameField],
value: item[this.valueField],
percent: (item[this.percentField] > 0 || item[this.percentField] === 0 || item[this.percentField] === '0') ? item[this.percentField] : (item[this.valueField] / totalValidValue * 100),
actualPercent: item[this.valueField] / totalValidValue,
itemStyle: {
color: this.getColor(item[this.colorField] ? item[this.colorField] : DEFAULT_CHART_COLORS[validItems.length % DEFAULT_CHART_COLORS.length]),
},
selected: true,
sourceItem: item
};
finalItem.displayPercent = formatPercent(finalItem.percent, 2, '&lt;0.01');
finalItem.displayValue = this.getDisplayCurrency(finalItem.value, this.defaultCurrency);
validItems.push(finalItem);
}
}
return validItems;
},
hasUnselectedItem: function () {
for (let i = 0; i < this.validItems.length; i++) {
const item = this.validItems[i];
if (this.selectedLegends && !this.selectedLegends[item.id]) {
return true;
}
}
return false;
},
firstItemAndHalfCurrentItemTotalPercent: function () {
let totalValue = 0;
let firstValue = null;
let firstToCurrentTotalValue = 0;
for (let i = 0; i < this.validItems.length; i++) {
const item = this.validItems[i];
if (this.selectedLegends && !this.selectedLegends[item.id]) {
continue;
}
if (firstValue === null) {
firstValue = item.value;
}
if (firstValue !== null) {
if (i < this.selectedIndex) {
firstToCurrentTotalValue += item.value;
} else if (i === this.selectedIndex) {
firstToCurrentTotalValue += item.value / 2;
}
}
totalValue += item.value;
}
if (firstToCurrentTotalValue && totalValue > 0) {
return firstToCurrentTotalValue / totalValue;
} else {
return 0;
}
},
chartOptions: function () {
const self = this;
return {
tooltip: {
trigger: 'item',
backgroundColor: self.isDarkMode ? '#333' : '#fff',
borderColor: self.isDarkMode ? '#333' : '#fff',
textStyle: {
color: self.isDarkMode ? '#eee' : '#333'
},
formatter: params => {
const name = params.data ? params.data.displayName : '';
const value = params.data ? params.data.displayValue : self.getDisplayCurrency(params.value);
let percent = params.data ? params.data.displayPercent : (params.percent + '%');
if (self.hasUnselectedItem) {
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: self.validItems.map(item => item.name),
selected: self.selectedLegends,
textStyle: {
color: self.isDarkMode ? '#eee' : '#333'
},
formatter: id => {
return self.itemsMap[id] && self.nameField && self.itemsMap[id][self.nameField] ? self.itemsMap[id][self.nameField] : id;
}
},
series: [
{
type: 'pie',
data: self.validItems,
top: 50,
startAngle: -90 + self.firstItemAndHalfCurrentItemTotalPercent * 360,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
}
},
label: {
color: self.isDarkMode ? '#eee' : '#333',
formatter: params => {
return params.data ? params.data.displayName : '';
}
},
animation: !self.skeleton
}
],
media: [
{
query: {
minWidth: 600
},
option: {
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
type: 'pie',
top: 0
}
]
}
}
]
}
} }
},
watch: {
'items': function () {
this.selectedIndex = 0;
}
},
setup() {
const theme = useTheme();
return { map[id] = item;
globalTheme: theme }
};
},
methods: {
clickItem: function (e) {
if (!this.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') { return map;
this.selectedIndex = e.dataIndex; });
return;
}
if (!e.data || !e.data.sourceItem) { const validItems = computed<DesktopPieChartDataItem[]>(() => {
return; let totalValidValue = 0;
}
this.$emit('click', e.data.sourceItem); for (let i = 0; i < props.items.length; i++) {
}, const item = props.items[i];
onLegendSelectChanged: function (e) { const value = item[props.valueField];
this.selectedLegends = e.selected;
const selectedItem = this.validItems[this.selectedIndex];
if (!selectedItem || !this.selectedLegends[selectedItem.id]) { if (isNumber(value) && value > 0 && (!props.hiddenField || !item[props.hiddenField])) {
let newSelectedIndex = 0; totalValidValue += value;
for (let i = 0; i < this.validItems.length; i++) {
const item = this.validItems[i];
if (this.selectedLegends[item.id]) {
newSelectedIndex = i;
break;
}
}
this.selectedIndex = newSelectedIndex;
}
},
getColor: function (color) {
if (color && color !== DEFAULT_ICON_COLOR) {
color = '#' + color;
}
return color;
},
getDisplayCurrency(value, currencyCode) {
return this.$locale.formatAmountWithCurrency(this.settingsStore, this.userStore, value, currencyCode);
} }
} }
const validItems: DesktopPieChartDataItem[] = [];
for (let i = 0; i < props.items.length; i++) {
const item = props.items[i];
const value = item[props.valueField];
const percent = props.percentField ? item[props.percentField] : -1;
if (isNumber(value) && value > 0 &&
(!props.hiddenField || !item[props.hiddenField]) &&
(!props.minValidPercent || value / totalValidValue > props.minValidPercent)) {
const finalItem: DesktopPieChartDataItem = {
id: (props.idField && item[props.idField]) ? item[props.idField] as string : item[props.nameField] as string,
name: (props.idField && item[props.idField]) ? item[props.idField] as string : item[props.nameField] as string,
displayName: item[props.nameField] as string,
value: value,
percent: (isNumber(percent) && percent >= 0) ? percent : (value / totalValidValue * 100),
actualPercent: value / totalValidValue,
itemStyle: {
color: getColor((props.colorField && item[props.colorField]) ? item[props.colorField] as ColorValue : DEFAULT_CHART_COLORS[validItems.length % DEFAULT_CHART_COLORS.length]),
},
selected: true,
sourceItem: item
};
finalItem.displayPercent = formatPercent(finalItem.percent, 2, '&lt;0.01');
finalItem.displayValue = formatAmountWithCurrency(finalItem.value, props.defaultCurrency) as string;
validItems.push(finalItem);
}
}
return validItems;
});
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(() => {
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: validItems.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;
}
}
watch(() => props.items, () => {
selectedIndex.value = 0;
});
</script> </script>
<style scoped> <style scoped>