UNPKG

@pagamio/frontend-commons-lib

Version:

Pagamio library for Frontend reusable components like the form engine and table container

267 lines (266 loc) 12.6 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import ReactECharts from 'echarts-for-react'; import { useCallback, useMemo, useState } from 'react'; import Card from '../components/CardWrapper'; import ChartDetailsModal from '../components/ChartDetailsModal'; import ChartWrapper from '../components/ChartWrapper'; import { useChartData } from '../hooks/useChartData'; import { useTooltipFormatter } from '../hooks/useTooltipFormatter'; import { DashboardPaths } from '../utils'; import { createDistributionMetricOptions } from '../utils/chartOptions'; import { processTooltipFields } from '../utils/tooltipUtils'; const useMetricStatistics = (url, statisticsUrl, query, metricDetailData) => { // Get item Statistics Query data const itemStats = useChartData(statisticsUrl, { ...metricDetailData.itemStatisticsQuery.query, }); // Get value Metrics Query data const valueStats = useChartData(statisticsUrl, { ...metricDetailData.valueMetricsQuery.query, }); // Get average Metrics Query data const averageStats = useChartData(statisticsUrl, { ...metricDetailData.averageMetricsQuery.query, }); // Get full data const fullData = useChartData(url, { ...query, limit: undefined, }); // Get distribution Metrics Query data const distributionStats = useChartData(url, { ...metricDetailData.distributionMetricsQuery.query, }); return { itemStats, valueStats, averageStats, fullData, distributionStats, }; }; const createWaterfallData = (data, xAxisDataValueKey, seriesDataValueKey, colors, processedTooltipFields) => { if (!data?.length) return []; let runningTotal = 0; const processedData = data.map((item) => { const start = runningTotal; runningTotal += item[seriesDataValueKey]; // Add tooltip fields to data const tooltipData = {}; processedTooltipFields.forEach((field) => { if (item[field.toolTipkey] !== undefined) { tooltipData[field.toolTipkey] = item[field.toolTipkey]; } }); return { name: item[xAxisDataValueKey], value: item[seriesDataValueKey], itemStyle: { color: item[seriesDataValueKey] >= 0 ? colors.positive : colors.negative, }, start, end: runningTotal, ...tooltipData, // Include tooltip data }; }); // Add total bar processedData.push({ name: 'Total', value: runningTotal, itemStyle: { color: colors.total, }, start: 0, end: runningTotal, }); return processedData; }; const createBaseChartOptions = (data, { title, colors, xAxisLabel, yAxisLabel, xAxisDataValueKey, seriesDataValueKey, tooltipFormatter, chartToolTip, yAxisLabelFormatter, }) => { const processedData = createWaterfallData(data, xAxisDataValueKey, seriesDataValueKey, colors, []); return { tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, ...chartToolTip, formatter: tooltipFormatter, }, grid: { left: '3%', right: '4%', bottom: '3%', top: title ? '60' : '3%', containLabel: true, }, xAxis: { type: 'category', name: xAxisLabel, data: processedData.map((item) => item.name), axisLabel: { interval: 0, rotate: 30, }, }, yAxis: { type: 'value', name: yAxisLabel, splitLine: { lineStyle: { type: 'dashed', }, }, ...(yAxisLabelFormatter && { axisLabel: { formatter: yAxisLabelFormatter, }, }), }, series: [ { type: 'bar', stack: 'waterfall', data: processedData.map((item) => ({ value: item.value, itemStyle: item.itemStyle, })), label: { show: false, position: 'top', }, }, ], animation: true, animationDuration: 1000, animationEasing: 'cubicInOut', }; }; const WaterfallChart = ({ query, title, colors = { positive: '#34D399', // green-400 negative: '#F87171', // red-400 total: '#60A5FA', // blue-400 }, className = '', url = DashboardPaths.QUERY, xAxisDataValueKey, xAxisLabel = '', yAxisLabel = '', seriesDataValueKey, tooltipValueFormat, chartToolTip, tooltipTitle = 'Value', tooltipUnit = '', tooltipAdditionalFields = [], yAxisLabelFormatter, metricDetailData, showDetailsModal = true, ...props }) => { const [isModalOpen, setIsModalOpen] = useState(false); const { data: metricData = [], error, isEmpty, loading, refresh } = useChartData(url, query); const statisticsUrl = metricDetailData?.statisticsUrl ?? DashboardPaths.METRICS; const { itemStats, valueStats, averageStats, fullData, distributionStats } = useMetricStatistics(url, statisticsUrl, query, metricDetailData); // Replace duplicated tooltip formatter logic with hook const { tooltipFormatterFn } = useTooltipFormatter(tooltipValueFormat, tooltipTitle, tooltipUnit, tooltipAdditionalFields, metricData); const processedDistributionTooltipFields = useMemo(() => processTooltipFields(metricDetailData.distributionChartTooltip.tooltipAdditionalFields), [metricDetailData.distributionChartTooltip.tooltipAdditionalFields]); const getChartOptions = useCallback((data, customTitle) => { return createBaseChartOptions(data, { title: customTitle ?? title, colors, xAxisLabel, yAxisLabel, xAxisDataValueKey, seriesDataValueKey, tooltipFormatter: tooltipFormatterFn, chartToolTip, yAxisLabelFormatter, }); }, [ colors, xAxisDataValueKey, seriesDataValueKey, tooltipFormatterFn, chartToolTip, title, xAxisLabel, yAxisLabel, yAxisLabelFormatter, ]); const chartBarOptions = useMemo(() => getChartOptions(metricData), [getChartOptions, metricData]); const fullChartOptions = useMemo(() => getChartOptions(fullData.data ?? []), [getChartOptions, fullData.data]); const handleRefresh = useCallback((refreshFn) => { return () => { refreshFn().catch(() => { }); }; }, []); const distributionMetricData = useMemo(() => { return createDistributionMetricOptions({ distributionChartTooltip: metricDetailData.distributionChartTooltip, distributionData: distributionStats.data ?? [], processedTooltipFields: processedDistributionTooltipFields, xAxisDataValueKey: metricDetailData.distributionMetricsQuery.xAxisDataValueKey, seriesDataValueKey: metricDetailData.distributionMetricsQuery.seriesDataValueKey, }); }, [ distributionStats.data, metricDetailData.distributionMetricsQuery.seriesDataValueKey, metricDetailData.distributionMetricsQuery.xAxisDataValueKey, metricDetailData.distributionChartTooltip, processedDistributionTooltipFields, ]); const { topFiveItems, bottomFiveItems } = useMemo(() => { if (!fullData.data || fullData.data.length === 0) { return { topFiveItems: [], bottomFiveItems: [], }; } const topFiveMetric = fullData.data.slice(0, 5); const bottomFiveMetric = fullData.data.slice(-5).reverse(); return { topFiveItems: getChartOptions(topFiveMetric, metricDetailData.topFiveChartTitle), bottomFiveItems: getChartOptions(bottomFiveMetric, metricDetailData.bottomFiveChartTitle), }; }, [fullData.data, getChartOptions, metricDetailData]); const itemMetricsData = { value: itemStats.data?.[metricDetailData.itemStatisticsQuery.valueKey] ?? 0, previousValue: metricDetailData.itemStatisticsQuery.previousValueKey ? (itemStats.data?.[metricDetailData.itemStatisticsQuery.previousValueKey] ?? 0) : undefined, change: itemStats.data?.[metricDetailData.itemStatisticsQuery.changeKey] ?? 0, }; const valueMetricsData = { value: valueStats.data?.[metricDetailData.valueMetricsQuery.valueKey] ?? 0, previousValue: metricDetailData.valueMetricsQuery.previousValueKey ? (valueStats.data?.[metricDetailData.valueMetricsQuery.previousValueKey] ?? 0) : undefined, change: valueStats.data?.[metricDetailData.valueMetricsQuery.changeKey] ?? 0, }; const averageMetricsData = { value: averageStats.data?.[metricDetailData.averageMetricsQuery.valueKey] ?? 0, previousValue: metricDetailData.averageMetricsQuery.previousValueKey ? (averageStats.data?.[metricDetailData.averageMetricsQuery.previousValueKey] ?? 0) : undefined, change: averageStats.data?.[metricDetailData.averageMetricsQuery.changeKey] ?? 0, }; return (_jsxs(_Fragment, { children: [_jsx(Card, { title: title, ...props, onOpenDetails: metricData ? () => setIsModalOpen(true) : undefined, children: _jsx(ChartWrapper, { loading: loading, error: error, isEmpty: isEmpty, onRetry: refresh, children: _jsx("div", { className: "mt-[19px]", children: _jsx(ReactECharts, { option: chartBarOptions, style: { height: '350px' } }) }) }) }), showDetailsModal && (_jsx(ChartDetailsModal, { title: metricDetailData.title, isOpen: isModalOpen, onClose: () => setIsModalOpen(false), data: fullData.data ?? [], columns: metricDetailData.dataGridColumns, error: fullData.error, isEmpty: fullData.isEmpty, loading: fullData.loading, detailsTableTitle: metricDetailData.detailsTableTitle, refresh: fullData.refresh, renderDetailsChart: (fullData.data?.length ?? 0) <= 40, chartData: fullChartOptions, top5PerformingItems: topFiveItems, bottom5NonPerformingItems: bottomFiveItems, valueKey: metricDetailData.valueKey, dataGridQuery: query, searchKey: metricDetailData.dataGridSearchKey, searchInputPlaceHolder: metricDetailData.dataSearchInputPlaceHolder, itemMetricsData: { ...itemMetricsData, title: metricDetailData.itemStatisticsQuery.title, format: metricDetailData.itemStatisticsQuery.format, isEmpty: itemStats.isEmpty, loading: itemStats.loading, currency: itemStats.data?.additionalData?.currency, error: itemStats.error, refresh: handleRefresh(itemStats.refresh), }, valueMetricsData: { ...valueMetricsData, title: metricDetailData.valueMetricsQuery.title, format: metricDetailData.valueMetricsQuery.format, isEmpty: valueStats.isEmpty, loading: valueStats.loading, currency: valueStats.data?.additionalData?.currency, error: valueStats.error, refresh: handleRefresh(valueStats.refresh), }, averageMetricsData: { ...averageMetricsData, title: metricDetailData.averageMetricsQuery.title, format: metricDetailData.averageMetricsQuery.format, isEmpty: averageStats.isEmpty, loading: averageStats.loading, currency: averageStats.data?.additionalData?.currency, error: averageStats.error, refresh: handleRefresh(averageStats.refresh), }, distributionMetricsData: { data: distributionMetricData ?? [], title: metricDetailData.distributionMetricsQuery.title, isEmpty: distributionStats.isEmpty, loading: distributionStats.loading, error: distributionStats.error, refresh: handleRefresh(distributionStats.refresh), chartToolTip, } }))] })); }; export default WaterfallChart;