Skip to content

Commit

Permalink
Dashboard custom tooltips (#6327)
Browse files Browse the repository at this point in the history
Initial refactoring for custom tooltips
  • Loading branch information
Tymek authored Feb 23, 2024
1 parent 8228518 commit 153c60d
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Paper, styled, Typography } from '@mui/material';
import { VFC } from 'react';
import { Box, Paper, styled, Typography } from '@mui/material';
import { TooltipItem } from 'chart.js';
import { FC, VFC } from 'react';
import { objectId } from 'utils/objectId';

export type TooltipState = {
Expand All @@ -12,6 +13,7 @@ export type TooltipState = {
color: string;
value: string;
}[];
dataPoints: TooltipItem<any>[];
};

interface IChartTooltipProps {
Expand All @@ -38,54 +40,69 @@ const StyledLabelIcon = styled('span')(({ theme }) => ({
marginRight: theme.spacing(1),
}));

export const ChartTooltip: VFC<IChartTooltipProps> = ({ tooltip }) => (
<Paper
elevation={3}
export const ChartTooltipContainer: FC<IChartTooltipProps> = ({
tooltip,
children,
}) => (
<Box
sx={(theme) => ({
top: tooltip?.caretY,
left:
tooltip?.align === 'left'
? tooltip?.caretX + 40
: (tooltip?.caretX || 0) - 220,
left: tooltip?.align === 'left' ? tooltip?.caretX + 20 : 0,
right:
tooltip?.align === 'right' ? tooltip?.caretX + 20 : undefined,
position: 'absolute',
display: tooltip ? 'block' : 'none',
width: 220,
padding: theme.spacing(1.5, 2),
display: tooltip ? 'flex' : 'none',
pointerEvents: 'none',
zIndex: theme.zIndex.tooltip,
flexDirection: 'column',
alignItems: tooltip?.align === 'left' ? 'flex-start' : 'flex-end',
})}
>
{
<Typography
variant='body2'
sx={(theme) => ({
marginBottom: theme.spacing(1),
color: theme.palette.text.secondary,
})}
>
{tooltip?.title}
</Typography>
}
<StyledList>
{tooltip?.body.map((item) => (
<StyledItem key={objectId(item)}>
<StyledLabelIcon
sx={{
backgroundColor: item.color,
}}
>
{' '}
</StyledLabelIcon>
<Typography
variant='body2'
sx={{
display: 'inline-block',
}}
>
{item.title}
</Typography>
</StyledItem>
))}
</StyledList>
</Paper>
{children}
</Box>
);

export const ChartTooltip: VFC<IChartTooltipProps> = ({ tooltip }) => (
<ChartTooltipContainer tooltip={tooltip}>
<Paper
elevation={3}
sx={(theme) => ({
width: 220,
padding: theme.spacing(1.5, 2),
})}
>
{
<Typography
variant='body2'
sx={(theme) => ({
marginBottom: theme.spacing(1),
color: theme.palette.text.secondary,
})}
>
{tooltip?.title}
</Typography>
}
<StyledList>
{tooltip?.body.map((item) => (
<StyledItem key={objectId(item)}>
<StyledLabelIcon
sx={{
backgroundColor: item.color,
}}
>
{' '}
</StyledLabelIcon>
<Typography
variant='body2'
sx={{
display: 'inline-block',
}}
>
{item.title}
</Typography>
</StyledItem>
))}
</StyledList>
</Paper>
</ChartTooltipContainer>
);
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Filler,
type ChartData,
type ScatterDataPoint,
TooltipModel,
} from 'chart.js';
import { Line } from 'react-chartjs-2';
import 'chartjs-adapter-date-fns';
Expand All @@ -19,10 +20,44 @@ import {
useLocationSettings,
type ILocationSettings,
} from 'hooks/useLocationSettings';
import { ChartTooltip, TooltipState } from './ChartTooltip/ChartTooltip';
import {
ChartTooltip,
ChartTooltipContainer,
TooltipState,
} from './ChartTooltip/ChartTooltip';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { styled } from '@mui/material';

const createTooltip =
(setTooltip: React.Dispatch<React.SetStateAction<TooltipState | null>>) =>
(context: {
chart: Chart;
tooltip: TooltipModel<any>;
}) => {
const tooltip = context.tooltip;
if (tooltip.opacity === 0) {
setTooltip(null);
return;
}

setTooltip({
caretX:
tooltip?.xAlign === 'right'
? context.chart.width - tooltip?.caretX
: tooltip?.caretX,
caretY: tooltip?.caretY,
title: tooltip?.title?.join(' ') || '',
align: tooltip?.xAlign === 'right' ? 'right' : 'left',
body:
tooltip?.body?.map((item: any, index: number) => ({
title: item?.lines?.join(' '),
color: tooltip?.labelColors?.[index]?.borderColor as string,
value: '',
})) || [],
dataPoints: tooltip?.dataPoints || [],
});
};

const createOptions = (
theme: Theme,
locationSettings: ILocationSettings,
Expand Down Expand Up @@ -82,26 +117,7 @@ const createOptions = (
},
tooltip: {
enabled: false,
external: (context: any) => {
const tooltip = context.tooltip;
if (tooltip.opacity === 0) {
setTooltip(null);
return;
}

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,
})) || [],
});
},
external: createTooltip(setTooltip),
},
},
locale: locationSettings.locale,
Expand Down Expand Up @@ -211,7 +227,10 @@ const LineChartComponent: VFC<{
aspectRatio?: number;
cover?: ReactNode;
isLocalTooltip?: boolean;
}> = ({ data, aspectRatio, cover, isLocalTooltip }) => {
TooltipComponent?: ({
tooltip,
}: { tooltip: TooltipState | null }) => ReturnType<VFC>;
}> = ({ data, aspectRatio, cover, isLocalTooltip, TooltipComponent }) => {
const theme = useTheme();
const { locationSettings } = useLocationSettings();

Expand Down Expand Up @@ -239,7 +258,15 @@ const LineChartComponent: VFC<{
/>
<ConditionallyRender
condition={!cover}
show={<ChartTooltip tooltip={tooltip} />}
show={
TooltipComponent ? (
<ChartTooltipContainer tooltip={tooltip}>
<TooltipComponent tooltip={tooltip} />
</ChartTooltipContainer>
) : (
<ChartTooltip tooltip={tooltip} />
)
}
elseShow={
<StyledCover>
<StyledCoverContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,84 @@ import 'chartjs-adapter-date-fns';
import { ExecutiveSummarySchema } from 'openapi';
import { LineChart } from '../LineChart/LineChart';
import { useProjectChartData } from '../useProjectChartData';
import { TooltipState } from '../LineChart/ChartTooltip/ChartTooltip';
import { Box, Paper, styled } from '@mui/material';
import { Badge } from 'component/common/Badge/Badge';

interface IFlagsProjectChartProps {
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
}

const StyledTooltipItemContainer = styled(Paper)(({ theme }) => ({
padding: theme.spacing(2),
}));

const StyledItemHeader = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
gap: theme.spacing(2),
alignItems: 'center',
}));

const getHealthBadgeColor = (health?: number | null) => {
if (health === undefined || health === null) {
return 'info';
}

if (health >= 75) {
return 'success';
}

if (health >= 50) {
return 'warning';
}

return 'error';
};

const TooltipComponent: VFC<{ tooltip: TooltipState | null }> = ({
tooltip,
}) => {
const data = tooltip?.dataPoints.map((point) => {
return {
title: point.dataset.label,
color: point.dataset.borderColor,
value: point.raw as number,
};
});

return (
<Box
sx={(theme) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(3),
})}
>
{data?.map((point, index) => (
<StyledTooltipItemContainer elevation={3} key={point.title}>
<StyledItemHeader>
<div>{point.title}</div>{' '}
<Badge color={getHealthBadgeColor(point.value)}>
{point.value}%
</Badge>
</StyledItemHeader>
</StyledTooltipItemContainer>
)) || null}
</Box>
);
};

export const ProjectHealthChart: VFC<IFlagsProjectChartProps> = ({
projectFlagTrends,
}) => {
const data = useProjectChartData(projectFlagTrends, 'health');

return <LineChart data={data} isLocalTooltip />;
return (
<LineChart
data={data}
isLocalTooltip
TooltipComponent={TooltipComponent}
/>
);
};

0 comments on commit 153c60d

Please sign in to comment.