mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-16 07:57:33 +08:00
add candlestick chart for account balance trends
This commit is contained in:
@@ -15,6 +15,7 @@ import type { AccountInfoResponse } from '@/models/account.ts';
|
||||
import type { TransactionReconciliationStatementResponseItem } from '@/models/transaction.ts';
|
||||
|
||||
import { isDefined, isArray } from '@/lib/common.ts';
|
||||
import { sumAmounts } from '@/lib/numeral.ts';
|
||||
import {
|
||||
getYearAndMonthFromUnixTime,
|
||||
getYearFirstUnixTimeBySpecifiedUnixTime,
|
||||
@@ -27,13 +28,19 @@ import {
|
||||
import { getAllDateRangesByYearMonthRange } from '@/lib/statistics.ts';
|
||||
|
||||
export interface AccountBalanceUnixTimeAndBalanceRange extends UnixTimeRange {
|
||||
minUnixTimeBalance: number;
|
||||
maxUnixTimeBalance: number;
|
||||
minUnixTimeOpeningBalance: number;
|
||||
minUnixTimeClosingBalance: number;
|
||||
maxUnixTimeClosingBalance: number;
|
||||
}
|
||||
|
||||
export interface AccountBalanceTrendsChartItem {
|
||||
displayDate: string;
|
||||
amount: number;
|
||||
openingBalance: number;
|
||||
closingBalance: number;
|
||||
minimumBalance: number;
|
||||
maximumBalance: number;
|
||||
medianBalance: number;
|
||||
averageBalance: number;
|
||||
}
|
||||
|
||||
export interface CommonAccountBalanceTrendsChartProps {
|
||||
@@ -52,19 +59,22 @@ export function useAccountBalanceTrendsChartBase(props: CommonAccountBalanceTren
|
||||
}
|
||||
|
||||
let minUnixTime = Number.MAX_SAFE_INTEGER, maxUnixTime = 0;
|
||||
let minUnixTimeBalance = 0, maxUnixTimeBalance = 0;
|
||||
let minUnixTimeOpeningBalance = 0;
|
||||
let minUnixTimeClosingBalance = 0;
|
||||
let maxUnixTimeClosingBalance = 0;
|
||||
|
||||
for (let i = 0; i < props.items.length; i++) {
|
||||
const item = props.items[i];
|
||||
|
||||
if (item.time < minUnixTime) {
|
||||
minUnixTime = item.time;
|
||||
minUnixTimeBalance = item.accountBalance;
|
||||
minUnixTimeOpeningBalance = item.accountOpeningBalance;
|
||||
minUnixTimeClosingBalance = item.accountClosingBalance;
|
||||
}
|
||||
|
||||
if (item.time > maxUnixTime) {
|
||||
maxUnixTime = item.time;
|
||||
maxUnixTimeBalance = item.accountBalance;
|
||||
maxUnixTimeClosingBalance = item.accountClosingBalance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +85,9 @@ export function useAccountBalanceTrendsChartBase(props: CommonAccountBalanceTren
|
||||
return {
|
||||
minUnixTime: minUnixTime,
|
||||
maxUnixTime: maxUnixTime,
|
||||
minUnixTimeBalance: minUnixTimeBalance,
|
||||
maxUnixTimeBalance: maxUnixTimeBalance
|
||||
minUnixTimeOpeningBalance: minUnixTimeOpeningBalance,
|
||||
minUnixTimeClosingBalance: minUnixTimeClosingBalance,
|
||||
maxUnixTimeClosingBalance: maxUnixTimeClosingBalance
|
||||
};
|
||||
});
|
||||
|
||||
@@ -125,7 +136,12 @@ export function useAccountBalanceTrendsChartBase(props: CommonAccountBalanceTren
|
||||
dayDataItemsMap[dateRangeMinUnixTime] = dataItems;
|
||||
}
|
||||
|
||||
let lastAmount = dataDateRange.value.minUnixTimeBalance;
|
||||
let lastOpeningBalance = dataDateRange.value.minUnixTimeOpeningBalance;
|
||||
let lastClosingBalance = dataDateRange.value.minUnixTimeClosingBalance;
|
||||
let lastMinimumBalance = lastClosingBalance;
|
||||
let lastMaximumBalance = lastClosingBalance;
|
||||
let lastMedianBalance = lastClosingBalance;
|
||||
let lastAverageBalance = lastClosingBalance;
|
||||
|
||||
for (let i = 0; i < allDateRanges.value.length; i++) {
|
||||
const dateRange = allDateRanges.value[i];
|
||||
@@ -146,29 +162,56 @@ export function useAccountBalanceTrendsChartBase(props: CommonAccountBalanceTren
|
||||
}
|
||||
|
||||
if (isArray(dataItems)) {
|
||||
let lastUnixTime = 0;
|
||||
if (dataItems.length < 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let i = 0; i < dataItems.length; i++) {
|
||||
const dataItem = dataItems[i];
|
||||
dataItems.sort(function (data1: TransactionReconciliationStatementResponseItem, data2: TransactionReconciliationStatementResponseItem) {
|
||||
return data1.time - data2.time;
|
||||
});
|
||||
|
||||
if (dataItem.time >= lastUnixTime) {
|
||||
lastUnixTime = dataItem.time;
|
||||
const openingBalance = dataItems[0].accountOpeningBalance;
|
||||
const closingBalance = dataItems[dataItems.length - 1].accountClosingBalance;
|
||||
const minimumBalance = Math.min(...dataItems.map(item => item.accountClosingBalance));
|
||||
const maximumBalance = Math.max(...dataItems.map(item => item.accountClosingBalance));
|
||||
const medianBalance = dataItems[Math.floor(dataItems.length / 2)].accountClosingBalance;
|
||||
const averageBalance = Math.floor(sumAmounts(dataItems.map(item => item.accountClosingBalance)) / dataItems.length);
|
||||
|
||||
if (props.account.isAsset) {
|
||||
lastAmount = dataItem.accountBalance;
|
||||
} else if (props.account.isLiability) {
|
||||
lastAmount = -dataItem.accountBalance;
|
||||
} else {
|
||||
lastAmount = dataItem.accountBalance;
|
||||
}
|
||||
}
|
||||
if (props.account.isAsset) {
|
||||
lastOpeningBalance = openingBalance;
|
||||
lastClosingBalance = closingBalance;
|
||||
lastMinimumBalance = minimumBalance;
|
||||
lastMaximumBalance = maximumBalance;
|
||||
lastMedianBalance = medianBalance;
|
||||
lastAverageBalance = averageBalance;
|
||||
} else if (props.account.isLiability) {
|
||||
lastOpeningBalance = -openingBalance;
|
||||
lastClosingBalance = -closingBalance;
|
||||
lastMinimumBalance = -minimumBalance;
|
||||
lastMaximumBalance = -maximumBalance;
|
||||
lastMedianBalance = -medianBalance;
|
||||
lastAverageBalance = -averageBalance;
|
||||
} else {
|
||||
lastOpeningBalance = openingBalance;
|
||||
lastClosingBalance = closingBalance;
|
||||
lastMinimumBalance = minimumBalance;
|
||||
lastMaximumBalance = maximumBalance;
|
||||
lastMedianBalance = medianBalance;
|
||||
lastAverageBalance = averageBalance;
|
||||
}
|
||||
}
|
||||
|
||||
ret.push({
|
||||
displayDate: displayDate,
|
||||
amount: lastAmount
|
||||
openingBalance: lastOpeningBalance,
|
||||
closingBalance: lastClosingBalance,
|
||||
minimumBalance: lastMinimumBalance,
|
||||
maximumBalance: lastMaximumBalance,
|
||||
medianBalance: lastMedianBalance,
|
||||
averageBalance: lastAverageBalance
|
||||
});
|
||||
|
||||
lastOpeningBalance = lastClosingBalance;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
@@ -10,11 +10,17 @@ import type { CallbackDataParams } from 'echarts/types/dist/shared';
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
import { type CommonAccountBalanceTrendsChartProps, useAccountBalanceTrendsChartBase } from '@/components/base/AccountBalanceTrendsChartBase.ts'
|
||||
|
||||
import { useUserStore } from '@/stores/user.ts';
|
||||
|
||||
import type { NameValue } from '@/core/base.ts';
|
||||
import type { ColorValue } from '@/core/color.ts';
|
||||
import { ThemeType } from '@/core/theme.ts';
|
||||
import { TrendChartType } from '@/core/statistics.ts';
|
||||
import { AccountBalanceTrendChartType } from '@/core/statistics.ts';
|
||||
import { DEFAULT_CHART_COLORS } from '@/consts/color.ts';
|
||||
|
||||
import { isArray } from '@/lib/common.ts';
|
||||
import { getExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts';
|
||||
|
||||
interface DesktopAccountBalanceTrendsChartProps extends CommonAccountBalanceTrendsChartProps {
|
||||
legendName: string;
|
||||
skeleton?: boolean;
|
||||
@@ -26,21 +32,26 @@ interface AccountBalanceTrendsChartDataItem {
|
||||
name: string;
|
||||
itemStyle: {
|
||||
color: ColorValue;
|
||||
color0?: string;
|
||||
borderColor?: string;
|
||||
borderColor0?: string;
|
||||
};
|
||||
selected: boolean;
|
||||
type: string;
|
||||
areaStyle?: object;
|
||||
stack: string;
|
||||
animation: boolean;
|
||||
data: number[];
|
||||
data: (number | number[])[];
|
||||
}
|
||||
|
||||
const props = defineProps<DesktopAccountBalanceTrendsChartProps>();
|
||||
|
||||
const theme = useTheme();
|
||||
const { formatAmountWithCurrency } = useI18n();
|
||||
const { tt, formatAmountWithCurrency } = useI18n();
|
||||
const { allDataItems, allDisplayDateRanges } = useAccountBalanceTrendsChartBase(props);
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const isDarkMode = computed<boolean>(() => theme.global.name.value === ThemeType.Dark);
|
||||
|
||||
const allSeries = computed<AccountBalanceTrendsChartDataItem[]>(() => {
|
||||
@@ -57,15 +68,32 @@ const allSeries = computed<AccountBalanceTrendsChartDataItem[]>(() => {
|
||||
data: []
|
||||
};
|
||||
|
||||
if (props.type === TrendChartType.Area.type) {
|
||||
if (props.type === AccountBalanceTrendChartType.Area.type) {
|
||||
series.areaStyle = {};
|
||||
} else if (props.type === TrendChartType.Column.type) {
|
||||
} else if (props.type === AccountBalanceTrendChartType.Column.type) {
|
||||
series.type = 'bar';
|
||||
} else if (props.type === AccountBalanceTrendChartType.Candlestick.type) {
|
||||
const expenseIncomeAmountColor = getExpenseAndIncomeAmountColor(userStore.currentUserExpenseAmountColor, userStore.currentUserIncomeAmountColor, isDarkMode.value);
|
||||
series.type = 'candlestick';
|
||||
series.itemStyle.color = expenseIncomeAmountColor.incomeAmountColor;
|
||||
series.itemStyle.color0 = expenseIncomeAmountColor.expenseAmountColor;
|
||||
series.itemStyle.borderColor = expenseIncomeAmountColor.incomeAmountColor;
|
||||
series.itemStyle.borderColor0 = expenseIncomeAmountColor.expenseAmountColor;
|
||||
}
|
||||
|
||||
for (let i = 0; i < allDataItems.value.length; i++) {
|
||||
const item = allDataItems.value[i];
|
||||
series.data.push(item.amount);
|
||||
|
||||
if (props.type === AccountBalanceTrendChartType.Candlestick.type) {
|
||||
series.data.push([
|
||||
item.openingBalance,
|
||||
item.closingBalance,
|
||||
item.minimumBalance,
|
||||
item.maximumBalance
|
||||
]);
|
||||
} else {
|
||||
series.data.push(item.closingBalance);
|
||||
}
|
||||
}
|
||||
|
||||
return [series];
|
||||
@@ -82,7 +110,14 @@ const yAxisWidth = computed<number>(() => {
|
||||
|
||||
for (let i = 0; i < allSeries.value.length; i++) {
|
||||
for (let j = 0; j < allSeries.value[i].data.length; j++) {
|
||||
const value = allSeries.value[i].data[j];
|
||||
const data = allSeries.value[i].data[j];
|
||||
let value: number;
|
||||
|
||||
if (isArray(data)) {
|
||||
value = data[1]; // for candlestick, use closing balance
|
||||
} else {
|
||||
value = data as number; // for line or bar chart
|
||||
}
|
||||
|
||||
if (value > maxValue) {
|
||||
maxValue = value;
|
||||
@@ -134,13 +169,54 @@ const chartOptions = computed<object>(() => {
|
||||
color: isDarkMode.value ? '#eee' : '#333'
|
||||
},
|
||||
formatter: (params: CallbackDataParams[]) => {
|
||||
const amount = params[0].data as number;
|
||||
const value = formatAmountWithCurrency(amount, props.account.currency);
|
||||
if (props.type === AccountBalanceTrendChartType.Candlestick.type) {
|
||||
const dataIndex = params[0].dataIndex;
|
||||
const dataItem = allDataItems.value[dataIndex];
|
||||
const displayItems: NameValue[] = [
|
||||
{
|
||||
name: tt('Opening Balance'),
|
||||
value: formatAmountWithCurrency(dataItem.openingBalance, props.account.currency)
|
||||
},
|
||||
{
|
||||
name: tt('Closing Balance'),
|
||||
value: formatAmountWithCurrency(dataItem.closingBalance, props.account.currency)
|
||||
},
|
||||
{
|
||||
name: tt('Minimum Balance'),
|
||||
value: formatAmountWithCurrency(dataItem.minimumBalance, props.account.currency)
|
||||
},
|
||||
{
|
||||
name: tt('Maximum Balance'),
|
||||
value: formatAmountWithCurrency(dataItem.maximumBalance, props.account.currency)
|
||||
},
|
||||
{
|
||||
name: tt('Median Balance'),
|
||||
value: formatAmountWithCurrency(dataItem.medianBalance, props.account.currency)
|
||||
},
|
||||
{
|
||||
name: tt('Average Balance'),
|
||||
value: formatAmountWithCurrency(dataItem.averageBalance, props.account.currency)
|
||||
}
|
||||
];
|
||||
|
||||
return `${params[0].name}<br/>`
|
||||
+ '<div><span class="chart-pointer" style="background-color: #' + DEFAULT_CHART_COLORS[0] + '"></span>'
|
||||
+ `<span>${props.legendName}</span><span style="margin-left: 20px; float: right">${value}</span><br/>`
|
||||
+ '</div>';
|
||||
let tooltip = `${params[0].name} ${props.legendName}<br/>`;
|
||||
|
||||
for (let i = 0; i < displayItems.length; i++) {
|
||||
tooltip += `<div><span class="chart-pointer" style="background-color: #${DEFAULT_CHART_COLORS[i]}"></span>`
|
||||
+ `<span>${displayItems[i].name}</span><span style="margin-left: 20px; float: right">${displayItems[i].value}</span><br/>`
|
||||
+ `</div>`;
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
} else {
|
||||
const amount = params[0].data as number;
|
||||
const value = formatAmountWithCurrency(amount, props.account.currency);
|
||||
|
||||
return `${params[0].name}<br/>`
|
||||
+ '<div><span class="chart-pointer" style="background-color: #' + DEFAULT_CHART_COLORS[0] + '"></span>'
|
||||
+ `<span>${props.legendName}</span><span style="margin-left: 20px; float: right">${value}</span><br/>`
|
||||
+ '</div>';
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
:style="`top: ${virtualDataItems.topPosition}px`"
|
||||
:virtual-list-index="item.index"
|
||||
:title="item.displayDate"
|
||||
:after="formatAmountWithCurrency(item.amount, account.currency)"
|
||||
:after="formatAmountWithCurrency(item.closingBalance, account.currency)"
|
||||
v-for="item in virtualDataItems.items"
|
||||
>
|
||||
<template #media>
|
||||
@@ -90,19 +90,24 @@ const virtualDataItems = ref<MobileAccountBalanceTrendsChartVirtualListData>({
|
||||
|
||||
const allVirtualListItems = computed<MobileAccountBalanceTrendsChartItem[]>(() => {
|
||||
const ret: MobileAccountBalanceTrendsChartItem[] = [];
|
||||
let maxAmount = 0;
|
||||
let maxClosingBalance = 0;
|
||||
|
||||
for (let i = 0; i < allDataItems.value.length; i++) {
|
||||
const dataItem = allDataItems.value[i];
|
||||
|
||||
if (dataItem.amount > maxAmount) {
|
||||
maxAmount = dataItem.amount;
|
||||
if (dataItem.closingBalance > maxClosingBalance) {
|
||||
maxClosingBalance = dataItem.closingBalance;
|
||||
}
|
||||
|
||||
const finalDataItem: MobileAccountBalanceTrendsChartItem = {
|
||||
index: i,
|
||||
displayDate: dataItem.displayDate,
|
||||
amount: dataItem.amount,
|
||||
openingBalance: dataItem.openingBalance,
|
||||
closingBalance: dataItem.closingBalance,
|
||||
medianBalance: dataItem.medianBalance,
|
||||
averageBalance: dataItem.averageBalance,
|
||||
minimumBalance: dataItem.minimumBalance,
|
||||
maximumBalance: dataItem.maximumBalance,
|
||||
color: `#${DEFAULT_CHART_COLORS[0]}`,
|
||||
percent: 0.0
|
||||
};
|
||||
@@ -111,8 +116,8 @@ const allVirtualListItems = computed<MobileAccountBalanceTrendsChartItem[]>(() =
|
||||
}
|
||||
|
||||
for (let i = 0; i < ret.length; i++) {
|
||||
if (maxAmount > 0 && ret[i].amount > 0) {
|
||||
ret[i].percent = 100.0 * ret[i].amount / maxAmount;
|
||||
if (maxClosingBalance > 0 && ret[i].closingBalance > 0) {
|
||||
ret[i].percent = 100.0 * ret[i].closingBalance / maxClosingBalance;
|
||||
} else {
|
||||
ret[i].percent = 0.0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user