Fixed tooltips in bar charts

This commit is contained in:
Alejandro Celaya 2021-09-18 12:07:05 +02:00
parent 6780aa623b
commit 039a56f410
4 changed files with 77 additions and 53 deletions

View file

@ -19,7 +19,6 @@ import {
import { PieChartLegend } from './PieChartLegend';
export interface DefaultChartProps {
title: Function | string;
stats: Stats;
isBarChart?: boolean;
max?: number;
@ -28,45 +27,56 @@ export interface DefaultChartProps {
onClick?: (label: string) => void;
}
const generateGraphData = (
title: Function | string,
const generateChartDatasets = (
isBarChart: boolean,
data: number[],
highlightedData: number[],
highlightedLabel?: string,
): ChartDataset[] => {
const mainDataset: ChartDataset = {
label: highlightedLabel ? 'Non-selected' : 'Visits',
data,
backgroundColor: isBarChart ? MAIN_COLOR_ALPHA : [
'#97BBCD',
'#F7464A',
'#46BFBD',
'#FDB45C',
'#949FB1',
'#57A773',
'#414066',
'#08B2E3',
'#B6C454',
'#DCDCDC',
'#463730',
],
borderColor: isBarChart ? MAIN_COLOR : isDarkThemeEnabled() ? PRIMARY_DARK_COLOR : PRIMARY_LIGHT_COLOR,
borderWidth: 2,
};
if (!isBarChart) {
return [ mainDataset ];
}
const highlightedDataset: ChartDataset = {
label: highlightedLabel ?? 'Selected',
data: highlightedData,
backgroundColor: HIGHLIGHTED_COLOR_ALPHA,
borderColor: HIGHLIGHTED_COLOR,
borderWidth: 2,
};
return [ mainDataset, highlightedDataset ];
};
const generateChartData = (
isBarChart: boolean,
labels: string[],
data: number[],
highlightedData?: number[],
highlightedData: number[],
highlightedLabel?: string,
): ChartData => ({
labels,
datasets: [
{
title,
label: highlightedData ? 'Non-selected' : 'Visits',
data,
backgroundColor: isBarChart ? MAIN_COLOR_ALPHA : [
'#97BBCD',
'#F7464A',
'#46BFBD',
'#FDB45C',
'#949FB1',
'#57A773',
'#414066',
'#08B2E3',
'#B6C454',
'#DCDCDC',
'#463730',
],
borderColor: isBarChart ? MAIN_COLOR : isDarkThemeEnabled() ? PRIMARY_DARK_COLOR : PRIMARY_LIGHT_COLOR,
borderWidth: 2,
},
highlightedData && {
title,
label: highlightedLabel ?? 'Selected',
data: highlightedData,
backgroundColor: HIGHLIGHTED_COLOR_ALPHA,
borderColor: HIGHLIGHTED_COLOR,
borderWidth: 2,
},
].filter(Boolean) as ChartDataset[],
datasets: generateChartDatasets(isBarChart, data, highlightedData, highlightedLabel),
});
const dropLabelIfHidden = (label: string) => label.startsWith('hidden') ? '' : label;
@ -93,7 +103,7 @@ const chartElementAtEvent = (
const statsAreDefined = (stats: Stats | undefined): stats is Stats => !!stats && Object.keys(stats).length > 0;
const DefaultChart = (
{ title, isBarChart = false, stats, max, highlightedStats, highlightedLabel, onClick }: DefaultChartProps,
{ isBarChart = false, stats, max, highlightedStats, highlightedLabel, onClick }: DefaultChartProps,
) => {
const Component = isBarChart ? Bar : Doughnut;
const [ chartRef, setChartRef ] = useState<Chart | undefined>(); // Cannot use useRef here
@ -107,13 +117,14 @@ const DefaultChart = (
return acc;
}, { ...stats }),
);
const highlightedData = statsAreDefined(highlightedStats) ? fillTheGaps(highlightedStats, labels) : undefined;
const highlightedData = fillTheGaps(highlightedStats ?? {}, labels);
const options: ChartOptions = {
plugins: {
legend: { display: false },
tooltip: {
intersect: !isBarChart,
mode: isBarChart ? 'y' : 'index',
// Do not show tooltip on items with empty label when in a bar chart
filter: ({ label }) => !isBarChart || label !== '',
callbacks: {
@ -136,10 +147,10 @@ const DefaultChart = (
onHover: isBarChart ? pointerOnHover : undefined,
indexAxis: isBarChart ? 'y' : 'x',
};
const graphData = generateGraphData(title, isBarChart, labels, data, highlightedData, highlightedLabel);
const chartData = generateChartData(isBarChart, labels, data, highlightedData, highlightedLabel);
const height = determineHeight(isBarChart, labels);
// Provide a key based on the height, so that every time the dataset changes, a new graph is rendered
// Provide a key based on the height, so that every time the dataset changes, a new chart is rendered
return (
<div className="row">
<div className={classNames('col-sm-12', { 'col-md-7': !isBarChart })}>
@ -148,7 +159,7 @@ const DefaultChart = (
setChartRef(element ?? undefined);
}}
key={height}
data={graphData}
data={chartData}
options={options}
height={height}
getElementAtEvent={chartElementAtEvent(labels, onClick) as any}

View file

@ -4,6 +4,7 @@ import DefaultChart, { DefaultChartProps } from './DefaultChart';
import './GraphCard.scss';
interface GraphCardProps extends DefaultChartProps {
title: Function | string;
footer?: ReactNode;
}
@ -11,7 +12,7 @@ const GraphCard = ({ title, footer, ...rest }: GraphCardProps) => (
<Card>
<CardHeader className="graph-card__header">{typeof title === 'function' ? title() : title}</CardHeader>
<CardBody>
<DefaultChart title={title} {...rest} />
<DefaultChart {...rest} />
</CardBody>
{footer && <CardFooter className="graph-card__footer--sticky">{footer}</CardFooter>}
</Card>

View file

@ -1,4 +1,4 @@
import { useState, useMemo } from 'react';
import { useState, useMemo, FC } from 'react';
import {
Card,
CardHeader,
@ -183,14 +183,19 @@ const LineChartCard = (
() => fillTheGaps(groupVisitsByStep(step, reverse(highlightedVisits)), labels),
[ highlightedVisits, step, labels ],
);
const generateChartDatasets = (): ChartDataset[] => {
const mainDataset = generateDataset(groupedVisits, 'Visits', MAIN_COLOR);
const data: ChartData = {
labels,
datasets: [
generateDataset(groupedVisits, 'Visits', MAIN_COLOR),
highlightedVisits.length > 0 && generateDataset(groupedHighlighted, highlightedLabel, HIGHLIGHTED_COLOR),
].filter(Boolean) as ChartDataset[],
if (highlightedVisits.length === 0) {
return [ mainDataset ];
}
const highlightedDataset = generateDataset(groupedHighlighted, highlightedLabel, HIGHLIGHTED_COLOR);
return [ mainDataset, highlightedDataset ];
};
const generateChartData = (): ChartData => ({ labels, datasets: generateChartDatasets() });
const options: ChartOptions = {
maintainAspectRatio: false,
plugins: {
@ -213,8 +218,15 @@ const LineChartCard = (
title: { display: true, text: STEPS_MAP[step] },
},
},
onHover: (pointerOnHover) as any, // TODO Types seem to be incorrectly defined in @types/chart.js
onHover: pointerOnHover,
};
const LineChart: FC = () => (
<Line
data={generateChartData()}
options={options}
getElementAtEvent={chartElementAtEvent(labels, datasetsByPoint, setSelectedVisits) as any}
/>
);
return (
<Card>
@ -241,11 +253,10 @@ const LineChartCard = (
</div>
</CardHeader>
<CardBody className="line-chart-card__body">
<Line
data={data}
options={options}
getElementAtEvent={chartElementAtEvent(labels, datasetsByPoint, setSelectedVisits) as any}
/>
{/* 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 */}
{highlightedVisits.length > 0 && <LineChart />}
{highlightedVisits.length === 0 && <LineChart />}
</CardBody>
</Card>
);

View file

@ -14,6 +14,7 @@ const pickKeyFromPair = ([ key ]: StatsRow) => key;
const pickValueFromPair = ([ , value ]: StatsRow) => value;
interface SortableBarGraphProps extends DefaultChartProps {
title: Function | string;
sortingItems: Record<string, string>;
withPagination?: boolean;
extraHeaderContent?: Function;