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:
parent
822851814a
commit
153c60d335
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user