Merge pull request #631 from acelaya-forks/feature/react-chartjs-4

Feature/react chartjs 4
This commit is contained in:
Alejandro Celaya 2022-05-02 11:57:04 +02:00 committed by GitHub
commit 57a2a03469
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 38 additions and 28 deletions

View file

@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Changed
* [#616](https://github.com/shlinkio/shlink-web-client/pull/616) Updated to React 18.
* [#595](https://github.com/shlinkio/shlink-web-client/pull/595) Updated to react-chartjs-2 v4.1.0.
* [#594](https://github.com/shlinkio/shlink-web-client/pull/594) Updated to a new coding standard.
* [#603](https://github.com/shlinkio/shlink-web-client/pull/603) Migrated to new and maintained dependencies to parse CSV<->JSON.
* [#619](https://github.com/shlinkio/shlink-web-client/pull/619) Introduced react testing library, to progressively replace enzyme.

View file

@ -26,13 +26,14 @@ module.exports = {
},
transformIgnorePatterns: [
'<rootDir>/.stryker-tmp',
'/node_modules\\/(?!react-leaflet)\.(js|jsx|ts|tsx)$',
'node_modules\/(?!(\@react-leaflet|react-leaflet|leaflet|react-chartjs-2)\/)',
'^.+\\.module\\.scss$',
],
moduleNameMapper: {
'^.+\\.module\\.scss$': 'identity-obj-proxy',
// Reactstrap module resolution does not work in jest for some reason. Manually mapping it solves the problem
'reactstrap': '<rootDir>/node_modules/reactstrap/dist/reactstrap.umd.js',
'react-chartjs-2': '<rootDir>/node_modules/react-chartjs-2/dist/index.js',
},
moduleFileExtensions: ['js', 'ts', 'tsx', 'json'],
};

16
package-lock.json generated
View file

@ -27,7 +27,7 @@
"qs": "^6.9.6",
"ramda": "^0.27.2",
"react": "^18.1.0",
"react-chartjs-2": "^3.3.0",
"react-chartjs-2": "^4.1.0",
"react-colorful": "^5.5.1",
"react-copy-to-clipboard": "^5.0.4",
"react-datepicker": "^4.7.0",
@ -19110,12 +19110,12 @@
}
},
"node_modules/react-chartjs-2": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-3.3.0.tgz",
"integrity": "sha512-4Mt0SR2aiUbWi/4762odRBYSnbNKSs4HWc0o3IW43py5bMfmfpeZU95w6mbvtuLZH/M3GsPJMU8DvDc+5U9blQ==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-4.1.0.tgz",
"integrity": "sha512-AsUihxEp8Jm1oBhbEovE+w50m9PVNhz1sfwEIT4hZduRC0m14gHWHd0cUaxkFDb8HNkdMIGzsNlmVqKiOpU74g==",
"peerDependencies": {
"chart.js": "^3.5.0",
"react": "^16.8.0 || ^17.0.0"
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-colorful": {
@ -41617,9 +41617,9 @@
}
},
"react-chartjs-2": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-3.3.0.tgz",
"integrity": "sha512-4Mt0SR2aiUbWi/4762odRBYSnbNKSs4HWc0o3IW43py5bMfmfpeZU95w6mbvtuLZH/M3GsPJMU8DvDc+5U9blQ==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-4.1.0.tgz",
"integrity": "sha512-AsUihxEp8Jm1oBhbEovE+w50m9PVNhz1sfwEIT4hZduRC0m14gHWHd0cUaxkFDb8HNkdMIGzsNlmVqKiOpU74g==",
"requires": {}
},
"react-colorful": {

View file

@ -43,7 +43,7 @@
"qs": "^6.9.6",
"ramda": "^0.27.2",
"react": "^18.1.0",
"react-chartjs-2": "^3.3.0",
"react-chartjs-2": "^4.1.0",
"react-colorful": "^5.5.1",
"react-copy-to-clipboard": "^5.0.4",
"react-datepicker": "^4.7.0",

View file

@ -6,6 +6,7 @@ import { container } from './container';
import { store } from './container/store';
import { fixLeafletIcons } from './utils/helpers/leaflet';
import { register as registerServiceWorker } from './serviceWorkerRegistration';
import 'chart.js/auto'; // TODO Import specific ones to reduce bundle size https://react-chartjs-2.js.org/docs/migration-to-v4/#tree-shaking
import 'react-datepicker/dist/react-datepicker.css';
import 'leaflet/dist/leaflet.css';
import './index.scss';

View file

@ -1,7 +1,7 @@
import { FC } from 'react';
import { ChartData, ChartDataset, ChartOptions } from 'chart.js';
import { FC, MutableRefObject, useRef } from 'react';
import { ChartData, ChartDataset, ChartOptions, InteractionItem } from 'chart.js';
import { keys, values } from 'ramda';
import { Bar } from 'react-chartjs-2';
import { Bar, getElementAtEvent } from 'react-chartjs-2';
import { fillTheGaps } from '../../utils/helpers/visits';
import { pointerOnHover, renderChartLabel } from '../../utils/helpers/charts';
import { prettify } from '../../utils/helpers/numbers';
@ -57,8 +57,7 @@ const generateChartData = (
datasets: generateChartDatasets(data, highlightedData, highlightedLabel),
});
type ClickedCharts = [{ index: number }] | [];
const chartElementAtEvent = (labels: string[], onClick?: (label: string) => void) => ([chart]: ClickedCharts) => {
const chartElementAtEvent = (labels: string[], [chart]: InteractionItem[], onClick?: (label: string) => void) => {
if (!onClick || !chart) {
return;
}
@ -80,6 +79,8 @@ export const HorizontalBarChart: FC<HorizontalBarChartProps> = (
}, { ...stats }),
);
const highlightedData = fillTheGaps(highlightedStats ?? {}, labels);
const refWithStats = useRef(null);
const refWithoutStats = useRef(null);
const options: ChartOptions = {
plugins: {
@ -110,13 +111,14 @@ export const HorizontalBarChart: FC<HorizontalBarChartProps> = (
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) => (
const renderChartComponent = (customKey: string, theRef: MutableRefObject<any>) => (
<Bar
ref={theRef}
key={`${height}_${customKey}`}
data={chartData as any}
options={options as any}
height={height}
getElementAtEvent={chartElementAtEvent(labels, onClick) as any}
onClick={(e) => chartElementAtEvent(labels, getElementAtEvent(theRef.current, e), onClick)}
/>
);
@ -124,8 +126,8 @@ export const HorizontalBarChart: FC<HorizontalBarChartProps> = (
<>
{/* 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')}
{highlightedStats === undefined && renderChartComponent('without_stats')}
{highlightedStats !== undefined && renderChartComponent('with_stats', refWithStats)}
{highlightedStats === undefined && renderChartComponent('without_stats', refWithoutStats)}
</>
);
};

View file

@ -1,4 +1,4 @@
import { useState, useMemo } from 'react';
import { useState, useMemo, MutableRefObject, useRef } from 'react';
import {
Card,
CardHeader,
@ -8,7 +8,7 @@ import {
DropdownMenu,
DropdownItem,
} from 'reactstrap';
import { Line } from 'react-chartjs-2';
import { getElementAtEvent, Line } from 'react-chartjs-2';
import { always, cond, countBy, reverse } from 'ramda';
import {
add,
@ -21,7 +21,7 @@ import {
startOfISOWeek,
endOfISOWeek,
} from 'date-fns';
import { ChartData, ChartDataset, ChartOptions } from 'chart.js';
import { ChartData, ChartDataset, ChartOptions, InteractionItem } from 'chart.js';
import { NormalizedVisit, Stats } from '../types';
import { fillTheGaps } from '../../utils/helpers/visits';
import { useToggle } from '../../utils/helpers/hooks';
@ -148,8 +148,9 @@ let selectedLabel: string | null = null;
const chartElementAtEvent = (
labels: string[],
datasetsByPoint: Record<string, NormalizedVisit[]>,
[chart]: InteractionItem[],
setSelectedVisits?: (visits: NormalizedVisit[]) => void,
) => ([chart]: [{ index: number }]) => {
) => {
if (!setSelectedVisits || !chart) {
return;
}
@ -160,7 +161,7 @@ const chartElementAtEvent = (
setSelectedVisits([]);
selectedLabel = null;
} else {
setSelectedVisits(labels[index] ? datasetsByPoint[labels[index]] : []);
setSelectedVisits(labels[index] && datasetsByPoint[labels[index]] ? datasetsByPoint[labels[index]] : []);
selectedLabel = labels[index] ?? null;
}
};
@ -172,6 +173,8 @@ const LineChartCard = (
visits.length > 0 ? determineInitialStep(visits[visits.length - 1].date) : 'monthly',
);
const [skipNoVisits, toggleSkipNoVisits] = useToggle(true);
const refWithHighlightedVisits = useRef(null);
const refWithoutHighlightedVisits = useRef(null);
const datasetsByPoint = useMemo(() => visitsToDatasetGroups(step, visits), [step, visits]);
const groupedVisitsWithGaps = useMemo(() => groupVisitsByStep(step, reverse(visits)), [step, visits]);
@ -220,11 +223,13 @@ const LineChartCard = (
},
onHover: pointerOnHover,
};
const renderLineChart = () => (
const renderLineChart = (theRef: MutableRefObject<any>) => (
<Line
ref={theRef}
data={generateChartData() as any}
options={options as any}
getElementAtEvent={chartElementAtEvent(labels, datasetsByPoint, setSelectedVisits) as any}
onClick={(e) =>
chartElementAtEvent(labels, datasetsByPoint, getElementAtEvent(theRef.current, e), setSelectedVisits)}
/>
);
@ -255,8 +260,8 @@ const LineChartCard = (
<CardBody className="line-chart-card__body">
{/* 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 && renderLineChart()}
{highlightedVisits.length === 0 && renderLineChart()}
{highlightedVisits.length > 0 && renderLineChart(refWithHighlightedVisits)}
{highlightedVisits.length === 0 && renderLineChart(refWithoutHighlightedVisits)}
</CardBody>
</Card>
);