Keep tooltip within bounds of chart (well, for non-FR charts) (#970)
This commit is contained in:
parent
b83e5db563
commit
523689b525
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user