diff --git a/src/components/desktop/AxisChart.vue b/src/components/desktop/AxisChart.vue index 6d37037b..5ca5980d 100644 --- a/src/components/desktop/AxisChart.vue +++ b/src/components/desktop/AxisChart.vue @@ -1,5 +1,5 @@ - @@ -24,7 +24,7 @@ import { isArray } from '@/lib/common.ts'; import { getDisplayColor } from '@/lib/color.ts'; import { sortStatisticsItems } from '@/lib/statistics.ts'; -export type AxisChartDisplayType = 'area' | 'column' | 'bubble'; +export type AxisChartDisplayType = 'line' | 'area' | 'column' | 'bubble'; interface AxisChartDataItem { id: string; @@ -49,9 +49,11 @@ interface AxisChartTooltipItem extends SortableTransactionStatisticDataItem { } const props = defineProps<{ + class?: string; skeleton?: boolean; type: AxisChartDisplayType; stacked?: boolean; + oneHundredPercentStacked?: boolean; sortingType: number; showValue?: boolean; showTotalAmountInTooltip?: boolean; @@ -72,7 +74,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'click', itemId: string, categoryIndex: number): void; + (e: 'click', itemId: string, categoryIndex: number, item: Record): void; }>(); const theme = useTheme(); @@ -81,13 +83,29 @@ const { tt, getCurrentLanguageTextDirection, formatAmountToWesternArabicNumeralsWithoutDigitGrouping, - formatAmountToLocalizedNumeralsWithCurrency + formatAmountToLocalizedNumeralsWithCurrency, + formatPercentToLocalizedNumerals } = useI18n(); const selectedLegends = ref>({}); const textDirection = computed(() => getCurrentLanguageTextDirection()); const isDarkMode = computed(() => theme.global.name.value === ThemeType.Dark); +const finalClass = computed(() => { + let finalClass = ''; + + if (props.skeleton) { + finalClass += 'transition-in'; + } + + if (props.class) { + finalClass += ` ${props.class}`; + } else { + finalClass += ' axis-chart-container'; + } + + return finalClass; +}); const itemsMap = computed>>(() => { const map: Record> = {}; @@ -125,7 +143,8 @@ const itemsMap = computed>>(() => { const allSeries = computed(() => { const allSeries: AxisChartDataItem[] = []; - let maxAmount: number = 0; + const categoryTotalAmount: Record = {}; + let maxAmountOfAllData: number = 0; for (const item of props.items) { if (props.hiddenField && item[props.hiddenField]) { @@ -138,11 +157,32 @@ const allSeries = computed(() => { const allAmounts: number[] = item[props.valuesField] as number[]; - if (props.type === 'bubble') { - for (const amount of allAmounts) { - if (amount > maxAmount) { - maxAmount = amount; - } + for (const [amount, categoryIndex] of itemAndIndex(allAmounts)) { + let totalAmount: number = categoryTotalAmount[categoryIndex] ?? 0; + totalAmount += amount; + categoryTotalAmount[categoryIndex] = totalAmount; + + if (amount > maxAmountOfAllData) { + maxAmountOfAllData = amount; + } + } + } + + for (const item of props.items) { + if (props.hiddenField && item[props.hiddenField]) { + continue; + } + + if (!isArray(item[props.valuesField])) { + continue; + } + + const allAmounts: number[] = item[props.valuesField] as number[]; + + if (props.oneHundredPercentStacked) { + for (const [amount, categoryIndex] of itemAndIndex(allAmounts)) { + const totalAmount: number = categoryTotalAmount[categoryIndex] ?? 0; + allAmounts[categoryIndex] = totalAmount !== 0 ? amount * 100.0 / totalAmount : 0; } } @@ -164,14 +204,16 @@ const allSeries = computed(() => { finalItem.stack = item[props.idField] as string; } - if (props.type === 'area') { + if (props.type === 'line') { + finalItem.areaStyle = undefined; + } else if (props.type === 'area') { finalItem.areaStyle = {}; } else if (props.type === 'column') { finalItem.type = 'bar'; } else if (props.type === 'bubble') { finalItem.type = 'scatter'; finalItem.symbolSize = (data: number): number => { - return Math.sqrt(data) / Math.sqrt(maxAmount) * 80 + 5; + return Math.sqrt(data) / Math.sqrt(maxAmountOfAllData) * 80 + 5; } } @@ -202,8 +244,8 @@ const yAxisWidth = computed(() => { } } - const maxValueText = props.amountValue ? formatAmountToLocalizedNumeralsWithCurrency(maxValue, props.defaultCurrency) : maxValue.toString(); - const minValueText = props.amountValue ? formatAmountToLocalizedNumeralsWithCurrency(minValue, props.defaultCurrency) : minValue.toString(); + const maxValueText = getDisplayValue(maxValue); + const minValueText = getDisplayValue(minValue); const maxLengthText = maxValueText.length > minValueText.length ? maxValueText : minValueText; const canvas = document.createElement('canvas'); @@ -268,7 +310,7 @@ const chartOptions = computed(() => { for (const item of displayItems) { if (displayItems.length === 1 || item.totalAmount !== 0) { - const value = props.amountValue ? formatAmountToLocalizedNumeralsWithCurrency(item.totalAmount, props.defaultCurrency) : item.totalAmount.toString(); + const value = getDisplayValue(item.totalAmount); tooltip += ''; tooltip += `${item.name}${value}`; tooltip += ''; @@ -276,8 +318,8 @@ const chartOptions = computed(() => { } } - if (props.showTotalAmountInTooltip) { - const displayTotalAmount = props.amountValue ? formatAmountToLocalizedNumeralsWithCurrency(totalAmount, props.defaultCurrency) : totalAmount.toString(); + if (props.showTotalAmountInTooltip && !props.oneHundredPercentStacked) { + const displayTotalAmount = getDisplayValue(totalAmount); tooltip = (actualDisplayItemCount > 0 ? '' : '') + '' + `${props.totalNameInTooltip}${displayTotalAmount}` @@ -322,16 +364,18 @@ const chartOptions = computed(() => { yAxis: [ { type: 'value', + min: props.oneHundredPercentStacked ? 0 : undefined, + max: props.oneHundredPercentStacked ? 100 : undefined, axisLabel: { color: isDarkMode.value ? '#888' : '#666', formatter: (value: string) => { - return props.amountValue ? formatAmountToLocalizedNumeralsWithCurrency(parseInt(value), props.defaultCurrency): value; + return getDisplayValue(parseInt(value)); } }, axisPointer: { label: { formatter: (params: CallbackDataParams) => { - return props.amountValue ? formatAmountToLocalizedNumeralsWithCurrency(Math.trunc(params.value as number), props.defaultCurrency) : params.value; + return getDisplayValue(Math.trunc(params.value as number)); } } }, @@ -350,6 +394,18 @@ function getItemName(name: string): string { return props.translateName ? tt(name) : name; } +function getDisplayValue(value: number): string { + if (props.oneHundredPercentStacked) { + return formatPercentToLocalizedNumerals(value, 2, '<0.01'); + } + + if (props.amountValue) { + return formatAmountToLocalizedNumeralsWithCurrency(value, props.defaultCurrency); + } + + return value.toString(); +} + function clickItem(e: ECElementEvent): void { if (!props.enableClickItem || e.componentType !== 'series') { return; @@ -364,7 +420,7 @@ function clickItem(e: ECElementEvent): void { return; } - emit('click', itemId, e.dataIndex); + emit('click', itemId, e.dataIndex, item); } function exportData(): { headers: string[], data: string[][] } { @@ -404,13 +460,13 @@ defineExpose({ diff --git a/src/components/desktop/TrendsChart.vue b/src/components/desktop/TrendsChart.vue index 5c7e079d..2d4bc105 100644 --- a/src/components/desktop/TrendsChart.vue +++ b/src/components/desktop/TrendsChart.vue @@ -1,5 +1,5 @@ - + + diff --git a/src/core/explorer.ts b/src/core/explorer.ts index 8e00bd37..ceaeaf5c 100644 --- a/src/core/explorer.ts +++ b/src/core/explorer.ts @@ -146,6 +146,13 @@ export class TransactionExplorerChartType implements NameValue { public static readonly Pie = new TransactionExplorerChartType('Pie Chart', TransactionExplorerChartTypeValue.Pie, false); public static readonly Radar = new TransactionExplorerChartType('Radar Chart', TransactionExplorerChartTypeValue.Radar, false); + public static readonly ColumnStacked = new TransactionExplorerChartType('Column Chart (Stacked)', TransactionExplorerChartTypeValue.ColumnStacked, true); + public static readonly Column100PercentStacked = new TransactionExplorerChartType('Column Chart (100% Stacked)', TransactionExplorerChartTypeValue.Column100PercentStacked, true); + public static readonly ColumnGrouped = new TransactionExplorerChartType('Column Chart (Grouped)', TransactionExplorerChartTypeValue.ColumnGrouped, true); + public static readonly LineGrouped = new TransactionExplorerChartType('Line Chart (Grouped)', TransactionExplorerChartTypeValue.LineGrouped, true); + public static readonly AreaStacked = new TransactionExplorerChartType('Area Chart (Stacked)', TransactionExplorerChartTypeValue.AreaStacked, true); + public static readonly Area100PercentStacked = new TransactionExplorerChartType('Area Chart (100% Stacked)', TransactionExplorerChartTypeValue.Area100PercentStacked, true); + public static readonly BubbleGrouped = new TransactionExplorerChartType('Bubble Chart (Grouped)', TransactionExplorerChartTypeValue.BubbleGrouped, true); public static readonly Default = TransactionExplorerChartType.Pie; diff --git a/src/stores/explorer.ts b/src/stores/explorer.ts index b58c93f1..ab0204c4 100644 --- a/src/stores/explorer.ts +++ b/src/stores/explorer.ts @@ -87,14 +87,14 @@ export interface CategoriedInfo { } export interface CategoriedTransactions extends CategoriedInfo { - trasactions: Record; + trasactions: Record; } export interface CategoriedTransactionExplorerData extends CategoriedInfo { data: CategoriedTransactionExplorerDataItem[]; } -export interface SeriesedInfo { +export interface SeriesInfo { seriesName: string; seriesNameNeedI18n?: boolean; seriesNameI18nParameters?: Record; @@ -103,11 +103,11 @@ export interface SeriesedInfo { seriesDisplayOrders: number[]; } -export interface SeriesedTransactions extends SeriesedInfo { +export interface SeriesTransactions extends SeriesInfo { trasactions: TransactionInsightDataItem[]; } -export interface CategoriedTransactionExplorerDataItem extends SeriesedInfo { +export interface CategoriedTransactionExplorerDataItem extends SeriesInfo { value: number; } @@ -396,23 +396,23 @@ export const useExplorersStore = defineStore('explorers', () => { categoriedDataMap[categoriedInfo.categoryId] = categoriedData; } - const seriesedInfo = getDataCategoryInfo(timezoneUsedForDateRange, seriesDemension, queryName, queryIndex, transaction); - let seriesedData = categoriedData.trasactions[seriesedInfo.categoryId]; + const seriesInfo = getDataCategoryInfo(timezoneUsedForDateRange, seriesDemension, queryName, queryIndex, transaction); + let seriesData = categoriedData.trasactions[seriesInfo.categoryId]; - if (!seriesedData) { - seriesedData = { - seriesName: seriesedInfo.categoryName, - seriesNameNeedI18n: seriesedInfo.categoryNameNeedI18n, - seriesNameI18nParameters: seriesedInfo.categoryNameI18nParameters, - seriesId: seriesedInfo.categoryId, - seriesIdType: seriesedInfo.categoryIdType, - seriesDisplayOrders: seriesedInfo.categoryDisplayOrders, + if (!seriesData) { + seriesData = { + seriesName: seriesInfo.categoryName, + seriesNameNeedI18n: seriesInfo.categoryNameNeedI18n, + seriesNameI18nParameters: seriesInfo.categoryNameI18nParameters, + seriesId: seriesInfo.categoryId, + seriesIdType: seriesInfo.categoryIdType, + seriesDisplayOrders: seriesInfo.categoryDisplayOrders, trasactions: [] }; - categoriedData.trasactions[seriesedInfo.categoryId] = seriesedData; + categoriedData.trasactions[seriesInfo.categoryId] = seriesData; } - seriesedData.trasactions.push(transaction); + seriesData.trasactions.push(transaction); } function loadInsightsExplorerList(explorers: InsightsExplorerBasicInfo[]): void { @@ -633,18 +633,17 @@ export const useExplorersStore = defineStore('explorers', () => { for (const categoriedTransactions of values(categoriedDataMap)) { const dataItems: CategoriedTransactionExplorerDataItem[] = []; - let allSeriesedTransactions: Record = categoriedTransactions.trasactions; + let allSeriesTransactions: Record = categoriedTransactions.trasactions; - // merge all series into one for pie/radar chart - if (chartType === TransactionExplorerChartType.Pie || chartType === TransactionExplorerChartType.Radar) { + if (!chartType.seriesDimensionRequired) { const transactions: TransactionInsightDataItem[] = []; - for (const seriesedTransactions of values(categoriedTransactions.trasactions)) { - transactions.push(...seriesedTransactions.trasactions); + for (const seriesTransactions of values(categoriedTransactions.trasactions)) { + transactions.push(...seriesTransactions.trasactions); } - allSeriesedTransactions = {}; - allSeriesedTransactions['none'] = { + allSeriesTransactions = {}; + allSeriesTransactions['none'] = { seriesName: valueMetric?.name ?? 'Unknown', seriesNameNeedI18n: true, seriesId: 'none', @@ -654,13 +653,13 @@ export const useExplorersStore = defineStore('explorers', () => { }; } - for (const seriesedTransactions of values(allSeriesedTransactions)) { + for (const seriesTransactions of values(allSeriesTransactions)) { const allSourceAmountsInDefaultCurrency: number[] = []; let totalSourceAmountSumInDefaultCurrency: number = 0; let minimumSourceAmountInDefaultCurrency: number = Number.MAX_SAFE_INTEGER; let maximumSourceAmountInDefaultCurrency: number = Number.MIN_SAFE_INTEGER; - for (const transaction of seriesedTransactions.trasactions) { + for (const transaction of seriesTransactions.trasactions) { let amountInDefaultCurrency: number = transaction.sourceAmount; if (transaction.sourceAccount.currency !== defaultCurrency) { @@ -707,12 +706,12 @@ export const useExplorersStore = defineStore('explorers', () => { } dataItems.push({ - seriesName: seriesedTransactions.seriesName, - seriesNameNeedI18n: seriesedTransactions.seriesNameNeedI18n, - seriesNameI18nParameters: seriesedTransactions.seriesNameI18nParameters, - seriesId: seriesedTransactions.seriesId, - seriesIdType: seriesedTransactions.seriesIdType, - seriesDisplayOrders: seriesedTransactions.seriesDisplayOrders, + seriesName: seriesTransactions.seriesName, + seriesNameNeedI18n: seriesTransactions.seriesNameNeedI18n, + seriesNameI18nParameters: seriesTransactions.seriesNameI18nParameters, + seriesId: seriesTransactions.seriesId, + seriesIdType: seriesTransactions.seriesIdType, + seriesDisplayOrders: seriesTransactions.seriesDisplayOrders, value: value }); } diff --git a/src/views/desktop/insights/ExplorerPage.vue b/src/views/desktop/insights/ExplorerPage.vue index d8f804b2..88a3388f 100644 --- a/src/views/desktop/insights/ExplorerPage.vue +++ b/src/views/desktop/insights/ExplorerPage.vue @@ -763,6 +763,10 @@ watch(() => display.mdAndUp.value, (newValue) => { }); watch(activeTab, () => { + if (initing.value || loading.value) { + return; + } + router.push(getFilterLinkUrl()); }); diff --git a/src/views/desktop/insights/tabs/ExplorerChartTab.vue b/src/views/desktop/insights/tabs/ExplorerChartTab.vue index 529270dd..2f4c781b 100644 --- a/src/views/desktop/insights/tabs/ExplorerChartTab.vue +++ b/src/views/desktop/insights/tabs/ExplorerChartTab.vue @@ -23,7 +23,8 @@ :disabled="loading || disabled" :label="tt('Axis / Category')" :items="allTransactionExplorerDataDimensions" - v-model="currentExplorer.categoryDimension" + :model-value="currentExplorer.categoryDimension" + @update:model-value="updateCategoryDimensionType" /> - + + + + +