1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-23 00:22:19 +01:00

refactor: LineChart component (#6072)

Initial version of a reusable trend chart, with a tooltip and vertical highlight
This commit is contained in:
Tymoteusz Czech 2024-01-31 10:07:29 +01:00 committed by GitHub
parent f298d7d511
commit e6ccd83739
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 101 additions and 59 deletions

View File

@ -77,7 +77,7 @@ export const ExecutiveDashboard: VFC = () => {
<Box sx={(theme) => ({ paddingBottom: theme.spacing(4) })}> <Box sx={(theme) => ({ paddingBottom: theme.spacing(4) })}>
<PageHeader <PageHeader
titleElement={ titleElement={
<Typography variant='h1' component='h2'> <Typography variant='h1' component='span'>
Dashboard Dashboard
</Typography> </Typography>
} }

View File

@ -0,0 +1,3 @@
import { lazy } from 'react';
export const LineChart = lazy(() => import('./LineChartComponent'));

View File

@ -1,15 +1,15 @@
import { useMemo, useState, type VFC } from 'react'; import { useMemo, useState, type VFC } from 'react';
import { import {
Chart as ChartJS,
CategoryScale, CategoryScale,
LinearScale, LinearScale,
PointElement, PointElement,
LineElement, LineElement,
Title,
Tooltip, Tooltip,
Legend, Legend,
TimeScale, TimeScale,
Chart, Chart,
type ChartData,
type ScatterDataPoint,
} from 'chart.js'; } from 'chart.js';
import { Line } from 'react-chartjs-2'; import { Line } from 'react-chartjs-2';
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
@ -18,7 +18,6 @@ import {
useLocationSettings, useLocationSettings,
type ILocationSettings, type ILocationSettings,
} from 'hooks/useLocationSettings'; } from 'hooks/useLocationSettings';
import { ExecutiveSummarySchema } from 'openapi';
import { ChartTooltip, TooltipState } from './ChartTooltip/ChartTooltip'; import { ChartTooltip, TooltipState } from './ChartTooltip/ChartTooltip';
const createOptions = ( const createOptions = (
@ -34,7 +33,6 @@ const createOptions = (
labels: { labels: {
boxWidth: 12, boxWidth: 12,
padding: 30, padding: 30,
// usePointStyle: true,
generateLabels: (chart: Chart) => { generateLabels: (chart: Chart) => {
const datasets = chart.data.datasets; const datasets = chart.data.datasets;
const { const {
@ -122,8 +120,8 @@ const createOptions = (
unit: 'month', unit: 'month',
}, },
grid: { grid: {
color: theme.palette.divider, color: 'transparent',
borderColor: theme.palette.divider, borderColor: 'transparent',
}, },
ticks: { ticks: {
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
@ -132,47 +130,41 @@ const createOptions = (
}, },
}) as const; }) as const;
interface IUsersChartComponentProps { // Vertical line on the hovered chart, filled with gradient. Highlights a section of a chart when you hover over datapoints
userTrends: ExecutiveSummarySchema['userTrends']; const customHighlightPlugin = {
} id: 'customLine',
afterDraw: (chart: Chart) => {
const width = 26;
if (chart.tooltip?.opacity && chart.tooltip.x) {
const x = chart.tooltip.caretX;
const yAxis = chart.scales.y;
const ctx = chart.ctx;
ctx.save();
const gradient = ctx.createLinearGradient(
x,
yAxis.top,
x,
yAxis.bottom,
);
gradient.addColorStop(0, 'rgba(129, 122, 254, 0)');
gradient.addColorStop(1, 'rgba(129, 122, 254, 0.12)');
ctx.fillStyle = gradient;
ctx.fillRect(
x - width / 2,
yAxis.top,
width,
yAxis.bottom - yAxis.top,
);
ctx.restore();
}
},
};
const createData = ( const LineChartComponent: VFC<{
theme: Theme, data: ChartData<'line', (number | ScatterDataPoint | null)[], unknown>;
userTrends: ExecutiveSummarySchema['userTrends'], }> = ({ data }) => {
) => ({
labels: userTrends.map((item) => item.date),
datasets: [
{
label: 'Total users',
data: userTrends.map((item) => item.total),
borderColor: theme.palette.primary.light,
backgroundColor: theme.palette.primary.light,
fill: true,
},
{
label: 'Active users',
data: userTrends.map((item) => item.active),
borderColor: theme.palette.success.border,
backgroundColor: theme.palette.success.border,
},
{
label: 'Inactive users',
data: userTrends.map((item) => item.inactive),
borderColor: theme.palette.warning.border,
backgroundColor: theme.palette.warning.border,
},
],
});
const UsersChartComponent: VFC<IUsersChartComponentProps> = ({
userTrends,
}) => {
const theme = useTheme(); const theme = useTheme();
const { locationSettings } = useLocationSettings(); const { locationSettings } = useLocationSettings();
const data = useMemo(
() => createData(theme, userTrends),
[theme, userTrends],
);
const [tooltip, setTooltip] = useState<null | TooltipState>(null); const [tooltip, setTooltip] = useState<null | TooltipState>(null);
const options = useMemo( const options = useMemo(
@ -182,21 +174,25 @@ const UsersChartComponent: VFC<IUsersChartComponentProps> = ({
return ( return (
<> <>
<Line options={options} data={data} /> <Line
options={options}
data={data}
plugins={[customHighlightPlugin]}
/>
<ChartTooltip tooltip={tooltip} /> <ChartTooltip tooltip={tooltip} />
</> </>
); );
}; };
ChartJS.register( Chart.register(
CategoryScale, CategoryScale,
LinearScale, LinearScale,
PointElement, PointElement,
LineElement, LineElement,
TimeScale, TimeScale,
Title,
Tooltip, Tooltip,
Legend, Legend,
); );
export default UsersChartComponent; // for lazy-loading
export default LineChartComponent;

View File

@ -1,3 +1,42 @@
import { lazy } from 'react'; import { useMemo, type VFC } from 'react';
import 'chartjs-adapter-date-fns';
import { useTheme } from '@mui/material';
import { ExecutiveSummarySchema } from 'openapi';
import { LineChart } from '../LineChart/LineChart';
export const UsersChart = lazy(() => import('./UsersChartComponent')); interface IUsersChartProps {
userTrends: ExecutiveSummarySchema['userTrends'];
}
export const UsersChart: VFC<IUsersChartProps> = ({ userTrends }) => {
const theme = useTheme();
const data = useMemo(
() => ({
labels: userTrends.map((item) => item.date),
datasets: [
{
label: 'Total users',
data: userTrends.map((item) => item.total),
borderColor: theme.palette.primary.light,
backgroundColor: theme.palette.primary.light,
fill: true,
},
{
label: 'Active users',
data: userTrends.map((item) => item.active),
borderColor: theme.palette.success.border,
backgroundColor: theme.palette.success.border,
},
{
label: 'Inactive users',
data: userTrends.map((item) => item.inactive),
borderColor: theme.palette.warning.border,
backgroundColor: theme.palette.warning.border,
},
],
}),
[theme, userTrends],
);
return <LineChart data={data} />;
};

View File

@ -1,23 +1,27 @@
import { FC, ReactNode } from 'react'; import { FC, ReactNode } from 'react';
import { Paper, Typography } from '@mui/material'; import { Paper, Typography, styled } from '@mui/material';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
const StyledPaper = styled(Paper)(({ theme }) => ({
padding: theme.spacing(3),
borderRadius: `${theme.shape.borderRadiusLarge}px`,
minWidth: 0, // bugfix, see: https://github.com/chartjs/Chart.js/issues/4156#issuecomment-295180128
position: 'relative',
}));
export const Widget: FC<{ export const Widget: FC<{
title: ReactNode; title: ReactNode;
order?: number; order?: number;
span?: number; span?: number;
tooltip?: ReactNode; tooltip?: ReactNode;
}> = ({ title, order, children, span = 1, tooltip }) => ( }> = ({ title, order, children, span = 1, tooltip }) => (
<Paper <StyledPaper
elevation={0} elevation={0}
sx={(theme) => ({ sx={{
padding: 3,
borderRadius: `${theme.shape.borderRadiusLarge}px`,
order, order,
gridColumn: `span ${span}`, gridColumn: `span ${span}`,
minWidth: 0, // bugfix, see: https://github.com/chartjs/Chart.js/issues/4156#issuecomment-295180128 }}
})}
> >
<Typography <Typography
variant='h3' variant='h3'
@ -35,5 +39,5 @@ export const Widget: FC<{
/> />
</Typography> </Typography>
{children} {children}
</Paper> </StyledPaper>
); );