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
JavaScript
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;