Skip to content

Commit

Permalink
trend analysis supports aggregating amounts by month / quarter / year
Browse files Browse the repository at this point in the history
  • Loading branch information
mayswind committed Nov 5, 2024
1 parent c3a880e commit fe35cba
Show file tree
Hide file tree
Showing 9 changed files with 311 additions and 53 deletions.
123 changes: 95 additions & 28 deletions src/components/desktop/TrendsChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ import { useSettingsStore } from '@/stores/setting.js';
import { useUserStore } from '@/stores/user.js';
import colorConstants from '@/consts/color.js';
import datetimeConstants from '@/consts/datetime.js';
import statisticsConstants from '@/consts/statistics.js';
import { isNumber } from '@/lib/common.js';
import {
getYearMonthStringFromObject,
getAllYearMonthUnixTimesBetweenStartYearMonthAndEndYearMonth
isArray,
isNumber
} from '@/lib/common.js';
import {
getAllYearsStartAndEndUnixTimes,
getAllQuartersStartAndEndUnixTimes,
getAllMonthsStartAndEndUnixTimes,
getDateTypeByDateRange
} from '@/lib/datetime.js';
import {
sortStatisticsItems
Expand All @@ -29,6 +35,7 @@ export default {
'startYearMonth',
'endYearMonth',
'sortingType',
'dateAggregationType',
'idField',
'nameField',
'valueField',
Expand Down Expand Up @@ -67,15 +74,21 @@ export default {
id = this.getItemName(item[this.nameField]);
}
map[id] = item;
map[id] = {
[this.idField || 'id']: id,
[this.nameField || 'name']: item[this.nameField],
[this.hiddenField || 'hidden']: item[this.hiddenField],
[this.displayOrdersField || 'displayOrders']: item[this.displayOrdersField]
};
}
return map;
},
allYearMonthTimes: function () {
if (this.startYearMonth && this.endYearMonth) {
return getAllYearMonthUnixTimesBetweenStartYearMonthAndEndYearMonth(this.startYearMonth, this.endYearMonth);
} else if (this.items && this.items.length) {
allDateRanges: function () {
let startYearMonth = this.startYearMonth;
let endYearMonth = this.endYearMonth;
if ((!this.startYearMonth || !this.endYearMonth) && this.items && this.items.length) {
let minYear = Number.MAX_SAFE_INTEGER, minMonth = Number.MAX_SAFE_INTEGER, maxYear = 0, maxMonth = 0;
for (let i = 0; i < this.items.length; i++) {
Expand All @@ -96,20 +109,37 @@ export default {
}
}
return getAllYearMonthUnixTimesBetweenStartYearMonthAndEndYearMonth(`${minYear}-${minMonth}`, `${maxYear}-${maxMonth}`);
startYearMonth = `${minYear}-${minMonth}`;
endYearMonth = `${maxYear}-${maxMonth}`;
}
return [];
if (!startYearMonth || !endYearMonth) {
return [];
}
if (this.dateAggregationType === statisticsConstants.allDateAggregationTypes.Year.type) {
return getAllYearsStartAndEndUnixTimes(startYearMonth, endYearMonth);
} else if (this.dateAggregationType === statisticsConstants.allDateAggregationTypes.Quarter.type) {
return getAllQuartersStartAndEndUnixTimes(startYearMonth, endYearMonth);
} else { // if (this.dateAggregationType === statisticsConstants.allDateAggregationTypes.Month.type) {
return getAllMonthsStartAndEndUnixTimes(startYearMonth, endYearMonth);
}
},
allDisplayMonths: function () {
const allDisplayMonths = [];
for (let i = 0; i < this.allYearMonthTimes.length; i++) {
const yearMonthTime = this.allYearMonthTimes[i];
allDisplayMonths.push(this.$locale.formatUnixTimeToShortYearMonth(this.userStore, yearMonthTime.minUnixTime));
allDisplayDateRanges: function () {
const allDisplayDateRanges = [];
for (let i = 0; i < this.allDateRanges.length; i++) {
const dateRange = this.allDateRanges[i];
if (this.dateAggregationType === statisticsConstants.allDateAggregationTypes.Year.type) {
allDisplayDateRanges.push(this.$locale.formatUnixTimeToShortYear(this.userStore, dateRange.minUnixTime));
} else if (this.dateAggregationType === statisticsConstants.allDateAggregationTypes.Quarter.type) {
allDisplayDateRanges.push(this.$locale.formatYearQuarter(dateRange.year, dateRange.quarter));
} else { // if (this.dateAggregationType === statisticsConstants.allDateAggregationTypes.Month.type) {
allDisplayDateRanges.push(this.$locale.formatUnixTimeToShortYearMonth(this.userStore, dateRange.minUnixTime));
}
}
return allDisplayMonths;
return allDisplayDateRanges;
},
allSeries: function () {
const allSeries = [];
Expand All @@ -122,20 +152,49 @@ export default {
}
const allAmounts = [];
const yearMonthDataMap = {};
const dateRangeAmountMap = {};
for (let j = 0; j < item.items.length; j++) {
const dataItem = item.items[j];
yearMonthDataMap[`${dataItem.year}-${dataItem.month}`] = dataItem;
let dateRangeKey = '';
if (this.dateAggregationType === statisticsConstants.allDateAggregationTypes.Year.type) {
dateRangeKey = dataItem.year;
} else if (this.dateAggregationType === statisticsConstants.allDateAggregationTypes.Quarter.type) {
dateRangeKey = `${dataItem.year}-${Math.floor((dataItem.month - 1) / 3) + 1}`;
} else { // if (this.dateAggregationType === statisticsConstants.allDateAggregationTypes.Month.type) {
dateRangeKey = `${dataItem.year}-${dataItem.month}`;
}
const dataItems = dateRangeAmountMap[dateRangeKey] || [];
dataItems.push(dataItem);
dateRangeAmountMap[dateRangeKey] = dataItems;
}
for (let j = 0; j < this.allYearMonthTimes.length; j++) {
const yearMonth = getYearMonthStringFromObject(this.allYearMonthTimes[j]);
const dataItem = yearMonthDataMap[yearMonth];
for (let j = 0; j < this.allDateRanges.length; j++) {
const dateRange = this.allDateRanges[j];
let dateRangeKey = '';
if (this.dateAggregationType === statisticsConstants.allDateAggregationTypes.Year.type) {
dateRangeKey = dateRange.year;
} else if (this.dateAggregationType === statisticsConstants.allDateAggregationTypes.Quarter.type) {
dateRangeKey = `${dateRange.year}-${dateRange.quarter}`;
} else { // if (this.dateAggregationType === statisticsConstants.allDateAggregationTypes.Month.type) {
dateRangeKey = `${dateRange.year}-${dateRange.month + 1}`;
}
let amount = 0;
const dataItems = dateRangeAmountMap[dateRangeKey];
if (dataItem && isNumber(dataItem[this.valueField])) {
amount = dataItem[this.valueField];
if (isArray(dataItems)) {
for (let i = 0; i < dataItems.length; i++) {
const dataItem = dataItems[i];
if (isNumber(dataItem[this.valueField])) {
amount += dataItem[this.valueField];
}
}
}
allAmounts.push(amount);
Expand Down Expand Up @@ -293,7 +352,7 @@ export default {
xAxis: [
{
type: 'category',
data: self.allDisplayMonths
data: self.allDisplayDateRanges
}
],
yAxis: [
Expand Down Expand Up @@ -332,11 +391,19 @@ export default {
const id = e.seriesId;
const item = this.itemsMap[id];
const yearMonthTime = this.allYearMonthTimes[e.dataIndex];
const itemId = this.idField ? item[this.idField] : '';
const dateRange = this.allDateRanges[e.dataIndex];
const minUnixTime = dateRange.minUnixTime;
const maxUnixTime = dateRange.maxUnixTime;
const dateRangeType = getDateTypeByDateRange(minUnixTime, maxUnixTime, this.userStore.currentUserFirstDayOfWeek, datetimeConstants.allDateRangeScenes.Normal);
this.$emit('click', {
yearMonth: getYearMonthStringFromObject(yearMonthTime),
item: item
itemId: itemId,
dateRange: {
minTime: minUnixTime,
maxTime: maxUnixTime,
type: dateRangeType
}
});
},
getColor: function (color) {
Expand Down
26 changes: 26 additions & 0 deletions src/consts/statistics.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,29 @@ const allSortingTypesArray = [

const defaultSortingType = allSortingTypes.Amount.type;

const allDateAggregationTypes = {
Month: {
type: 0,
name: 'Aggregate by Month'
},
Quarter: {
type: 1,
name: 'Aggregate by Quarter'
},
Year: {
type: 2,
name: 'Aggregate by Year'
}
};

const allDateAggregationTypesArray = [
allDateAggregationTypes.Month,
allDateAggregationTypes.Quarter,
allDateAggregationTypes.Year
]

const defaultDateAggregationType = allDateAggregationTypes.Month.type;

export default {
allAnalysisTypes: allAnalysisTypes,
allCategoricalChartTypes: allCategoricalChartTypes,
Expand All @@ -185,4 +208,7 @@ export default {
allSortingTypes: allSortingTypes,
allSortingTypesArray: allSortingTypesArray,
defaultSortingType: defaultSortingType,
allDateAggregationTypes: allDateAggregationTypes,
allDateAggregationTypesArray: allDateAggregationTypesArray,
defaultDateAggregationType: defaultDateAggregationType,
};
80 changes: 79 additions & 1 deletion src/lib/datetime.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,22 @@ export function getSpecifiedDayFirstUnixTime(unixTime) {
return moment.unix(unixTime).set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).unix();
}

export function getYearFirstUnixTime(year) {
return moment().set({ year: year, month: 0, date: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }).unix();
}

export function getYearLastUnixTime(year) {
return moment.unix(getYearFirstUnixTime(year)).add(1, 'years').subtract(1, 'seconds').unix();
}

export function getQuarterFirstUnixTime(yearQuarter) {
return moment().set({ year: yearQuarter.year, month: (yearQuarter.quarter - 1) * 3, date: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }).unix();
}

export function getQuarterLastUnixTime(yearQuarter) {
return moment.unix(getQuarterFirstUnixTime(yearQuarter)).add(3, 'months').subtract(1, 'seconds').unix();
}

export function getYearMonthFirstUnixTime(yearMonth) {
if (isString(yearMonth)) {
yearMonth = getYearMonthObjectFromString(yearMonth);
Expand All @@ -301,7 +317,69 @@ export function getYearMonthLastUnixTime(yearMonth) {
return moment.unix(getYearMonthFirstUnixTime(yearMonth)).add(1, 'months').subtract(1, 'seconds').unix();
}

export function getAllYearMonthUnixTimesBetweenStartYearMonthAndEndYearMonth(startYearMonth, endYearMonth) {
export function getAllYearsStartAndEndUnixTimes(startYearMonth, endYearMonth) {
if (isString(startYearMonth)) {
startYearMonth = getYearMonthObjectFromString(startYearMonth);
}

if (isString(endYearMonth)) {
endYearMonth = getYearMonthObjectFromString(endYearMonth);
}

const allYearTimes = [];

for (let year = startYearMonth.year; year <= endYearMonth.year; year++) {
const yearTime = {
year: year
};

yearTime.minUnixTime = getYearFirstUnixTime(year);
yearTime.maxUnixTime = getYearLastUnixTime(year);

allYearTimes.push(yearTime);
}

return allYearTimes;
}

export function getAllQuartersStartAndEndUnixTimes(startYearMonth, endYearMonth) {
if (isString(startYearMonth)) {
startYearMonth = getYearMonthObjectFromString(startYearMonth);
}

if (isString(endYearMonth)) {
endYearMonth = getYearMonthObjectFromString(endYearMonth);
}

const allYearQuarterTimes = [];

for (let year = startYearMonth.year, month = startYearMonth.month; year < endYearMonth.year || (year === endYearMonth.year && ((month / 3) <= (endYearMonth.month / 3))); ) {
const yearQuarterTime = {
year: year,
quarter: Math.floor((month / 3)) + 1
};

yearQuarterTime.minUnixTime = getQuarterFirstUnixTime(yearQuarterTime);
yearQuarterTime.maxUnixTime = getQuarterLastUnixTime(yearQuarterTime);

allYearQuarterTimes.push(yearQuarterTime);

if (year === endYearMonth.year && month >= endYearMonth.month) {
break;
}

if (month >= 9) {
year++;
month = 0;
} else {
month += 3;
}
}

return allYearQuarterTimes;
}

export function getAllMonthsStartAndEndUnixTimes(startYearMonth, endYearMonth) {
if (isString(startYearMonth)) {
startYearMonth = getYearMonthObjectFromString(startYearMonth);
}
Expand Down
32 changes: 32 additions & 0 deletions src/lib/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,17 @@ function getI18nShortTimeFormat(translateFn, formatTypeValue) {
return getDateTimeFormat(translateFn, datetimeConstants.allShortTimeFormat, datetimeConstants.allShortTimeFormatArray, 'format.shortTime', defaultShortTimeFormatTypeName, datetimeConstants.defaultShortTimeFormat, formatTypeValue);
}

function formatYearQuarter(translateFn, year, quarter) {
if (1 <= quarter && quarter <= 4) {
return translateFn('format.yearQuarter.q' + quarter, {
year: year,
quarter: quarter
});
} else {
return '';
}
}

function isLongTime24HourFormat(translateFn, formatTypeValue) {
const defaultLongTimeFormatTypeName = translateFn('default.longTimeFormat');
const type = getDateTimeFormatType(datetimeConstants.allLongTimeFormat, datetimeConstants.allLongTimeFormatArray, defaultLongTimeFormatTypeName, datetimeConstants.defaultLongTimeFormat, formatTypeValue);
Expand Down Expand Up @@ -1142,6 +1153,25 @@ function getAllStatisticsSortingTypes(translateFn) {
return allSortingTypes;
}

function getAllStatisticsDateAggregationTypes(translateFn) {
const aggregationTypes = [];

for (const aggregationTypeField in statisticsConstants.allDateAggregationTypes) {
if (!Object.prototype.hasOwnProperty.call(statisticsConstants.allDateAggregationTypes, aggregationTypeField)) {
continue;
}

const aggregationType = statisticsConstants.allDateAggregationTypes[aggregationTypeField];

aggregationTypes.push({
type: aggregationType.type,
displayName: translateFn(aggregationType.name)
});
}

return aggregationTypes;
}

function getAllTransactionEditScopeTypes(translateFn) {
const allEditScopeTypes = [];

Expand Down Expand Up @@ -1632,6 +1662,7 @@ export function i18nFunctions(i18nGlobal) {
formatUnixTimeToShortMonthDay: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nShortMonthDayFormat(i18nGlobal.t, userStore.currentUserShortDateFormat), utcOffset, currentUtcOffset),
formatUnixTimeToLongTime: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nLongTimeFormat(i18nGlobal.t, userStore.currentUserLongTimeFormat), utcOffset, currentUtcOffset),
formatUnixTimeToShortTime: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nShortTimeFormat(i18nGlobal.t, userStore.currentUserShortTimeFormat), utcOffset, currentUtcOffset),
formatYearQuarter: (year, quarter) => formatYearQuarter(i18nGlobal.t, year, quarter),
isLongDateMonthAfterYear: (userStore) => isLongDateMonthAfterYear(i18nGlobal.t, userStore.currentUserLongDateFormat),
isShortDateMonthAfterYear: (userStore) => isShortDateMonthAfterYear(i18nGlobal.t, userStore.currentUserShortDateFormat),
isLongTime24HourFormat: (userStore) => isLongTime24HourFormat(i18nGlobal.t, userStore.currentUserLongTimeFormat),
Expand Down Expand Up @@ -1668,6 +1699,7 @@ export function i18nFunctions(i18nGlobal) {
getAllTrendChartTypes: () => getAllTrendChartTypes(i18nGlobal.t),
getAllStatisticsChartDataTypes: (analysisType) => getAllStatisticsChartDataTypes(i18nGlobal.t, analysisType),
getAllStatisticsSortingTypes: () => getAllStatisticsSortingTypes(i18nGlobal.t),
getAllStatisticsDateAggregationTypes: () => getAllStatisticsDateAggregationTypes(i18nGlobal.t),
getAllTransactionEditScopeTypes: () => getAllTransactionEditScopeTypes(i18nGlobal.t),
getAllTransactionScheduledFrequencyTypes: () => getAllTransactionScheduledFrequencyTypes(i18nGlobal.t),
getAllTransactionDefaultCategories: (categoryType, locale) => getAllTransactionDefaultCategories(categoryType, locale, i18nGlobal.t),
Expand Down
Loading

0 comments on commit fe35cba

Please sign in to comment.