diff --git a/web/components/charts/contract/binary.tsx b/web/components/charts/contract/binary.tsx index a3f04a29..564ef68c 100644 --- a/web/components/charts/contract/binary.tsx +++ b/web/components/charts/contract/binary.tsx @@ -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>) => { - const { p, xScale } = props - const { x, y, datum } = p +const BinaryChartTooltip = (props: TooltipProps>) => { + const { data, mouseX, xScale } = props const [start, end] = xScale.domain() + const d = xScale.invert(mouseX) return ( - {datum && } - {formatDateInRange(x, start, end)} - {formatPct(y)} + {data.obj && } + {formatDateInRange(d, start, end)} + {formatPct(data.y)} ) } diff --git a/web/components/charts/contract/choice.tsx b/web/components/charts/contract/choice.tsx index 7c9ec07a..665a01cd 100644 --- a/web/components/charts/contract/choice.tsx +++ b/web/components/charts/contract/choice.tsx @@ -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>) => { - const { p, xScale } = props - const { x, y, datum } = p + () => (props: TooltipProps>) => { + 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 ( <> - {datum && } + {data.obj && ( + + )} - {formatDateInRange(x, start, end)} + {formatDateInRange(d, start, end)} diff --git a/web/components/charts/contract/numeric.tsx b/web/components/charts/contract/numeric.tsx index ac300361..f8051308 100644 --- a/web/components/charts/contract/numeric.tsx +++ b/web/components/charts/contract/numeric.tsx @@ -21,12 +21,15 @@ const getNumericChartData = (contract: NumericContract) => { })) } -const NumericChartTooltip = (props: TooltipProps) => { - const { x, y } = props.p +const NumericChartTooltip = ( + props: TooltipProps +) => { + const { data, mouseX, xScale } = props + const x = xScale.invert(mouseX) return ( <> {formatLargeNumber(x)} - {formatPct(y, 2)} + {formatPct(data.y, 2)} ) } diff --git a/web/components/charts/contract/pseudo-numeric.tsx b/web/components/charts/contract/pseudo-numeric.tsx index adf2e493..04b1fafb 100644 --- a/web/components/charts/contract/pseudo-numeric.tsx +++ b/web/components/charts/contract/pseudo-numeric.tsx @@ -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>) => { - const { p, xScale } = props - const { x, y, datum } = p +const PseudoNumericChartTooltip = ( + props: TooltipProps> +) => { + const { data, mouseX, xScale } = props const [start, end] = xScale.domain() + const d = xScale.invert(mouseX) return ( - {datum && } - {formatDateInRange(x, start, end)} - {formatLargeNumber(y)} + {data.obj && } + {formatDateInRange(d, start, end)} + {formatLargeNumber(data.y)} ) } diff --git a/web/components/charts/generic-charts.tsx b/web/components/charts/generic-charts.tsx index 5ae30ad4..152b264c 100644 --- a/web/components/charts/generic-charts.tsx +++ b/web/components/charts/generic-charts.tsx @@ -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 = >( + data: P[], + xScale: ContinuousScale +) => { + 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 =

(props: { data: P[] w: number @@ -39,7 +53,7 @@ export const DistributionChart =

(props: { xScale: ScaleContinuousNumeric yScale: ScaleContinuousNumeric onMouseOver?: (p: P | undefined) => void - Tooltip?: TooltipComponent

+ Tooltip?: TooltipComponent }) => { const { color, data, yScale, w, h, Tooltip } = props @@ -50,7 +64,6 @@ export const DistributionChart =

(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(xScale).ticks(w / 100) @@ -58,6 +71,8 @@ export const DistributionChart =

(props: { return { xAxis, yAxis } }, [w, xScale, yScale]) + const onMouseOver = useEvent(betAtPointSelector(data, xScale)) + const onSelect = useEvent((ev: D3BrushEvent

) => { if (ev.selection) { const [mouseX0, mouseX1] = ev.selection as [number, number] @@ -69,14 +84,6 @@ export const DistributionChart =

(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 ( (props: { xScale: ScaleTime yScale: ScaleContinuousNumeric onMouseOver?: (p: P | undefined) => void - Tooltip?: TooltipComponent

+ Tooltip?: TooltipComponent pct?: boolean }) => { const { colors, data, yScale, w, h, Tooltip, pct } = props @@ -119,7 +126,6 @@ export const MultiValueHistoryChart =

(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 =

(props: { return d3Stack(data) }, [data]) + const onMouseOver = useEvent(betAtPointSelector(data, xScale)) + const onSelect = useEvent((ev: D3BrushEvent

) => { if (ev.selection) { const [mouseX0, mouseX1] = ev.selection as [number, number] @@ -152,14 +160,6 @@ export const MultiValueHistoryChart =

(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 ( (props: { xScale: ScaleTime yScale: ScaleContinuousNumeric onMouseOver?: (p: P | undefined) => void - Tooltip?: TooltipComponent

+ Tooltip?: TooltipComponent pct?: boolean }) => { const { color, data, yScale, w, h, Tooltip, pct } = props @@ -204,7 +204,6 @@ export const SingleValueHistoryChart =

(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 =

(props: { return { xAxis, yAxis } }, [w, h, pct, xScale, yScale]) + const onMouseOver = useEvent(betAtPointSelector(data, xScale)) + const onSelect = useEvent((ev: D3BrushEvent

) => { if (ev.selection) { const [mouseX0, mouseX1] = ev.selection as [number, number] @@ -229,14 +230,6 @@ export const SingleValueHistoryChart =

(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 ( = { x: X; y: Y; datum?: T } +export type Point = { x: X; y: Y; obj?: T } + +export interface ContinuousScale extends AxisScale { + invert(n: number): T +} + export type XScale

= P extends Point ? AxisScale : never export type YScale

= P extends Point ? AxisScale : never @@ -118,18 +123,18 @@ export const AreaWithTopStroke = (props: { ) } -export const SVGChart = >(props: { +export const SVGChart = (props: { children: ReactNode w: number h: number xAxis: Axis yAxis: Axis onSelect?: (ev: D3BrushEvent) => void - onMouseOver?: (mouseX: number, mouseY: number) => P | undefined - Tooltip?: TooltipComponent

+ onMouseOver?: (mouseX: number, mouseY: number) => TT | undefined + Tooltip?: TooltipComponent }) => { 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(null) const innerW = w - MARGIN_X const innerH = h - MARGIN_Y @@ -148,7 +153,7 @@ export const SVGChart = >(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 = >(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 (

- {mouseState && Tooltip && ( - - + {mouse && Tooltip && ( + + )} @@ -243,8 +254,13 @@ export const getTooltipPosition = ( return result } -export type TooltipProps

= { p: P; xScale: XScale

} -export type TooltipComponent

= React.ComponentType> +export type TooltipProps = { + mouseX: number + mouseY: number + xScale: ContinuousScale + data: T +} +export type TooltipComponent = React.ComponentType> export const TooltipContainer = (props: { pos: TooltipPosition className?: string