Files
ezbookkeeping/src/views/desktop/insights/tabs/ExplorerChartTab.vue
T
2026-04-24 00:01:09 +08:00

1008 lines
44 KiB
Vue

<template>
<v-card-text class="px-5 py-0 mb-4">
<v-row>
<v-col cols="12">
<div class="d-flex overflow-x-auto align-center gap-2 pt-2">
<v-select
class="flex-0-0"
min-width="150"
item-title="name"
item-value="value"
density="compact"
:disabled="loading || disabled"
:label="tt('Chart Type')"
:items="allTransactionExplorerChartTypes"
:model-value="currentExplorer.chartType"
@update:model-value="updateChartType"
/>
<v-select
class="flex-0-0"
min-width="150"
item-title="name"
item-value="value"
density="compact"
:disabled="loading || disabled || isDefined(TransactionExplorerChartType.valueOf(currentExplorer.chartType)?.fixedCategoryDimension)"
:label="tt('Axis / Category')"
:items="allTransactionExplorerDataDimensions"
:model-value="TransactionExplorerChartType.valueOf(currentExplorer.chartType)?.fixedCategoryDimension ?? currentExplorer.categoryDimension"
@update:model-value="updateCategoryDimensionType"
/>
<v-select
class="flex-0-0"
min-width="150"
item-title="name"
item-value="value"
density="compact"
:disabled="loading || disabled || !TransactionExplorerChartType.valueOf(currentExplorer.chartType)?.seriesDimensionRequired"
:label="tt('Series')"
:items="allTransactionExplorerDataDimensions"
:model-value="TransactionExplorerChartType.valueOf(currentExplorer.chartType)?.seriesDimensionRequired ? currentExplorer.seriesDimension : TransactionExplorerDataDimension.None.value"
@update:model-value="currentExplorer.seriesDimension = $event as TransactionExplorerDataDimensionType"
>
<template #item="{ props, item }">
<v-list-item :disabled="item.value === currentExplorer.categoryDimension && item.value !== TransactionExplorerDataDimension.SeriesDimensionDefault.value" v-bind="props">
<template #title>
<div class="text-truncate">{{ item.raw.name }}</div>
</template>
</v-list-item>
</template>
</v-select>
<v-select
class="flex-0-0"
min-width="220"
item-title="name"
item-value="value"
density="compact"
:disabled="loading || disabled"
:label="tt('Number of Amount Ranges')"
:items="allAmountRangeCounts"
v-model="currentExplorer.amountRangeCount"
v-if="isUsingAmountRange"
/>
<v-select
class="flex-0-0"
min-width="150"
item-title="name"
item-value="value"
density="compact"
:disabled="loading || disabled"
:label="tt('Value Metric')"
:items="allTransactionExplorerValueMetrics"
v-model="currentExplorer.valueMetric"
/>
<v-select
class="flex-0-0"
min-width="150"
item-title="displayName"
item-value="type"
density="compact"
:disabled="loading || disabled || isDefined(TransactionExplorerChartType.valueOf(currentExplorer.chartType)?.fixedSortingType)"
:label="tt('Sort Order')"
:items="allTransactionExplorerChartSortingTypes"
v-model="currentExplorer.chartSortingType"
/>
<v-spacer class="flex-1-1"/>
</div>
</v-col>
</v-row>
</v-card-text>
<v-card-text :class="{ 'readonly': loading }" v-if="currentExplorer.chartType === TransactionExplorerChartType.Pie.value">
<pie-chart
:items="[
{id: '1', name: '---', value: 60, color: '7c7c7f'},
{id: '2', name: '---', value: 20, color: 'a5a5aa'},
{id: '3', name: '---', value: 20, color: 'c5c5c9'}
]"
:skeleton="true"
id-field="id"
name-field="name"
value-field="value"
color-field="color"
v-if="loading"
/>
<pie-chart
:items="categoryDimensionTransactionExplorerData && categoryDimensionTransactionExplorerData.length ? categoryDimensionTransactionExplorerData : []"
:show-value="true"
:show-percent="true"
:enable-click-item="true"
:amount-value="TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.isAmount"
:percent-value="TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.isPercent"
:default-currency="defaultCurrency"
id-field="id"
name-field="name"
value-field="totalAmount"
v-else-if="!loading"
@click="onClickPieChartItem"
/>
</v-card-text>
<v-card-text :class="{ 'readonly': loading }" v-else-if="currentExplorer.chartType === TransactionExplorerChartType.Radar.value">
<radar-chart
:items="[
{name: '---', value: 10},
{name: '---', value: 10},
{name: '---', value: 10},
{name: '---', value: 10},
{name: '---', value: 10},
{name: '---', value: 10}
]"
:skeleton="true"
name-field="name"
value-field="value"
v-if="loading"
/>
<radar-chart
:items="categoryDimensionTransactionExplorerData && categoryDimensionTransactionExplorerData.length ? categoryDimensionTransactionExplorerData : []"
:show-value="true"
:show-percent="true"
:amount-value="TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.isAmount"
:percent-value="TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.isPercent"
:default-currency="defaultCurrency"
name-field="name"
value-field="totalAmount"
v-else-if="!loading"
/>
</v-card-text>
<v-card-text :class="{ 'readonly': loading }" v-else-if="TransactionExplorerChartType.valueOf(currentExplorer.chartType)?.seriesDimensionRequired && axisChartDisplayType">
<axis-chart
:skeleton="true"
:type="axisChartDisplayType"
:sorting-type="currentExplorer.chartSortingType"
:all-category-names="[]"
:items="[]"
category-type-name=""
name-field="name"
values-field="values"
v-if="loading"
/>
<axis-chart
ref="axisChart"
:type="axisChartDisplayType"
:stacked="axisChartStacked"
:one-hundred-percent-stacked="axisChart100PercentStacked"
:sorting-type="currentExplorer.chartSortingType"
:show-value="true"
:show-total-amount-in-tooltip="TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.supportSum"
:total-name-in-tooltip="currentExplorer.valueMetric === TransactionExplorerValueMetric.TransactionCount.value ? tt('Total Transactions') : tt('Total Amount')"
:category-type-name="currentTransactionExplorerCategoryDimensionName"
:all-category-names="categoriedNamesSortedByDisplayOrder"
:items="seriesDimensionTransactionExplorerData"
:amount-value="TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.isAmount"
:percent-value="TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.isPercent"
:default-currency="defaultCurrency"
:enable-click-item="true"
:tooltip-extra-column-names="axisChartTooltipExtraColumnNames"
:tooltip-extra-column-total-values="axisChartShowYearOverYear || axisChartShowPeriodOverPeriod ? getAxisChartTooltipExtraColumnTotalValues : undefined"
:tooltip-extra-column-values="axisChartShowYearOverYear || axisChartShowPeriodOverPeriod ? getAxisChartTooltipExtraColumnValues : undefined"
id-field="id"
name-field="name"
values-field="categoryValues"
display-orders-field="displayOrders"
@click="onClickTrendChartItem"
v-else-if="!loading"
/>
</v-card-text>
<v-card-text :class="{ 'readonly': loading }" v-else-if="TransactionExplorerChartType.valueOf(currentExplorer.chartType)?.seriesDimensionRequired && hierarchyChartDisplayType">
<hierarchy-chart
:skeleton="true"
:type="hierarchyChartDisplayType"
:all-category-names="[]"
:items="[]"
category-type-name=""
name-field="name"
values-field="values"
v-if="loading"
/>
<hierarchy-chart
ref="hierarchyChart"
:type="hierarchyChartDisplayType"
:show-value="true"
:category-type-name="currentTransactionExplorerCategoryDimensionName"
:all-category-names="categoriedNamesSortedByDisplayOrder"
:items="seriesDimensionTransactionExplorerData"
:amount-value="TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.isAmount"
:percent-value="TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.isPercent"
:default-currency="defaultCurrency"
name-field="name"
values-field="categoryValues"
v-else-if="!loading"
/>
</v-card-text>
<v-card-text :class="{ 'readonly': loading }" v-else-if="currentExplorer.chartType === TransactionExplorerChartType.Heatmap.value">
<heat-map-chart
:skeleton="true"
:all-category-names="[]"
:items="[]"
:value-type-name="tt(TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.name ?? 'Value')"
category-type-name=""
name-field="name"
values-field="values"
v-if="loading"
/>
<heat-map-chart
ref="heatmapChart"
:show-value="true"
:category-type-name="currentTransactionExplorerCategoryDimensionName"
:all-category-names="categoriedNamesSortedByDisplayOrder"
:items="seriesDimensionTransactionExplorerData"
:value-type-name="tt(TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.name ?? 'Value')"
:amount-value="TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.isAmount"
:percent-value="TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.isPercent"
:default-currency="defaultCurrency"
name-field="name"
values-field="categoryValues"
v-else-if="!loading"
/>
</v-card-text>
<v-card-text :class="{ 'readonly': loading }" v-else-if="currentExplorer.chartType === TransactionExplorerChartType.CalendarHeatmap.value">
<calendar-heat-map-chart
:skeleton="true"
:items="[]"
:value-type-name="tt(TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.name ?? 'Value')"
id-field="id"
value-field="totalAmount"
v-if="loading"
/>
<calendar-heat-map-chart
:show-value="true"
:items="categoryDimensionTransactionExplorerData && categoryDimensionTransactionExplorerData.length ? categoryDimensionTransactionExplorerData : []"
:value-type-name="tt(TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.name ?? 'Value')"
:amount-value="TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.isAmount"
:percent-value="TransactionExplorerValueMetric.valueOf(currentExplorer.valueMetric)?.isPercent"
:default-currency="defaultCurrency"
id-field="id"
value-field="totalAmount"
v-else-if="!loading"
/>
</v-card-text>
</template>
<script setup lang="ts">
import AxisChart, { type AxisChartDisplayType } from '@/components/desktop/AxisChart.vue';
import HierarchyChart, { type HierarchyChartDisplayType } from '@/components/desktop/HierarchyChart.vue';
import HeatMapChart from '@/components/desktop/HeatMapChart.vue';
import { computed, useTemplateRef } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from '@/locales/helpers.ts';
import { useUserStore } from '@/stores/user.ts';
import {
type CategoriedInfo,
type CategoriedTransactionExplorerData,
type SeriesInfo,
type CategoriedTransactionExplorerDataItem,
TransactionExplorerDimensionType,
useExplorersStore
} from '@/stores/explorer.ts';
import { type NameValue, type NameNumeralValue, type TypeAndDisplayName, itemAndIndex, entries } from '@/core/base.ts';
import { NumeralSystem } from '@/core/numeral.ts';
import { Month, WeekDay } from '@/core/datetime.ts';
import { ChartSortingType, ExportMermaidChartType } from '@/core/statistics.ts';
import {
TransactionExplorerDataDimensionType,
TransactionExplorerDataDimension,
TransactionExplorerValueMetric,
TransactionExplorerChartTypeValue,
TransactionExplorerChartType
} from '@/core/explorer.ts';
import { type SortableTransactionStatisticDataItem } from '@/models/transaction.ts';
import type { InsightsExplorer } from '@/models/explorer.ts';
import { isDefined, isNumber, findNameByValue } from '@/lib/common.ts';
import { getCurrentDateTime, parseDateTimeFromString } from '@/lib/datetime.ts';
import { sortStatisticsItems } from '@/lib/statistics.ts';
type AxisChartType = InstanceType<typeof AxisChart>;
type HierarchyChartType = InstanceType<typeof HierarchyChart>;
type HeatMapChartType = InstanceType<typeof HeatMapChart>;
interface InsightsExplorerDataTableTabProps {
loading?: boolean;
disabled?: boolean;
}
interface CategoryDimensionData extends SortableTransactionStatisticDataItem {
id: string;
dimension: TransactionExplorerDimensionType;
name: string;
displayOrders: number[];
totalAmount: number;
}
interface SortableCategoriedTransactionExplorerDataItem extends SortableTransactionStatisticDataItem {
name: string;
displayOrders: number[];
totalAmount: number;
originalItem: CategoriedTransactionExplorerData;
}
interface SeriesDimensionData extends SortableTransactionStatisticDataItem, Record<string, unknown> {
id: string;
dimension: TransactionExplorerDimensionType;
name: string;
displayOrders: number[];
categoryValues: number[];
totalAmount: number;
}
defineProps<InsightsExplorerDataTableTabProps>();
const router = useRouter();
const {
tt,
getAllStatisticsSortingTypes,
getAllTransactionExplorerDataDimensions,
getAllTransactionExplorerValueMetrics,
getAllTransactionExplorerChartTypes,
getMonthLongName,
getMonthdayShortName,
getWeekdayLongName,
getQuarterName,
getCurrencyName,
formatDateTimeToShortDateTime,
formatDateTimeToShortDate,
formatDateTimeToShortTime,
formatDateTimeToGregorianLikeShortYear,
formatDateTimeToGregorianLikeShortYearMonth,
formatDateTimeToGregorianLikeYearQuarter,
formatGregorianYearToGregorianLikeFiscalYear,
formatAmountToLocalizedNumerals,
formatAmountToWesternArabicNumeralsWithoutDigitGrouping,
formatNumberToLocalizedNumerals,
formatPercentToLocalizedNumerals
} = useI18n();
const userStore = useUserStore();
const explorersStore = useExplorersStore();
const axisChart = useTemplateRef<AxisChartType>('axisChart');
const hierarchyChart = useTemplateRef<HierarchyChartType>('hierarchyChart');
const heatmapChart = useTemplateRef<HeatMapChartType>('heatmapChart');
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
const allTransactionExplorerDataDimensions = computed<NameValue[]>(() => getAllTransactionExplorerDataDimensions());
const allTransactionExplorerValueMetrics = computed<NameValue[]>(() => getAllTransactionExplorerValueMetrics());
const allTransactionExplorerChartTypes = computed<NameValue[]>(() => getAllTransactionExplorerChartTypes());
const allTransactionExplorerChartSortingTypes = computed<TypeAndDisplayName[]>(() => getAllStatisticsSortingTypes(true));
const currentTransactionExplorerCategoryDimensionName = computed<string>(() => findNameByValue(allTransactionExplorerDataDimensions.value, currentExplorer.value.categoryDimension) ?? tt('Unknown'));
const currentExplorer = computed<InsightsExplorer>(() => explorersStore.currentInsightsExplorer);
const isUsingAmountRange = computed<boolean>(() => explorersStore.isUsingAmountRange);
const allAmountRangeCounts = computed<NameNumeralValue[]>(() => {
const pageCounts: NameNumeralValue[] = [];
for (let i = 3; i <= 20; i++) {
pageCounts.push({ value: i, name: formatNumberToLocalizedNumerals(i) });
}
return pageCounts;
});
const categoryDimensionTransactionExplorerData = computed<CategoryDimensionData[]>(() => {
if (currentExplorer.value.chartType !== TransactionExplorerChartType.Pie.value
&& currentExplorer.value.chartType !== TransactionExplorerChartType.Radar.value
&& currentExplorer.value.chartType !== TransactionExplorerChartType.CalendarHeatmap.value) {
return [];
}
if (!explorersStore.categoriedTransactionExplorerData || !explorersStore.categoriedTransactionExplorerData.length) {
return [];
}
const result: CategoryDimensionData[] = [];
for (const categoriedData of explorersStore.categoriedTransactionExplorerData) {
const data = categoriedData.data[0];
if (!isDefined(data)) {
continue;
}
const displayName = getCategoriedDataDisplayName(categoriedData);
result.push({
id: categoriedData.categoryId,
dimension: categoriedData.categoryIdType,
name: displayName,
displayOrders: categoriedData.categoryDisplayOrders,
totalAmount: data.value
});
}
sortStatisticsItems(result, currentExplorer.value.chartSortingType);
return result;
});
const categoriedDataSortedByDisplayOrder = computed<SortableCategoriedTransactionExplorerDataItem[]>(() => {
if (!explorersStore.categoriedTransactionExplorerData || !explorersStore.categoriedTransactionExplorerData.length) {
return [];
}
const result: SortableCategoriedTransactionExplorerDataItem[] = [];
for (const categoriedData of explorersStore.categoriedTransactionExplorerData) {
result.push({
name: getCategoriedDataDisplayName(categoriedData),
displayOrders: categoriedData.categoryDisplayOrders,
totalAmount: 0,
originalItem: categoriedData
});
}
sortStatisticsItems(result, ChartSortingType.DisplayOrder.type);
return result;
});
const categoriedNamesSortedByDisplayOrder = computed<string[]>(() => {
if (!categoriedDataSortedByDisplayOrder.value || !categoriedDataSortedByDisplayOrder.value.length) {
return [];
}
const result: string[] = [];
for (const categoriedData of categoriedDataSortedByDisplayOrder.value) {
result.push(categoriedData.name);
}
return result;
});
const seriesDimensionTransactionExplorerData = computed<SeriesDimensionData[]>(() => {
if (!TransactionExplorerChartType.valueOf(currentExplorer.value.chartType)?.seriesDimensionRequired) {
return [];
}
if (!explorersStore.categoriedTransactionExplorerData || !explorersStore.categoriedTransactionExplorerData.length) {
return [];
}
const result: SeriesDimensionData[] = [];
const seriesDimensionDataMap: Record<string, SeriesDimensionData> = {};
for (const categoriedData of explorersStore.categoriedTransactionExplorerData) {
for (const seriesData of categoriedData.data) {
const displayName = getCategoriedDataDisplayName(seriesData);
let seriesDimensionData: SeriesDimensionData | undefined = seriesDimensionDataMap[seriesData.seriesId];
if (!seriesDimensionData) {
seriesDimensionData = {
id: seriesData.seriesId,
dimension: seriesData.seriesIdType,
name: displayName,
displayOrders: seriesData.seriesDisplayOrders,
categoryValues: [],
totalAmount: 0
};
seriesDimensionDataMap[seriesData.seriesId] = seriesDimensionData;
result.push(seriesDimensionData);
}
}
}
for (const categoriedData of categoriedDataSortedByDisplayOrder.value) {
const seriesDataMap: Record<string, CategoriedTransactionExplorerDataItem> = {};
for (const seriesData of categoriedData.originalItem.data) {
seriesDataMap[seriesData.seriesId] = seriesData;
}
for (const seriesDimensionData of result) {
const seriesData = seriesDataMap[seriesDimensionData.id];
if (isDefined(seriesData)) {
seriesDimensionData.categoryValues.push(seriesData.value);
seriesDimensionData.totalAmount += seriesData.value;
} else {
seriesDimensionData.categoryValues.push(0);
}
}
}
sortStatisticsItems(result, currentExplorer.value.chartSortingType);
return result;
});
const seriesDimensionTransactionExplorerDataMap = computed<Record<string, SeriesDimensionData>>(() => {
const result: Record<string, SeriesDimensionData> = {};
for (const seriesDimensionData of seriesDimensionTransactionExplorerData.value) {
result[seriesDimensionData.id] = seriesDimensionData;
}
return result;
});
const axisChartCategoryIndexYoYMap = computed<Record<number, number>>(() => {
const result: Record<number, number> = {};
if (!axisChartShowYearOverYear.value) {
return result;
}
const dateKeyToIndex: Record<string, number> = {};
const dateKeyToPreviousYearDateKey: Record<string, string> = {};
for (const [item, categoryIndex] of itemAndIndex(categoriedDataSortedByDisplayOrder.value)) {
const categoriedData = item.originalItem;
const name = categoriedData.categoryId;
const dimessionType = categoriedData.categoryIdType;
const dimension = currentExplorer.value.categoryDimension;
if (dimension === TransactionExplorerDataDimension.DateTimeByYearMonthDay.value) {
const dateTime = parseDateTimeFromString(name, dimessionType);
if (dateTime) {
dateKeyToIndex[dateTime.getGregorianCalendarYearDashMonthDashDay()] = categoryIndex;
dateKeyToPreviousYearDateKey[dateTime.getGregorianCalendarYearDashMonthDashDay()] = dateTime.add(-1, 'years').getGregorianCalendarYearDashMonthDashDay();
}
} else if (dimension === TransactionExplorerDataDimension.DateTimeByYearMonth.value) {
const dateTime = parseDateTimeFromString(name, dimessionType);
if (dateTime) {
dateKeyToIndex[dateTime.getGregorianCalendarYearDashMonth()] = categoryIndex;
dateKeyToPreviousYearDateKey[dateTime.getGregorianCalendarYearDashMonth()] = dateTime.add(-1, 'years').getGregorianCalendarYearDashMonth();
}
} else if (dimension === TransactionExplorerDataDimension.DateTimeByYearQuarter.value) {
const parts = name.split('-');
const year = parts.length === 2 ? parseInt(parts[0] as string) : 0;
const quarter = parts.length === 2 ? parseInt(parts[1] as string) : 0;
dateKeyToIndex[`${year}-Q${quarter}`] = categoryIndex;
dateKeyToPreviousYearDateKey[`${year}-Q${quarter}`] = `${year - 1}-Q${quarter}`;
} else if (dimension === TransactionExplorerDataDimension.DateTimeByYear.value) {
const year = parseInt(name);
dateKeyToIndex[name] = categoryIndex;
dateKeyToPreviousYearDateKey[name] = (year - 1).toString(10);
} else if (dimension === TransactionExplorerDataDimension.DateTimeByFiscalYear.value) {
const year = parseInt(name);
dateKeyToIndex[name] = categoryIndex;
dateKeyToPreviousYearDateKey[name] = (year - 1).toString(10);
}
}
for (const [dateKey, previousYearDateKey] of entries(dateKeyToPreviousYearDateKey)) {
const categoryIndex = dateKeyToIndex[dateKey];
const previousYearCategoryIndex = dateKeyToIndex[previousYearDateKey];
if (isNumber(categoryIndex) && isNumber(previousYearCategoryIndex)) {
result[categoryIndex] = previousYearCategoryIndex;
}
}
return result;
});
const axisChartDisplayType = computed<AxisChartDisplayType | undefined>(() => {
if (currentExplorer.value.chartType === TransactionExplorerChartType.ColumnStacked.value
|| currentExplorer.value.chartType === TransactionExplorerChartType.Column100PercentStacked.value
|| currentExplorer.value.chartType === TransactionExplorerChartType.ColumnGrouped.value) {
return 'column';
} else if (currentExplorer.value.chartType === TransactionExplorerChartType.LineGrouped.value) {
return 'line';
} else if (currentExplorer.value.chartType === TransactionExplorerChartType.AreaStacked.value
|| currentExplorer.value.chartType === TransactionExplorerChartType.Area100PercentStacked.value) {
return 'area';
} else if (currentExplorer.value.chartType === TransactionExplorerChartType.BubbleGrouped.value) {
return 'bubble';
} else {
return undefined;
}
});
const hierarchyChartDisplayType = computed<HierarchyChartDisplayType | undefined>(() => {
if (currentExplorer.value.chartType === TransactionExplorerChartType.Treemap.value) {
return 'treemap';
} else if (currentExplorer.value.chartType === TransactionExplorerChartType.Sunburst.value) {
return 'sunburst';
} else {
return undefined;
}
});
const axisChartStacked = computed<boolean>(() => {
return (currentExplorer.value.chartType === TransactionExplorerChartType.ColumnStacked.value
|| currentExplorer.value.chartType === TransactionExplorerChartType.Column100PercentStacked.value
|| currentExplorer.value.chartType === TransactionExplorerChartType.AreaStacked.value
|| currentExplorer.value.chartType === TransactionExplorerChartType.Area100PercentStacked.value);
});
const axisChart100PercentStacked = computed<boolean>(() => {
return (currentExplorer.value.chartType === TransactionExplorerChartType.Column100PercentStacked.value
|| currentExplorer.value.chartType === TransactionExplorerChartType.Area100PercentStacked.value);
});
const axisChartShowYearOverYear = computed<boolean>(() => {
const dimession = currentExplorer.value.categoryDimension;
return dimession === TransactionExplorerDataDimension.DateTimeByYearMonthDay.value
|| dimession === TransactionExplorerDataDimension.DateTimeByYearMonth.value
|| dimession === TransactionExplorerDataDimension.DateTimeByYearQuarter.value
|| dimession === TransactionExplorerDataDimension.DateTimeByYear.value
|| dimession === TransactionExplorerDataDimension.DateTimeByFiscalYear.value;
});
const axisChartShowPeriodOverPeriod = computed<boolean>(() => {
const dimession = currentExplorer.value.categoryDimension;
return dimession === TransactionExplorerDataDimension.DateTimeByYearMonthDay.value
|| dimession === TransactionExplorerDataDimension.DateTimeByYearMonth.value
|| dimession === TransactionExplorerDataDimension.DateTimeByYearQuarter.value;
});
const axisChartTooltipExtraColumnNames = computed<string[]>(() => {
const extraColumnNames: string[] = [];
const dimession = currentExplorer.value.categoryDimension;
if (axisChartShowYearOverYear.value) {
extraColumnNames.push(tt('Year-over-Year'));
}
if (axisChartShowPeriodOverPeriod.value) {
if (dimession === TransactionExplorerDataDimension.DateTimeByYearQuarter.value) {
extraColumnNames.push(tt('Quarter-over-Quarter'));
} else if (dimession === TransactionExplorerDataDimension.DateTimeByYearMonth.value) {
extraColumnNames.push(tt('Month-over-Month'));
} else if (dimession === TransactionExplorerDataDimension.DateTimeByYearMonthDay.value) {
extraColumnNames.push(tt('Day-over-Day'));
} else {
extraColumnNames.push(tt('Period-over-Period'));
}
}
return extraColumnNames;
});
function getCategoriedDataDisplayName(info: CategoriedInfo | SeriesInfo): string {
let name: string = '';
let needI18n: boolean | undefined = false;
let i18nParameters: Record<string, unknown> | undefined = undefined;
let dimessionType: TransactionExplorerDimensionType = TransactionExplorerDimensionType.Other;
let dimessionValue: TransactionExplorerDataDimensionType = TransactionExplorerDataDimension.None.value;
if ('categoryName' in info) {
name = info.categoryName;
needI18n = info.categoryNameNeedI18n;
i18nParameters = info.categoryNameI18nParameters;
dimessionType = info.categoryIdType;
dimessionValue = currentExplorer.value.categoryDimension;
} else if ('seriesName' in info) {
name = info.seriesName;
needI18n = info.seriesNameNeedI18n;
i18nParameters = info.seriesNameI18nParameters;
dimessionType = info.seriesIdType;
dimessionValue = currentExplorer.value.seriesDimension;
}
const dimession = TransactionExplorerDataDimension.valueOf(dimessionValue);
let displayName: string = name;
// convert the name to i18n if needed
if (needI18n && i18nParameters) {
displayName = tt(name, i18nParameters);
} else if (needI18n && !i18nParameters) {
displayName = tt(name);
}
// convert the name to formatted date time if needed
if (dimession === TransactionExplorerDataDimension.DateTime) {
const dateTime = parseDateTimeFromString(name, dimessionType);
displayName = dateTime ? formatDateTimeToShortDateTime(dateTime) : tt('Unknown');
} else if (dimession === TransactionExplorerDataDimension.DateTimeByYearMonthDay) {
const dateTime = parseDateTimeFromString(name, dimessionType);
displayName = dateTime ? formatDateTimeToShortDate(dateTime) : tt('Unknown');
} else if (dimession === TransactionExplorerDataDimension.DateTimeByYearMonth) {
const dateTime = parseDateTimeFromString(name, dimessionType);
displayName = dateTime ? formatDateTimeToGregorianLikeShortYearMonth(dateTime) : tt('Unknown');
} else if (dimession === TransactionExplorerDataDimension.DateTimeByYearQuarter) {
const parts = name.split('-');
const year = parts.length === 2 ? parts[0] : '';
const quarter = parts.length === 2 ? parseInt(parts[1] as string) : 0;
const quarterLastMonth = quarter * 3;
const dateTime = year && quarterLastMonth ? parseDateTimeFromString(`${year}-${quarterLastMonth.toString(10).padStart(2, NumeralSystem.WesternArabicNumerals.digitZero)}`, TransactionExplorerDimensionType.YearMonth) : undefined;
displayName = dateTime ? formatDateTimeToGregorianLikeYearQuarter(dateTime) : tt('Unknown');
} else if (dimession === TransactionExplorerDataDimension.DateTimeByYear) {
const dateTime = parseDateTimeFromString(name, dimessionType);
displayName = dateTime ? formatDateTimeToGregorianLikeShortYear(dateTime) : tt('Unknown');
} else if (dimession === TransactionExplorerDataDimension.DateTimeByFiscalYear) {
displayName = formatGregorianYearToGregorianLikeFiscalYear(parseInt(name));
} else if (dimession === TransactionExplorerDataDimension.DateTimeByDayOfWeek) {
const weekDay = WeekDay.parse(name);
displayName = weekDay ? getWeekdayLongName(weekDay) : tt('Unknown');
} else if (dimession === TransactionExplorerDataDimension.DateTimeByDayOfMonth) {
displayName = getMonthdayShortName(parseInt(name));
} else if (dimession === TransactionExplorerDataDimension.DateTimeByMonthOfYear) {
const month = Month.valueOf(parseInt(name));
displayName = month ? getMonthLongName(month.name) : tt('Unknown');
} else if (dimession === TransactionExplorerDataDimension.DateTimeByQuarterOfYear) {
displayName = getQuarterName(parseInt(name));
} else if (dimession === TransactionExplorerDataDimension.DateTimeByHourOfDay) {
const dateTime = getCurrentDateTime().set({
hour: parseInt(name),
minute: 0,
second: 0,
millisecond: 0
});
displayName = formatDateTimeToShortTime(dateTime);
} else if (dimession === TransactionExplorerDataDimension.SourceAccountCurrency || dimession === TransactionExplorerDataDimension.DestinationAccountCurrency) {
if (!needI18n) {
displayName = getCurrencyName(name);
}
}
if (dimession === TransactionExplorerDataDimension.SourceAmount
|| dimession === TransactionExplorerDataDimension.DestinationAmount) {
if (name !== '' && name !== 'none' && Number.isFinite(parseInt(name))) {
displayName = formatAmountToLocalizedNumerals(parseInt(name), defaultCurrency.value);
}
}
if (dimession?.isSourceAmountRange || dimession?.isDestinationAmountRange) {
const rangeParts = name.split('|');
if (rangeParts && rangeParts.length === 2 && Number.isFinite(parseInt(rangeParts[0] as string)) && Number.isFinite(parseInt(rangeParts[1] as string))) {
const from = formatAmountToLocalizedNumerals(parseInt(rangeParts[0] as string), defaultCurrency.value);
const to = formatAmountToLocalizedNumerals(parseInt(rangeParts[1] as string), defaultCurrency.value);
displayName = `${from} ~ ${to}`;
}
}
return displayName;
}
function formatDisplayChangeRate(current: number, reference: number): string {
if (reference === 0 && current === 0) {
return formatPercentToLocalizedNumerals(0, 2, '<0.01');
}
if (reference === 0) {
return '-';
}
const rate = (current - reference) / reference * 100;
return formatPercentToLocalizedNumerals(rate, 2, '<0.01');
}
function getAxisChartTooltipExtraColumnTotalValues(categoryIndex: number, totalValue: number, visibleSeriesIds: string[]): string[] {
const extraColumnValues: string[] = [];
if (!axisChartShowYearOverYear.value && !axisChartShowPeriodOverPeriod.value) {
return extraColumnValues;
}
if (axisChartShowYearOverYear.value) {
const yoyReferenceIndex = axisChartCategoryIndexYoYMap.value[categoryIndex];
let displayChangeRate = '-';
if (isNumber(yoyReferenceIndex)) {
let referenceTotalValue = 0;
for (const seriesId of visibleSeriesIds) {
const seriesDimensionData = seriesDimensionTransactionExplorerDataMap.value[seriesId];
if (seriesDimensionData && seriesDimensionData.categoryValues) {
referenceTotalValue += seriesDimensionData.categoryValues[yoyReferenceIndex] ?? 0;
}
}
displayChangeRate = formatDisplayChangeRate(totalValue, referenceTotalValue);
}
extraColumnValues.push(displayChangeRate);
}
if (axisChartShowPeriodOverPeriod.value) {
const popReferenceIndex = categoryIndex - 1;
let displayChangeRate = '-';
if (popReferenceIndex >= 0) {
let referenceTotalValue = 0;
for (const seriesId of visibleSeriesIds) {
const seriesDimensionData = seriesDimensionTransactionExplorerDataMap.value[seriesId];
if (seriesDimensionData && seriesDimensionData.categoryValues) {
referenceTotalValue += seriesDimensionData.categoryValues[popReferenceIndex] ?? 0;
}
}
displayChangeRate = formatDisplayChangeRate(totalValue, referenceTotalValue);
}
extraColumnValues.push(displayChangeRate);
}
return extraColumnValues;
}
function getAxisChartTooltipExtraColumnValues(seriesId: string, categoryIndex: number, currentValue: number): string[] {
const extraColumnValues: string[] = [];
if (!axisChartShowYearOverYear.value && !axisChartShowPeriodOverPeriod.value) {
return extraColumnValues;
}
const seriesDimensionData = seriesDimensionTransactionExplorerDataMap.value[seriesId];
if (!seriesDimensionData || !seriesDimensionData.categoryValues) {
return extraColumnValues;
}
const values = seriesDimensionData.categoryValues;
if (axisChartShowYearOverYear.value) {
const yoyReferenceIndex = axisChartCategoryIndexYoYMap.value[categoryIndex];
let displayChangeRate = '-';
if (isNumber(yoyReferenceIndex) && yoyReferenceIndex >= 0 && yoyReferenceIndex < values.length) {
displayChangeRate = formatDisplayChangeRate(currentValue, values[yoyReferenceIndex] ?? 0);
}
extraColumnValues.push(displayChangeRate);
}
if (axisChartShowPeriodOverPeriod.value) {
const popReferenceIndex = categoryIndex - 1;
let displayChangeRate = '-';
if (popReferenceIndex >= 0 && popReferenceIndex < values.length) {
displayChangeRate = formatDisplayChangeRate(currentValue, values[popReferenceIndex] ?? 0);
}
extraColumnValues.push(displayChangeRate);
}
return extraColumnValues;
}
function updateChartType(chartTypeValue: TransactionExplorerChartTypeValue): void {
if (currentExplorer.value.chartType !== chartTypeValue) {
currentExplorer.value.chartType = chartTypeValue;
}
const chartType = TransactionExplorerChartType.valueOf(chartTypeValue);
if (isDefined(chartType?.fixedCategoryDimension)) {
currentExplorer.value.categoryDimension = chartType.fixedCategoryDimension;
}
if (!chartType?.seriesDimensionRequired) {
currentExplorer.value.seriesDimension = TransactionExplorerDataDimension.None.value;
}
if (isDefined(chartType?.fixedSortingType)) {
currentExplorer.value.chartSortingType = chartType.fixedSortingType;
}
}
function updateCategoryDimensionType(dimensionType: TransactionExplorerDataDimensionType): void {
if (currentExplorer.value.categoryDimension !== dimensionType) {
currentExplorer.value.categoryDimension = dimensionType;
}
if (currentExplorer.value.seriesDimension === currentExplorer.value.categoryDimension) {
currentExplorer.value.seriesDimension = TransactionExplorerDataDimension.None.value;
}
}
function onClickPieChartItem(item: Record<string, unknown>): void {
if (!item || !('id' in item) || !('dimension' in item)) {
return;
}
const data = (item as unknown) as CategoryDimensionData;
const params: string = explorersStore.getTransactionListPageParams(data.dimension, data.id);
if (params) {
router.push(`/transaction/list?${params}`);
}
}
function onClickTrendChartItem(itemId: string, categoryIndex: number): void {
const categoryData = categoriedDataSortedByDisplayOrder.value[categoryIndex];
if (categoryData) {
const params: string = explorersStore.getTransactionListPageParams(categoryData.originalItem.categoryIdType, categoryData.originalItem.categoryId);
if (params && categoryData.originalItem.categoryId !== 'none' && categoryData.originalItem.categoryId !== 'unknown') {
router.push(`/transaction/list?${params}`);
}
}
}
function buildExportResults(): { headers: string[], data: string[][], supportedMermaidCharts?: ExportMermaidChartType[] } | undefined {
if (currentExplorer.value.chartType === TransactionExplorerChartType.Pie.value || currentExplorer.value.chartType === TransactionExplorerChartType.Radar.value || currentExplorer.value.chartType === TransactionExplorerChartType.CalendarHeatmap.value) {
const valueMetric = TransactionExplorerValueMetric.valueOf(currentExplorer.value.valueMetric);
let supportedMermaidCharts: ExportMermaidChartType[] | undefined = undefined;
if (currentExplorer.value.chartType === TransactionExplorerChartType.Pie.value) {
supportedMermaidCharts = [ ExportMermaidChartType.PieChart ];
}
return {
headers: [
tt('Name'),
tt(valueMetric?.name ?? 'Unknown')
],
data: categoryDimensionTransactionExplorerData.value.map(data => [
data.name,
valueMetric?.isAmount ? formatAmountToWesternArabicNumeralsWithoutDigitGrouping(data.totalAmount, defaultCurrency.value) : data.totalAmount.toString(10)
]),
supportedMermaidCharts: supportedMermaidCharts
};
} else if (TransactionExplorerChartType.valueOf(currentExplorer.value.chartType)?.seriesDimensionRequired && axisChartDisplayType.value) {
const results = axisChart.value?.exportData();
if (!results) {
return undefined;
}
let supportedMermaidCharts: ExportMermaidChartType[] | undefined = undefined;
if (results.headers.length === 2 &&
(
currentExplorer.value.chartType === TransactionExplorerChartType.ColumnStacked.value ||
currentExplorer.value.chartType === TransactionExplorerChartType.Column100PercentStacked.value ||
currentExplorer.value.chartType === TransactionExplorerChartType.ColumnGrouped.value
)
) {
supportedMermaidCharts = [ ExportMermaidChartType.XYChartBar ];
} else if (results.headers.length === 2 &&
(
currentExplorer.value.chartType === TransactionExplorerChartType.AreaStacked.value ||
currentExplorer.value.chartType === TransactionExplorerChartType.Area100PercentStacked.value
)
) {
supportedMermaidCharts = [ ExportMermaidChartType.XYChartLine ];
} else if (currentExplorer.value.chartType === TransactionExplorerChartType.LineGrouped.value) {
supportedMermaidCharts = [ ExportMermaidChartType.XYChartLine ];
}
return {
headers: results.headers,
data: results.data,
supportedMermaidCharts: supportedMermaidCharts
};
} else if (TransactionExplorerChartType.valueOf(currentExplorer.value.chartType)?.seriesDimensionRequired && hierarchyChartDisplayType.value) {
const results = hierarchyChart.value?.exportData();
if (!results) {
return undefined;
}
return {
headers: results.headers,
data: results.data,
supportedMermaidCharts: undefined
};
} else if (TransactionExplorerChartType.valueOf(currentExplorer.value.chartType)?.seriesDimensionRequired && currentExplorer.value.chartType === TransactionExplorerChartType.Heatmap.value) {
const results = heatmapChart.value?.exportData();
if (!results) {
return undefined;
}
return {
headers: results.headers,
data: results.data,
supportedMermaidCharts: undefined
};
} else {
return undefined;
}
}
defineExpose({
buildExportResults
});
</script>