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'; import { PieChartLegend } from './PieChartLegend';
export interface DefaultChartProps { export interface DefaultChartProps {
title: Function | string;
stats: Stats; stats: Stats;
isBarChart?: boolean; isBarChart?: boolean;
max?: number; max?: number;
@ -28,45 +27,56 @@ export interface DefaultChartProps {
onClick?: (label: string) => void; onClick?: (label: string) => void;
} }
const generateGraphData = ( const generateChartDatasets = (
title: Function | string, 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, isBarChart: boolean,
labels: string[], labels: string[],
data: number[], data: number[],
highlightedData?: number[], highlightedData: number[],
highlightedLabel?: string, highlightedLabel?: string,
): ChartData => ({ ): ChartData => ({
labels, labels,
datasets: [ datasets: generateChartDatasets(isBarChart, data, highlightedData, highlightedLabel),
{
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[],
}); });
const dropLabelIfHidden = (label: string) => label.startsWith('hidden') ? '' : label; 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 statsAreDefined = (stats: Stats | undefined): stats is Stats => !!stats && Object.keys(stats).length > 0;
const DefaultChart = ( 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 Component = isBarChart ? Bar : Doughnut;
const [ chartRef, setChartRef ] = useState<Chart | undefined>(); // Cannot use useRef here const [ chartRef, setChartRef ] = useState<Chart | undefined>(); // Cannot use useRef here
@ -107,13 +117,14 @@ const DefaultChart = (
return acc; return acc;
}, { ...stats }), }, { ...stats }),
); );
const highlightedData = statsAreDefined(highlightedStats) ? fillTheGaps(highlightedStats, labels) : undefined; const highlightedData = fillTheGaps(highlightedStats ?? {}, labels);
const options: ChartOptions = { const options: ChartOptions = {
plugins: { plugins: {
legend: { display: false }, legend: { display: false },
tooltip: { tooltip: {
intersect: !isBarChart, intersect: !isBarChart,
mode: isBarChart ? 'y' : 'index',
// Do not show tooltip on items with empty label when in a bar chart // Do not show tooltip on items with empty label when in a bar chart
filter: ({ label }) => !isBarChart || label !== '', filter: ({ label }) => !isBarChart || label !== '',
callbacks: { callbacks: {
@ -136,10 +147,10 @@ const DefaultChart = (
onHover: isBarChart ? pointerOnHover : undefined, onHover: isBarChart ? pointerOnHover : undefined,
indexAxis: isBarChart ? 'y' : 'x', 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); 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 ( return (
<div className="row"> <div className="row">
<div className={classNames('col-sm-12', { 'col-md-7': !isBarChart })}> <div className={classNames('col-sm-12', { 'col-md-7': !isBarChart })}>
@ -148,7 +159,7 @@ const DefaultChart = (
setChartRef(element ?? undefined); setChartRef(element ?? undefined);
}} }}
key={height} key={height}
data={graphData} data={chartData}
options={options} options={options}
height={height} height={height}
getElementAtEvent={chartElementAtEvent(labels, onClick) as any} getElementAtEvent={chartElementAtEvent(labels, onClick) as any}

View file

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

View file

@ -1,4 +1,4 @@
import { useState, useMemo } from 'react'; import { useState, useMemo, FC } from 'react';
import { import {
Card, Card,
CardHeader, CardHeader,
@ -183,14 +183,19 @@ const LineChartCard = (
() => fillTheGaps(groupVisitsByStep(step, reverse(highlightedVisits)), labels), () => fillTheGaps(groupVisitsByStep(step, reverse(highlightedVisits)), labels),
[ highlightedVisits, step, labels ], [ highlightedVisits, step, labels ],
); );
const generateChartDatasets = (): ChartDataset[] => {
const mainDataset = generateDataset(groupedVisits, 'Visits', MAIN_COLOR);
const data: ChartData = { if (highlightedVisits.length === 0) {
labels, return [ mainDataset ];
datasets: [ }
generateDataset(groupedVisits, 'Visits', MAIN_COLOR),
highlightedVisits.length > 0 && generateDataset(groupedHighlighted, highlightedLabel, HIGHLIGHTED_COLOR), const highlightedDataset = generateDataset(groupedHighlighted, highlightedLabel, HIGHLIGHTED_COLOR);
].filter(Boolean) as ChartDataset[],
return [ mainDataset, highlightedDataset ];
}; };
const generateChartData = (): ChartData => ({ labels, datasets: generateChartDatasets() });
const options: ChartOptions = { const options: ChartOptions = {
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { plugins: {
@ -213,8 +218,15 @@ const LineChartCard = (
title: { display: true, text: STEPS_MAP[step] }, 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 ( return (
<Card> <Card>
@ -241,11 +253,10 @@ const LineChartCard = (
</div> </div>
</CardHeader> </CardHeader>
<CardBody className="line-chart-card__body"> <CardBody className="line-chart-card__body">
<Line {/* It's VERY IMPORTANT to render two different components here, as one has 1 dataset and the other has 2 */}
data={data} {/* Using the same component causes a crash when switching from 1 to 2 datasets, and then back to 1 dataset */}
options={options} {highlightedVisits.length > 0 && <LineChart />}
getElementAtEvent={chartElementAtEvent(labels, datasetsByPoint, setSelectedVisits) as any} {highlightedVisits.length === 0 && <LineChart />}
/>
</CardBody> </CardBody>
</Card> </Card>
); );

View file

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