update sankey chart display rules

This commit is contained in:
MaysWind
2025-10-28 01:52:03 +08:00
parent 4684de9705
commit f8fc955408
@@ -18,6 +18,7 @@ import type {
SortableTransactionStatisticDataItem,
TransactionStatisticResponseItemWithInfo
} from '@/models/transaction.ts';
import type { Account } from '@/models/account.ts';
import { values } from '@/core/base.ts';
import { ThemeType } from '@/core/theme.ts';
@@ -28,6 +29,15 @@ 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,
SecondaryExpenseCategory = 4,
PrimaryExpenseCategory = 5
}
enum SankeyChartNodeItemType {
Account = 'account',
Category = 'category'
@@ -89,6 +99,7 @@ const sankeyData = computed<SankeyChartData>(() => {
const secondaryExpenseCategoryNodesMap: Record<string, SankeyChartNodeItem> = {};
const primaryExpenseCategoryNodesMap: Record<string, SankeyChartNodeItem> = {};
const linksMap: Record<string, SankeyChartLinkItem> = {};
const accountsMap: Record<string, Account> = {};
for (const item of props.items) {
if (!item.primaryAccount || !item.account || !item.primaryCategory || !item.category || !item.amountInDefaultCurrency) {
@@ -105,6 +116,7 @@ const sankeyData = computed<SankeyChartData>(() => {
const incomeAccountNameId = `income_account:${item.account.id}`;
const expenseAccountNameId = `expense_account:${item.account.id}`;
accountsMap[item.account.id] = item.account;
updateNodeItem(incomeAccountNodesMap, {
itemType: SankeyChartNodeItemType.Account,
@@ -112,8 +124,8 @@ const sankeyData = computed<SankeyChartData>(() => {
name: item.account.name,
nameId: incomeAccountNameId,
displayOrders: [item.account.displayOrder],
amount: (item.primaryCategory.type == CategoryType.Income || item.primaryCategory.type == CategoryType.Transfer) ? item.amountInDefaultCurrency : 0,
depth: 2
amount: item.primaryCategory.type == CategoryType.Income ? item.amountInDefaultCurrency : 0,
depth: SankeyChartDepth.Account
});
updateNodeItem(expenseAccountNodesMap, {
@@ -123,7 +135,7 @@ const sankeyData = computed<SankeyChartData>(() => {
nameId: expenseAccountNameId,
displayOrders: [item.account.displayOrder],
amount: item.primaryCategory.type == CategoryType.Expense ? item.amountInDefaultCurrency : 0,
depth: 3
depth: SankeyChartDepth.AccountWithTransfer
});
if (item.primaryCategory.type == CategoryType.Income) {
@@ -134,7 +146,7 @@ const sankeyData = computed<SankeyChartData>(() => {
nameId: item.primaryCategory.id,
displayOrders: [item.primaryCategory.displayOrder],
amount: item.amountInDefaultCurrency,
depth: 0
depth: SankeyChartDepth.PrimaryIncomeCategory
});
updateNodeItem(secondaryIncomeCategoryNodesMap, {
@@ -144,7 +156,7 @@ const sankeyData = computed<SankeyChartData>(() => {
nameId: item.category.id,
displayOrders: [item.primaryCategory.displayOrder, item.category.displayOrder],
amount: item.amountInDefaultCurrency,
depth: 1
depth: SankeyChartDepth.SecondaryIncomeCategory
});
updateLinkItem(linksMap, {
@@ -178,7 +190,7 @@ const sankeyData = computed<SankeyChartData>(() => {
nameId: item.category.id,
displayOrders: [item.primaryCategory.displayOrder, item.category.displayOrder],
amount: item.amountInDefaultCurrency,
depth: 4
depth: SankeyChartDepth.SecondaryExpenseCategory
});
updateNodeItem(primaryExpenseCategoryNodesMap, {
@@ -188,7 +200,7 @@ const sankeyData = computed<SankeyChartData>(() => {
nameId: item.primaryCategory.id,
displayOrders: [item.primaryCategory.displayOrder],
amount: item.amountInDefaultCurrency,
depth: 5
depth: SankeyChartDepth.PrimaryExpenseCategory
});
updateLinkItem(linksMap, {
@@ -216,6 +228,7 @@ const sankeyData = computed<SankeyChartData>(() => {
});
} 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,
@@ -230,20 +243,17 @@ const sankeyData = computed<SankeyChartData>(() => {
}
}
for (const expenseAccountNode of values(expenseAccountNodesMap)) {
const incomeAccountNode = incomeAccountNodesMap[`income_account:${expenseAccountNode.itemId}`];
if (!incomeAccountNode) {
continue;
}
for (const account of values(accountsMap)) {
const incomeAccountNameId = `income_account:${account.id}`;
const expenseAccountNameId = `expense_account:${account.id}`;
let totalOutflowAmount = 0;
let totalInflowAmount = 0;
for (const link of values(linksMap)) {
if (link.sourceItemType === SankeyChartNodeItemType.Account && link.sourceItemId === expenseAccountNode.itemId) {
if (link.sourceItemType === SankeyChartNodeItemType.Account && link.sourceItemId === account.id) {
totalOutflowAmount += link.value;
} else if (link.targetItemType === SankeyChartNodeItemType.Account && link.targetItemId === expenseAccountNode.itemId) {
} else if (link.targetItemType === SankeyChartNodeItemType.Account && link.targetItemId === account.id) {
totalInflowAmount += link.value;
}
}
@@ -251,16 +261,38 @@ const sankeyData = computed<SankeyChartData>(() => {
const amountDifference = totalOutflowAmount - totalInflowAmount;
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
});
}
if (Math.abs(amountDifference) > 0) {
updateLinkItem(linksMap, {
sourceItemType: SankeyChartNodeItemType.Account,
sourceItemId: incomeAccountNode.itemId,
source: incomeAccountNode.nameId,
sourceName: incomeAccountNode.displayName,
sourceItemId: account.id,
source: incomeAccountNameId,
sourceName: account.name,
targetItemType: SankeyChartNodeItemType.Account,
targetItemId: expenseAccountNode.itemId,
target: expenseAccountNode.nameId,
targetName: expenseAccountNode.displayName,
value: amountDifference
targetItemId: account.id,
target: expenseAccountNameId,
targetName: account.name,
value: Math.abs(amountDifference)
});
}
}
@@ -295,14 +327,15 @@ const chartOptions = computed<object>(() => {
color: isDarkMode.value ? '#eee' : '#333'
},
formatter: (params: CallbackDataParams) => {
const value = isNumber(params.value) ? params.value as number : 0;
const displayValue = formatAmountToLocalizedNumeralsWithCurrency(value, props.defaultCurrency);
if (params.dataType === 'node') {
const dataItem = params.data as SankeyChartNodeItem;
const value = dataItem.totalAmount;
const displayValue = formatAmountToLocalizedNumeralsWithCurrency(value, props.defaultCurrency);
return `<div><span>${dataItem.displayName}</span><span class="ms-5" style="float: inline-end">${displayValue}</span></div>`;
} else if (params.dataType === 'edge') {
const dataItem = params.data as SankeyChartLinkItem;
const value = isNumber(params.value) ? params.value as number : 0;
const displayValue = formatAmountToLocalizedNumeralsWithCurrency(value, props.defaultCurrency);
return `<div><span>${dataItem.sourceDisplayName}${dataItem.targetDisplayName}</span><span class="ms-5" style="float: inline-end">${displayValue}</span></div>`;
} else {
return '';
@@ -316,6 +349,7 @@ const chartOptions = computed<object>(() => {
top: 10,
bottom: 10,
roam: true,
layoutIterations: 0,
label: {
formatter: (params: CallbackDataParams) => {
const dataItem = params.data as SankeyChartNodeItem;
@@ -324,7 +358,7 @@ const chartOptions = computed<object>(() => {
},
levels: [
{
depth: 0,
depth: SankeyChartDepth.PrimaryIncomeCategory,
itemStyle: {
color: expenseIncomeAmountColor.incomeAmountColor,
opacity: 0.6
@@ -335,7 +369,7 @@ const chartOptions = computed<object>(() => {
}
},
{
depth: 1,
depth: SankeyChartDepth.SecondaryIncomeCategory,
itemStyle: {
color: expenseIncomeAmountColor.incomeAmountColor,
opacity: 0.4
@@ -346,7 +380,7 @@ const chartOptions = computed<object>(() => {
}
},
{
depth: 2,
depth: SankeyChartDepth.Account,
itemStyle: {
color: '#c07d43',
opacity: 0.5
@@ -357,7 +391,7 @@ const chartOptions = computed<object>(() => {
}
},
{
depth: 3,
depth: SankeyChartDepth.AccountWithTransfer,
itemStyle: {
color: '#c07d43',
opacity: 0.5
@@ -368,7 +402,7 @@ const chartOptions = computed<object>(() => {
}
},
{
depth: 4,
depth: SankeyChartDepth.SecondaryExpenseCategory,
itemStyle: {
color: expenseIncomeAmountColor.expenseAmountColor,
opacity: 0.4
@@ -379,7 +413,7 @@ const chartOptions = computed<object>(() => {
}
},
{
depth: 5,
depth: SankeyChartDepth.PrimaryExpenseCategory,
itemStyle: {
color: expenseIncomeAmountColor.expenseAmountColor,
opacity: 0.6