mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-08-01 16:41:53 +00:00
Extract shlink-web-component outside of src folder
This commit is contained in:
134
shlink-web-component/visits/charts/HorizontalBarChart.tsx
Normal file
134
shlink-web-component/visits/charts/HorizontalBarChart.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import type { ChartData, ChartDataset, ChartOptions, InteractionItem } from 'chart.js';
|
||||
import { keys, values } from 'ramda';
|
||||
import type { FC, MutableRefObject } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { Bar, getElementAtEvent } from 'react-chartjs-2';
|
||||
import { pointerOnHover, renderChartLabel } from '../../../src/utils/helpers/charts';
|
||||
import { prettify } from '../../../src/utils/helpers/numbers';
|
||||
import { HIGHLIGHTED_COLOR, HIGHLIGHTED_COLOR_ALPHA, MAIN_COLOR, MAIN_COLOR_ALPHA } from '../../../src/utils/theme';
|
||||
import type { Stats } from '../types';
|
||||
import { fillTheGaps } from '../utils';
|
||||
|
||||
export interface HorizontalBarChartProps {
|
||||
stats: Stats;
|
||||
max?: number;
|
||||
highlightedStats?: Stats;
|
||||
highlightedLabel?: string;
|
||||
onClick?: (label: string) => void;
|
||||
}
|
||||
|
||||
const dropLabelIfHidden = (label: string) => (label.startsWith('hidden') ? '' : label);
|
||||
const statsAreDefined = (stats: Stats | undefined): stats is Stats => !!stats && Object.keys(stats).length > 0;
|
||||
const determineHeight = (labels: string[]): number | undefined => (labels.length > 20 ? labels.length * 10 : undefined);
|
||||
|
||||
const generateChartDatasets = (
|
||||
data: number[],
|
||||
highlightedData: number[],
|
||||
highlightedLabel?: string,
|
||||
): ChartDataset[] => {
|
||||
const mainDataset: ChartDataset = {
|
||||
data,
|
||||
label: highlightedLabel ? 'Non-selected' : 'Visits',
|
||||
backgroundColor: MAIN_COLOR_ALPHA,
|
||||
borderColor: MAIN_COLOR,
|
||||
borderWidth: 2,
|
||||
};
|
||||
|
||||
if (highlightedData.every((value) => value === 0)) {
|
||||
return [mainDataset];
|
||||
}
|
||||
|
||||
const highlightedDataset: ChartDataset = {
|
||||
label: highlightedLabel ?? 'Selected',
|
||||
data: highlightedData,
|
||||
backgroundColor: HIGHLIGHTED_COLOR_ALPHA,
|
||||
borderColor: HIGHLIGHTED_COLOR,
|
||||
borderWidth: 2,
|
||||
};
|
||||
|
||||
return [mainDataset, highlightedDataset];
|
||||
};
|
||||
const generateChartData = (
|
||||
labels: string[],
|
||||
data: number[],
|
||||
highlightedData: number[],
|
||||
highlightedLabel?: string,
|
||||
): ChartData => ({
|
||||
labels,
|
||||
datasets: generateChartDatasets(data, highlightedData, highlightedLabel),
|
||||
});
|
||||
|
||||
const chartElementAtEvent = (labels: string[], [chart]: InteractionItem[], onClick?: (label: string) => void) => {
|
||||
if (!onClick || !chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClick(labels[chart.index]);
|
||||
};
|
||||
|
||||
export const HorizontalBarChart: FC<HorizontalBarChartProps> = (
|
||||
{ stats, highlightedStats, highlightedLabel, onClick, max },
|
||||
) => {
|
||||
const labels = keys(stats).map(dropLabelIfHidden);
|
||||
const data = values(
|
||||
!statsAreDefined(highlightedStats) ? stats : keys(highlightedStats).reduce((acc, highlightedKey) => {
|
||||
if (acc[highlightedKey]) {
|
||||
acc[highlightedKey] -= highlightedStats[highlightedKey];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, { ...stats }),
|
||||
);
|
||||
const highlightedData = fillTheGaps(highlightedStats ?? {}, labels);
|
||||
const refWithStats = useRef(null);
|
||||
const refWithoutStats = useRef(null);
|
||||
|
||||
const options: ChartOptions = {
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
mode: 'y',
|
||||
// Do not show tooltip on items with empty label when in a bar chart
|
||||
filter: ({ label }) => label !== '',
|
||||
callbacks: { label: renderChartLabel },
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
stacked: true,
|
||||
max,
|
||||
ticks: {
|
||||
precision: 0,
|
||||
callback: prettify,
|
||||
},
|
||||
},
|
||||
y: { stacked: true },
|
||||
},
|
||||
onHover: pointerOnHover,
|
||||
indexAxis: 'y',
|
||||
};
|
||||
const chartData = generateChartData(labels, data, highlightedData, highlightedLabel);
|
||||
const height = determineHeight(labels);
|
||||
|
||||
// Provide a key based on the height, to force re-render every time the dataset changes (example, due to pagination)
|
||||
const renderChartComponent = (customKey: string, theRef: MutableRefObject<any>) => (
|
||||
<Bar
|
||||
ref={theRef}
|
||||
key={`${height}_${customKey}`}
|
||||
data={chartData as any}
|
||||
options={options as any}
|
||||
height={height}
|
||||
onClick={(e) => chartElementAtEvent(labels, getElementAtEvent(theRef.current, e), onClick)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* It's VERY IMPORTANT to render two different components here, as one has 1 dataset and the other has 2 */}
|
||||
{/* Using the same component causes a crash when switching from 1 to 2 datasets, and then back to 1 dataset */}
|
||||
{highlightedStats !== undefined && renderChartComponent('with_stats', refWithStats)}
|
||||
{highlightedStats === undefined && renderChartComponent('without_stats', refWithoutStats)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user