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 ### Changed
* [#616](https://github.com/shlinkio/shlink-web-client/pull/616) Updated to React 18. * [#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. * [#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. * [#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. * [#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: [ transformIgnorePatterns: [
'<rootDir>/.stryker-tmp', '<rootDir>/.stryker-tmp',
'/node_modules\\/(?!react-leaflet)\.(js|jsx|ts|tsx)$', 'node_modules\/(?!(\@react-leaflet|react-leaflet|leaflet|react-chartjs-2)\/)',
'^.+\\.module\\.scss$', '^.+\\.module\\.scss$',
], ],
moduleNameMapper: { moduleNameMapper: {
'^.+\\.module\\.scss$': 'identity-obj-proxy', '^.+\\.module\\.scss$': 'identity-obj-proxy',
// Reactstrap module resolution does not work in jest for some reason. Manually mapping it solves the problem // 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', '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'], moduleFileExtensions: ['js', 'ts', 'tsx', 'json'],
}; };

16
package-lock.json generated
View file

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

View file

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

View file

@ -6,6 +6,7 @@ import { container } from './container';
import { store } from './container/store'; import { store } from './container/store';
import { fixLeafletIcons } from './utils/helpers/leaflet'; import { fixLeafletIcons } from './utils/helpers/leaflet';
import { register as registerServiceWorker } from './serviceWorkerRegistration'; 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 'react-datepicker/dist/react-datepicker.css';
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import './index.scss'; import './index.scss';

View file

@ -1,7 +1,7 @@
import { FC } from 'react'; import { FC, MutableRefObject, useRef } from 'react';
import { ChartData, ChartDataset, ChartOptions } from 'chart.js'; import { ChartData, ChartDataset, ChartOptions, InteractionItem } from 'chart.js';
import { keys, values } from 'ramda'; 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 { fillTheGaps } from '../../utils/helpers/visits';
import { pointerOnHover, renderChartLabel } from '../../utils/helpers/charts'; import { pointerOnHover, renderChartLabel } from '../../utils/helpers/charts';
import { prettify } from '../../utils/helpers/numbers'; import { prettify } from '../../utils/helpers/numbers';
@ -57,8 +57,7 @@ const generateChartData = (
datasets: generateChartDatasets(data, highlightedData, highlightedLabel), datasets: generateChartDatasets(data, highlightedData, highlightedLabel),
}); });
type ClickedCharts = [{ index: number }] | []; const chartElementAtEvent = (labels: string[], [chart]: InteractionItem[], onClick?: (label: string) => void) => {
const chartElementAtEvent = (labels: string[], onClick?: (label: string) => void) => ([chart]: ClickedCharts) => {
if (!onClick || !chart) { if (!onClick || !chart) {
return; return;
} }
@ -80,6 +79,8 @@ export const HorizontalBarChart: FC<HorizontalBarChartProps> = (
}, { ...stats }), }, { ...stats }),
); );
const highlightedData = fillTheGaps(highlightedStats ?? {}, labels); const highlightedData = fillTheGaps(highlightedStats ?? {}, labels);
const refWithStats = useRef(null);
const refWithoutStats = useRef(null);
const options: ChartOptions = { const options: ChartOptions = {
plugins: { plugins: {
@ -110,13 +111,14 @@ export const HorizontalBarChart: FC<HorizontalBarChartProps> = (
const height = determineHeight(labels); const height = determineHeight(labels);
// Provide a key based on the height, to force re-render every time the dataset changes (example, due to pagination) // 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 <Bar
ref={theRef}
key={`${height}_${customKey}`} key={`${height}_${customKey}`}
data={chartData as any} data={chartData as any}
options={options as any} options={options as any}
height={height} 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 */} {/* 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 */} {/* 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('with_stats', refWithStats)}
{highlightedStats === undefined && renderChartComponent('without_stats')} {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 { import {
Card, Card,
CardHeader, CardHeader,
@ -8,7 +8,7 @@ import {
DropdownMenu, DropdownMenu,
DropdownItem, DropdownItem,
} from 'reactstrap'; } from 'reactstrap';
import { Line } from 'react-chartjs-2'; import { getElementAtEvent, Line } from 'react-chartjs-2';
import { always, cond, countBy, reverse } from 'ramda'; import { always, cond, countBy, reverse } from 'ramda';
import { import {
add, add,
@ -21,7 +21,7 @@ import {
startOfISOWeek, startOfISOWeek,
endOfISOWeek, endOfISOWeek,
} from 'date-fns'; } from 'date-fns';
import { ChartData, ChartDataset, ChartOptions } from 'chart.js'; import { ChartData, ChartDataset, ChartOptions, InteractionItem } from 'chart.js';
import { NormalizedVisit, Stats } from '../types'; import { NormalizedVisit, Stats } from '../types';
import { fillTheGaps } from '../../utils/helpers/visits'; import { fillTheGaps } from '../../utils/helpers/visits';
import { useToggle } from '../../utils/helpers/hooks'; import { useToggle } from '../../utils/helpers/hooks';
@ -148,8 +148,9 @@ let selectedLabel: string | null = null;
const chartElementAtEvent = ( const chartElementAtEvent = (
labels: string[], labels: string[],
datasetsByPoint: Record<string, NormalizedVisit[]>, datasetsByPoint: Record<string, NormalizedVisit[]>,
[chart]: InteractionItem[],
setSelectedVisits?: (visits: NormalizedVisit[]) => void, setSelectedVisits?: (visits: NormalizedVisit[]) => void,
) => ([chart]: [{ index: number }]) => { ) => {
if (!setSelectedVisits || !chart) { if (!setSelectedVisits || !chart) {
return; return;
} }
@ -160,7 +161,7 @@ const chartElementAtEvent = (
setSelectedVisits([]); setSelectedVisits([]);
selectedLabel = null; selectedLabel = null;
} else { } else {
setSelectedVisits(labels[index] ? datasetsByPoint[labels[index]] : []); setSelectedVisits(labels[index] && datasetsByPoint[labels[index]] ? datasetsByPoint[labels[index]] : []);
selectedLabel = labels[index] ?? null; selectedLabel = labels[index] ?? null;
} }
}; };
@ -172,6 +173,8 @@ const LineChartCard = (
visits.length > 0 ? determineInitialStep(visits[visits.length - 1].date) : 'monthly', visits.length > 0 ? determineInitialStep(visits[visits.length - 1].date) : 'monthly',
); );
const [skipNoVisits, toggleSkipNoVisits] = useToggle(true); const [skipNoVisits, toggleSkipNoVisits] = useToggle(true);
const refWithHighlightedVisits = useRef(null);
const refWithoutHighlightedVisits = useRef(null);
const datasetsByPoint = useMemo(() => visitsToDatasetGroups(step, visits), [step, visits]); const datasetsByPoint = useMemo(() => visitsToDatasetGroups(step, visits), [step, visits]);
const groupedVisitsWithGaps = useMemo(() => groupVisitsByStep(step, reverse(visits)), [step, visits]); const groupedVisitsWithGaps = useMemo(() => groupVisitsByStep(step, reverse(visits)), [step, visits]);
@ -220,11 +223,13 @@ const LineChartCard = (
}, },
onHover: pointerOnHover, onHover: pointerOnHover,
}; };
const renderLineChart = () => ( const renderLineChart = (theRef: MutableRefObject<any>) => (
<Line <Line
ref={theRef}
data={generateChartData() as any} data={generateChartData() as any}
options={options 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"> <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 */} {/* 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 */} {/* 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(refWithHighlightedVisits)}
{highlightedVisits.length === 0 && renderLineChart()} {highlightedVisits.length === 0 && renderLineChart(refWithoutHighlightedVisits)}
</CardBody> </CardBody>
</Card> </Card>
); );