diff --git a/src/utils/helpers/charts.ts b/src/utils/helpers/charts.ts new file mode 100644 index 00000000..807519a9 --- /dev/null +++ b/src/utils/helpers/charts.ts @@ -0,0 +1,5 @@ +import { ChangeEvent, FC } from 'react'; + +export const pointerOnHover = ({ target }: ChangeEvent, chartElement: FC[]) => { + target.style.cursor = chartElement[0] ? 'pointer' : 'default'; +}; diff --git a/src/visits/VisitsStats.tsx b/src/visits/VisitsStats.tsx index b31cd5fc..bd48fec8 100644 --- a/src/visits/VisitsStats.tsx +++ b/src/visits/VisitsStats.tsx @@ -128,6 +128,7 @@ const VisitsStats: FC = ( visits={normalizedVisits} highlightedVisits={highlightedVisits} highlightedLabel={highlightedLabel} + setSelectedVisits={setSelectedVisits} />
diff --git a/src/visits/helpers/DefaultChart.tsx b/src/visits/helpers/DefaultChart.tsx index 01a38fb2..fcba3a5c 100644 --- a/src/visits/helpers/DefaultChart.tsx +++ b/src/visits/helpers/DefaultChart.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent, useRef } from 'react'; +import React, { useRef } from 'react'; import { Doughnut, HorizontalBar } from 'react-chartjs-2'; import { keys, values } from 'ramda'; import classNames from 'classnames'; @@ -7,6 +7,7 @@ import { fillTheGaps, renderDoughnutChartLabel, renderNonDoughnutChartLabel } fr import { Stats } from '../types'; import { prettify } from '../../utils/helpers/numbers'; import './DefaultChart.scss'; +import { pointerOnHover } from '../../utils/helpers/charts'; export interface DefaultChartProps { title: Function | string; @@ -145,11 +146,7 @@ const DefaultChart = ( label: isBarChart ? renderNonDoughnutChartLabel('xLabel') : renderDoughnutChartLabel, }, }, - onHover: !isBarChart ? undefined : ((e: ChangeEvent, chartElement: HorizontalBar[] | Doughnut[]) => { - const { target } = e; - - target.style.cursor = chartElement[0] ? 'pointer' : 'default'; - }) as any, // TODO Types seem to be incorrectly defined + onHover: !isBarChart ? undefined : (pointerOnHover) as any, // TODO Types seem to be incorrectly defined in @types/chart.js }; const graphData = generateGraphData(title, isBarChart, labels, data, highlightedData, highlightedLabel); const height = determineHeight(isBarChart, labels); diff --git a/src/visits/helpers/LineChartCard.tsx b/src/visits/helpers/LineChartCard.tsx index bcac1b41..bfc6adcb 100644 --- a/src/visits/helpers/LineChartCard.tsx +++ b/src/visits/helpers/LineChartCard.tsx @@ -11,7 +11,7 @@ import { import { Line } from 'react-chartjs-2'; import { always, cond, reverse } from 'ramda'; import moment from 'moment'; -import { ChartData, ChartDataSets, ChartOptions } from 'chart.js'; +import Chart, { ChartData, ChartDataSets, ChartOptions } from 'chart.js'; import { NormalizedVisit, Stats } from '../types'; import { fillTheGaps, renderNonDoughnutChartLabel } from '../../utils/helpers/visits'; import { useToggle } from '../../utils/helpers/hooks'; @@ -19,12 +19,14 @@ import { rangeOf } from '../../utils/utils'; import ToggleSwitch from '../../utils/ToggleSwitch'; import { prettify } from '../../utils/helpers/numbers'; import './LineChartCard.scss'; +import { pointerOnHover } from '../../utils/helpers/charts'; interface LineChartCardProps { title: string; highlightedLabel?: string; visits: NormalizedVisit[]; highlightedVisits: NormalizedVisit[]; + setSelectedVisits?: (visits: NormalizedVisit[]) => void; } type Step = 'monthly' | 'weekly' | 'daily' | 'hourly'; @@ -71,13 +73,25 @@ const groupVisitsByStep = (step: Step, visits: NormalizedVisit[]): Stats => visi (acc, visit) => { const key = STEP_TO_DATE_FORMAT[step](visit.date); - acc[key] = acc[key] ? acc[key] + 1 : 1; + acc[key] = (acc[key] || 0) + 1; return acc; }, {}, ); +const visitsToDatasetGroups = (step: Step, visits: NormalizedVisit[]) => visits.reduce( + (acc, visit) => { + const key = STEP_TO_DATE_FORMAT[step](visit.date); + + acc[key] = acc[key] ?? []; + acc[key].push(visit); + + return acc; + }, + {} as Record, +); + const generateLabels = (step: Step, visits: NormalizedVisit[]): string[] => { const unit = STEP_TO_DATE_UNIT_MAP[step]; const formatter = STEP_TO_DATE_FORMAT[step]; @@ -115,12 +129,37 @@ const generateDataset = (data: number[], label: string, color: string): ChartDat backgroundColor: color, }); -const LineChartCard = ({ title, visits, highlightedVisits, highlightedLabel = 'Selected' }: LineChartCardProps) => { +let selectedLabel: string | null = null; + +const chartElementAtEvent = ( + datasetsByPoint: Record, + setSelectedVisits?: (visits: NormalizedVisit[]) => void, +) => ([ chart ]: [{ _index: number; _chart: Chart }]) => { + if (!setSelectedVisits || !chart) { + return; + } + + const { _index: index, _chart: { data } } = chart; + const { labels } = data as { labels: string[] }; + + if (selectedLabel === labels[index]) { + setSelectedVisits([]); + selectedLabel = null; + } else { + setSelectedVisits(labels[index] && datasetsByPoint[labels[index]] || []); + selectedLabel = labels[index] ?? null; + } +}; + +const LineChartCard = ( + { title, visits, highlightedVisits, highlightedLabel = 'Selected', setSelectedVisits }: LineChartCardProps, +) => { const [ step, setStep ] = useState( visits.length > 0 ? determineInitialStep(visits[visits.length - 1].date) : 'monthly', ); const [ skipNoVisits, toggleSkipNoVisits ] = useToggle(true); + const datasetsByPoint = useMemo(() => visitsToDatasetGroups(step, visits), [ step, visits ]); const groupedVisitsWithGaps = useMemo(() => groupVisitsByStep(step, reverse(visits)), [ step, visits ]); const [ labels, groupedVisits ] = useMemo( () => generateLabelsAndGroupedVisits(visits, groupedVisitsWithGaps, step, skipNoVisits), @@ -166,6 +205,7 @@ const LineChartCard = ({ title, visits, highlightedVisits, highlightedLabel = 'S label: renderNonDoughnutChartLabel('yLabel'), }, }, + onHover: (pointerOnHover) as any, // TODO Types seem to be incorrectly defined in @types/chart.js }; return ( @@ -193,7 +233,11 @@ const LineChartCard = ({ title, visits, highlightedVisits, highlightedLabel = 'S
- + ); diff --git a/test/visits/helpers/LineChartCard.test.tsx b/test/visits/helpers/LineChartCard.test.tsx index 8aaf2a48..27cfc33a 100644 --- a/test/visits/helpers/LineChartCard.test.tsx +++ b/test/visits/helpers/LineChartCard.test.tsx @@ -53,7 +53,7 @@ describe('', () => { const wrapper = createWrapper(); const chart = wrapper.find(Line); - expect(chart.prop('options')).toEqual({ + expect(chart.prop('options')).toEqual(expect.objectContaining({ maintainAspectRatio: false, legend: { display: false }, scales: { @@ -72,7 +72,7 @@ describe('', () => { intersect: false, axis: 'x', }), - }); + })); }); it.each([