mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Merge pull request #631 from acelaya-forks/feature/react-chartjs-4
Feature/react chartjs 4
This commit is contained in:
commit
57a2a03469
7 changed files with 38 additions and 28 deletions
|
@ -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.
|
||||||
|
|
|
@ -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
16
package-lock.json
generated
|
@ -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": {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue