Last active
March 7, 2024 17:53
-
-
Save alettieri/e5bfa8982e0cd2c38ebc6b3bf94a0462 to your computer and use it in GitHub Desktop.
Recharts Tooltip on Points
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import { Typography, useTheme, Box, Theme, SxProps } from '@mui/material'; | |
import { LineChart, Line, Legend, Tooltip, TooltipProps } from 'recharts'; | |
import { LinePointItem } from 'recharts/types/cartesian/Line'; | |
// Leaving out some imports for brevity... | |
import { | |
PlanItem, | |
PlanItemGroup, | |
Plan, | |
PlanVersionAction, | |
IPlanItemProjectedRule, | |
PlanItemProjectedCalculationChoices, | |
} from '../../shared/types/plan'; | |
import { ChartLoading } from '../ChartLoading'; | |
import { DashedIcon, LineIcon } from './'; | |
import { PlanItemChartTooltip } from './PlanItemChartTooltip'; | |
type OnMouseMoveHandler = (typeof LineChart.defaultProps)['onMouseMove']; | |
const planItemChartLegendStyles: SxProps<Theme> = { | |
display: 'flex', | |
justifyContent: 'center', | |
gap: 4, | |
'& .PlanItemChartLegend-item': { | |
display: 'inline-flex', | |
gap: 2, | |
fontSize: 'body3.fontSize', | |
alignItems: 'center', | |
}, | |
'& .MuiSvgIcon-root': { | |
width: 8, | |
}, | |
}; | |
const getLinePointItems = ( | |
projectedPoints?: Array<LinePointItem>, | |
actualPoints?: Array<LinePointItem> | |
) => { | |
if (projectedPoints && actualPoints) { | |
// Merge the actual and projected points together | |
// We want to keep the indexes aligned so we can use the index to find the correct point | |
return actualPoints.map((point, index) => { | |
if (point.x && point.y) { | |
return point; | |
} | |
return projectedPoints[index]; | |
}); | |
} | |
return []; | |
}; | |
const PlanItemChartLegend = () => { | |
return ( | |
<Box | |
className="PlanItemChartLegend-root" | |
sx={planItemChartLegendStyles} | |
> | |
<Box className="PlanItemChartLegend-item"> | |
<LineIcon color="primary" fontSize="small" /> | |
<Typography variant="inherit">Actuals</Typography> | |
</Box> | |
<Box display="inline-flex" className="PlanItemChartLegend-item"> | |
<DashedIcon fontSize="small" sx={{ color: 'grey[500]' }} /> | |
<Typography variant="inherit">Forecast</Typography> | |
</Box> | |
</Box> | |
); | |
}; | |
const PlanItemChart = ({ | |
planItem, | |
projectedRule, | |
group, | |
planId, | |
}: { | |
planItem: PlanItem; | |
projectedRule: IPlanItemProjectedRule; | |
group: PlanItemGroup; | |
planId: Plan['id']; | |
}) => { | |
const [toolTipIndex, updateToolTipIndex] = React.useState(0); | |
const theme = useTheme(); | |
const actualRef = React.useRef<null | (Line & SVGPathElement)>(null); | |
const projectedRef = React.useRef<null | (Line & SVGPathElement)>(null); | |
const previewArgs = React.useMemo<PatchPlanVersionArg>(() => { | |
const updatedPlanItem: PlanItem = { | |
...planItem, | |
projected_rule: projectedRule, | |
}; | |
const groups = [ | |
{ | |
...group, | |
plan_items: [updatedPlanItem], | |
}, | |
]; | |
return { | |
planId, | |
groups, | |
action: PlanVersionAction.Publish, | |
}; | |
}, [planItem, projectedRule, planId, group]); | |
const { createPlanVersionPreview } = usePlanVersionPreviewQuery( | |
previewArgs, | |
Boolean(planItem) && | |
projectedRule?.calculation !== | |
PlanItemProjectedCalculationChoices.Unforecasted, | |
projectedRule | |
); | |
const handleMouseMove = React.useCallback<OnMouseMoveHandler>( | |
(coords) => { | |
if ( | |
coords.isTooltipActive && | |
coords.activeTooltipIndex !== undefined | |
) { | |
updateToolTipIndex(coords.activeTooltipIndex); | |
} | |
}, | |
[updateToolTipIndex] | |
); | |
const toolTipPosition = React.useMemo< | |
TooltipProps<number, string>['position'] | |
>(() => { | |
const linePointItems = getLinePointItems( | |
projectedRef.current?.props?.points, | |
actualRef.current?.props?.points | |
); | |
if (linePointItems[toolTipIndex]) { | |
// Grab the current tooltip position based off the point index list | |
const point = linePointItems[toolTipIndex]; | |
return { x: point.x, y: point.y }; | |
} | |
return undefined; | |
}, [toolTipIndex]); | |
return ( | |
<ChartLoading isLoading={createPlanVersionPreview.isFetching}> | |
<ResponsiveChartContainer height="110px"> | |
<LineChart | |
data={createPlanVersionPreview.data?.[0]} | |
margin={{ right: 8, left: 8 }} | |
onMouseMove={handleMouseMove} | |
> | |
<Tooltip | |
content={<PlanItemChartTooltip />} | |
position={toolTipPosition} | |
/> | |
<Line | |
type="monotone" | |
dataKey="p" | |
stroke={theme.palette.grey[500]} | |
activeDot={{ r: 4 }} | |
dot={{ r: 2 }} | |
ref={projectedRef} | |
/> | |
<Line | |
type="monotone" | |
dataKey="a" | |
stroke={theme.palette.primary.main} | |
activeDot={{ r: 4 }} | |
dot={{ r: 2 }} | |
ref={actualRef} | |
/> | |
<Legend | |
verticalAlign="bottom" | |
wrapperStyle={{ | |
bottom: 0, | |
fontSize: '10px', | |
}} | |
content={<PlanItemChartLegend />} | |
/> | |
</LineChart> | |
</ResponsiveChartContainer> | |
</ChartLoading> | |
); | |
}; | |
export { PlanItemChart }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import { Typography, Box, SxProps, Theme, alpha } from '@mui/material'; | |
import { format } from 'date-fns'; | |
import { TooltipProps } from 'recharts'; | |
import { IPlanVersionPreviewResponse } from '../../shared/types/plan'; | |
import { getDateFromString } from '../../shared/utils/date-utils'; | |
import { convertNumberToCurrency } from '../../utils/currency-utils'; | |
const formatDateValue = (date: string) => { | |
return format(getDateFromString(date), 'MMM-yy'); | |
}; | |
const styles: SxProps<Theme> = { | |
'--_height': '48px', | |
'--_width': '68px', | |
'--_offsetX': '-50%', | |
'--_offsetY': 'calc(-1 * (var(--_height) + 18px))', | |
'--_tooltip-bg': (theme) => alpha(theme.palette.grey[700], 0.9), | |
display: 'flex', | |
flexDirection: 'column', | |
gap: 1, | |
textAlign: 'center', | |
minWidth: 'var(--_width)', | |
height: 'var(--_height)', | |
p: '10px', | |
backgroundColor: 'var(--_tooltip-bg)', | |
color: 'common.white', | |
fontSize: 'tooltip.fontSize', | |
fontWeight: 'tooltip.fontWeight', | |
transform: `translate(var(--_offsetX), var(--_offsetY))`, | |
borderRadius: 1, | |
position: 'relative', | |
overflow: 'clip-content', | |
'& .PlanItemChartTooltip-arrow': { | |
position: 'absolute', | |
bottom: '-7px', | |
left: '50%', | |
width: 0, | |
height: 0, | |
borderLeft: '8px solid transparent', | |
borderRight: '8px solid transparent', | |
borderTop: '8px solid var(--_tooltip-bg)', | |
ml: '-8px', | |
}, | |
}; | |
export const PlanItemChartTooltip = (props: TooltipProps<number, string>) => { | |
const toolTipContent = React.useMemo<null | { | |
value: string; | |
date: string; | |
}>(() => { | |
if (props.payload.length > 0) { | |
const payload = props.payload[0]; | |
const dataKey = payload?.dataKey as 'p' | 'a'; | |
const dataPoint = | |
payload.payload as IPlanVersionPreviewResponse[0][0]; | |
const value = Reflect.get(dataPoint, dataKey); | |
const date = formatDateValue(dataPoint.d); | |
return { value: convertNumberToCurrency(value), date }; | |
} | |
return null; | |
}, [props.payload]); | |
if (toolTipContent !== null) { | |
return ( | |
<Box className="PlanItemChartTooltip-root" sx={styles}> | |
<Typography variant="inherit">{toolTipContent.date}</Typography> | |
<Typography variant="inherit"> | |
{toolTipContent.value} | |
</Typography> | |
<Box className="PlanItemChartTooltip-arrow" /> | |
</Box> | |
); | |
} | |
return null; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment