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

Dashboard custom tooltips (#6327)

Initial refactoring for custom tooltips
This commit is contained in:
Tymoteusz Czech 2024-02-23 12:02:26 +01:00 committed by GitHub
parent 822851814a
commit 153c60d335
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 182 additions and 69 deletions

View File

@ -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 = {
@ -12,6 +13,7 @@ export type TooltipState = {
color: string;
value: string;
}[];
dataPoints: TooltipItem<any>[];
};
interface IChartTooltipProps {
@ -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>
);

View File

@ -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';
@ -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,
@ -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,
@ -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();
@ -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>

View File

@ -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}
/>
);
};