mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-19 17:24:26 +08:00
add overview sankey chart for categorical analysis on desktop version
This commit is contained in:
@@ -0,0 +1,520 @@
|
|||||||
|
<template>
|
||||||
|
<v-chart autoresize class="account-category-sankey-chart-container" :class="{ 'transition-in': skeleton }" :option="chartOptions"
|
||||||
|
@click="clickItem" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useTheme } from 'vuetify';
|
||||||
|
|
||||||
|
import type { ECElementEvent } from 'echarts/core';
|
||||||
|
import type { CallbackDataParams } from 'echarts/types/dist/shared';
|
||||||
|
|
||||||
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
|
|
||||||
|
import { useUserStore } from '@/stores/user.ts';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
SortableTransactionStatisticDataItem,
|
||||||
|
TransactionStatisticResponseItemWithInfo
|
||||||
|
} from '@/models/transaction.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 SankeyChartNodeItemType {
|
||||||
|
Account = 'account',
|
||||||
|
Category = 'category'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SankeyChartData {
|
||||||
|
nodes: SankeyChartNodeItem[];
|
||||||
|
links: SankeyChartLinkItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SankeyChartNodeItem extends SortableTransactionStatisticDataItem {
|
||||||
|
itemType: SankeyChartNodeItemType;
|
||||||
|
itemId: string;
|
||||||
|
name: string;
|
||||||
|
nameId: string;
|
||||||
|
displayName: string;
|
||||||
|
displayOrders: number[];
|
||||||
|
totalAmount: number;
|
||||||
|
depth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SankeyChartLinkItem {
|
||||||
|
sourceItemType: SankeyChartNodeItemType;
|
||||||
|
sourceItemId: string;
|
||||||
|
source: string;
|
||||||
|
sourceDisplayName: string;
|
||||||
|
targetItemType: SankeyChartNodeItemType;
|
||||||
|
targetItemId: string;
|
||||||
|
target: string;
|
||||||
|
targetDisplayName: string;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
skeleton?: boolean;
|
||||||
|
items: TransactionStatisticResponseItemWithInfo[];
|
||||||
|
sortingType: number;
|
||||||
|
defaultCurrency?: string;
|
||||||
|
enableClickItem?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'click', sourceItemType: 'account' | 'category', sourceItemId: string, targetItemType?: 'account' | 'category', targetItemId?: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const { formatAmountToLocalizedNumeralsWithCurrency } = useI18n();
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
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> = {};
|
||||||
|
|
||||||
|
for (const item of props.items) {
|
||||||
|
if (!item.primaryAccount || !item.account || !item.primaryCategory || !item.category || !item.amountInDefaultCurrency) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.account.hidden || item.primaryAccount.hidden || item.category.hidden || item.primaryCategory.hidden) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.relatedAccount && (item.relatedAccountType === TransactionRelatedAccountType.TransferFrom || item.relatedAccount.hidden || !item.relatedPrimaryAccount || item.relatedPrimaryAccount.hidden)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const incomeAccountNameId = `income_account:${item.account.id}`;
|
||||||
|
const expenseAccountNameId = `expense_account:${item.account.id}`;
|
||||||
|
|
||||||
|
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.primaryCategory.type == CategoryType.Transfer) ? item.amountInDefaultCurrency : 0,
|
||||||
|
depth: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 3
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 4
|
||||||
|
});
|
||||||
|
|
||||||
|
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: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const expenseAccountNode of values(expenseAccountNodesMap)) {
|
||||||
|
const incomeAccountNode = incomeAccountNodesMap[`income_account:${expenseAccountNode.itemId}`];
|
||||||
|
|
||||||
|
if (!incomeAccountNode) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalOutflowAmount = 0;
|
||||||
|
let totalInflowAmount = 0;
|
||||||
|
|
||||||
|
for (const link of values(linksMap)) {
|
||||||
|
if (link.sourceItemType === SankeyChartNodeItemType.Account && link.sourceItemId === expenseAccountNode.itemId) {
|
||||||
|
totalOutflowAmount += link.value;
|
||||||
|
} else if (link.targetItemType === SankeyChartNodeItemType.Account && link.targetItemId === expenseAccountNode.itemId) {
|
||||||
|
totalInflowAmount += link.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const amountDifference = totalOutflowAmount - totalInflowAmount;
|
||||||
|
|
||||||
|
if (amountDifference > 0) {
|
||||||
|
updateLinkItem(linksMap, {
|
||||||
|
sourceItemType: SankeyChartNodeItemType.Account,
|
||||||
|
sourceItemId: incomeAccountNode.itemId,
|
||||||
|
source: incomeAccountNode.nameId,
|
||||||
|
sourceName: incomeAccountNode.displayName,
|
||||||
|
targetItemType: SankeyChartNodeItemType.Account,
|
||||||
|
targetItemId: expenseAccountNode.itemId,
|
||||||
|
target: expenseAccountNode.nameId,
|
||||||
|
targetName: expenseAccountNode.displayName,
|
||||||
|
value: amountDifference
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartOptions = computed<object>(() => {
|
||||||
|
const expenseIncomeAmountColor = getExpenseAndIncomeAmountColor(userStore.currentUserExpenseAmountColor, userStore.currentUserIncomeAmountColor, isDarkMode.value);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
backgroundColor: isDarkMode.value ? '#333' : '#fff',
|
||||||
|
borderColor: isDarkMode.value ? '#333' : '#fff',
|
||||||
|
textStyle: {
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
return `<div><span>${dataItem.sourceDisplayName} → ${dataItem.targetDisplayName}</span><span class="ms-5" style="float: inline-end">${displayValue}</span></div>`;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'sankey',
|
||||||
|
left: 10,
|
||||||
|
top: 10,
|
||||||
|
bottom: 10,
|
||||||
|
label: {
|
||||||
|
formatter: (params: CallbackDataParams) => {
|
||||||
|
const dataItem = params.data as SankeyChartNodeItem;
|
||||||
|
return dataItem.displayName;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
levels: [
|
||||||
|
{
|
||||||
|
depth: 0,
|
||||||
|
itemStyle: {
|
||||||
|
color: expenseIncomeAmountColor.incomeAmountColor,
|
||||||
|
opacity: 0.6
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
color: 'source',
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
depth: 1,
|
||||||
|
itemStyle: {
|
||||||
|
color: expenseIncomeAmountColor.incomeAmountColor,
|
||||||
|
opacity: 0.4
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
color: 'source',
|
||||||
|
opacity: 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
depth: 2,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#c07d43',
|
||||||
|
opacity: 0.5
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
color: 'source',
|
||||||
|
opacity: 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
depth: 3,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#c07d43',
|
||||||
|
opacity: 0.5
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
color: 'source',
|
||||||
|
opacity: 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
depth: 4,
|
||||||
|
itemStyle: {
|
||||||
|
color: expenseIncomeAmountColor.expenseAmountColor,
|
||||||
|
opacity: 0.4
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
color: 'source',
|
||||||
|
opacity: 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
depth: 5,
|
||||||
|
itemStyle: {
|
||||||
|
color: expenseIncomeAmountColor.expenseAmountColor,
|
||||||
|
opacity: 0.6
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
color: 'source',
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
emphasis: {
|
||||||
|
focus: 'adjacency'
|
||||||
|
},
|
||||||
|
data: sankeyData.value.nodes,
|
||||||
|
links: sankeyData.value.links,
|
||||||
|
animation: !props.skeleton
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
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[] = [];
|
||||||
|
|
||||||
|
for (const node of values(nodesMap)) {
|
||||||
|
nodesArray.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
sortStatisticsItems(nodesArray, props.sortingType);
|
||||||
|
|
||||||
|
for (const node of nodesArray) {
|
||||||
|
node.name = node.nameId;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!e.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.dataType === 'node') {
|
||||||
|
const dataItem = e.data as SankeyChartNodeItem;
|
||||||
|
emit('click', dataItem.itemType, dataItem.itemId);
|
||||||
|
} else if (e.dataType === 'edge') {
|
||||||
|
const dataItem = e.data as SankeyChartLinkItem;
|
||||||
|
|
||||||
|
if (dataItem.sourceItemType === dataItem.targetItemType && dataItem.sourceItemId === dataItem.targetItemId) {
|
||||||
|
emit('click', dataItem.sourceItemType, dataItem.sourceItemId);
|
||||||
|
} else {
|
||||||
|
emit('click', dataItem.sourceItemType, dataItem.sourceItemId, dataItem.targetItemType, dataItem.targetItemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.account-category-sankey-chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 460px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.account-category-sankey-chart-container {
|
||||||
|
height: 660px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-category-sankey-chart-container.transition-in {
|
||||||
|
animation: radar-chart-skeleton-fade-in 2s 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes radar-chart-skeleton-fade-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -261,7 +261,7 @@ function onLegendSelectChanged(e: { selected: Record<string, boolean> }): void {
|
|||||||
|
|
||||||
@media (min-width: 600px) {
|
@media (min-width: 600px) {
|
||||||
.pie-chart-container {
|
.pie-chart-container {
|
||||||
height: 560px;
|
height: 610px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ const chartOptions = computed<object>(() => {
|
|||||||
|
|
||||||
@media (min-width: 600px) {
|
@media (min-width: 600px) {
|
||||||
.radar-chart-container {
|
.radar-chart-container {
|
||||||
height: 560px;
|
height: 610px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+34
-22
@@ -89,35 +89,41 @@ export class AccountBalanceTrendChartType implements TypeAndName {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ChartDataType implements TypeAndName {
|
export class ChartDataType implements TypeAndName {
|
||||||
private static readonly allInstances: ChartDataType[] = [];
|
private static readonly allInstancesForAll: ChartDataType[] = [];
|
||||||
|
private static readonly allInstancesForDesktop: ChartDataType[] = [];
|
||||||
private static readonly allInstancesByType: Record<number, ChartDataType> = {};
|
private static readonly allInstancesByType: Record<number, ChartDataType> = {};
|
||||||
|
|
||||||
public static readonly OutflowsByAccount = new ChartDataType(11, 'Outflows By Account', StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
|
public static readonly Overview = new ChartDataType(16, 'Overview', true, true, StatisticsAnalysisType.CategoricalAnalysis);
|
||||||
public static readonly ExpenseByAccount = new ChartDataType(0, 'Expense By Account', StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
|
public static readonly OutflowsByAccount = new ChartDataType(11, 'Outflows By Account', false, false, StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
|
||||||
public static readonly ExpenseByPrimaryCategory = new ChartDataType(1, 'Expense By Primary Category', StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
|
public static readonly ExpenseByAccount = new ChartDataType(0, 'Expense By Account', false, false, StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
|
||||||
public static readonly ExpenseBySecondaryCategory = new ChartDataType(2, 'Expense By Secondary Category', StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
|
public static readonly ExpenseByPrimaryCategory = new ChartDataType(1, 'Expense By Primary Category', false, false, StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
|
||||||
public static readonly InflowsByAccount = new ChartDataType(12, 'Inflows By Account', StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
|
public static readonly ExpenseBySecondaryCategory = new ChartDataType(2, 'Expense By Secondary Category', false, false, StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
|
||||||
public static readonly IncomeByAccount = new ChartDataType(3, 'Income By Account', StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
|
public static readonly InflowsByAccount = new ChartDataType(12, 'Inflows By Account', false, false, StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
|
||||||
public static readonly IncomeByPrimaryCategory = new ChartDataType(4, 'Income By Primary Category', StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
|
public static readonly IncomeByAccount = new ChartDataType(3, 'Income By Account', false, false, StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
|
||||||
public static readonly IncomeBySecondaryCategory = new ChartDataType(5, 'Income By Secondary Category', StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
|
public static readonly IncomeByPrimaryCategory = new ChartDataType(4, 'Income By Primary Category', false, false, StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
|
||||||
public static readonly AccountTotalAssets = new ChartDataType(6, 'Account Total Assets', StatisticsAnalysisType.CategoricalAnalysis);
|
public static readonly IncomeBySecondaryCategory = new ChartDataType(5, 'Income By Secondary Category', false, false, StatisticsAnalysisType.CategoricalAnalysis, StatisticsAnalysisType.TrendAnalysis);
|
||||||
public static readonly AccountTotalLiabilities = new ChartDataType(7, 'Account Total Liabilities', StatisticsAnalysisType.CategoricalAnalysis);
|
public static readonly AccountTotalAssets = new ChartDataType(6, 'Account Total Assets', false, false, StatisticsAnalysisType.CategoricalAnalysis);
|
||||||
public static readonly TotalOutflows = new ChartDataType(13, 'Total Outflows', StatisticsAnalysisType.TrendAnalysis);
|
public static readonly AccountTotalLiabilities = new ChartDataType(7, 'Account Total Liabilities', false, false, StatisticsAnalysisType.CategoricalAnalysis);
|
||||||
public static readonly TotalExpense = new ChartDataType(8, 'Total Expense', StatisticsAnalysisType.TrendAnalysis);
|
public static readonly TotalOutflows = new ChartDataType(13, 'Total Outflows', false, false, StatisticsAnalysisType.TrendAnalysis);
|
||||||
public static readonly TotalInflows = new ChartDataType(14, 'Total Inflows', StatisticsAnalysisType.TrendAnalysis);
|
public static readonly TotalExpense = new ChartDataType(8, 'Total Expense', false, false, StatisticsAnalysisType.TrendAnalysis);
|
||||||
public static readonly TotalIncome = new ChartDataType(9, 'Total Income', StatisticsAnalysisType.TrendAnalysis);
|
public static readonly TotalInflows = new ChartDataType(14, 'Total Inflows', false, false, StatisticsAnalysisType.TrendAnalysis);
|
||||||
public static readonly NetCashFlow = new ChartDataType(15, 'Net Cash Flow', StatisticsAnalysisType.TrendAnalysis);
|
public static readonly TotalIncome = new ChartDataType(9, 'Total Income', false, false, StatisticsAnalysisType.TrendAnalysis);
|
||||||
public static readonly NetIncome = new ChartDataType(10, 'Net Income', StatisticsAnalysisType.TrendAnalysis);
|
public static readonly NetCashFlow = new ChartDataType(15, 'Net Cash Flow', false, false, StatisticsAnalysisType.TrendAnalysis);
|
||||||
|
public static readonly NetIncome = new ChartDataType(10, 'Net Income', false, false, StatisticsAnalysisType.TrendAnalysis);
|
||||||
|
|
||||||
public static readonly Default = ChartDataType.ExpenseByPrimaryCategory;
|
public static readonly Default = ChartDataType.ExpenseByPrimaryCategory;
|
||||||
|
|
||||||
public readonly type: number;
|
public readonly type: number;
|
||||||
public readonly name: string;
|
public readonly name: string;
|
||||||
|
public readonly desktopOnly: boolean = false;
|
||||||
|
public readonly specialChart: boolean = false;
|
||||||
private readonly availableAnalysisTypes: Record<number, boolean>;
|
private readonly availableAnalysisTypes: Record<number, boolean>;
|
||||||
|
|
||||||
private constructor(type: number, name: string, ...availableAnalysisTypes: StatisticsAnalysisType[]) {
|
private constructor(type: number, name: string, desktopOnly: boolean, specialChart: boolean, ...availableAnalysisTypes: StatisticsAnalysisType[]) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.desktopOnly = desktopOnly;
|
||||||
|
this.specialChart = specialChart;
|
||||||
this.availableAnalysisTypes = {};
|
this.availableAnalysisTypes = {};
|
||||||
|
|
||||||
if (availableAnalysisTypes) {
|
if (availableAnalysisTypes) {
|
||||||
@@ -126,7 +132,11 @@ export class ChartDataType implements TypeAndName {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChartDataType.allInstances.push(this);
|
if (!desktopOnly) {
|
||||||
|
ChartDataType.allInstancesForAll.push(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChartDataType.allInstancesForDesktop.push(this);
|
||||||
ChartDataType.allInstancesByType[type] = this;
|
ChartDataType.allInstancesByType[type] = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,14 +144,16 @@ export class ChartDataType implements TypeAndName {
|
|||||||
return this.availableAnalysisTypes[analysisType] || false;
|
return this.availableAnalysisTypes[analysisType] || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static values(analysisType?: StatisticsAnalysisType): ChartDataType[] {
|
public static values(analysisType?: StatisticsAnalysisType, withDesktopOnlyChart?: boolean): ChartDataType[] {
|
||||||
|
const availableInstances: ChartDataType[] = withDesktopOnlyChart ? ChartDataType.allInstancesForDesktop : ChartDataType.allInstancesForAll;
|
||||||
|
|
||||||
if (analysisType === undefined) {
|
if (analysisType === undefined) {
|
||||||
return ChartDataType.allInstances;
|
return availableInstances;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ret: ChartDataType[] = [];
|
const ret: ChartDataType[] = [];
|
||||||
|
|
||||||
for (const chartDataType of ChartDataType.allInstances) {
|
for (const chartDataType of availableInstances) {
|
||||||
if (chartDataType.isAvailableAnalysisType(analysisType)) {
|
if (chartDataType.isAvailableAnalysisType(analysisType)) {
|
||||||
ret.push(chartDataType);
|
ret.push(chartDataType);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -52,7 +52,7 @@ import 'vuetify/styles';
|
|||||||
|
|
||||||
import * as echarts from 'echarts/core';
|
import * as echarts from 'echarts/core';
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
import { LineChart, BarChart, PieChart, CandlestickChart, RadarChart } from 'echarts/charts';
|
import { LineChart, BarChart, PieChart, CandlestickChart, RadarChart, SankeyChart } from 'echarts/charts';
|
||||||
import {
|
import {
|
||||||
GridComponent,
|
GridComponent,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
@@ -105,6 +105,7 @@ import DateRangeSelectionDialog from '@/components/desktop/DateRangeSelectionDia
|
|||||||
import MonthSelectionDialog from '@/components/desktop/MonthSelectionDialog.vue';
|
import MonthSelectionDialog from '@/components/desktop/MonthSelectionDialog.vue';
|
||||||
import MonthRangeSelectionDialog from '@/components/desktop/MonthRangeSelectionDialog.vue';
|
import MonthRangeSelectionDialog from '@/components/desktop/MonthRangeSelectionDialog.vue';
|
||||||
import AccountBalanceTrendsChart from '@/components/desktop/AccountBalanceTrendsChart.vue';
|
import AccountBalanceTrendsChart from '@/components/desktop/AccountBalanceTrendsChart.vue';
|
||||||
|
import AccountAndCategorySankeyChart from '@/components/desktop/AccountAndCategorySankeyChart.vue';
|
||||||
import SwitchToMobileDialog from '@/components/desktop/SwitchToMobileDialog.vue';
|
import SwitchToMobileDialog from '@/components/desktop/SwitchToMobileDialog.vue';
|
||||||
|
|
||||||
import '@/styles/desktop/template/vuetify/index.scss';
|
import '@/styles/desktop/template/vuetify/index.scss';
|
||||||
@@ -498,6 +499,7 @@ echarts.use([
|
|||||||
PieChart,
|
PieChart,
|
||||||
CandlestickChart,
|
CandlestickChart,
|
||||||
RadarChart,
|
RadarChart,
|
||||||
|
SankeyChart,
|
||||||
GridComponent,
|
GridComponent,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
LegendComponent
|
LegendComponent
|
||||||
@@ -544,6 +546,7 @@ app.component('DateRangeSelectionDialog', DateRangeSelectionDialog);
|
|||||||
app.component('MonthSelectionDialog', MonthSelectionDialog);
|
app.component('MonthSelectionDialog', MonthSelectionDialog);
|
||||||
app.component('MonthRangeSelectionDialog', MonthRangeSelectionDialog);
|
app.component('MonthRangeSelectionDialog', MonthRangeSelectionDialog);
|
||||||
app.component('AccountBalanceTrendsChart', AccountBalanceTrendsChart);
|
app.component('AccountBalanceTrendsChart', AccountBalanceTrendsChart);
|
||||||
|
app.component('AccountAndCategorySankeyChart', AccountAndCategorySankeyChart);
|
||||||
app.component('SwitchToMobileDialog', SwitchToMobileDialog);
|
app.component('SwitchToMobileDialog', SwitchToMobileDialog);
|
||||||
|
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
|||||||
@@ -1517,6 +1517,7 @@
|
|||||||
"Area Chart": "Flächendiagramm",
|
"Area Chart": "Flächendiagramm",
|
||||||
"Column Chart": "Säulendiagramm",
|
"Column Chart": "Säulendiagramm",
|
||||||
"Candlestick Chart": "Candlestick Chart",
|
"Candlestick Chart": "Candlestick Chart",
|
||||||
|
"Sankey Chart": "Sankey Chart",
|
||||||
"Sort by": "Sortieren nach",
|
"Sort by": "Sortieren nach",
|
||||||
"Map": "Karte",
|
"Map": "Karte",
|
||||||
"Provider": "Anbieter",
|
"Provider": "Anbieter",
|
||||||
|
|||||||
@@ -1517,6 +1517,7 @@
|
|||||||
"Area Chart": "Area Chart",
|
"Area Chart": "Area Chart",
|
||||||
"Column Chart": "Column Chart",
|
"Column Chart": "Column Chart",
|
||||||
"Candlestick Chart": "Candlestick Chart",
|
"Candlestick Chart": "Candlestick Chart",
|
||||||
|
"Sankey Chart": "Sankey Chart",
|
||||||
"Sort by": "Sort by",
|
"Sort by": "Sort by",
|
||||||
"Map": "Map",
|
"Map": "Map",
|
||||||
"Provider": "Provider",
|
"Provider": "Provider",
|
||||||
|
|||||||
@@ -1517,6 +1517,7 @@
|
|||||||
"Area Chart": "Gráfico de área",
|
"Area Chart": "Gráfico de área",
|
||||||
"Column Chart": "Gráfico de columnas",
|
"Column Chart": "Gráfico de columnas",
|
||||||
"Candlestick Chart": "Candlestick Chart",
|
"Candlestick Chart": "Candlestick Chart",
|
||||||
|
"Sankey Chart": "Sankey Chart",
|
||||||
"Sort by": "Ordenar por",
|
"Sort by": "Ordenar por",
|
||||||
"Map": "Mapa",
|
"Map": "Mapa",
|
||||||
"Provider": "Proveedor",
|
"Provider": "Proveedor",
|
||||||
|
|||||||
@@ -1517,6 +1517,7 @@
|
|||||||
"Area Chart": "Graphique en aires",
|
"Area Chart": "Graphique en aires",
|
||||||
"Column Chart": "Graphique en colonnes",
|
"Column Chart": "Graphique en colonnes",
|
||||||
"Candlestick Chart": "Graphique en chandelier",
|
"Candlestick Chart": "Graphique en chandelier",
|
||||||
|
"Sankey Chart": "Sankey Chart",
|
||||||
"Sort by": "Trier par",
|
"Sort by": "Trier par",
|
||||||
"Map": "Carte",
|
"Map": "Carte",
|
||||||
"Provider": "Fournisseur",
|
"Provider": "Fournisseur",
|
||||||
|
|||||||
@@ -2352,7 +2352,7 @@ export function useI18n() {
|
|||||||
getAllCategoricalChartTypes: (withDesktopOnlyChart?: boolean) => getLocalizedDisplayNameAndType(CategoricalChartType.values(!!withDesktopOnlyChart)),
|
getAllCategoricalChartTypes: (withDesktopOnlyChart?: boolean) => getLocalizedDisplayNameAndType(CategoricalChartType.values(!!withDesktopOnlyChart)),
|
||||||
getAllTrendChartTypes: () => getLocalizedDisplayNameAndType(TrendChartType.values()),
|
getAllTrendChartTypes: () => getLocalizedDisplayNameAndType(TrendChartType.values()),
|
||||||
getAllAccountBalanceTrendChartTypes: () => getLocalizedDisplayNameAndType(AccountBalanceTrendChartType.values()),
|
getAllAccountBalanceTrendChartTypes: () => getLocalizedDisplayNameAndType(AccountBalanceTrendChartType.values()),
|
||||||
getAllStatisticsChartDataTypes: (analysisType: StatisticsAnalysisType) => getLocalizedDisplayNameAndType(ChartDataType.values(analysisType)),
|
getAllStatisticsChartDataTypes: (analysisType: StatisticsAnalysisType, withDesktopOnlyChart?: boolean) => getLocalizedDisplayNameAndType(ChartDataType.values(analysisType, withDesktopOnlyChart)),
|
||||||
getAllStatisticsSortingTypes: () => getLocalizedDisplayNameAndType(ChartSortingType.values()),
|
getAllStatisticsSortingTypes: () => getLocalizedDisplayNameAndType(ChartSortingType.values()),
|
||||||
getAllStatisticsDateAggregationTypes: () => getLocalizedChartDateAggregationTypeAndDisplayName(true),
|
getAllStatisticsDateAggregationTypes: () => getLocalizedChartDateAggregationTypeAndDisplayName(true),
|
||||||
getAllStatisticsDateAggregationTypesWithShortName: () => getLocalizedChartDateAggregationTypeAndDisplayName(false),
|
getAllStatisticsDateAggregationTypesWithShortName: () => getLocalizedChartDateAggregationTypeAndDisplayName(false),
|
||||||
|
|||||||
@@ -1517,6 +1517,7 @@
|
|||||||
"Area Chart": "Grafico ad area",
|
"Area Chart": "Grafico ad area",
|
||||||
"Column Chart": "Grafico a colonne",
|
"Column Chart": "Grafico a colonne",
|
||||||
"Candlestick Chart": "Candlestick Chart",
|
"Candlestick Chart": "Candlestick Chart",
|
||||||
|
"Sankey Chart": "Sankey Chart",
|
||||||
"Sort by": "Ordina per",
|
"Sort by": "Ordina per",
|
||||||
"Map": "Mappa",
|
"Map": "Mappa",
|
||||||
"Provider": "Fornitore",
|
"Provider": "Fornitore",
|
||||||
|
|||||||
@@ -1517,6 +1517,7 @@
|
|||||||
"Area Chart": "エリアチャート",
|
"Area Chart": "エリアチャート",
|
||||||
"Column Chart": "列チャート",
|
"Column Chart": "列チャート",
|
||||||
"Candlestick Chart": "Candlestick Chart",
|
"Candlestick Chart": "Candlestick Chart",
|
||||||
|
"Sankey Chart": "Sankey Chart",
|
||||||
"Sort by": "ソート順",
|
"Sort by": "ソート順",
|
||||||
"Map": "地図",
|
"Map": "地図",
|
||||||
"Provider": "プロバイダー",
|
"Provider": "プロバイダー",
|
||||||
|
|||||||
@@ -1517,6 +1517,7 @@
|
|||||||
"Area Chart": "영역 차트",
|
"Area Chart": "영역 차트",
|
||||||
"Column Chart": "세로 막대 차트",
|
"Column Chart": "세로 막대 차트",
|
||||||
"Candlestick Chart": "캠들스틱 차트",
|
"Candlestick Chart": "캠들스틱 차트",
|
||||||
|
"Sankey Chart": "Sankey Chart",
|
||||||
"Sort by": "정렬 기준",
|
"Sort by": "정렬 기준",
|
||||||
"Map": "지도",
|
"Map": "지도",
|
||||||
"Provider": "제공자",
|
"Provider": "제공자",
|
||||||
|
|||||||
@@ -1517,6 +1517,7 @@
|
|||||||
"Area Chart": "Vlakdiagram",
|
"Area Chart": "Vlakdiagram",
|
||||||
"Column Chart": "Kolomdiagram",
|
"Column Chart": "Kolomdiagram",
|
||||||
"Candlestick Chart": "Candlestickdiagram",
|
"Candlestick Chart": "Candlestickdiagram",
|
||||||
|
"Sankey Chart": "Sankey Chart",
|
||||||
"Sort by": "Sorteren op",
|
"Sort by": "Sorteren op",
|
||||||
"Map": "Kaart",
|
"Map": "Kaart",
|
||||||
"Provider": "Provider",
|
"Provider": "Provider",
|
||||||
|
|||||||
@@ -1517,6 +1517,7 @@
|
|||||||
"Area Chart": "Gráfico de Área",
|
"Area Chart": "Gráfico de Área",
|
||||||
"Column Chart": "Gráfico de Colunas",
|
"Column Chart": "Gráfico de Colunas",
|
||||||
"Candlestick Chart": "Candlestick Chart",
|
"Candlestick Chart": "Candlestick Chart",
|
||||||
|
"Sankey Chart": "Sankey Chart",
|
||||||
"Sort by": "Ordenar por",
|
"Sort by": "Ordenar por",
|
||||||
"Map": "Mapa",
|
"Map": "Mapa",
|
||||||
"Provider": "Provedor",
|
"Provider": "Provedor",
|
||||||
|
|||||||
@@ -1517,6 +1517,7 @@
|
|||||||
"Area Chart": "Диаграмма с областями",
|
"Area Chart": "Диаграмма с областями",
|
||||||
"Column Chart": "Столбчатая диаграмма",
|
"Column Chart": "Столбчатая диаграмма",
|
||||||
"Candlestick Chart": "Candlestick Chart",
|
"Candlestick Chart": "Candlestick Chart",
|
||||||
|
"Sankey Chart": "Sankey Chart",
|
||||||
"Sort by": "Сортировать по",
|
"Sort by": "Сортировать по",
|
||||||
"Map": "Карта",
|
"Map": "Карта",
|
||||||
"Provider": "Провайдер",
|
"Provider": "Провайдер",
|
||||||
|
|||||||
@@ -1517,6 +1517,7 @@
|
|||||||
"Area Chart": "กราฟพื้นที่",
|
"Area Chart": "กราฟพื้นที่",
|
||||||
"Column Chart": "กราฟคอลัมน์",
|
"Column Chart": "กราฟคอลัมน์",
|
||||||
"Candlestick Chart": "กราฟแท่งเทียน",
|
"Candlestick Chart": "กราฟแท่งเทียน",
|
||||||
|
"Sankey Chart": "Sankey Chart",
|
||||||
"Sort by": "จัดเรียงตาม",
|
"Sort by": "จัดเรียงตาม",
|
||||||
"Map": "แผนที่",
|
"Map": "แผนที่",
|
||||||
"Provider": "ผู้ให้บริการ",
|
"Provider": "ผู้ให้บริการ",
|
||||||
|
|||||||
@@ -1517,6 +1517,7 @@
|
|||||||
"Area Chart": "Діаграма з областями",
|
"Area Chart": "Діаграма з областями",
|
||||||
"Column Chart": "Стовпчикова діаграма",
|
"Column Chart": "Стовпчикова діаграма",
|
||||||
"Candlestick Chart": "Candlestick Chart",
|
"Candlestick Chart": "Candlestick Chart",
|
||||||
|
"Sankey Chart": "Sankey Chart",
|
||||||
"Sort by": "Сортувати за",
|
"Sort by": "Сортувати за",
|
||||||
"Map": "Карта",
|
"Map": "Карта",
|
||||||
"Provider": "Провайдер",
|
"Provider": "Провайдер",
|
||||||
|
|||||||
@@ -1517,6 +1517,7 @@
|
|||||||
"Area Chart": "Biểu đồ diện tích",
|
"Area Chart": "Biểu đồ diện tích",
|
||||||
"Column Chart": "Biểu đồ cột",
|
"Column Chart": "Biểu đồ cột",
|
||||||
"Candlestick Chart": "Candlestick Chart",
|
"Candlestick Chart": "Candlestick Chart",
|
||||||
|
"Sankey Chart": "Sankey Chart",
|
||||||
"Sort by": "Sắp xếp theo",
|
"Sort by": "Sắp xếp theo",
|
||||||
"Map": "Bản đồ",
|
"Map": "Bản đồ",
|
||||||
"Provider": "Nhà cung cấp",
|
"Provider": "Nhà cung cấp",
|
||||||
|
|||||||
@@ -1517,6 +1517,7 @@
|
|||||||
"Area Chart": "面积图",
|
"Area Chart": "面积图",
|
||||||
"Column Chart": "柱状图",
|
"Column Chart": "柱状图",
|
||||||
"Candlestick Chart": "K线图",
|
"Candlestick Chart": "K线图",
|
||||||
|
"Sankey Chart": "桑基图",
|
||||||
"Sort by": "排序方式",
|
"Sort by": "排序方式",
|
||||||
"Map": "地图",
|
"Map": "地图",
|
||||||
"Provider": "提供者",
|
"Provider": "提供者",
|
||||||
|
|||||||
@@ -1517,6 +1517,7 @@
|
|||||||
"Area Chart": "面積圖",
|
"Area Chart": "面積圖",
|
||||||
"Column Chart": "柱狀圖",
|
"Column Chart": "柱狀圖",
|
||||||
"Candlestick Chart": "K線圖",
|
"Candlestick Chart": "K線圖",
|
||||||
|
"Sankey Chart": "桑基圖",
|
||||||
"Sort by": "排序方式",
|
"Sort by": "排序方式",
|
||||||
"Map": "地圖",
|
"Map": "地圖",
|
||||||
"Provider": "提供者",
|
"Provider": "提供者",
|
||||||
|
|||||||
@@ -722,6 +722,33 @@ export interface SortableTransactionStatisticDataItem {
|
|||||||
readonly totalAmount: number;
|
readonly totalAmount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TransactionStatisticResponseItemWithInfo extends TransactionStatisticResponseItem {
|
||||||
|
categoryId: string;
|
||||||
|
accountId: string;
|
||||||
|
relatedAccountId?: string;
|
||||||
|
amount: number;
|
||||||
|
account?: Account;
|
||||||
|
primaryAccount?: Account;
|
||||||
|
relatedAccount?: Account;
|
||||||
|
relatedPrimaryAccount?: Account;
|
||||||
|
relatedAccountType?: number;
|
||||||
|
category?: TransactionCategory;
|
||||||
|
primaryCategory?: TransactionCategory;
|
||||||
|
amountInDefaultCurrency: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionStatisticResponseWithInfo {
|
||||||
|
readonly startTime: number;
|
||||||
|
readonly endTime: number;
|
||||||
|
readonly items: TransactionStatisticResponseItemWithInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionStatisticTrendsResponseItemWithInfo {
|
||||||
|
readonly year: number;
|
||||||
|
readonly month: number; // 1-based (1 = January, 12 = December)
|
||||||
|
readonly items: TransactionStatisticResponseItemWithInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
export type TransactionStatisticDataItemType = 'category' | 'account' | 'total';
|
export type TransactionStatisticDataItemType = 'category' | 'account' | 'total';
|
||||||
|
|
||||||
export interface TransactionStatisticDataItemBase extends SortableTransactionStatisticDataItem {
|
export interface TransactionStatisticDataItemBase extends SortableTransactionStatisticDataItem {
|
||||||
|
|||||||
+76
-30
@@ -28,12 +28,13 @@ import {
|
|||||||
import { DEFAULT_ACCOUNT_ICON, DEFAULT_CATEGORY_ICON } from '@/consts/icon.ts';
|
import { DEFAULT_ACCOUNT_ICON, DEFAULT_CATEGORY_ICON } from '@/consts/icon.ts';
|
||||||
import { DEFAULT_ACCOUNT_COLOR, DEFAULT_CATEGORY_COLOR } from '@/consts/color.ts';
|
import { DEFAULT_ACCOUNT_COLOR, DEFAULT_CATEGORY_COLOR } from '@/consts/color.ts';
|
||||||
|
|
||||||
import type { Account } from '@/models/account.ts';
|
|
||||||
import type { TransactionCategory } from '@/models/transaction_category.ts';
|
|
||||||
import type {
|
import type {
|
||||||
TransactionStatisticResponse,
|
TransactionStatisticResponse,
|
||||||
TransactionStatisticResponseItem,
|
TransactionStatisticResponseItem,
|
||||||
TransactionStatisticTrendsResponseItem,
|
TransactionStatisticTrendsResponseItem,
|
||||||
|
TransactionStatisticResponseItemWithInfo,
|
||||||
|
TransactionStatisticResponseWithInfo,
|
||||||
|
TransactionStatisticTrendsResponseItemWithInfo,
|
||||||
TransactionStatisticDataItemType,
|
TransactionStatisticDataItemType,
|
||||||
TransactionStatisticDataItemBase,
|
TransactionStatisticDataItemBase,
|
||||||
TransactionCategoricalAnalysisData,
|
TransactionCategoricalAnalysisData,
|
||||||
@@ -61,33 +62,6 @@ import { sortStatisticsItems } from '@/lib/statistics.ts';
|
|||||||
import logger from '@/lib/logger.ts';
|
import logger from '@/lib/logger.ts';
|
||||||
import services from '@/lib/services.ts';
|
import services from '@/lib/services.ts';
|
||||||
|
|
||||||
interface TransactionStatisticResponseItemWithInfo extends TransactionStatisticResponseItem {
|
|
||||||
categoryId: string;
|
|
||||||
accountId: string;
|
|
||||||
relatedAccountId?: string;
|
|
||||||
amount: number;
|
|
||||||
account?: Account;
|
|
||||||
primaryAccount?: Account;
|
|
||||||
relatedAccount?: Account;
|
|
||||||
relatedPrimaryAccount?: Account;
|
|
||||||
relatedAccountType?: number;
|
|
||||||
category?: TransactionCategory;
|
|
||||||
primaryCategory?: TransactionCategory;
|
|
||||||
amountInDefaultCurrency: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TransactionStatisticResponseWithInfo {
|
|
||||||
readonly startTime: number;
|
|
||||||
readonly endTime: number;
|
|
||||||
readonly items: TransactionStatisticResponseItemWithInfo[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TransactionStatisticTrendsResponseItemWithInfo {
|
|
||||||
readonly year: number;
|
|
||||||
readonly month: number; // 1-based (1 = January, 12 = December)
|
|
||||||
readonly items: TransactionStatisticResponseItemWithInfo[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WritableTransactionCategoricalAnalysisData {
|
interface WritableTransactionCategoricalAnalysisData {
|
||||||
totalAmount: number;
|
totalAmount: number;
|
||||||
totalNonNegativeAmount: number;
|
totalNonNegativeAmount: number;
|
||||||
@@ -229,6 +203,36 @@ export const useStatisticsStore = defineStore('statistics', () => {
|
|||||||
return getCategoryTotalAmountItems(transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.items, transactionStatisticsFilter.value);
|
return getCategoryTotalAmountItems(transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.items, transactionStatisticsFilter.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const categoricalAllAnalysisData = computed<TransactionStatisticResponseWithInfo | null>(() => {
|
||||||
|
if (!transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value || !transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.items) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allDataItems: TransactionStatisticResponseItemWithInfo[] = [];
|
||||||
|
|
||||||
|
for (const item of transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.items) {
|
||||||
|
if (!item.primaryAccount || !item.account || !item.primaryCategory || !item.category) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transactionStatisticsFilter.value.filterAccountIds && transactionStatisticsFilter.value.filterAccountIds[item.account.id]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transactionStatisticsFilter.value.filterCategoryIds && transactionStatisticsFilter.value.filterCategoryIds[item.category.id]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
allDataItems.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
startTime: transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.startTime,
|
||||||
|
endTime: transactionCategoryStatisticsDataWithCategoryAndAccountInfo.value.endTime,
|
||||||
|
items: allDataItems
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const accountTotalAmountAnalysisData = computed<WritableTransactionCategoricalAnalysisData | null>(() => {
|
const accountTotalAmountAnalysisData = computed<WritableTransactionCategoricalAnalysisData | null>(() => {
|
||||||
if (!accountsStore.allPlainAccounts) {
|
if (!accountsStore.allPlainAccounts) {
|
||||||
return null;
|
return null;
|
||||||
@@ -1043,7 +1047,48 @@ export const useStatisticsStore = defineStore('statistics', () => {
|
|||||||
querys.push('type=3');
|
querys.push('type=3');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemId && (transactionStatisticsFilter.value.chartDataType === ChartDataType.InflowsByAccount.type
|
if (itemId && transactionStatisticsFilter.value.chartDataType === ChartDataType.Overview.type) {
|
||||||
|
const items = itemId.split('-');
|
||||||
|
const sourceItems = (items[0] || '').split(':');
|
||||||
|
const queryAccountIds: string[] = [];
|
||||||
|
const queryCategoryIds: string[] = [];
|
||||||
|
|
||||||
|
if (sourceItems.length === 2) {
|
||||||
|
if (sourceItems[0] === 'account') {
|
||||||
|
queryAccountIds.push(sourceItems[1] as string);
|
||||||
|
} else if (sourceItems[0] === 'category') {
|
||||||
|
queryCategoryIds.push(sourceItems[1] as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length === 2) {
|
||||||
|
const targetItems = (items[1] || '').split(':');
|
||||||
|
|
||||||
|
if (targetItems.length === 2) {
|
||||||
|
if (targetItems[0] === 'account') {
|
||||||
|
queryAccountIds.push(targetItems[1] as string);
|
||||||
|
} else if (targetItems[0] === 'category') {
|
||||||
|
queryCategoryIds.push(targetItems[1] as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryAccountIds.length) {
|
||||||
|
if (queryAccountIds.length === 2) {
|
||||||
|
querys.push('type=4');
|
||||||
|
}
|
||||||
|
|
||||||
|
querys.push('accountIds=' + queryAccountIds.join(','));
|
||||||
|
} else {
|
||||||
|
querys.push('accountIds=' + getFinalAccountIdsByFilteredAccountIds(accountsStore.allAccountsMap, transactionStatisticsFilter.value.filterAccountIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryCategoryIds.length) {
|
||||||
|
querys.push('categoryIds=' + queryCategoryIds.join(','));
|
||||||
|
} else {
|
||||||
|
querys.push('categoryIds=' + getFinalCategoryIdsByFilteredCategoryIds(transactionCategoriesStore.allTransactionCategoriesMap, transactionStatisticsFilter.value.filterCategoryIds));
|
||||||
|
}
|
||||||
|
} else if (itemId && (transactionStatisticsFilter.value.chartDataType === ChartDataType.InflowsByAccount.type
|
||||||
|| transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeByAccount.type
|
|| transactionStatisticsFilter.value.chartDataType === ChartDataType.IncomeByAccount.type
|
||||||
|| transactionStatisticsFilter.value.chartDataType === ChartDataType.OutflowsByAccount.type
|
|| transactionStatisticsFilter.value.chartDataType === ChartDataType.OutflowsByAccount.type
|
||||||
|| transactionStatisticsFilter.value.chartDataType === ChartDataType.ExpenseByAccount.type
|
|| transactionStatisticsFilter.value.chartDataType === ChartDataType.ExpenseByAccount.type
|
||||||
@@ -1197,6 +1242,7 @@ export const useStatisticsStore = defineStore('statistics', () => {
|
|||||||
transactionStatisticsStateInvalid,
|
transactionStatisticsStateInvalid,
|
||||||
// computed states
|
// computed states
|
||||||
categoricalAnalysisChartDataCategory,
|
categoricalAnalysisChartDataCategory,
|
||||||
|
categoricalAllAnalysisData,
|
||||||
categoricalAnalysisData,
|
categoricalAnalysisData,
|
||||||
trendsAnalysisData,
|
trendsAnalysisData,
|
||||||
// functions
|
// functions
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { StatisticsAnalysisType, ChartDataType, ChartSortingType, ChartDateAggre
|
|||||||
import { DISPLAY_HIDDEN_AMOUNT } from '@/consts/numeral.ts';
|
import { DISPLAY_HIDDEN_AMOUNT } from '@/consts/numeral.ts';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
TransactionStatisticResponseWithInfo,
|
||||||
TransactionCategoricalAnalysisData,
|
TransactionCategoricalAnalysisData,
|
||||||
TransactionCategoricalAnalysisDataItem,
|
TransactionCategoricalAnalysisDataItem,
|
||||||
TransactionTrendsAnalysisData
|
TransactionTrendsAnalysisData
|
||||||
@@ -230,6 +231,7 @@ export function useStatisticsTransactionPageBase() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const categoricalAnalysisData = computed<TransactionCategoricalAnalysisData>(() => statisticsStore.categoricalAnalysisData);
|
const categoricalAnalysisData = computed<TransactionCategoricalAnalysisData>(() => statisticsStore.categoricalAnalysisData);
|
||||||
|
const categoricalAllAnalysisData = computed<TransactionStatisticResponseWithInfo | null>(() => statisticsStore.categoricalAllAnalysisData);
|
||||||
const trendsAnalysisData = computed<TransactionTrendsAnalysisData | null>(() => statisticsStore.trendsAnalysisData);
|
const trendsAnalysisData = computed<TransactionTrendsAnalysisData | null>(() => statisticsStore.trendsAnalysisData);
|
||||||
|
|
||||||
function canShowCustomDateRange(dateRangeType: number): boolean {
|
function canShowCustomDateRange(dateRangeType: number): boolean {
|
||||||
@@ -301,6 +303,7 @@ export function useStatisticsTransactionPageBase() {
|
|||||||
showTotalAmountInTrendsChart,
|
showTotalAmountInTrendsChart,
|
||||||
translateNameInTrendsChart,
|
translateNameInTrendsChart,
|
||||||
categoricalAnalysisData,
|
categoricalAnalysisData,
|
||||||
|
categoricalAllAnalysisData,
|
||||||
trendsAnalysisData,
|
trendsAnalysisData,
|
||||||
// functions
|
// functions
|
||||||
canShowCustomDateRange,
|
canShowCustomDateRange,
|
||||||
|
|||||||
@@ -21,6 +21,17 @@
|
|||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
:items="allChartTypes"
|
:items="allChartTypes"
|
||||||
v-model="queryChartType"
|
v-model="queryChartType"
|
||||||
|
v-show="!isQuerySpecialChartType"
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
item-title="displayName"
|
||||||
|
item-value="type"
|
||||||
|
class="mt-2"
|
||||||
|
density="compact"
|
||||||
|
:disabled="true"
|
||||||
|
:items="[{ displayName: tt('Sankey Chart'), type: 0 }]"
|
||||||
|
:model-value="0"
|
||||||
|
v-show="isQuerySpecialChartType && queryChartDataType === ChartDataType.Overview.type"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-6 mt-4">
|
<div class="mx-6 mt-4">
|
||||||
@@ -38,7 +49,7 @@
|
|||||||
<v-tabs show-arrows class="my-4" direction="vertical"
|
<v-tabs show-arrows class="my-4" direction="vertical"
|
||||||
:disabled="loading" v-model="queryChartDataType">
|
:disabled="loading" v-model="queryChartDataType">
|
||||||
<v-tab class="tab-text-truncate" :key="dataType.type" :value="dataType.type"
|
<v-tab class="tab-text-truncate" :key="dataType.type" :value="dataType.type"
|
||||||
v-for="dataType in ChartDataType.values()"
|
v-for="dataType in ChartDataType.values(undefined, true)"
|
||||||
v-show="dataType.isAvailableAnalysisType(queryAnalysisType)">
|
v-show="dataType.isAvailableAnalysisType(queryAnalysisType)">
|
||||||
<span class="text-truncate">{{ tt(dataType.name) }}</span>
|
<span class="text-truncate">{{ tt(dataType.name) }}</span>
|
||||||
<v-tooltip activator="parent" location="right">{{ tt(dataType.name) }}</v-tooltip>
|
<v-tooltip activator="parent" location="right">{{ tt(dataType.name) }}</v-tooltip>
|
||||||
@@ -48,7 +59,7 @@
|
|||||||
<v-main>
|
<v-main>
|
||||||
<v-window class="d-flex flex-grow-1 disable-tab-transition w-100-window-container" v-model="activeTab">
|
<v-window class="d-flex flex-grow-1 disable-tab-transition w-100-window-container" v-model="activeTab">
|
||||||
<v-window-item value="statisticsPage">
|
<v-window-item value="statisticsPage">
|
||||||
<v-card variant="flat" :min-height="queryAnalysisType === StatisticsAnalysisType.TrendAnalysis ? '860' : '700'">
|
<v-card variant="flat" :min-height="queryAnalysisType === StatisticsAnalysisType.TrendAnalysis ? '860' : '760'">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="title-and-toolbar d-flex align-center">
|
<div class="title-and-toolbar d-flex align-center">
|
||||||
<v-btn class="me-3 d-md-none" density="compact" color="default" variant="plain"
|
<v-btn class="me-3 d-md-none" density="compact" color="default" variant="plain"
|
||||||
@@ -160,7 +171,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<v-card-text class="statistics-overview-title pt-0" :class="{ 'disabled': loading }"
|
<v-card-text class="statistics-overview-title pt-0" :class="{ 'disabled': loading }"
|
||||||
v-if="queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && (initing || (categoricalAnalysisData && categoricalAnalysisData.items && categoricalAnalysisData.items.length))">
|
v-if="queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && !isQuerySpecialChartType && (initing || (categoricalAnalysisData && categoricalAnalysisData.items && categoricalAnalysisData.items.length))">
|
||||||
<span class="statistics-subtitle">{{ totalAmountName }}</span>
|
<span class="statistics-subtitle">{{ totalAmountName }}</span>
|
||||||
<span class="statistics-overview-amount ms-3"
|
<span class="statistics-overview-amount ms-3"
|
||||||
:class="statisticsTextColor"
|
:class="statisticsTextColor"
|
||||||
@@ -173,12 +184,29 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-text class="statistics-overview-title pt-0"
|
<v-card-text class="statistics-overview-title pt-0"
|
||||||
v-else-if="!initing && ((queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && (!categoricalAnalysisData || !categoricalAnalysisData.items || !categoricalAnalysisData.items.length))
|
v-else-if="!initing && ((queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && !isQuerySpecialChartType && (!categoricalAnalysisData || !categoricalAnalysisData.items || !categoricalAnalysisData.items.length))
|
||||||
|| (queryAnalysisType === StatisticsAnalysisType.TrendAnalysis && (!trendsAnalysisData || !trendsAnalysisData.items || !trendsAnalysisData.items.length)))">
|
|| (queryAnalysisType === StatisticsAnalysisType.TrendAnalysis && (!trendsAnalysisData || !trendsAnalysisData.items || !trendsAnalysisData.items.length)))">
|
||||||
<span class="statistics-subtitle statistics-overview-empty-tip">{{ tt('No transaction data') }}</span>
|
<span class="statistics-subtitle statistics-overview-empty-tip">{{ tt('No transaction data') }}</span>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-text :class="{ 'readonly': loading }" v-if="queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && query.categoricalChartType === CategoricalChartType.Pie.type">
|
<v-card-text :class="{ 'readonly': loading }" v-if="queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && queryChartDataType === ChartDataType.Overview.type">
|
||||||
|
<account-and-category-sankey-chart
|
||||||
|
:items="[]"
|
||||||
|
:sorting-type="querySortingType"
|
||||||
|
:skeleton="true"
|
||||||
|
v-if="initing"
|
||||||
|
/>
|
||||||
|
<account-and-category-sankey-chart
|
||||||
|
:items="categoricalAllAnalysisData && categoricalAllAnalysisData.items && categoricalAllAnalysisData.items.length ? categoricalAllAnalysisData.items : []"
|
||||||
|
:sorting-type="querySortingType"
|
||||||
|
:enable-click-item="true"
|
||||||
|
:default-currency="defaultCurrency"
|
||||||
|
v-else-if="!initing"
|
||||||
|
@click="onClickSankeyChartItem"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-text :class="{ 'readonly': loading }" v-if="queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && !isQuerySpecialChartType && query.categoricalChartType === CategoricalChartType.Pie.type">
|
||||||
<pie-chart
|
<pie-chart
|
||||||
:items="[
|
:items="[
|
||||||
{id: '1', name: '---', value: 60, color: '7c7c7f'},
|
{id: '1', name: '---', value: 60, color: '7c7c7f'},
|
||||||
@@ -191,7 +219,7 @@
|
|||||||
value-field="value"
|
value-field="value"
|
||||||
color-field="color"
|
color-field="color"
|
||||||
v-if="initing"
|
v-if="initing"
|
||||||
></pie-chart>
|
/>
|
||||||
<pie-chart
|
<pie-chart
|
||||||
:items="categoricalAnalysisData && categoricalAnalysisData.items && categoricalAnalysisData.items.length ? categoricalAnalysisData.items : []"
|
:items="categoricalAnalysisData && categoricalAnalysisData.items && categoricalAnalysisData.items.length ? categoricalAnalysisData.items : []"
|
||||||
:min-valid-percent="0.0001"
|
:min-valid-percent="0.0001"
|
||||||
@@ -208,7 +236,7 @@
|
|||||||
/>
|
/>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-text :class="{ 'readonly': loading }" v-if="queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && query.categoricalChartType === CategoricalChartType.Bar.type">
|
<v-card-text :class="{ 'readonly': loading }" v-if="queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && !isQuerySpecialChartType && query.categoricalChartType === CategoricalChartType.Bar.type">
|
||||||
<v-list rounded lines="two" v-if="initing">
|
<v-list rounded lines="two" v-if="initing">
|
||||||
<template :key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]">
|
<template :key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]">
|
||||||
<v-list-item class="ps-0">
|
<v-list-item class="ps-0">
|
||||||
@@ -263,7 +291,7 @@
|
|||||||
</v-list>
|
</v-list>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-text :class="{ 'readonly': loading }" v-if="queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && query.categoricalChartType === CategoricalChartType.Radar.type">
|
<v-card-text :class="{ 'readonly': loading }" v-if="queryAnalysisType === StatisticsAnalysisType.CategoricalAnalysis && !isQuerySpecialChartType && query.categoricalChartType === CategoricalChartType.Radar.type">
|
||||||
<radar-chart
|
<radar-chart
|
||||||
:items="[
|
:items="[
|
||||||
{name: '---', value: 10},
|
{name: '---', value: 10},
|
||||||
@@ -277,7 +305,7 @@
|
|||||||
name-field="name"
|
name-field="name"
|
||||||
value-field="value"
|
value-field="value"
|
||||||
v-if="initing"
|
v-if="initing"
|
||||||
></radar-chart>
|
/>
|
||||||
<radar-chart
|
<radar-chart
|
||||||
:items="categoricalAnalysisData && categoricalAnalysisData.items && categoricalAnalysisData.items.length ? categoricalAnalysisData.items : []"
|
:items="categoricalAnalysisData && categoricalAnalysisData.items && categoricalAnalysisData.items.length ? categoricalAnalysisData.items : []"
|
||||||
:min-valid-percent="0.0001"
|
:min-valid-percent="0.0001"
|
||||||
@@ -493,6 +521,7 @@ const {
|
|||||||
showTotalAmountInTrendsChart,
|
showTotalAmountInTrendsChart,
|
||||||
translateNameInTrendsChart,
|
translateNameInTrendsChart,
|
||||||
categoricalAnalysisData,
|
categoricalAnalysisData,
|
||||||
|
categoricalAllAnalysisData,
|
||||||
trendsAnalysisData,
|
trendsAnalysisData,
|
||||||
canShowCustomDateRange,
|
canShowCustomDateRange,
|
||||||
getTransactionCategoricalAnalysisDataItemDisplayColor,
|
getTransactionCategoricalAnalysisDataItemDisplayColor,
|
||||||
@@ -576,6 +605,10 @@ const querySortingType = computed<number>({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isQuerySpecialChartType = computed<boolean>(() => {
|
||||||
|
return ChartDataType.valueOf(queryChartDataType.value)?.specialChart ?? false;
|
||||||
|
});
|
||||||
|
|
||||||
const statisticsTextColor = computed<string>(() => {
|
const statisticsTextColor = computed<string>(() => {
|
||||||
if (query.value.chartDataType === ChartDataType.OutflowsByAccount.type ||
|
if (query.value.chartDataType === ChartDataType.OutflowsByAccount.type ||
|
||||||
query.value.chartDataType === ChartDataType.ExpenseByAccount.type ||
|
query.value.chartDataType === ChartDataType.ExpenseByAccount.type ||
|
||||||
@@ -705,7 +738,8 @@ function reload(force: boolean): Promise<unknown> | null {
|
|||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
if (query.value.chartDataType === ChartDataType.OutflowsByAccount.type ||
|
if (query.value.chartDataType === ChartDataType.Overview.type ||
|
||||||
|
query.value.chartDataType === ChartDataType.OutflowsByAccount.type ||
|
||||||
query.value.chartDataType === ChartDataType.ExpenseByAccount.type ||
|
query.value.chartDataType === ChartDataType.ExpenseByAccount.type ||
|
||||||
query.value.chartDataType === ChartDataType.ExpenseByPrimaryCategory.type ||
|
query.value.chartDataType === ChartDataType.ExpenseByPrimaryCategory.type ||
|
||||||
query.value.chartDataType === ChartDataType.ExpenseBySecondaryCategory.type ||
|
query.value.chartDataType === ChartDataType.ExpenseBySecondaryCategory.type ||
|
||||||
@@ -1019,6 +1053,23 @@ function exportResults(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClickSankeyChartItem(sourceItemType: 'account' | 'category', sourceItemId: string, targetItemType?: 'account' | 'category', targetItemId?: string): void {
|
||||||
|
if (sourceItemType === 'category' && targetItemType === 'category' && sourceItemId && targetItemId) {
|
||||||
|
const sourceCategory = transactionCategoriesStore.allTransactionCategoriesMap[sourceItemId];
|
||||||
|
const targetCategory = transactionCategoriesStore.allTransactionCategoriesMap[targetItemId];
|
||||||
|
|
||||||
|
if (sourceCategory?.parentId === targetCategory?.id) {
|
||||||
|
router.push(getTransactionItemLinkUrl(`${sourceItemType}:${sourceItemId}`));
|
||||||
|
return;
|
||||||
|
} else if (targetCategory?.parentId === sourceCategory?.id) {
|
||||||
|
router.push(getTransactionItemLinkUrl(`${targetItemType}:${targetItemId}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push(getTransactionItemLinkUrl(`${sourceItemType}:${sourceItemId}` + (targetItemType && targetItemId ? `-${targetItemType}:${targetItemId}` : '')));
|
||||||
|
}
|
||||||
|
|
||||||
function onClickPieChartItem(item: Record<string, unknown>): void {
|
function onClickPieChartItem(item: Record<string, unknown>): void {
|
||||||
router.push(getTransactionItemLinkUrl(item['id'] as string));
|
router.push(getTransactionItemLinkUrl(item['id'] as string));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user