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)}`;
}
}
};