mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02: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