show total income and total expense in categorical overview chart

This commit is contained in:
MaysWind
2025-11-02 00:28:22 +08:00
parent b690316aa7
commit f3ccd3b66d
5 changed files with 523 additions and 316 deletions

View File

@@ -14,33 +14,31 @@ import { useI18n } from '@/locales/helpers.ts';
import { useUserStore } from '@/stores/user.ts';
import type {
SortableTransactionStatisticDataItem,
TransactionStatisticResponseItemWithInfo
import {
type TransactionCategoricalOverviewAnalysisDataItem,
type TransactionCategoricalOverviewAnalysisDataItemOutflowItem,
TransactionCategoricalOverviewAnalysisDataItemType
} from '@/models/transaction.ts';
import type { Account } from '@/models/account.ts';
import { values } from '@/core/base.ts';
import { ThemeType } from '@/core/theme.ts';
import { CategoryType } from '@/core/category.ts';
import { TransactionRelatedAccountType } from '@/core/transaction.ts';
import { isNumber } from '@/lib/common.ts';
import { sortStatisticsItems } from '@/lib/statistics.ts';
import { getExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts';
enum SankeyChartDepth {
PrimaryIncomeCategory = 0,
SecondaryIncomeCategory = 1,
Account = 2,
AccountWithTransfer = 3,
AccountForIncome = 2,
AccountForExpense = 3,
SecondaryExpenseCategory = 4,
PrimaryExpenseCategory = 5
}
enum SankeyChartNodeItemType {
Account = 'account',
Category = 'category'
Category = 'category',
NetCashFlow = 'netCashFlow'
}
interface SankeyChartData {
@@ -48,16 +46,20 @@ interface SankeyChartData {
links: SankeyChartLinkItem[];
}
interface SankeyChartNodeItem extends SortableTransactionStatisticDataItem {
interface SankeyChartNodeItem {
dateItemType: TransactionCategoricalOverviewAnalysisDataItemType;
itemType: SankeyChartNodeItemType;
itemId: string;
name: string;
nameId: string;
displayName: string;
displayOrders: number[];
totalAmount: number;
accountNetCashFlow?: number;
percent?: number;
depth: number;
itemStyle?: {
color?: string;
opacity?: number;
}
}
interface SankeyChartLinkItem {
@@ -74,8 +76,7 @@ interface SankeyChartLinkItem {
const props = defineProps<{
skeleton?: boolean;
items: TransactionStatisticResponseItemWithInfo[];
sortingType: number;
items: TransactionCategoricalOverviewAnalysisDataItem[];
defaultCurrency?: string;
enableClickItem?: boolean;
}>();
@@ -87,230 +88,134 @@ const emit = defineEmits<{
const theme = useTheme();
const {
tt,
formatAmountToLocalizedNumeralsWithCurrency,
formatPercentToLocalizedNumerals
} = useI18n();
const userStore = useUserStore();
const overviewDataItemTypeSankeyChartNodeItemTypeMap: Record<TransactionCategoricalOverviewAnalysisDataItemType, SankeyChartNodeItemType> = {
[TransactionCategoricalOverviewAnalysisDataItemType.IncomeByPrimaryCategory]: SankeyChartNodeItemType.Category,
[TransactionCategoricalOverviewAnalysisDataItemType.IncomeBySecondaryCategory]: SankeyChartNodeItemType.Category,
[TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount]: SankeyChartNodeItemType.Account,
[TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByAccount]: SankeyChartNodeItemType.Account,
[TransactionCategoricalOverviewAnalysisDataItemType.NetCashFlow]: SankeyChartNodeItemType.NetCashFlow,
[TransactionCategoricalOverviewAnalysisDataItemType.ExpenseBySecondaryCategory]: SankeyChartNodeItemType.Category,
[TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByPrimaryCategory]: SankeyChartNodeItemType.Category
};
const overviewDataItemTypeSankeyChartNodeItemDepthMap: Record<TransactionCategoricalOverviewAnalysisDataItemType, number> = {
[TransactionCategoricalOverviewAnalysisDataItemType.IncomeByPrimaryCategory]: SankeyChartDepth.PrimaryIncomeCategory,
[TransactionCategoricalOverviewAnalysisDataItemType.IncomeBySecondaryCategory]: SankeyChartDepth.SecondaryIncomeCategory,
[TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount]: SankeyChartDepth.AccountForIncome,
[TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByAccount]: SankeyChartDepth.AccountForExpense,
[TransactionCategoricalOverviewAnalysisDataItemType.NetCashFlow]: SankeyChartDepth.SecondaryExpenseCategory,
[TransactionCategoricalOverviewAnalysisDataItemType.ExpenseBySecondaryCategory]: SankeyChartDepth.SecondaryExpenseCategory,
[TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByPrimaryCategory]: SankeyChartDepth.PrimaryExpenseCategory
};
const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark);
const sankeyData = computed<SankeyChartData>(() => {
const primaryIncomeCategoryNodesMap: Record<string, SankeyChartNodeItem> = {};
const secondaryIncomeCategoryNodesMap: Record<string, SankeyChartNodeItem> = {};
const incomeAccountNodesMap: Record<string, SankeyChartNodeItem> = {};
const expenseAccountNodesMap: Record<string, SankeyChartNodeItem> = {};
const secondaryExpenseCategoryNodesMap: Record<string, SankeyChartNodeItem> = {};
const primaryExpenseCategoryNodesMap: Record<string, SankeyChartNodeItem> = {};
const linksMap: Record<string, SankeyChartLinkItem> = {};
const accountsMap: Record<string, Account> = {};
const nodes: SankeyChartNodeItem[] = [];
const links: SankeyChartLinkItem[] = [];
for (const item of props.items) {
if (!item.primaryAccount || !item.account || !item.primaryCategory || !item.category || !item.amountInDefaultCurrency) {
if (item.hidden) {
continue;
}
if (item.account.hidden || item.primaryAccount.hidden || item.category.hidden || item.primaryCategory.hidden) {
const itemType = overviewDataItemTypeSankeyChartNodeItemTypeMap[item.type];
const depth = overviewDataItemTypeSankeyChartNodeItemDepthMap[item.type];
if (!itemType || itemType === SankeyChartNodeItemType.NetCashFlow || depth === undefined) {
continue;
}
if (item.relatedAccount && (item.relatedAccountType === TransactionRelatedAccountType.TransferFrom || item.relatedAccount.hidden || !item.relatedPrimaryAccount || item.relatedPrimaryAccount.hidden)) {
if (item.totalAmount === 0 && item.outflows.length === 0) {
continue;
}
const incomeAccountNameId = `income_account:${item.account.id}`;
const expenseAccountNameId = `expense_account:${item.account.id}`;
accountsMap[item.account.id] = item.account;
const nodeItem: SankeyChartNodeItem = {
dateItemType: item.type,
itemType: itemType,
itemId: item.id,
name: `${item.type}:${item.id}`,
displayName: item.name,
totalAmount: item.totalAmount,
percent: item.percent,
depth: depth
};
updateNodeItem(incomeAccountNodesMap, {
itemType: SankeyChartNodeItemType.Account,
id: item.account.id,
name: item.account.name,
nameId: incomeAccountNameId,
displayOrders: [item.account.displayOrder],
amount: item.primaryCategory.type == CategoryType.Income ? item.amountInDefaultCurrency : 0,
depth: SankeyChartDepth.Account
});
updateNodeItem(expenseAccountNodesMap, {
itemType: SankeyChartNodeItemType.Account,
id: item.account.id,
name: item.account.name,
nameId: expenseAccountNameId,
displayOrders: [item.account.displayOrder],
amount: item.primaryCategory.type == CategoryType.Expense ? item.amountInDefaultCurrency : 0,
depth: SankeyChartDepth.AccountWithTransfer
});
if (item.primaryCategory.type == CategoryType.Income) {
updateNodeItem(primaryIncomeCategoryNodesMap, {
itemType: SankeyChartNodeItemType.Category,
id: item.primaryCategory.id,
name: item.primaryCategory.name,
nameId: item.primaryCategory.id,
displayOrders: [item.primaryCategory.displayOrder],
amount: item.amountInDefaultCurrency,
depth: SankeyChartDepth.PrimaryIncomeCategory
});
updateNodeItem(secondaryIncomeCategoryNodesMap, {
itemType: SankeyChartNodeItemType.Category,
id: item.category.id,
name: item.category.name,
nameId: item.category.id,
displayOrders: [item.primaryCategory.displayOrder, item.category.displayOrder],
amount: item.amountInDefaultCurrency,
depth: SankeyChartDepth.SecondaryIncomeCategory
});
updateLinkItem(linksMap, {
sourceItemType: SankeyChartNodeItemType.Category,
sourceItemId: item.primaryCategory.id,
source: item.primaryCategory.id,
sourceName: item.primaryCategory.name,
targetItemType: SankeyChartNodeItemType.Category,
targetItemId: item.category.id,
target: item.category.id,
targetName: item.category.name,
value: item.amountInDefaultCurrency
});
updateLinkItem(linksMap, {
sourceItemType: SankeyChartNodeItemType.Category,
sourceItemId: item.category.id,
source: item.category.id,
sourceName: item.category.name,
targetItemType: SankeyChartNodeItemType.Account,
targetItemId: item.account.id,
target: incomeAccountNameId,
targetName: item.account.name,
value: item.amountInDefaultCurrency
});
} else if (item.primaryCategory.type == CategoryType.Expense) {
updateNodeItem(secondaryExpenseCategoryNodesMap, {
itemType: SankeyChartNodeItemType.Category,
id: item.category.id,
name: item.category.name,
nameId: item.category.id,
displayOrders: [item.primaryCategory.displayOrder, item.category.displayOrder],
amount: item.amountInDefaultCurrency,
depth: SankeyChartDepth.SecondaryExpenseCategory
});
updateNodeItem(primaryExpenseCategoryNodesMap, {
itemType: SankeyChartNodeItemType.Category,
id: item.primaryCategory.id,
name: item.primaryCategory.name,
nameId: item.primaryCategory.id,
displayOrders: [item.primaryCategory.displayOrder],
amount: item.amountInDefaultCurrency,
depth: SankeyChartDepth.PrimaryExpenseCategory
});
updateLinkItem(linksMap, {
sourceItemType: SankeyChartNodeItemType.Account,
sourceItemId: item.account.id,
source: expenseAccountNameId,
sourceName: item.account.name,
targetItemType: SankeyChartNodeItemType.Category,
targetItemId: item.category.id,
target: item.category.id,
targetName: item.category.name,
value: item.amountInDefaultCurrency
});
updateLinkItem(linksMap, {
sourceItemType: SankeyChartNodeItemType.Category,
sourceItemId: item.category.id,
source: item.category.id,
sourceName: item.category.name,
targetItemType: SankeyChartNodeItemType.Category,
targetItemId: item.primaryCategory.id,
target: item.primaryCategory.id,
targetName: item.primaryCategory.name,
value: item.amountInDefaultCurrency
});
} else if (item.primaryCategory.type == CategoryType.Transfer && item.relatedAccount) {
const relatedAccountNameId = `expense_account:${item.relatedAccount.id}`;
accountsMap[item.relatedAccount.id] = item.relatedAccount;
updateLinkItem(linksMap, {
sourceItemType: SankeyChartNodeItemType.Account,
sourceItemId: item.account.id,
source: incomeAccountNameId,
sourceName: item.account.name,
targetItemType: SankeyChartNodeItemType.Account,
targetItemId: item.relatedAccount.id,
target: relatedAccountNameId,
targetName: item.relatedAccount.name,
value: item.amountInDefaultCurrency
});
if (!isNumber(nodeItem.percent) && nodeItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount) {
nodeItem.itemStyle = {
color: '#aaa',
opacity: 0.5
};
}
}
for (const account of values(accountsMap)) {
const incomeAccountNameId = `income_account:${account.id}`;
const expenseAccountNameId = `expense_account:${account.id}`;
if (nodeItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByAccount) {
for (const outflowItem of item.outflows) {
if (outflowItem.relatedItem.type !== TransactionCategoricalOverviewAnalysisDataItemType.NetCashFlow) {
continue;
}
let totalOutflowAmount = 0;
let totalInflowAmount = 0;
for (const link of values(linksMap)) {
if (link.sourceItemType === SankeyChartNodeItemType.Account && link.sourceItemId === account.id) {
totalOutflowAmount += link.value;
} else if (link.targetItemType === SankeyChartNodeItemType.Account && link.targetItemId === account.id) {
totalInflowAmount += link.value;
nodeItem.accountNetCashFlow = (nodeItem.accountNetCashFlow ?? 0) + outflowItem.amount;
}
}
const amountDifference = totalOutflowAmount - totalInflowAmount;
nodes.push(nodeItem);
if (amountDifference > 0) {
updateNodeItem(incomeAccountNodesMap, {
itemType: SankeyChartNodeItemType.Account,
id: account.id,
name: account.name,
nameId: incomeAccountNameId,
displayOrders: [account.displayOrder],
amount: amountDifference,
depth: SankeyChartDepth.AccountWithTransfer
});
} else if (amountDifference < 0) {
updateNodeItem(expenseAccountNodesMap, {
itemType: SankeyChartNodeItemType.Account,
id: account.id,
name: account.name,
nameId: expenseAccountNameId,
displayOrders: [account.displayOrder],
amount: -amountDifference,
depth: SankeyChartDepth.AccountWithTransfer
});
const combinedOutflows: Record<string, TransactionCategoricalOverviewAnalysisDataItemOutflowItem> = {};
for (const outflowItem of item.outflows) {
const relatedItem = outflowItem.relatedItem;
if (!relatedItem) {
continue;
}
if (outflowItem.relatedItem) {
const key = `${item.type}:${item.id}-${outflowItem.relatedItem.type}:${outflowItem.relatedItem.id}`;
let combinedOutflow: TransactionCategoricalOverviewAnalysisDataItemOutflowItem | undefined = combinedOutflows[key];
if (!combinedOutflow) {
combinedOutflow = {
relatedItem: outflowItem.relatedItem,
amount: 0
};
combinedOutflows[key] = combinedOutflow;
}
combinedOutflow.amount += outflowItem.amount;
}
}
if (Math.abs(amountDifference) > 0) {
updateLinkItem(linksMap, {
sourceItemType: SankeyChartNodeItemType.Account,
sourceItemId: account.id,
source: incomeAccountNameId,
sourceName: account.name,
targetItemType: SankeyChartNodeItemType.Account,
targetItemId: account.id,
target: expenseAccountNameId,
targetName: account.name,
value: Math.abs(amountDifference)
});
for (const outflowItem of values(combinedOutflows)) {
const relatedItem = outflowItem.relatedItem;
const transferItemType = overviewDataItemTypeSankeyChartNodeItemTypeMap[relatedItem.type];
if (!transferItemType) {
continue;
}
const linkItem: SankeyChartLinkItem = {
sourceItemType: itemType,
sourceItemId: item.id,
source: `${item.type}:${item.id}`,
sourceDisplayName: item.name,
targetItemType: transferItemType,
targetItemId: relatedItem.id,
target: `${relatedItem.type}:${relatedItem.id}`,
targetDisplayName: relatedItem.name,
value: outflowItem.amount
};
links.push(linkItem);
}
}
const nodes: SankeyChartNodeItem[] = [];
const links: SankeyChartLinkItem[] = [];
addFinalSortedNodeItems(primaryIncomeCategoryNodesMap, nodes);
addFinalSortedNodeItems(secondaryIncomeCategoryNodesMap, nodes);
addFinalSortedNodeItems(incomeAccountNodesMap, nodes);
addFinalSortedNodeItems(expenseAccountNodesMap, nodes);
addFinalSortedNodeItems(secondaryExpenseCategoryNodesMap, nodes);
addFinalSortedNodeItems(primaryExpenseCategoryNodesMap, nodes);
addFinalLinkItems(linksMap, links);
const ret: SankeyChartData = {
nodes: nodes,
links: links
@@ -335,15 +240,44 @@ const chartOptions = computed<object>(() => {
const dataItem = params.data as SankeyChartNodeItem;
const value = dataItem.totalAmount;
const displayValue = formatAmountToLocalizedNumeralsWithCurrency(value, props.defaultCurrency);
let displayTypeName = '';
if (dataItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.IncomeByPrimaryCategory) {
displayTypeName = tt('Income By Primary Category');
} else if (dataItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.IncomeBySecondaryCategory) {
displayTypeName = tt('Income By Secondary Category');
} else if (dataItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount) {
displayTypeName = tt('Income By Account');
} else if (dataItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByAccount) {
displayTypeName = tt('Expense By Account');
} else if (dataItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.NetCashFlow) {
displayTypeName = tt('Net Cash Flow');
} else if (dataItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.ExpenseBySecondaryCategory) {
displayTypeName = tt('Expense By Secondary Category');
} else if (dataItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByPrimaryCategory) {
displayTypeName = tt('Expense By Primary Category');
}
let tooltip = `<div><span>${dataItem.displayName}</span>`;
if (isNumber(dataItem.percent)) {
if (displayTypeName && (dataItem.dateItemType !== TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount || isNumber(dataItem.percent))) {
tooltip = `<div class="mb-1">${displayTypeName}</div>` + tooltip;
} else if (dataItem.dateItemType === TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount) {
tooltip = `<div class="mb-1">${tt('Account Balance')}</div>` + tooltip;
}
if (isNumber(dataItem.percent) && dataItem.percent > 0) {
const displayPercent = formatPercentToLocalizedNumerals(dataItem.percent, 2, '&lt;0.01');
tooltip += `<span class="ms-1" style="float: inline-end">(${displayPercent})</span>`;
}
tooltip += `<span class="ms-5" style="float: inline-end">${displayValue}</span></div>`;
if (isNumber(dataItem.accountNetCashFlow) && dataItem.accountNetCashFlow !== 0) {
const displayAccountNetCashFlow = formatAmountToLocalizedNumeralsWithCurrency(dataItem.accountNetCashFlow, props.defaultCurrency);
tooltip += `<div class="mt-1"><span>${tt('Net Cash Flow')}</span><span class="ms-5" style="float: inline-end">${displayAccountNetCashFlow}</span></div>`;
}
return tooltip;
} else if (params.dataType === 'edge') {
const dataItem = params.data as SankeyChartLinkItem;
@@ -359,7 +293,7 @@ const chartOptions = computed<object>(() => {
{
type: 'sankey',
left: 10,
top: 10,
top: 0,
bottom: 10,
roam: true,
layoutIterations: 0,
@@ -393,7 +327,7 @@ const chartOptions = computed<object>(() => {
}
},
{
depth: SankeyChartDepth.Account,
depth: SankeyChartDepth.AccountForIncome,
itemStyle: {
color: '#c07d43',
opacity: 0.5
@@ -404,7 +338,7 @@ const chartOptions = computed<object>(() => {
}
},
{
depth: SankeyChartDepth.AccountWithTransfer,
depth: SankeyChartDepth.AccountForExpense,
itemStyle: {
color: '#c07d43',
opacity: 0.5
@@ -448,78 +382,6 @@ const chartOptions = computed<object>(() => {
};
});
function updateNodeItem(nodesMap: Record<string, SankeyChartNodeItem>, { itemType, id, name, nameId, displayOrders, amount, depth }: { itemType: SankeyChartNodeItemType, id: string, name: string, nameId: string, displayOrders: number[], amount: number, depth: number }) {
const node: SankeyChartNodeItem | undefined = nodesMap[nameId];
if (!node) {
nodesMap[nameId] = {
itemType: itemType,
itemId: id,
name: name,
nameId: nameId,
displayName: name,
displayOrders: displayOrders,
totalAmount: amount,
depth: depth
};
} else {
node.totalAmount += amount;
}
}
function updateLinkItem(linksMap: Record<string, SankeyChartLinkItem>, { sourceItemType, sourceItemId, source, sourceName, targetItemType, targetItemId, target, targetName, value }: { sourceItemType: SankeyChartNodeItemType, sourceItemId: string, source: string, sourceName: string, targetItemType: SankeyChartNodeItemType, targetItemId: string, target: string, targetName: string, value: number }) {
const key = `${source}:${target}`;
const link: SankeyChartLinkItem | undefined = linksMap[key];
if (!link) {
linksMap[key] = {
sourceItemType: sourceItemType,
sourceItemId: sourceItemId,
source: source,
sourceDisplayName: sourceName,
targetItemType: targetItemType,
targetItemId: targetItemId,
target: target,
targetDisplayName: targetName,
value: value
};
} else {
link.value += value;
}
}
function addFinalSortedNodeItems(nodesMap: Record<string, SankeyChartNodeItem>, allNodesArray: SankeyChartNodeItem[]): void {
const nodesArray: SankeyChartNodeItem[] = [];
let totalAmount = 0;
for (const node of values(nodesMap)) {
if (node.totalAmount > 0) {
totalAmount += node.totalAmount;
}
nodesArray.push(node);
}
sortStatisticsItems(nodesArray, props.sortingType);
for (const node of nodesArray) {
node.name = node.nameId;
node.percent = node.totalAmount > 0 && totalAmount > 0 ? (node.totalAmount / totalAmount) * 100 : undefined;
}
allNodesArray.push(...nodesArray);
}
function addFinalLinkItems(linksMap: Record<string, SankeyChartLinkItem>, allLinksArray: SankeyChartLinkItem[]): void {
const linksArray: SankeyChartLinkItem[] = [];
for (const link of values(linksMap)) {
linksArray.push(link);
}
allLinksArray.push(...linksArray);
}
function clickItem(e: ECElementEvent): void {
if (!props.enableClickItem || e.componentType !== 'series' || e.seriesType !=='sankey') {
return;
@@ -531,13 +393,22 @@ function clickItem(e: ECElementEvent): void {
if (e.dataType === 'node') {
const dataItem = e.data as SankeyChartNodeItem;
if (dataItem.itemType === SankeyChartNodeItemType.NetCashFlow) {
return;
}
emit('click', dataItem.itemType, dataItem.itemId);
} else if (e.dataType === 'edge') {
const dataItem = e.data as SankeyChartLinkItem;
if (dataItem.sourceItemType === SankeyChartNodeItemType.NetCashFlow) {
return;
}
if (dataItem.sourceItemType === dataItem.targetItemType && dataItem.sourceItemId === dataItem.targetItemId) {
emit('click', dataItem.sourceItemType, dataItem.sourceItemId);
} else {
} else if (dataItem.targetItemType !== SankeyChartNodeItemType.NetCashFlow) {
emit('click', dataItem.sourceItemType, dataItem.sourceItemId, dataItem.targetItemType, dataItem.targetItemId);
}
}

View File

@@ -762,6 +762,41 @@ export interface TransactionStatisticDataItemBase extends SortableTransactionSta
readonly totalAmount: number;
}
export interface TransactionCategoricalOverviewAnalysisData {
readonly totalIncome: number;
readonly totalExpense: number;
readonly items: TransactionCategoricalOverviewAnalysisDataItem[];
}
export enum TransactionCategoricalOverviewAnalysisDataItemType {
IncomeByPrimaryCategory = 'incomeByPrimaryCategory',
IncomeBySecondaryCategory = 'incomeBySecondaryCategory',
IncomeByAccount = 'incomeByAccount',
ExpenseByAccount = 'expenseByAccount',
NetCashFlow = 'netCashFlow',
ExpenseBySecondaryCategory = 'expenseBySecondaryCategory',
ExpenseByPrimaryCategory = 'expenseByPrimaryCategory'
}
export interface TransactionCategoricalOverviewAnalysisDataItem extends SortableTransactionStatisticDataItem {
readonly id: string;
readonly name: string;
readonly type: TransactionCategoricalOverviewAnalysisDataItemType;
readonly displayOrders: number[];
readonly hidden: boolean;
readonly inflows: TransactionCategoricalOverviewAnalysisDataItemOutflowItem[];
readonly outflows: TransactionCategoricalOverviewAnalysisDataItemOutflowItem[];
totalAmount: number;
totalNonNegativeAmount: number;
includeInPercent?: boolean;
percent?: number;
}
export interface TransactionCategoricalOverviewAnalysisDataItemOutflowItem {
readonly relatedItem: TransactionCategoricalOverviewAnalysisDataItem;
amount: number;
}
export interface TransactionCategoricalAnalysisData {
readonly totalAmount: number;
readonly items: TransactionCategoricalAnalysisDataItem[];

View File

@@ -28,20 +28,23 @@ import {
import { DEFAULT_ACCOUNT_ICON, DEFAULT_CATEGORY_ICON } from '@/consts/icon.ts';
import { DEFAULT_ACCOUNT_COLOR, DEFAULT_CATEGORY_COLOR } from '@/consts/color.ts';
import type {
TransactionStatisticResponse,
TransactionStatisticResponseItem,
TransactionStatisticTrendsResponseItem,
TransactionStatisticResponseItemWithInfo,
TransactionStatisticResponseWithInfo,
TransactionStatisticTrendsResponseItemWithInfo,
TransactionStatisticDataItemType,
TransactionStatisticDataItemBase,
TransactionCategoricalAnalysisData,
TransactionCategoricalAnalysisDataItem,
TransactionTrendsAnalysisData,
TransactionTrendsAnalysisDataItem,
TransactionTrendsAnalysisDataAmount
import {
type TransactionStatisticResponse,
type TransactionStatisticResponseItem,
type TransactionStatisticTrendsResponseItem,
type TransactionStatisticResponseItemWithInfo,
type TransactionStatisticResponseWithInfo,
type TransactionStatisticTrendsResponseItemWithInfo,
type TransactionStatisticDataItemType,
type TransactionStatisticDataItemBase,
type TransactionCategoricalOverviewAnalysisData,
type TransactionCategoricalOverviewAnalysisDataItem,
type TransactionCategoricalAnalysisData,
type TransactionCategoricalAnalysisDataItem,
type TransactionTrendsAnalysisData,
type TransactionTrendsAnalysisDataItem,
type TransactionTrendsAnalysisDataAmount,
TransactionCategoricalOverviewAnalysisDataItemType
} from '@/models/transaction.ts';
import {
@@ -203,36 +206,276 @@ export const useStatisticsStore = defineStore('statistics', () => {
return getCategoryTotalAmountItems(transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.items, transactionStatisticsFilter.value);
});
const categoricalAllAnalysisData = computed<TransactionStatisticResponseWithInfo | null>(() => {
const categoricalOverviewAnalysisData = computed<TransactionCategoricalOverviewAnalysisData | null>(() => {
if (!transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value || !transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.items) {
return null;
}
const allDataItems: TransactionStatisticResponseItemWithInfo[] = [];
const allDataItemsMap: Record<string, TransactionCategoricalOverviewAnalysisDataItem> = {};
const allIncomeByPrimaryCategoryDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [];
const allIncomeBySecondaryCategoryDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [];
const allIncomeByAccountDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [];
const allExpenseByAccountDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [];
const allExpenseBySecondaryCategoryDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [];
const allExpenseByPrimaryCategoryDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [];
const allOpeningBalanceDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [];
const allNetCashFlowDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [];
let totalIncome: number = 0;
let totalExpense: number = 0;
for (const item of transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.items) {
if (!item.primaryAccount || !item.account || !item.primaryCategory || !item.category) {
continue;
}
if (item.relatedAccount && item.relatedAccountType === TransactionRelatedAccountType.TransferFrom) {
continue;
}
if (!isNumber(item.amountInDefaultCurrency)) {
continue;
}
if (transactionStatisticsFilter.value.filterAccountIds && transactionStatisticsFilter.value.filterAccountIds[item.account.id]) {
continue;
}
if (transactionStatisticsFilter.value.filterAccountIds && item.relatedAccount && transactionStatisticsFilter.value.filterAccountIds[item.relatedAccount.id]) {
continue;
}
if (transactionStatisticsFilter.value.filterCategoryIds && transactionStatisticsFilter.value.filterCategoryIds[item.category.id]) {
continue;
}
allDataItems.push(item);
if (item.category.type === CategoryType.Income) {
totalIncome += item.amountInDefaultCurrency;
} else if (item.category.type === CategoryType.Expense) {
totalExpense += item.amountInDefaultCurrency;
}
const incomeByAccountKey = `${TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount}:${item.account.id}`;
const expenseByAccountKey = `${TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByAccount}:${item.account.id}`;
let incomeByAccountItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[incomeByAccountKey];
let expenseByAccountItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[expenseByAccountKey];
if (!incomeByAccountItem) {
incomeByAccountItem = createNewTransactionCategoricalOverviewAnalysisDataItem(
item.account.id,
item.account.name,
TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount,
[item.primaryAccount.category, item.primaryAccount.displayOrder, item.account.displayOrder],
item.primaryAccount.hidden || item.account.hidden);
allDataItemsMap[incomeByAccountKey] = incomeByAccountItem;
allIncomeByAccountDataItems.push(incomeByAccountItem);
}
if (!expenseByAccountItem) {
expenseByAccountItem = createNewTransactionCategoricalOverviewAnalysisDataItem(
item.account.id,
item.account.name,
TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByAccount,
[item.primaryAccount.category, item.primaryAccount.displayOrder, item.account.displayOrder],
item.primaryAccount.hidden || item.account.hidden);
allDataItemsMap[expenseByAccountKey] = expenseByAccountItem;
allExpenseByAccountDataItems.push(expenseByAccountItem);
}
if (item.category.type === CategoryType.Income) {
const primaryCategoryItemKey = `${TransactionCategoricalOverviewAnalysisDataItemType.IncomeByPrimaryCategory}:${item.primaryCategory.id}`;
const secondaryCategoryItemKey = `${TransactionCategoricalOverviewAnalysisDataItemType.IncomeBySecondaryCategory}:${item.category.id}`;
let primaryCategoryDataItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[primaryCategoryItemKey];
let secondaryCategoryDataItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[secondaryCategoryItemKey];
if (!primaryCategoryDataItem) {
primaryCategoryDataItem = createNewTransactionCategoricalOverviewAnalysisDataItem(
item.primaryCategory.id,
item.primaryCategory.name,
TransactionCategoricalOverviewAnalysisDataItemType.IncomeByPrimaryCategory,
[item.primaryCategory.displayOrder],
item.primaryCategory.hidden);
allDataItemsMap[primaryCategoryItemKey] = primaryCategoryDataItem;
allIncomeByPrimaryCategoryDataItems.push(primaryCategoryDataItem);
}
if (!secondaryCategoryDataItem) {
secondaryCategoryDataItem = createNewTransactionCategoricalOverviewAnalysisDataItem(
item.category.id,
item.category.name,
TransactionCategoricalOverviewAnalysisDataItemType.IncomeBySecondaryCategory,
[item.primaryCategory.displayOrder, item.category.displayOrder],
item.primaryCategory.hidden || item.category.hidden);
allDataItemsMap[secondaryCategoryItemKey] = secondaryCategoryDataItem;
allIncomeBySecondaryCategoryDataItems.push(secondaryCategoryDataItem);
}
primaryCategoryDataItem.totalAmount += item.amountInDefaultCurrency;
primaryCategoryDataItem.totalNonNegativeAmount += item.amountInDefaultCurrency > 0 ? item.amountInDefaultCurrency : 0;
primaryCategoryDataItem.includeInPercent = true;
primaryCategoryDataItem.outflows.push({ amount: item.amountInDefaultCurrency, relatedItem: secondaryCategoryDataItem });
secondaryCategoryDataItem.totalAmount += item.amountInDefaultCurrency;
secondaryCategoryDataItem.totalNonNegativeAmount += item.amountInDefaultCurrency > 0 ? item.amountInDefaultCurrency : 0;
secondaryCategoryDataItem.includeInPercent = true;
secondaryCategoryDataItem.inflows.push({ amount: item.amountInDefaultCurrency, relatedItem: primaryCategoryDataItem });
secondaryCategoryDataItem.outflows.push({ amount: item.amountInDefaultCurrency, relatedItem: incomeByAccountItem });
incomeByAccountItem.totalAmount += item.amountInDefaultCurrency;
incomeByAccountItem.totalNonNegativeAmount += item.amountInDefaultCurrency > 0 ? item.amountInDefaultCurrency : 0;
incomeByAccountItem.includeInPercent = true;
incomeByAccountItem.inflows.push({ amount: item.amountInDefaultCurrency, relatedItem: secondaryCategoryDataItem });
} else if (item.category.type === CategoryType.Expense) {
const primaryCategoryItemKey = `${TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByPrimaryCategory}:${item.primaryCategory.id}`;
const secondaryCategoryItemKey = `${TransactionCategoricalOverviewAnalysisDataItemType.ExpenseBySecondaryCategory}:${item.category.id}`;
let primaryCategoryDataItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[primaryCategoryItemKey];
let secondaryCategoryDataItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[secondaryCategoryItemKey];
if (!primaryCategoryDataItem) {
primaryCategoryDataItem = createNewTransactionCategoricalOverviewAnalysisDataItem(
item.primaryCategory.id,
item.primaryCategory.name,
TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByPrimaryCategory,
[item.primaryCategory.displayOrder],
item.primaryCategory.hidden);
allDataItemsMap[primaryCategoryItemKey] = primaryCategoryDataItem;
allExpenseByPrimaryCategoryDataItems.push(primaryCategoryDataItem);
}
if (!secondaryCategoryDataItem) {
secondaryCategoryDataItem = createNewTransactionCategoricalOverviewAnalysisDataItem(
item.category.id,
item.category.name,
TransactionCategoricalOverviewAnalysisDataItemType.ExpenseBySecondaryCategory,
[item.primaryCategory.displayOrder, item.category.displayOrder],
item.primaryCategory.hidden || item.category.hidden);
allDataItemsMap[secondaryCategoryItemKey] = secondaryCategoryDataItem;
allExpenseBySecondaryCategoryDataItems.push(secondaryCategoryDataItem);
}
expenseByAccountItem.totalAmount += item.amountInDefaultCurrency;
expenseByAccountItem.totalNonNegativeAmount += item.amountInDefaultCurrency > 0 ? item.amountInDefaultCurrency : 0;
expenseByAccountItem.includeInPercent = true;
expenseByAccountItem.outflows.push({ amount: item.amountInDefaultCurrency, relatedItem: secondaryCategoryDataItem });
secondaryCategoryDataItem.totalAmount += item.amountInDefaultCurrency;
secondaryCategoryDataItem.totalNonNegativeAmount += item.amountInDefaultCurrency > 0 ? item.amountInDefaultCurrency : 0
secondaryCategoryDataItem.includeInPercent = true;
secondaryCategoryDataItem.inflows.push({ amount: item.amountInDefaultCurrency, relatedItem: expenseByAccountItem });
secondaryCategoryDataItem.outflows.push({ amount: item.amountInDefaultCurrency, relatedItem: primaryCategoryDataItem });
primaryCategoryDataItem.totalAmount += item.amountInDefaultCurrency;
primaryCategoryDataItem.totalNonNegativeAmount += item.amountInDefaultCurrency > 0 ? item.amountInDefaultCurrency : 0;
primaryCategoryDataItem.includeInPercent = true;
primaryCategoryDataItem.inflows.push({ amount: item.amountInDefaultCurrency, relatedItem: secondaryCategoryDataItem });
} else if (item.category.type === CategoryType.Transfer && item.relatedPrimaryAccount && item.relatedAccount) {
const transferToAccountKey = `${TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByAccount}:${item.relatedAccount.id}`;
let transferToAccountItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[transferToAccountKey];
if (!transferToAccountItem) {
transferToAccountItem = createNewTransactionCategoricalOverviewAnalysisDataItem(
item.relatedAccount.id,
item.relatedAccount.name,
TransactionCategoricalOverviewAnalysisDataItemType.ExpenseByAccount,
[item.relatedPrimaryAccount.category, item.relatedPrimaryAccount.displayOrder, item.relatedAccount.displayOrder],
item.relatedPrimaryAccount.hidden || item.relatedAccount.hidden);
allDataItemsMap[transferToAccountKey] = transferToAccountItem;
allExpenseByAccountDataItems.push(transferToAccountItem);
}
incomeByAccountItem.outflows.push({ amount: item.amountInDefaultCurrency, relatedItem: transferToAccountItem });
transferToAccountItem.inflows.push({ amount: item.amountInDefaultCurrency, relatedItem: incomeByAccountItem });
}
}
sortCategoricalOverviewAnalysisDataItems(allIncomeByPrimaryCategoryDataItems, transactionStatisticsFilter.value);
sortCategoricalOverviewAnalysisDataItems(allIncomeBySecondaryCategoryDataItems, transactionStatisticsFilter.value);
sortCategoricalOverviewAnalysisDataItems(allIncomeByAccountDataItems, transactionStatisticsFilter.value);
sortCategoricalOverviewAnalysisDataItems(allExpenseByAccountDataItems, transactionStatisticsFilter.value);
sortCategoricalOverviewAnalysisDataItems(allExpenseBySecondaryCategoryDataItems, transactionStatisticsFilter.value);
sortCategoricalOverviewAnalysisDataItems(allExpenseByPrimaryCategoryDataItems, transactionStatisticsFilter.value);
for (const item of allExpenseByAccountDataItems) {
const incomeByAccountKey = `${TransactionCategoricalOverviewAnalysisDataItemType.IncomeByAccount}:${item.id}`;
const incomeByAccountItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[incomeByAccountKey];
let accountTotalInflowsAmount: number = 0;
let accountTotalIncomeAmount: number = 0;
let accountTotalTransferAmount: number = 0;
let accountTotalOutflowsAmount: number = 0;
if (incomeByAccountItem) {
for (const inflow of incomeByAccountItem.inflows) {
accountTotalInflowsAmount += inflow.amount;
accountTotalIncomeAmount += inflow.amount;
}
for (const outflow of incomeByAccountItem.outflows) {
accountTotalTransferAmount += outflow.amount;
}
}
for (const inflow of item.inflows) {
if (inflow.relatedItem.type === item.type && inflow.relatedItem.id === item.id) {
continue;
}
accountTotalInflowsAmount += inflow.amount;
}
for (const outflow of item.outflows) {
accountTotalOutflowsAmount += outflow.amount;
}
const accountBalance: number = accountTotalIncomeAmount - accountTotalTransferAmount - accountTotalOutflowsAmount;
const accountNetCashFlow: number = accountTotalInflowsAmount - accountTotalTransferAmount - accountTotalOutflowsAmount;
if (incomeByAccountItem && accountsStore.allAccountsMap[item.id]?.isAsset) {
if (accountBalance > 0) { // has positive balance, transfer the amount from income account to expense account
incomeByAccountItem.outflows.push({ amount: accountBalance + accountTotalOutflowsAmount, relatedItem: item });
item.inflows.push({ amount: accountBalance + accountTotalOutflowsAmount, relatedItem: incomeByAccountItem });
} else if (accountNetCashFlow < 0) { // has negative net cash flow, add the difference to income account
incomeByAccountItem.totalAmount += -accountNetCashFlow;
incomeByAccountItem.totalNonNegativeAmount += -accountNetCashFlow > 0 ? -accountNetCashFlow : 0;
incomeByAccountItem.outflows.push({ amount: -accountNetCashFlow, relatedItem: item });
item.inflows.push({ amount: -accountNetCashFlow, relatedItem: incomeByAccountItem });
}
}
if (accountNetCashFlow > 0) {
let netCashFlowItem: TransactionCategoricalOverviewAnalysisDataItem | undefined = allDataItemsMap[TransactionCategoricalOverviewAnalysisDataItemType.NetCashFlow];
if (!netCashFlowItem) {
netCashFlowItem = createNewTransactionCategoricalOverviewAnalysisDataItem(
TransactionCategoricalOverviewAnalysisDataItemType.NetCashFlow,
'Net Cash Flow',
TransactionCategoricalOverviewAnalysisDataItemType.NetCashFlow,
[Number.MAX_SAFE_INTEGER],
false);
allDataItemsMap[TransactionCategoricalOverviewAnalysisDataItemType.NetCashFlow] = netCashFlowItem;
allNetCashFlowDataItems.push(netCashFlowItem);
}
item.outflows.push({ amount: accountNetCashFlow, relatedItem: netCashFlowItem });
netCashFlowItem.totalAmount += accountNetCashFlow;
netCashFlowItem.totalNonNegativeAmount += accountNetCashFlow > 0 ? accountNetCashFlow : 0;
netCashFlowItem.inflows.push({ amount: accountNetCashFlow, relatedItem: item });
}
}
const allDataItems: TransactionCategoricalOverviewAnalysisDataItem[] = [
...allIncomeByPrimaryCategoryDataItems,
...allIncomeBySecondaryCategoryDataItems,
...allIncomeByAccountDataItems,
...allOpeningBalanceDataItems,
...allExpenseByAccountDataItems,
...allExpenseBySecondaryCategoryDataItems,
...allNetCashFlowDataItems,
...allExpenseByPrimaryCategoryDataItems
];
return {
startTime: transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.startTime,
endTime: transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.endTime,
totalIncome: totalIncome,
totalExpense: totalExpense,
items: allDataItems
};
});
@@ -466,6 +709,42 @@ export const useStatisticsStore = defineStore('statistics', () => {
return trendsData;
});
function createNewTransactionCategoricalOverviewAnalysisDataItem(id: string, name: string, type: TransactionCategoricalOverviewAnalysisDataItemType, displayOrders: number[], hidden: boolean): TransactionCategoricalOverviewAnalysisDataItem {
const dataItem: TransactionCategoricalOverviewAnalysisDataItem = {
id: id,
name: name,
type: type,
displayOrders: displayOrders,
hidden: hidden,
inflows: [],
outflows: [],
totalAmount: 0,
totalNonNegativeAmount: 0
};
return dataItem;
}
function sortCategoricalOverviewAnalysisDataItems(items: TransactionCategoricalOverviewAnalysisDataItem[], transactionStatisticsFilter: TransactionStatisticsFilter): void {
let totalNonNegativeAmount: number = 0;
for (const item of items) {
totalNonNegativeAmount += item.totalNonNegativeAmount;
}
if (totalNonNegativeAmount > 0) {
for (const item of items) {
if (!item.includeInPercent) {
continue;
}
item.percent = item.totalAmount * 100 / totalNonNegativeAmount;
}
}
sortStatisticsItems(items, transactionStatisticsFilter.sortingType);
}
function assembleAccountAndCategoryInfo(items: TransactionStatisticResponseItem[]): TransactionStatisticResponseItemWithInfo[] {
const finalItems: TransactionStatisticResponseItemWithInfo[] = [];
const defaultCurrency = userStore.currentUserDefaultCurrency;
@@ -1289,7 +1568,7 @@ export const useStatisticsStore = defineStore('statistics', () => {
transactionStatisticsStateInvalid,
// computed states
categoricalAnalysisChartDataCategory,
categoricalAllAnalysisData,
categoricalOverviewAnalysisData,
categoricalAnalysisData,
trendsAnalysisData,
// functions

View File

@@ -20,7 +20,7 @@ import {
import { DISPLAY_HIDDEN_AMOUNT } from '@/consts/numeral.ts';
import type {
TransactionStatisticResponseWithInfo,
TransactionCategoricalOverviewAnalysisData,
TransactionCategoricalAnalysisData,
TransactionCategoricalAnalysisDataItem,
TransactionTrendsAnalysisData
@@ -249,8 +249,8 @@ export function useStatisticsTransactionPageBase() {
query.value.chartDataType === ChartDataType.NetIncome.type;
});
const categoricalOverviewAnalysisData = computed<TransactionCategoricalOverviewAnalysisData | null>(() => statisticsStore.categoricalOverviewAnalysisData);
const categoricalAnalysisData = computed<TransactionCategoricalAnalysisData>(() => statisticsStore.categoricalAnalysisData);
const categoricalAllAnalysisData = computed<TransactionStatisticResponseWithInfo | null>(() => statisticsStore.categoricalAllAnalysisData);
const trendsAnalysisData = computed<TransactionTrendsAnalysisData | null>(() => statisticsStore.trendsAnalysisData);
function canShowCustomDateRange(dateRangeType: number): boolean {
@@ -323,8 +323,8 @@ export function useStatisticsTransactionPageBase() {
showTotalAmountInTrendsChart,
showStackedInTrendsChart,
translateNameInTrendsChart,
categoricalOverviewAnalysisData,
categoricalAnalysisData,
categoricalAllAnalysisData,
trendsAnalysisData,
// functions
canShowCustomDateRange,

View File

@@ -172,7 +172,27 @@
</template>
<v-card-text class="statistics-overview-title pt-0" :class="{ 'disabled': loading }"
v-if="queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && !isQuerySpecialChartType && (initing || (categoricalAnalysisData && categoricalAnalysisData.items && categoricalAnalysisData.items.length))">
v-if="queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && isQuerySpecialChartType && queryChartDataType === ChartDataType.Overview.type && (initing || categoricalOverviewAnalysisData && categoricalOverviewAnalysisData.items && categoricalOverviewAnalysisData.items.length)">
<span class="statistics-subtitle">{{ tt('Total Income') }}</span>
<span class="statistics-overview-amount ms-3 text-income"
v-if="!initing && categoricalOverviewAnalysisData && categoricalOverviewAnalysisData.items && categoricalOverviewAnalysisData.items.length">
{{ getDisplayAmount(categoricalOverviewAnalysisData.totalIncome, defaultCurrency) }}
</span>
<v-skeleton-loader class="skeleton-no-margin ms-3 mb-2"
width="120px" type="text" :loading="true"
v-else-if="initing"></v-skeleton-loader>
<span class="statistics-subtitle ms-3">{{ tt('Total Expense') }}</span>
<span class="statistics-overview-amount ms-3 text-expense"
v-if="!initing && categoricalOverviewAnalysisData && categoricalOverviewAnalysisData.items && categoricalOverviewAnalysisData.items.length">
{{ getDisplayAmount(categoricalOverviewAnalysisData.totalExpense, defaultCurrency) }}
</span>
<v-skeleton-loader class="skeleton-no-margin ms-3 mb-2"
width="120px" type="text" :loading="true"
v-else-if="initing"></v-skeleton-loader>
</v-card-text>
<v-card-text class="statistics-overview-title pt-0" :class="{ 'disabled': loading }"
v-else-if="queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && !isQuerySpecialChartType && (initing || (categoricalAnalysisData && categoricalAnalysisData.items && categoricalAnalysisData.items.length))">
<span class="statistics-subtitle">{{ totalAmountName }}</span>
<span class="statistics-overview-amount ms-3"
:class="statisticsTextColor"
@@ -185,8 +205,11 @@
</v-card-text>
<v-card-text class="statistics-overview-title pt-0"
v-else-if="!initing && ((queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && !isQuerySpecialChartType && (!categoricalAnalysisData || !categoricalAnalysisData.items || !categoricalAnalysisData.items.length))
|| (queryAnalysisType === StatisticsAnalysisType.TrendAnalysis && (!trendsAnalysisData || !trendsAnalysisData.items || !trendsAnalysisData.items.length)))">
v-else-if="!initing && (
(queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && isQuerySpecialChartType && queryChartDataType === ChartDataType.Overview.type && (!categoricalOverviewAnalysisData || !categoricalOverviewAnalysisData.items || !categoricalOverviewAnalysisData.items.length))
|| (queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && !isQuerySpecialChartType && (!categoricalAnalysisData || !categoricalAnalysisData.items || !categoricalAnalysisData.items.length))
|| (queryAnalysisType === StatisticsAnalysisType.TrendAnalysis && (!trendsAnalysisData || !trendsAnalysisData.items || !trendsAnalysisData.items.length))
)">
<span class="statistics-subtitle statistics-overview-empty-tip">{{ tt('No transaction data') }}</span>
</v-card-text>
@@ -198,8 +221,7 @@
v-if="initing"
/>
<account-and-category-sankey-chart
:items="categoricalAllAnalysisData && categoricalAllAnalysisData.items && categoricalAllAnalysisData.items.length ? categoricalAllAnalysisData.items : []"
:sorting-type="querySortingType"
:items="categoricalOverviewAnalysisData && categoricalOverviewAnalysisData.items && categoricalOverviewAnalysisData.items.length ? categoricalOverviewAnalysisData.items : []"
:enable-click-item="true"
:default-currency="defaultCurrency"
v-else-if="!initing"
@@ -526,8 +548,8 @@ const {
showTotalAmountInTrendsChart,
showStackedInTrendsChart,
translateNameInTrendsChart,
categoricalOverviewAnalysisData,
categoricalAnalysisData,
categoricalAllAnalysisData,
trendsAnalysisData,
canShowCustomDateRange,
getTransactionCategoricalAnalysisDataItemDisplayColor,