From e88d803232d4c98e860878547b637d9b8b739442 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sun, 9 Jun 2024 02:31:13 +0800 Subject: [PATCH] add trends analysis chart --- src/components/desktop/TrendsChart.vue | 293 ++++++++++++++++++ src/consts/statistics.js | 2 +- src/desktop-main.js | 5 +- src/lib/datetime.js | 37 +++ src/stores/statistics.js | 100 +++++- .../desktop/statistics/TransactionPage.vue | 70 ++++- .../mobile/statistics/TransactionPage.vue | 2 +- 7 files changed, 496 insertions(+), 13 deletions(-) create mode 100644 src/components/desktop/TrendsChart.vue diff --git a/src/components/desktop/TrendsChart.vue b/src/components/desktop/TrendsChart.vue new file mode 100644 index 00000000..dc0b979d --- /dev/null +++ b/src/components/desktop/TrendsChart.vue @@ -0,0 +1,293 @@ + + + + + diff --git a/src/consts/statistics.js b/src/consts/statistics.js index 0cfbb6eb..4dcf5d27 100644 --- a/src/consts/statistics.js +++ b/src/consts/statistics.js @@ -39,7 +39,7 @@ const allTrendChartTypesArray = [ } ]; -const defaultTrendChartType = allTrendChartTypes.Area; +const defaultTrendChartType = allTrendChartTypes.Column; const allChartDataTypes = { ExpenseByAccount: { diff --git a/src/desktop-main.js b/src/desktop-main.js index 3a8a2a45..add34426 100644 --- a/src/desktop-main.js +++ b/src/desktop-main.js @@ -50,7 +50,7 @@ import 'vuetify/styles'; import * as echarts from 'echarts/core'; import { CanvasRenderer } from 'echarts/renderers'; -import { BarChart, PieChart } from 'echarts/charts'; +import { LineChart, BarChart, PieChart } from 'echarts/charts'; import { GridComponent, TooltipComponent, @@ -92,6 +92,7 @@ import StepsBar from '@/components/desktop/StepsBar.vue'; import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue'; import SnackBar from '@/components/desktop/SnackBar.vue'; import PieChartComponent from '@/components/desktop/PieChart.vue'; +import TrendsChartComponent from '@/components/desktop/TrendsChart.vue'; import DateRangeSelectionDialog from '@/components/desktop/DateRangeSelectionDialog.vue'; import MonthRangeSelectionDialog from '@/components/desktop/MonthRangeSelectionDialog.vue'; import SwitchToMobileDialog from '@/components/desktop/SwitchToMobileDialog.vue'; @@ -422,6 +423,7 @@ const vuetify = createVuetify({ echarts.use([ CanvasRenderer, + LineChart, BarChart, PieChart, GridComponent, @@ -453,6 +455,7 @@ app.component('StepsBar', StepsBar); app.component('ConfirmDialog', ConfirmDialog); app.component('SnackBar', SnackBar); app.component('PieChart', PieChartComponent); +app.component('TrendsChart', TrendsChartComponent); app.component('DateRangeSelectionDialog', DateRangeSelectionDialog); app.component('MonthRangeSelectionDialog', MonthRangeSelectionDialog); app.component('SwitchToMobileDialog', SwitchToMobileDialog); diff --git a/src/lib/datetime.js b/src/lib/datetime.js index 7e6b7fcf..9a07b5b5 100644 --- a/src/lib/datetime.js +++ b/src/lib/datetime.js @@ -328,6 +328,43 @@ export function getYearMonthLastUnixTime(yearMonth) { return moment.unix(getYearMonthFirstUnixTime(yearMonth)).add(1, 'months').subtract(1, 'seconds').unix(); } +export function getAllYearMonthUnixTimesBetweenStartYearMonthAndEndYearMonth(startYearMonth, endYearMonth) { + if (isString(startYearMonth)) { + startYearMonth = getYearMonthObjectFromString(startYearMonth); + } + + if (isString(endYearMonth)) { + endYearMonth = getYearMonthObjectFromString(endYearMonth); + } + + const allYearMonthTimes = []; + + for (let year = startYearMonth.year, month = startYearMonth.month; year <= endYearMonth.year || month <= endYearMonth.month; ) { + const yearMonthTime = { + year: year, + month: month + }; + + yearMonthTime.minUnixTime = getYearMonthFirstUnixTime(yearMonthTime); + yearMonthTime.maxUnixTime = getYearMonthLastUnixTime(yearMonthTime); + + allYearMonthTimes.push(yearMonthTime); + + if (year === endYearMonth.year && month === endYearMonth.month) { + break; + } + + if (month >= 11) { + year++; + month = 0; + } else { + month++; + } + } + + return allYearMonthTimes; +} + export function getDateTimeFormatType(allFormatMap, allFormatArray, localeDefaultFormatTypeName, systemDefaultFormatType, formatTypeValue) { if (formatTypeValue > dateTimeConstants.defaultDateTimeFormatValue && allFormatArray[formatTypeValue - 1] && allFormatArray[formatTypeValue - 1].key) { return allFormatArray[formatTypeValue - 1]; diff --git a/src/stores/statistics.js b/src/stores/statistics.js index 8d74b97e..7c87c326 100644 --- a/src/stores/statistics.js +++ b/src/stores/statistics.js @@ -422,6 +422,95 @@ export const useStatisticsStore = defineStore('statistics', { totalAmount: combinedData.totalAmount, items: allStatisticsItems }; + }, + transactionCategoryTrendsDataWithCategoryAndAccountInfo(state) { + const trendsData = state.transactionCategoryTrendsData; + const finalTrendsData = []; + + if (trendsData && trendsData.length) { + const userStore = useUserStore(); + const accountsStore = useAccountsStore(); + const transactionCategoriesStore = useTransactionCategoriesStore(); + const exchangeRatesStore = useExchangeRatesStore(); + + for (let i = 0; i < trendsData.length; i++) { + const trendItem = trendsData[i]; + const finalTrendItem = { + year: trendItem.year, + month: trendItem.month, + items: [] + }; + + if (trendItem && trendItem.items && trendItem.items.length) { + finalTrendItem.items = assembleAccountAndCategoryInfo(userStore, accountsStore, transactionCategoriesStore, exchangeRatesStore, trendItem.items); + } + + finalTrendsData.push(finalTrendItem); + } + } + + return finalTrendsData; + }, + trendsAnalysisData(state) { + if (!state.transactionCategoryTrendsDataWithCategoryAndAccountInfo || !state.transactionCategoryTrendsDataWithCategoryAndAccountInfo.length) { + return null; + } + + const combinedDataMap = {}; + + for (let i = 0; i < state.transactionCategoryTrendsDataWithCategoryAndAccountInfo.length; i++) { + const trendItem = state.transactionCategoryTrendsDataWithCategoryAndAccountInfo[i]; + const totalAmountItems = getCategoryTotalAmountItems(trendItem.items, state.transactionStatisticsFilter); + + for (let id in totalAmountItems.items) { + if (!Object.prototype.hasOwnProperty.call(totalAmountItems.items, id)) { + continue; + } + + const item = totalAmountItems.items[id]; + let combinedData = combinedDataMap[id]; + + if (!combinedData) { + combinedData = { + name: item.name, + type: item.type, + id: item.id, + icon: item.icon, + color: item.color, + hidden: item.hidden, + displayOrders: item.displayOrders, + totalAmount: 0, + items: [] + }; + } + + combinedData.items.push({ + year: trendItem.year, + month: trendItem.month, + totalAmount: item.totalAmount + }); + + combinedData.totalAmount += item.totalAmount; + combinedDataMap[id] = combinedData; + } + } + + const totalAmountsTrends = []; + + for (let id in combinedDataMap) { + if (!Object.prototype.hasOwnProperty.call(combinedDataMap, id)) { + continue; + } + + const trendData = combinedDataMap[id]; + totalAmountsTrends.push(trendData); + } + + sortCategoryTotalAmountItems(totalAmountsTrends, state.transactionStatisticsFilter); + + return { + items: totalAmountsTrends + }; } }, actions: { @@ -625,7 +714,7 @@ export const useStatisticsStore = defineStore('statistics', { this.transactionStatisticsFilter.sortingType = filter.sortingType; } }, - getTransactionListPageParams(item) { + getTransactionListPageParams(analysisType, item, dateRange) { const querys = []; if (this.transactionStatisticsFilter.chartDataType === statisticsConstants.allChartDataTypes.IncomeByAccount.type @@ -650,7 +739,8 @@ export const useStatisticsStore = defineStore('statistics', { querys.push('categoryId=' + item.id); } - if (this.transactionStatisticsFilter.chartDataType !== statisticsConstants.allChartDataTypes.AccountTotalAssets.type + if (analysisType === statisticsConstants.allAnalysisTypes.CategoricalAnalysis + && this.transactionStatisticsFilter.chartDataType !== statisticsConstants.allChartDataTypes.AccountTotalAssets.type && this.transactionStatisticsFilter.chartDataType !== statisticsConstants.allChartDataTypes.AccountTotalLiabilities.type) { querys.push('dateType=' + this.transactionStatisticsFilter.categoricalChartDateType); @@ -658,6 +748,10 @@ export const useStatisticsStore = defineStore('statistics', { querys.push('minTime=' + this.transactionStatisticsFilter.categoricalChartStartTime); querys.push('maxTime=' + this.transactionStatisticsFilter.categoricalChartEndTime); } + } else if (analysisType === statisticsConstants.allAnalysisTypes.TrendAnalysis && dateRange) { + querys.push('dateType=' + dateRange.type); + querys.push('minTime=' + dateRange.minTime); + querys.push('maxTime=' + dateRange.maxTime); } return querys.join('&'); @@ -709,8 +803,6 @@ export const useStatisticsStore = defineStore('statistics', { const settingsStore = useSettingsStore(); return new Promise((resolve, reject) => { - resolve(true); - services.getTransactionStatisticsTrends({ startYearMonth: self.transactionStatisticsFilter.trendChartStartYearMonth, endYearMonth: self.transactionStatisticsFilter.trendChartEndYearMonth, diff --git a/src/views/desktop/statistics/TransactionPage.vue b/src/views/desktop/statistics/TransactionPage.vue index a7de0b18..bd4f2428 100644 --- a/src/views/desktop/statistics/TransactionPage.vue +++ b/src/views/desktop/statistics/TransactionPage.vue @@ -131,7 +131,8 @@ + v-else-if="!initing && ((analysisType === allAnalysisTypes.CategoricalAnalysis && (!categoricalAnalysisData || !categoricalAnalysisData.items || !categoricalAnalysisData.items.length)) + || (analysisType === allAnalysisTypes.TrendAnalysis && (!trendsAnalysisData || !trendsAnalysisData.items || !trendsAnalysisData.items.length)))"> {{ $t('No transaction data') }} @@ -220,6 +221,37 @@ + + + + + @@ -446,6 +478,9 @@ export default { categoricalAnalysisData() { return this.statisticsStore.categoricalAnalysisData; }, + trendsAnalysisData() { + return this.statisticsStore.trendsAnalysisData; + }, statisticsTextColor() { if (this.query.chartDataType === this.allChartDataTypes.ExpenseByAccount.type || this.query.chartDataType === this.allChartDataTypes.ExpenseByPrimaryCategory.type || @@ -470,11 +505,21 @@ export default { }, watch: { 'analysisType': function (newValue) { - if (!isChartDataTypeAvailableForAnalysisType(this.query.chartDataType, newValue)) { - this.query.chartDataType = statisticsConstants.defaultChartDataType; + const self = this; + + if (!isChartDataTypeAvailableForAnalysisType(self.query.chartDataType, newValue)) { + self.query.chartDataType = statisticsConstants.defaultChartDataType; } - this.reload(null); + self.initing = true; + + const promise = self.reload(null); + + if (promise) { + promise.then(() => { + self.initing = false; + }); + } }, 'query.chartDataType': function (newValue) { this.statisticsStore.updateTransactionStatisticsFilter({ @@ -572,6 +617,8 @@ export default { } }); } + + return dispatchPromise; }, setChartType(chartType) { if (this.analysisType === statisticsConstants.allAnalysisTypes.CategoricalAnalysis) { @@ -736,6 +783,17 @@ export default { clickPieChartItem(item) { this.$router.push(this.getItemLinkUrl(item)); }, + clickTrendChartItem(item) { + const minUnixTime = getYearMonthFirstUnixTime(item.yearMonth); + const maxUnixTime = getYearMonthLastUnixTime(item.yearMonth); + const dateRangeType = getDateTypeByDateRange(minUnixTime, maxUnixTime, this.firstDayOfWeek, datetimeConstants.allDateRangeScenes.Normal); + + this.$router.push(this.getItemLinkUrl(item.item, { + minTime: minUnixTime, + maxTime: maxUnixTime, + type: dateRangeType, + })); + }, getDisplayAmount(amount, currency, textLimit) { amount = this.getDisplayCurrency(amount, currency); @@ -761,8 +819,8 @@ export default { getDisplayPercent(value, precision, lowPrecisionValue) { return formatPercent(value, precision, lowPrecisionValue); }, - getItemLinkUrl(item) { - return `/transaction/list?${this.statisticsStore.getTransactionListPageParams(item)}`; + getItemLinkUrl(item, dateRange) { + return `/transaction/list?${this.statisticsStore.getTransactionListPageParams(this.analysisType, item, dateRange)}`; } } } diff --git a/src/views/mobile/statistics/TransactionPage.vue b/src/views/mobile/statistics/TransactionPage.vue index b1b3df82..c0ca27fb 100644 --- a/src/views/mobile/statistics/TransactionPage.vue +++ b/src/views/mobile/statistics/TransactionPage.vue @@ -569,7 +569,7 @@ export default { return formatPercent(value, precision, lowPrecisionValue); }, getItemLinkUrl(item) { - return `/transaction/list?${this.statisticsStore.getTransactionListPageParams(item)}`; + return `/transaction/list?${this.statisticsStore.getTransactionListPageParams(statisticsConstants.allAnalysisTypes.CategoricalAnalysis, item)}`; } } };