Keep tooltip within bounds of chart (well, for non-FR charts) (#970)

This commit is contained in:
Marshall Polaris 2022-09-29 22:45:31 -07:00 committed by GitHub
parent b83e5db563
commit 523689b525
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -25,6 +25,8 @@ export type YScale<P> = P extends Point<infer _, infer Y> ? AxisScale<Y> : never
export const MARGIN = { top: 20, right: 10, bottom: 20, left: 40 } export const MARGIN = { top: 20, right: 10, bottom: 20, left: 40 }
export const MARGIN_X = MARGIN.right + MARGIN.left export const MARGIN_X = MARGIN.right + MARGIN.left
export const MARGIN_Y = MARGIN.top + MARGIN.bottom export const MARGIN_Y = MARGIN.top + MARGIN.bottom
const MARGIN_STYLE = `${MARGIN.top}px ${MARGIN.right}px ${MARGIN.bottom}px ${MARGIN.left}px`
const MARGIN_XFORM = `translate(${MARGIN.left}, ${MARGIN.top})`
export const XAxis = <X,>(props: { w: number; h: number; axis: Axis<X> }) => { export const XAxis = <X,>(props: { w: number; h: number; axis: Axis<X> }) => {
const { h, axis } = props const { h, axis } = props
@ -128,7 +130,7 @@ export const SVGChart = <X, Y, P extends Point<X, Y>>(props: {
Tooltip?: TooltipComponent<P> Tooltip?: TooltipComponent<P>
}) => { }) => {
const { children, w, h, xAxis, yAxis, onMouseOver, onSelect, Tooltip } = props const { children, w, h, xAxis, yAxis, onMouseOver, onSelect, Tooltip } = props
const [mouseState, setMouseState] = useState<TooltipPosition & { p: P }>() const [mouseState, setMouseState] = useState<{ pos: TooltipPosition; p: P }>()
const overlayRef = useRef<SVGGElement>(null) const overlayRef = useRef<SVGGElement>(null)
const innerW = w - MARGIN_X const innerW = w - MARGIN_X
const innerH = h - MARGIN_Y const innerH = h - MARGIN_Y
@ -170,7 +172,8 @@ export const SVGChart = <X, Y, P extends Point<X, Y>>(props: {
const [mouseX, mouseY] = pointer(ev) const [mouseX, mouseY] = pointer(ev)
const p = onMouseOver(mouseX, mouseY) const p = onMouseOver(mouseX, mouseY)
if (p != null) { if (p != null) {
setMouseState({ top: mouseY - 10, left: mouseX + 60, p }) const pos = getTooltipPosition(mouseX, mouseY, innerW, innerH)
setMouseState({ pos, p })
} else { } else {
setMouseState(undefined) setMouseState(undefined)
} }
@ -184,15 +187,15 @@ export const SVGChart = <X, Y, P extends Point<X, Y>>(props: {
return ( return (
<div className="relative"> <div className="relative">
{mouseState && Tooltip && ( {mouseState && Tooltip && (
<TooltipContainer top={mouseState.top} left={mouseState.left}> <TooltipContainer pos={mouseState.pos}>
<Tooltip xScale={xAxis.scale()} p={mouseState.p} /> <Tooltip xScale={xAxis.scale()} p={mouseState.p} />
</TooltipContainer> </TooltipContainer>
)} )}
<svg className="w-full" width={w} height={h} viewBox={`0 0 ${w} ${h}`}> <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
<clipPath id={clipPathId}> <clipPath id={clipPathId}>
<rect x={0} y={0} width={innerW} height={innerH} /> <rect x={0} y={0} width={innerW} height={innerH} />
</clipPath> </clipPath>
<g transform={`translate(${MARGIN.left}, ${MARGIN.top})`}> <g transform={MARGIN_XFORM}>
<XAxis axis={xAxis} w={innerW} h={innerH} /> <XAxis axis={xAxis} w={innerW} h={innerH} />
<YAxis axis={yAxis} w={innerW} h={innerH} /> <YAxis axis={yAxis} w={innerW} h={innerH} />
<g clipPath={`url(#${clipPathId})`}>{children}</g> <g clipPath={`url(#${clipPathId})`}>{children}</g>
@ -214,20 +217,48 @@ export const SVGChart = <X, Y, P extends Point<X, Y>>(props: {
) )
} }
export type TooltipPosition = {
top?: number
right?: number
bottom?: number
left?: number
}
export const getTooltipPosition = (
mouseX: number,
mouseY: number,
w: number,
h: number
) => {
const result: TooltipPosition = {}
if (mouseX <= (3 * w) / 4) {
result.left = mouseX + 10 // in the left three quarters
} else {
result.right = w - mouseX + 10 // in the right quarter
}
if (mouseY <= h / 4) {
result.top = mouseY + 10 // in the top quarter
} else {
result.bottom = h - mouseY + 10 // in the bottom three quarters
}
return result
}
export type TooltipProps<P> = { p: P; xScale: XScale<P> } export type TooltipProps<P> = { p: P; xScale: XScale<P> }
export type TooltipComponent<P> = React.ComponentType<TooltipProps<P>> export type TooltipComponent<P> = React.ComponentType<TooltipProps<P>>
export type TooltipPosition = { top: number; left: number } export const TooltipContainer = (props: {
export const TooltipContainer = ( pos: TooltipPosition
props: TooltipPosition & { className?: string; children: React.ReactNode } className?: string
) => { children: React.ReactNode
const { top, left, className, children } = props }) => {
const { pos, className, children } = props
return ( return (
<div <div
className={clsx( className={clsx(
className, className,
'pointer-events-none absolute z-10 whitespace-pre rounded border-2 border-black bg-white/90 p-2' 'pointer-events-none absolute z-10 whitespace-pre rounded border-2 border-black bg-white/90 p-2'
)} )}
style={{ top, left }} style={{ margin: MARGIN_STYLE, ...pos }}
> >
{children} {children}
</div> </div>