From 8862425120f25e422cf8ded9f7251f787efe5918 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Wed, 28 Sep 2022 21:43:04 -0700 Subject: [PATCH] Clean up chart tooltip handling (#959) --- web/components/charts/contract/binary.tsx | 3 +- web/components/charts/contract/choice.tsx | 3 +- web/components/charts/contract/numeric.tsx | 3 +- .../charts/contract/pseudo-numeric.tsx | 3 +- web/components/charts/generic-charts.tsx | 256 +++++++----------- web/components/charts/helpers.tsx | 91 +++++-- 6 files changed, 173 insertions(+), 186 deletions(-) diff --git a/web/components/charts/contract/binary.tsx b/web/components/charts/contract/binary.tsx index 74ac472b..1f163266 100644 --- a/web/components/charts/contract/binary.tsx +++ b/web/components/charts/contract/binary.tsx @@ -32,7 +32,8 @@ const getBetPoints = (bets: Bet[]) => { } const BinaryChartTooltip = (props: SingleValueHistoryTooltipProps) => { - const { x, y, xScale, datum } = props + const { p, xScale } = props + const { x, y, datum } = p const [start, end] = xScale.domain() return ( diff --git a/web/components/charts/contract/choice.tsx b/web/components/charts/contract/choice.tsx index 08d20442..11b1f8c3 100644 --- a/web/components/charts/contract/choice.tsx +++ b/web/components/charts/contract/choice.tsx @@ -162,7 +162,8 @@ export const ChoiceContractChart = (props: { const ChoiceTooltip = useMemo( () => (props: MultiValueHistoryTooltipProps) => { - const { x, y, xScale, datum } = props + const { p, xScale } = props + const { x, y, datum } = p const [start, end] = xScale.domain() const legendItems = sortBy( y.map((p, i) => ({ diff --git a/web/components/charts/contract/numeric.tsx b/web/components/charts/contract/numeric.tsx index b45a6cca..de1d1a0c 100644 --- a/web/components/charts/contract/numeric.tsx +++ b/web/components/charts/contract/numeric.tsx @@ -25,7 +25,8 @@ const getNumericChartData = (contract: NumericContract) => { } const NumericChartTooltip = (props: SingleValueDistributionTooltipProps) => { - const { x, y } = props + const { p } = props + const { x, y } = p return ( {formatPct(y, 2)} {formatLargeNumber(x)} diff --git a/web/components/charts/contract/pseudo-numeric.tsx b/web/components/charts/contract/pseudo-numeric.tsx index 56359bc7..fcfe9d38 100644 --- a/web/components/charts/contract/pseudo-numeric.tsx +++ b/web/components/charts/contract/pseudo-numeric.tsx @@ -46,7 +46,8 @@ const getBetPoints = (bets: Bet[], scaleP: (p: number) => number) => { const PseudoNumericChartTooltip = ( props: SingleValueHistoryTooltipProps ) => { - const { x, y, xScale, datum } = props + const { p, xScale } = props + const { x, y, datum } = p const [start, end] = xScale.domain() return ( diff --git a/web/components/charts/generic-charts.tsx b/web/components/charts/generic-charts.tsx index d9872b0e..8bbfc659 100644 --- a/web/components/charts/generic-charts.tsx +++ b/web/components/charts/generic-charts.tsx @@ -3,7 +3,6 @@ import { bisector } from 'd3-array' import { axisBottom, axisLeft } from 'd3-axis' import { D3BrushEvent } from 'd3-brush' import { ScaleTime, ScaleContinuousNumeric } from 'd3-scale' -import { pointer } from 'd3-selection' import { curveLinear, curveStepAfter, @@ -18,17 +17,25 @@ import { AreaPath, AreaWithTopStroke, TooltipContent, - TooltipContainer, - TooltipPosition, formatPct, } from './helpers' import { useEvent } from 'web/hooks/use-event' -export type MultiPoint = { x: Date; y: number[]; datum?: T } -export type HistoryPoint = { x: Date; y: number; datum?: T } -export type DistributionPoint = { x: number; y: number; datum?: T } - -type PositionValue

= TooltipPosition & { p: P } +export type MultiPoint = { + x: Date + y: number[] + datum?: T +} +export type HistoryPoint = { + x: Date + y: number + datum?: T +} +export type DistributionPoint = { + x: number + y: number + datum?: T +} const getTickValues = (min: number, max: number, n: number) => { const step = (max - min) / (n - 1) @@ -48,8 +55,6 @@ export const SingleValueDistributionChart = (props: { const [viewXScale, setViewXScale] = useState>() - const [mouseState, setMouseState] = - useState>>() const xScale = viewXScale ?? props.xScale const px = useCallback((p: DistributionPoint) => xScale(p.x), [xScale]) @@ -69,67 +74,48 @@ export const SingleValueDistributionChart = (props: { setViewXScale(() => xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)]) ) - setMouseState(undefined) } else { setViewXScale(undefined) - setMouseState(undefined) } }) - const onMouseOver = useEvent((ev: React.PointerEvent) => { - if (ev.pointerType === 'mouse') { - const [mouseX, mouseY] = pointer(ev) - const queryX = xScale.invert(mouseX) - const item = data[xBisector.left(data, queryX) - 1] - if (item == null) { - // this can happen if you are on the very left or right edge of the chart, - // so your queryX is out of bounds - return - } - const p = { x: queryX, y: item.y, datum: item.datum } - setMouseState({ top: mouseY - 10, left: mouseX + 60, p }) + const onMouseOver = useEvent((mouseX: number) => { + const queryX = xScale.invert(mouseX) + const item = data[xBisector.left(data, queryX) - 1] + if (item == null) { + // this can happen if you are on the very left or right edge of the chart, + // so your queryX is out of bounds + return } - }) - - const onMouseLeave = useEvent(() => { - setMouseState(undefined) + return { x: queryX, y: item.y, datum: item.datum } }) return ( -

- {mouseState && Tooltip && ( - - - - )} - - - -
+ + + ) } -export type SingleValueDistributionTooltipProps = - DistributionPoint & { - xScale: React.ComponentProps< - typeof SingleValueDistributionChart - >['xScale'] - } +export type SingleValueDistributionTooltipProps = { + p: DistributionPoint + xScale: React.ComponentProps>['xScale'] +} export const MultiValueHistoryChart = (props: { data: MultiPoint[] @@ -144,7 +130,6 @@ export const MultiValueHistoryChart = (props: { const { colors, data, yScale, w, h, Tooltip, pct } = props const [viewXScale, setViewXScale] = useState>() - const [mouseState, setMouseState] = useState>>() const xScale = viewXScale ?? props.xScale type SP = SeriesPoint> @@ -177,65 +162,49 @@ export const MultiValueHistoryChart = (props: { setViewXScale(() => xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)]) ) - setMouseState(undefined) } else { setViewXScale(undefined) - setMouseState(undefined) } }) - const onMouseOver = useEvent((ev: React.PointerEvent) => { - if (ev.pointerType === 'mouse') { - const [mouseX, mouseY] = pointer(ev) - const queryX = xScale.invert(mouseX) - const item = data[xBisector.left(data, queryX) - 1] - if (item == null) { - // this can happen if you are on the very left or right edge of the chart, - // so your queryX is out of bounds - return - } - const p = { x: queryX, y: item.y, datum: item.datum } - setMouseState({ top: mouseY - 10, left: mouseX + 60, p }) + const onMouseOver = useEvent((mouseX: number) => { + const queryX = xScale.invert(mouseX) + const item = data[xBisector.left(data, queryX) - 1] + if (item == null) { + // this can happen if you are on the very left or right edge of the chart, + // so your queryX is out of bounds + return } - }) - - const onMouseLeave = useEvent(() => { - setMouseState(undefined) + return { x: queryX, y: item.y, datum: item.datum } }) return ( -
- {mouseState && Tooltip && ( - - - - )} - - {series.map((s, i) => ( - - ))} - -
+ + {series.map((s, i) => ( + + ))} + ) } -export type MultiValueHistoryTooltipProps = MultiPoint & { +export type MultiValueHistoryTooltipProps = { + p: MultiPoint xScale: React.ComponentProps>['xScale'] } @@ -252,7 +221,6 @@ export const SingleValueHistoryChart = (props: { const { color, data, pct, yScale, w, h, Tooltip } = props const [viewXScale, setViewXScale] = useState>() - const [mouseState, setMouseState] = useState>>() const xScale = viewXScale ?? props.xScale const px = useCallback((p: HistoryPoint) => xScale(p.x), [xScale]) @@ -276,61 +244,45 @@ export const SingleValueHistoryChart = (props: { setViewXScale(() => xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)]) ) - setMouseState(undefined) } else { setViewXScale(undefined) - setMouseState(undefined) } }) - const onMouseOver = useEvent((ev: React.PointerEvent) => { - if (ev.pointerType === 'mouse') { - const [mouseX, mouseY] = pointer(ev) - const queryX = xScale.invert(mouseX) - const item = data[xBisector.left(data, queryX) - 1] - if (item == null) { - // this can happen if you are on the very left or right edge of the chart, - // so your queryX is out of bounds - return - } - const p = { x: queryX, y: item.y, datum: item.datum } - setMouseState({ top: mouseY - 10, left: mouseX + 60, p }) + const onMouseOver = useEvent((mouseX: number) => { + const queryX = xScale.invert(mouseX) + const item = data[xBisector.left(data, queryX) - 1] + if (item == null) { + // this can happen if you are on the very left or right edge of the chart, + // so your queryX is out of bounds + return } - }) - - const onMouseLeave = useEvent(() => { - setMouseState(undefined) + return { x: queryX, y: item.y, datum: item.datum } }) return ( -
- {mouseState && Tooltip && ( - - - - )} - - - -
+ + + ) } -export type SingleValueHistoryTooltipProps = HistoryPoint & { +export type SingleValueHistoryTooltipProps = { + p: HistoryPoint xScale: React.ComponentProps>['xScale'] } diff --git a/web/components/charts/helpers.tsx b/web/components/charts/helpers.tsx index 2ed59ce2..55bb6e90 100644 --- a/web/components/charts/helpers.tsx +++ b/web/components/charts/helpers.tsx @@ -1,5 +1,13 @@ -import { ReactNode, SVGProps, memo, useRef, useEffect, useMemo } from 'react' -import { select } from 'd3-selection' +import { + ReactNode, + SVGProps, + memo, + useRef, + useEffect, + useMemo, + useState, +} from 'react' +import { pointer, select } from 'd3-selection' import { Axis } from 'd3-axis' import { brushX, D3BrushEvent } from 'd3-brush' import { area, line, curveStepAfter, CurveFactory } from 'd3-shape' @@ -108,19 +116,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?: (ev: React.PointerEvent) => void - onMouseLeave?: (ev: React.PointerEvent) => void - pct?: boolean + onMouseOver?: (mouseX: number, mouseY: number) => P | undefined + Tooltip?: TooltipContent<{ xScale: XS } & { p: P }> }) => { - const { children, w, h, xAxis, yAxis, onMouseOver, onMouseLeave, onSelect } = - props + const { children, w, h, xAxis, yAxis, onMouseOver, onSelect, Tooltip } = props + const [mouseState, setMouseState] = useState() const overlayRef = useRef(null) const innerW = w - MARGIN_X const innerH = h - MARGIN_Y @@ -139,6 +146,7 @@ export const SVGChart = (props: { if (!justSelected.current) { justSelected.current = true onSelect(ev) + setMouseState(undefined) if (overlayRef.current) { select(overlayRef.current).call(brush.clear) } @@ -156,29 +164,52 @@ export const SVGChart = (props: { } }, [innerW, innerH, onSelect]) + const onPointerMove = (ev: React.PointerEvent) => { + if (ev.pointerType === 'mouse' && onMouseOver) { + const [mouseX, mouseY] = pointer(ev) + const p = onMouseOver(mouseX, mouseY) + if (p != null) { + setMouseState({ top: mouseY - 10, left: mouseX + 60, p }) + } else { + setMouseState(undefined) + } + } + } + + const onPointerLeave = () => { + setMouseState(undefined) + } + return ( - - - - - - - - {children} - - - +
+ {mouseState && Tooltip && ( + + + + )} + + + + + + + + {children} + + + +
) }