More refactoring to make chart tooltips more flexible (#975)
This commit is contained in:
parent
1fc2f15dae
commit
38b7c898f6
|
@ -25,19 +25,19 @@ const getBetPoints = (bets: Bet[]) => {
|
|||
return sortBy(bets, (b) => b.createdTime).map((b) => ({
|
||||
x: new Date(b.createdTime),
|
||||
y: b.probAfter,
|
||||
datum: b,
|
||||
obj: b,
|
||||
}))
|
||||
}
|
||||
|
||||
const BinaryChartTooltip = (props: TooltipProps<HistoryPoint<Bet>>) => {
|
||||
const { p, xScale } = props
|
||||
const { x, y, datum } = p
|
||||
const BinaryChartTooltip = (props: TooltipProps<Date, HistoryPoint<Bet>>) => {
|
||||
const { data, mouseX, xScale } = props
|
||||
const [start, end] = xScale.domain()
|
||||
const d = xScale.invert(mouseX)
|
||||
return (
|
||||
<Row className="items-center gap-2">
|
||||
{datum && <Avatar size="xs" avatarUrl={datum.userAvatarUrl} />}
|
||||
<span className="font-semibold">{formatDateInRange(x, start, end)}</span>
|
||||
<span className="text-greyscale-6">{formatPct(y)}</span>
|
||||
{data.obj && <Avatar size="xs" avatarUrl={data.obj.userAvatarUrl} />}
|
||||
<span className="font-semibold">{formatDateInRange(d, start, end)}</span>
|
||||
<span className="text-greyscale-6">{formatPct(data.y)}</span>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ const getBetPoints = (answers: Answer[], bets: Bet[]) => {
|
|||
points.push({
|
||||
x: new Date(bet.createdTime),
|
||||
y: answers.map((a) => sharesByOutcome[a.id] ** 2 / sharesSquared),
|
||||
datum: bet,
|
||||
obj: bet,
|
||||
})
|
||||
}
|
||||
return points
|
||||
|
@ -181,12 +181,12 @@ export const ChoiceContractChart = (props: {
|
|||
const yScale = scaleLinear([0, 1], [height - MARGIN_Y, 0])
|
||||
|
||||
const ChoiceTooltip = useMemo(
|
||||
() => (props: TooltipProps<MultiPoint<Bet>>) => {
|
||||
const { p, xScale } = props
|
||||
const { x, y, datum } = p
|
||||
() => (props: TooltipProps<Date, MultiPoint<Bet>>) => {
|
||||
const { data, mouseX, xScale } = props
|
||||
const [start, end] = xScale.domain()
|
||||
const d = xScale.invert(mouseX)
|
||||
const legendItems = sortBy(
|
||||
y.map((p, i) => ({
|
||||
data.y.map((p, i) => ({
|
||||
color: CATEGORY_COLORS[i],
|
||||
label: answers[i].text,
|
||||
value: formatPct(p),
|
||||
|
@ -197,9 +197,11 @@ export const ChoiceContractChart = (props: {
|
|||
return (
|
||||
<>
|
||||
<Row className="items-center gap-2">
|
||||
{datum && <Avatar size="xxs" avatarUrl={datum.userAvatarUrl} />}
|
||||
{data.obj && (
|
||||
<Avatar size="xxs" avatarUrl={data.obj.userAvatarUrl} />
|
||||
)}
|
||||
<span className="text-semibold text-base">
|
||||
{formatDateInRange(x, start, end)}
|
||||
{formatDateInRange(d, start, end)}
|
||||
</span>
|
||||
</Row>
|
||||
<Legend className="max-w-xs" items={legendItems} />
|
||||
|
|
|
@ -21,12 +21,15 @@ const getNumericChartData = (contract: NumericContract) => {
|
|||
}))
|
||||
}
|
||||
|
||||
const NumericChartTooltip = (props: TooltipProps<DistributionPoint>) => {
|
||||
const { x, y } = props.p
|
||||
const NumericChartTooltip = (
|
||||
props: TooltipProps<number, DistributionPoint>
|
||||
) => {
|
||||
const { data, mouseX, xScale } = props
|
||||
const x = xScale.invert(mouseX)
|
||||
return (
|
||||
<>
|
||||
<span className="text-semibold">{formatLargeNumber(x)}</span>
|
||||
<span className="text-greyscale-6">{formatPct(y, 2)}</span>
|
||||
<span className="text-greyscale-6">{formatPct(data.y, 2)}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -37,19 +37,21 @@ const getBetPoints = (bets: Bet[], scaleP: (p: number) => number) => {
|
|||
return sortBy(bets, (b) => b.createdTime).map((b) => ({
|
||||
x: new Date(b.createdTime),
|
||||
y: scaleP(b.probAfter),
|
||||
datum: b,
|
||||
obj: b,
|
||||
}))
|
||||
}
|
||||
|
||||
const PseudoNumericChartTooltip = (props: TooltipProps<HistoryPoint<Bet>>) => {
|
||||
const { p, xScale } = props
|
||||
const { x, y, datum } = p
|
||||
const PseudoNumericChartTooltip = (
|
||||
props: TooltipProps<Date, HistoryPoint<Bet>>
|
||||
) => {
|
||||
const { data, mouseX, xScale } = props
|
||||
const [start, end] = xScale.domain()
|
||||
const d = xScale.invert(mouseX)
|
||||
return (
|
||||
<Row className="items-center gap-2">
|
||||
{datum && <Avatar size="xs" avatarUrl={datum.userAvatarUrl} />}
|
||||
<span className="font-semibold">{formatDateInRange(x, start, end)}</span>
|
||||
<span className="text-greyscale-6">{formatLargeNumber(y)}</span>
|
||||
{data.obj && <Avatar size="xs" avatarUrl={data.obj.userAvatarUrl} />}
|
||||
<span className="font-semibold">{formatDateInRange(d, start, end)}</span>
|
||||
<span className="text-greyscale-6">{formatLargeNumber(data.y)}</span>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
import { range } from 'lodash'
|
||||
|
||||
import {
|
||||
ContinuousScale,
|
||||
SVGChart,
|
||||
AreaPath,
|
||||
AreaWithTopStroke,
|
||||
|
@ -31,6 +32,19 @@ const getTickValues = (min: number, max: number, n: number) => {
|
|||
return [min, ...range(1, n - 1).map((i) => min + step * i), max]
|
||||
}
|
||||
|
||||
const betAtPointSelector = <X, Y, P extends Point<X, Y>>(
|
||||
data: P[],
|
||||
xScale: ContinuousScale<X>
|
||||
) => {
|
||||
const bisect = bisector((p: P) => p.x)
|
||||
return (posX: number) => {
|
||||
const x = xScale.invert(posX)
|
||||
const item = data[bisect.left(data, x) - 1]
|
||||
const result = item ? { ...item, x: posX } : undefined
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export const DistributionChart = <P extends DistributionPoint>(props: {
|
||||
data: P[]
|
||||
w: number
|
||||
|
@ -39,7 +53,7 @@ export const DistributionChart = <P extends DistributionPoint>(props: {
|
|||
xScale: ScaleContinuousNumeric<number, number>
|
||||
yScale: ScaleContinuousNumeric<number, number>
|
||||
onMouseOver?: (p: P | undefined) => void
|
||||
Tooltip?: TooltipComponent<P>
|
||||
Tooltip?: TooltipComponent<number, P>
|
||||
}) => {
|
||||
const { color, data, yScale, w, h, Tooltip } = props
|
||||
|
||||
|
@ -50,7 +64,6 @@ export const DistributionChart = <P extends DistributionPoint>(props: {
|
|||
const px = useCallback((p: P) => xScale(p.x), [xScale])
|
||||
const py0 = yScale(yScale.domain()[0])
|
||||
const py1 = useCallback((p: P) => yScale(p.y), [yScale])
|
||||
const xBisector = bisector((p: P) => p.x)
|
||||
|
||||
const { xAxis, yAxis } = useMemo(() => {
|
||||
const xAxis = axisBottom<number>(xScale).ticks(w / 100)
|
||||
|
@ -58,6 +71,8 @@ export const DistributionChart = <P extends DistributionPoint>(props: {
|
|||
return { xAxis, yAxis }
|
||||
}, [w, xScale, yScale])
|
||||
|
||||
const onMouseOver = useEvent(betAtPointSelector(data, xScale))
|
||||
|
||||
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
|
||||
if (ev.selection) {
|
||||
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
||||
|
@ -69,14 +84,6 @@ export const DistributionChart = <P extends DistributionPoint>(props: {
|
|||
}
|
||||
})
|
||||
|
||||
const onMouseOver = useEvent((mouseX: number) => {
|
||||
const queryX = xScale.invert(mouseX)
|
||||
const item = data[xBisector.left(data, queryX) - 1]
|
||||
const result = item ? { ...item, x: queryX } : undefined
|
||||
props.onMouseOver?.(result)
|
||||
return result
|
||||
})
|
||||
|
||||
return (
|
||||
<SVGChart
|
||||
w={w}
|
||||
|
@ -107,7 +114,7 @@ export const MultiValueHistoryChart = <P extends MultiPoint>(props: {
|
|||
xScale: ScaleTime<number, number>
|
||||
yScale: ScaleContinuousNumeric<number, number>
|
||||
onMouseOver?: (p: P | undefined) => void
|
||||
Tooltip?: TooltipComponent<P>
|
||||
Tooltip?: TooltipComponent<Date, P>
|
||||
pct?: boolean
|
||||
}) => {
|
||||
const { colors, data, yScale, w, h, Tooltip, pct } = props
|
||||
|
@ -119,7 +126,6 @@ export const MultiValueHistoryChart = <P extends MultiPoint>(props: {
|
|||
const px = useCallback((p: SP) => xScale(p.data.x), [xScale])
|
||||
const py0 = useCallback((p: SP) => yScale(p[0]), [yScale])
|
||||
const py1 = useCallback((p: SP) => yScale(p[1]), [yScale])
|
||||
const xBisector = bisector((p: P) => p.x)
|
||||
|
||||
const { xAxis, yAxis } = useMemo(() => {
|
||||
const [min, max] = yScale.domain()
|
||||
|
@ -141,6 +147,8 @@ export const MultiValueHistoryChart = <P extends MultiPoint>(props: {
|
|||
return d3Stack(data)
|
||||
}, [data])
|
||||
|
||||
const onMouseOver = useEvent(betAtPointSelector(data, xScale))
|
||||
|
||||
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
|
||||
if (ev.selection) {
|
||||
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
||||
|
@ -152,14 +160,6 @@ export const MultiValueHistoryChart = <P extends MultiPoint>(props: {
|
|||
}
|
||||
})
|
||||
|
||||
const onMouseOver = useEvent((mouseX: number) => {
|
||||
const queryX = xScale.invert(mouseX)
|
||||
const item = data[xBisector.left(data, queryX) - 1]
|
||||
const result = item ? { ...item, x: queryX } : undefined
|
||||
props.onMouseOver?.(result)
|
||||
return result
|
||||
})
|
||||
|
||||
return (
|
||||
<SVGChart
|
||||
w={w}
|
||||
|
@ -193,7 +193,7 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
|||
xScale: ScaleTime<number, number>
|
||||
yScale: ScaleContinuousNumeric<number, number>
|
||||
onMouseOver?: (p: P | undefined) => void
|
||||
Tooltip?: TooltipComponent<P>
|
||||
Tooltip?: TooltipComponent<Date, P>
|
||||
pct?: boolean
|
||||
}) => {
|
||||
const { color, data, yScale, w, h, Tooltip, pct } = props
|
||||
|
@ -204,7 +204,6 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
|||
const px = useCallback((p: P) => xScale(p.x), [xScale])
|
||||
const py0 = yScale(yScale.domain()[0])
|
||||
const py1 = useCallback((p: P) => yScale(p.y), [yScale])
|
||||
const xBisector = bisector((p: P) => p.x)
|
||||
|
||||
const { xAxis, yAxis } = useMemo(() => {
|
||||
const [min, max] = yScale.domain()
|
||||
|
@ -218,6 +217,8 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
|||
return { xAxis, yAxis }
|
||||
}, [w, h, pct, xScale, yScale])
|
||||
|
||||
const onMouseOver = useEvent(betAtPointSelector(data, xScale))
|
||||
|
||||
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
|
||||
if (ev.selection) {
|
||||
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
||||
|
@ -229,14 +230,6 @@ export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
|||
}
|
||||
})
|
||||
|
||||
const onMouseOver = useEvent((mouseX: number) => {
|
||||
const queryX = xScale.invert(mouseX)
|
||||
const item = data[xBisector.left(data, queryX) - 1]
|
||||
const result = item ? { ...item, x: queryX } : undefined
|
||||
props.onMouseOver?.(result)
|
||||
return result
|
||||
})
|
||||
|
||||
return (
|
||||
<SVGChart
|
||||
w={w}
|
||||
|
|
|
@ -17,7 +17,12 @@ import clsx from 'clsx'
|
|||
|
||||
import { Contract } from 'common/contract'
|
||||
|
||||
export type Point<X, Y, T = unknown> = { x: X; y: Y; datum?: T }
|
||||
export type Point<X, Y, T = unknown> = { x: X; y: Y; obj?: T }
|
||||
|
||||
export interface ContinuousScale<T> extends AxisScale<T> {
|
||||
invert(n: number): T
|
||||
}
|
||||
|
||||
export type XScale<P> = P extends Point<infer X, infer _> ? AxisScale<X> : never
|
||||
export type YScale<P> = P extends Point<infer _, infer Y> ? AxisScale<Y> : never
|
||||
|
||||
|
@ -118,18 +123,18 @@ export const AreaWithTopStroke = <P,>(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export const SVGChart = <X, Y, P extends Point<X, Y>>(props: {
|
||||
export const SVGChart = <X, TT>(props: {
|
||||
children: ReactNode
|
||||
w: number
|
||||
h: number
|
||||
xAxis: Axis<X>
|
||||
yAxis: Axis<number>
|
||||
onSelect?: (ev: D3BrushEvent<any>) => void
|
||||
onMouseOver?: (mouseX: number, mouseY: number) => P | undefined
|
||||
Tooltip?: TooltipComponent<P>
|
||||
onMouseOver?: (mouseX: number, mouseY: number) => TT | undefined
|
||||
Tooltip?: TooltipComponent<X, TT>
|
||||
}) => {
|
||||
const { children, w, h, xAxis, yAxis, onMouseOver, onSelect, Tooltip } = props
|
||||
const [mouseState, setMouseState] = useState<{ pos: TooltipPosition; p: P }>()
|
||||
const [mouse, setMouse] = useState<{ x: number; y: number; data: TT }>()
|
||||
const overlayRef = useRef<SVGGElement>(null)
|
||||
const innerW = w - MARGIN_X
|
||||
const innerH = h - MARGIN_Y
|
||||
|
@ -148,7 +153,7 @@ export const SVGChart = <X, Y, P extends Point<X, Y>>(props: {
|
|||
if (!justSelected.current) {
|
||||
justSelected.current = true
|
||||
onSelect(ev)
|
||||
setMouseState(undefined)
|
||||
setMouse(undefined)
|
||||
if (overlayRef.current) {
|
||||
select(overlayRef.current).call(brush.clear)
|
||||
}
|
||||
|
@ -168,26 +173,32 @@ export const SVGChart = <X, Y, P extends Point<X, Y>>(props: {
|
|||
|
||||
const onPointerMove = (ev: React.PointerEvent) => {
|
||||
if (ev.pointerType === 'mouse' && onMouseOver) {
|
||||
const [mouseX, mouseY] = pointer(ev)
|
||||
const p = onMouseOver(mouseX, mouseY)
|
||||
if (p != null) {
|
||||
const pos = getTooltipPosition(mouseX, mouseY, innerW, innerH)
|
||||
setMouseState({ pos, p })
|
||||
const [x, y] = pointer(ev)
|
||||
const data = onMouseOver(x, y)
|
||||
if (data !== undefined) {
|
||||
setMouse({ x, y, data })
|
||||
} else {
|
||||
setMouseState(undefined)
|
||||
setMouse(undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onPointerLeave = () => {
|
||||
setMouseState(undefined)
|
||||
setMouse(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{mouseState && Tooltip && (
|
||||
<TooltipContainer pos={mouseState.pos}>
|
||||
<Tooltip xScale={xAxis.scale()} p={mouseState.p} />
|
||||
{mouse && Tooltip && (
|
||||
<TooltipContainer
|
||||
pos={getTooltipPosition(mouse.x, mouse.y, innerW, innerH)}
|
||||
>
|
||||
<Tooltip
|
||||
xScale={xAxis.scale()}
|
||||
mouseX={mouse.x}
|
||||
mouseY={mouse.y}
|
||||
data={mouse.data}
|
||||
/>
|
||||
</TooltipContainer>
|
||||
)}
|
||||
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
|
||||
|
@ -243,8 +254,13 @@ export const getTooltipPosition = (
|
|||
return result
|
||||
}
|
||||
|
||||
export type TooltipProps<P> = { p: P; xScale: XScale<P> }
|
||||
export type TooltipComponent<P> = React.ComponentType<TooltipProps<P>>
|
||||
export type TooltipProps<X, T> = {
|
||||
mouseX: number
|
||||
mouseY: number
|
||||
xScale: ContinuousScale<X>
|
||||
data: T
|
||||
}
|
||||
export type TooltipComponent<X, T> = React.ComponentType<TooltipProps<X, T>>
|
||||
export const TooltipContainer = (props: {
|
||||
pos: TooltipPosition
|
||||
className?: string
|
||||
|
|
Loading…
Reference in New Issue
Block a user