1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

Feat/dashboard chart tooltip (#6038)

Initial version of new chart tooltip
This commit is contained in:
Tymoteusz Czech 2024-01-26 14:33:11 +01:00 committed by GitHub
parent 4a025a4b4b
commit 61c6583e24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 195 additions and 28 deletions

View File

@ -9,7 +9,8 @@ import { FlagStats } from './FlagStats/FlagStats';
const StyledGrid = styled(Box)(({ theme }) => ({ const StyledGrid = styled(Box)(({ theme }) => ({
display: 'grid', display: 'grid',
gridTemplateColumns: `repeat(auto-fill, minmax(600px, 1fr))`, gridTemplateColumns: `300px 1fr`,
// TODO: responsive grid size
gridAutoRows: '1fr', gridAutoRows: '1fr',
gap: theme.spacing(2), gap: theme.spacing(2),
})); }));

View File

@ -0,0 +1,89 @@
import { Paper, styled, Typography } from '@mui/material';
import { VFC } from 'react';
export type TooltipState = {
caretX: number;
caretY: number;
title: string;
align: 'left' | 'right';
body: {
title: string;
color: string;
value: string;
}[];
};
interface IChartTooltipProps {
tooltip: TooltipState | null;
}
const StyledList = styled('ul')(({ theme }) => ({
listStyle: 'none',
margin: 0,
padding: 0,
}));
const StyledItem = styled('li')(({ theme }) => ({
marginBottom: theme.spacing(0.5),
display: 'flex',
alignItems: 'center',
}));
const StyledLabelIcon = styled('span')(({ theme }) => ({
display: 'inline-block',
width: 8,
height: 8,
borderRadius: '50%',
marginRight: theme.spacing(1),
}));
export const ChartTooltip: VFC<IChartTooltipProps> = ({ tooltip }) => (
<Paper
elevation={3}
sx={(theme) => ({
top: tooltip?.caretY,
left:
tooltip?.align === 'left'
? tooltip?.caretX + 40
: (tooltip?.caretX || 0) - 220,
position: 'absolute',
display: tooltip ? 'block' : 'none',
width: 220,
padding: theme.spacing(1.5, 2),
pointerEvents: 'none',
})}
>
{
<Typography
variant='body2'
sx={(theme) => ({
marginBottom: theme.spacing(1),
color: theme.palette.text.secondary,
})}
>
{tooltip?.title}
</Typography>
}
<StyledList>
{tooltip?.body.map((item) => (
<StyledItem key={item.title}>
<StyledLabelIcon
sx={{
backgroundColor: item.color,
}}
>
{' '}
</StyledLabelIcon>
<Typography
variant='body2'
sx={{
display: 'inline-block',
}}
>
{item.title}
</Typography>
</StyledItem>
))}
</StyledList>
</Paper>
);

View File

@ -1,4 +1,4 @@
import { useMemo, type VFC } from 'react'; import { useMemo, useState, type VFC } from 'react';
import { import {
Chart as ChartJS, Chart as ChartJS,
CategoryScale, CategoryScale,
@ -9,34 +9,88 @@ import {
Tooltip, Tooltip,
Legend, Legend,
TimeScale, TimeScale,
Chart,
} 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';
import { Paper, Theme, useTheme } from '@mui/material'; import { Paper, Theme, Typography, useTheme } from '@mui/material';
import { import {
useLocationSettings, useLocationSettings,
type ILocationSettings, type ILocationSettings,
} from 'hooks/useLocationSettings'; } from 'hooks/useLocationSettings';
import { formatDateYMD } from 'utils/formatDate';
import { ExecutiveSummarySchema } from 'openapi'; import { ExecutiveSummarySchema } from 'openapi';
import { ChartTooltip, TooltipState } from './ChartTooltip/ChartTooltip';
const createOptions = (theme: Theme, locationSettings: ILocationSettings) => const createOptions = (
theme: Theme,
locationSettings: ILocationSettings,
setTooltip: React.Dispatch<React.SetStateAction<TooltipState | null>>,
) =>
({ ({
responsive: true, responsive: true,
plugins: { plugins: {
legend: { legend: {
position: 'bottom', position: 'bottom',
labels: {
boxWidth: 12,
padding: 30,
// usePointStyle: true,
generateLabels: (chart: Chart) => {
const datasets = chart.data.datasets;
const {
labels: {
usePointStyle,
pointStyle,
textAlign,
color,
},
} = chart?.legend?.options || {
labels: {},
};
return (chart as any)
._getSortedDatasetMetas()
.map((meta: any) => {
const style = meta.controller.getStyle(
usePointStyle ? 0 : undefined,
);
return {
text: datasets[meta.index].label,
fillStyle: style.backgroundColor,
fontColor: color,
hidden: !meta.visible,
lineWidth: 0,
borderRadius: 6,
strokeStyle: style.borderColor,
pointStyle: pointStyle || style.pointStyle,
textAlign: textAlign || style.textAlign,
datasetIndex: meta.index,
};
});
},
},
}, },
tooltip: { tooltip: {
callbacks: { enabled: false,
title: (tooltipItems: any) => { external: (context: any) => {
const item = tooltipItems?.[0]; const tooltipModel = context.tooltip;
const date = if (tooltipModel.opacity === 0) {
item?.chart?.data?.labels?.[item.dataIndex]; setTooltip(null);
return date return;
? formatDateYMD(date, locationSettings.locale) }
: '';
}, const tooltip = context.tooltip;
setTooltip({
caretX: tooltip?.caretX,
caretY: tooltip?.caretY,
title: tooltip?.title?.join(' ') || '',
align: tooltip?.xAlign || 'left',
body:
tooltip?.body?.map((item: any, index: number) => ({
title: item?.lines?.join(' '),
color: tooltip?.labelColors?.[index]
?.borderColor,
})) || [],
});
}, },
}, },
}, },
@ -45,9 +99,16 @@ const createOptions = (theme: Theme, locationSettings: ILocationSettings) =>
intersect: false, intersect: false,
axis: 'x', axis: 'x',
}, },
elements: {
point: {
radius: 0,
},
},
// cubicInterpolationMode: 'monotone',
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
scales: { scales: {
y: { y: {
beginAtZero: true,
type: 'linear', type: 'linear',
grid: { grid: {
color: theme.palette.divider, color: theme.palette.divider,
@ -84,23 +145,21 @@ const createData = (
{ {
label: 'Total users', label: 'Total users',
data: userTrends.map((item) => item.total), data: userTrends.map((item) => item.total),
borderColor: theme.palette.primary.main, borderColor: theme.palette.primary.light,
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.light,
fill: true,
},
{
label: 'Inactive users',
data: userTrends.map((item) => item.inactive),
borderColor: theme.palette.error.main,
backgroundColor: theme.palette.error.main,
fill: true, fill: true,
}, },
{ {
label: 'Active users', label: 'Active users',
data: userTrends.map((item) => item.active), data: userTrends.map((item) => item.active),
borderColor: theme.palette.success.main, borderColor: theme.palette.success.border,
backgroundColor: theme.palette.success.main, backgroundColor: theme.palette.success.border,
fill: true, },
{
label: 'Inactive users',
data: userTrends.map((item) => item.inactive),
borderColor: theme.palette.warning.border,
backgroundColor: theme.palette.warning.border,
}, },
], ],
}); });
@ -115,11 +174,29 @@ const UsersChartComponent: VFC<IUsersChartComponentProps> = ({
[theme, userTrends], [theme, userTrends],
); );
const options = createOptions(theme, locationSettings); const [tooltip, setTooltip] = useState<null | TooltipState>(null);
const options = useMemo(
() => createOptions(theme, locationSettings, setTooltip),
[theme, locationSettings],
);
return ( return (
<Paper sx={(theme) => ({ padding: theme.spacing(4) })}> <Paper
elevation={0}
sx={(theme) => ({
padding: theme.spacing(3),
position: 'relative',
})}
>
<Typography
variant='h3'
sx={(theme) => ({ marginBottom: theme.spacing(3) })}
>
Users
</Typography>
<Line options={options} data={data} /> <Line options={options} data={data} />
<ChartTooltip tooltip={tooltip} />
</Paper> </Paper>
); );
}; };