Clean up chart tooltip handling (#959)
This commit is contained in:
parent
be010da9f5
commit
8862425120
|
@ -32,7 +32,8 @@ const getBetPoints = (bets: Bet[]) => {
|
|||
}
|
||||
|
||||
const BinaryChartTooltip = (props: SingleValueHistoryTooltipProps<Bet>) => {
|
||||
const { x, y, xScale, datum } = props
|
||||
const { p, xScale } = props
|
||||
const { x, y, datum } = p
|
||||
const [start, end] = xScale.domain()
|
||||
return (
|
||||
<Row className="items-center gap-2 text-sm">
|
||||
|
|
|
@ -162,7 +162,8 @@ export const ChoiceContractChart = (props: {
|
|||
|
||||
const ChoiceTooltip = useMemo(
|
||||
() => (props: MultiValueHistoryTooltipProps<Bet>) => {
|
||||
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) => ({
|
||||
|
|
|
@ -25,7 +25,8 @@ const getNumericChartData = (contract: NumericContract) => {
|
|||
}
|
||||
|
||||
const NumericChartTooltip = (props: SingleValueDistributionTooltipProps) => {
|
||||
const { x, y } = props
|
||||
const { p } = props
|
||||
const { x, y } = p
|
||||
return (
|
||||
<span className="text-sm">
|
||||
<strong>{formatPct(y, 2)}</strong> {formatLargeNumber(x)}
|
||||
|
|
|
@ -46,7 +46,8 @@ const getBetPoints = (bets: Bet[], scaleP: (p: number) => number) => {
|
|||
const PseudoNumericChartTooltip = (
|
||||
props: SingleValueHistoryTooltipProps<Bet>
|
||||
) => {
|
||||
const { x, y, xScale, datum } = props
|
||||
const { p, xScale } = props
|
||||
const { x, y, datum } = p
|
||||
const [start, end] = xScale.domain()
|
||||
return (
|
||||
<Row className="items-center gap-2 text-sm">
|
||||
|
|
|
@ -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<T = never> = { x: Date; y: number[]; datum?: T }
|
||||
export type HistoryPoint<T = never> = { x: Date; y: number; datum?: T }
|
||||
export type DistributionPoint<T = never> = { x: number; y: number; datum?: T }
|
||||
|
||||
type PositionValue<P> = TooltipPosition & { p: P }
|
||||
export type MultiPoint<T = never> = {
|
||||
x: Date
|
||||
y: number[]
|
||||
datum?: T
|
||||
}
|
||||
export type HistoryPoint<T = never> = {
|
||||
x: Date
|
||||
y: number
|
||||
datum?: T
|
||||
}
|
||||
export type DistributionPoint<T = never> = {
|
||||
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 = <T,>(props: {
|
|||
|
||||
const [viewXScale, setViewXScale] =
|
||||
useState<ScaleContinuousNumeric<number, number>>()
|
||||
const [mouseState, setMouseState] =
|
||||
useState<PositionValue<DistributionPoint<T>>>()
|
||||
const xScale = viewXScale ?? props.xScale
|
||||
|
||||
const px = useCallback((p: DistributionPoint<T>) => xScale(p.x), [xScale])
|
||||
|
@ -69,67 +74,48 @@ export const SingleValueDistributionChart = <T,>(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 (
|
||||
<div className="relative">
|
||||
{mouseState && Tooltip && (
|
||||
<TooltipContainer className="text-sm" {...mouseState}>
|
||||
<Tooltip xScale={xScale} {...mouseState.p} />
|
||||
</TooltipContainer>
|
||||
)}
|
||||
<SVGChart
|
||||
w={w}
|
||||
h={h}
|
||||
xAxis={xAxis}
|
||||
yAxis={yAxis}
|
||||
onSelect={onSelect}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<AreaWithTopStroke
|
||||
color={color}
|
||||
data={data}
|
||||
px={px}
|
||||
py0={py0}
|
||||
py1={py1}
|
||||
curve={curveLinear}
|
||||
/>
|
||||
</SVGChart>
|
||||
</div>
|
||||
<SVGChart
|
||||
w={w}
|
||||
h={h}
|
||||
xAxis={xAxis}
|
||||
yAxis={yAxis}
|
||||
onSelect={onSelect}
|
||||
onMouseOver={onMouseOver}
|
||||
Tooltip={Tooltip}
|
||||
>
|
||||
<AreaWithTopStroke
|
||||
color={color}
|
||||
data={data}
|
||||
px={px}
|
||||
py0={py0}
|
||||
py1={py1}
|
||||
curve={curveLinear}
|
||||
/>
|
||||
</SVGChart>
|
||||
)
|
||||
}
|
||||
|
||||
export type SingleValueDistributionTooltipProps<T = unknown> =
|
||||
DistributionPoint<T> & {
|
||||
xScale: React.ComponentProps<
|
||||
typeof SingleValueDistributionChart<T>
|
||||
>['xScale']
|
||||
}
|
||||
export type SingleValueDistributionTooltipProps<T = unknown> = {
|
||||
p: DistributionPoint<T>
|
||||
xScale: React.ComponentProps<typeof SingleValueDistributionChart<T>>['xScale']
|
||||
}
|
||||
|
||||
export const MultiValueHistoryChart = <T,>(props: {
|
||||
data: MultiPoint<T>[]
|
||||
|
@ -144,7 +130,6 @@ export const MultiValueHistoryChart = <T,>(props: {
|
|||
const { colors, data, yScale, w, h, Tooltip, pct } = props
|
||||
|
||||
const [viewXScale, setViewXScale] = useState<ScaleTime<number, number>>()
|
||||
const [mouseState, setMouseState] = useState<PositionValue<MultiPoint<T>>>()
|
||||
const xScale = viewXScale ?? props.xScale
|
||||
|
||||
type SP = SeriesPoint<MultiPoint<T>>
|
||||
|
@ -177,65 +162,49 @@ export const MultiValueHistoryChart = <T,>(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 (
|
||||
<div className="relative">
|
||||
{mouseState && Tooltip && (
|
||||
<TooltipContainer top={mouseState.top} left={mouseState.left}>
|
||||
<Tooltip xScale={xScale} {...mouseState.p} />
|
||||
</TooltipContainer>
|
||||
)}
|
||||
<SVGChart
|
||||
w={w}
|
||||
h={h}
|
||||
xAxis={xAxis}
|
||||
yAxis={yAxis}
|
||||
onSelect={onSelect}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{series.map((s, i) => (
|
||||
<AreaPath
|
||||
key={i}
|
||||
data={s}
|
||||
px={px}
|
||||
py0={py0}
|
||||
py1={py1}
|
||||
curve={curveStepAfter}
|
||||
fill={colors[i]}
|
||||
/>
|
||||
))}
|
||||
</SVGChart>
|
||||
</div>
|
||||
<SVGChart
|
||||
w={w}
|
||||
h={h}
|
||||
xAxis={xAxis}
|
||||
yAxis={yAxis}
|
||||
onSelect={onSelect}
|
||||
onMouseOver={onMouseOver}
|
||||
Tooltip={Tooltip}
|
||||
>
|
||||
{series.map((s, i) => (
|
||||
<AreaPath
|
||||
key={i}
|
||||
data={s}
|
||||
px={px}
|
||||
py0={py0}
|
||||
py1={py1}
|
||||
curve={curveStepAfter}
|
||||
fill={colors[i]}
|
||||
/>
|
||||
))}
|
||||
</SVGChart>
|
||||
)
|
||||
}
|
||||
|
||||
export type MultiValueHistoryTooltipProps<T = unknown> = MultiPoint<T> & {
|
||||
export type MultiValueHistoryTooltipProps<T = unknown> = {
|
||||
p: MultiPoint<T>
|
||||
xScale: React.ComponentProps<typeof MultiValueHistoryChart<T>>['xScale']
|
||||
}
|
||||
|
||||
|
@ -252,7 +221,6 @@ export const SingleValueHistoryChart = <T,>(props: {
|
|||
const { color, data, pct, yScale, w, h, Tooltip } = props
|
||||
|
||||
const [viewXScale, setViewXScale] = useState<ScaleTime<number, number>>()
|
||||
const [mouseState, setMouseState] = useState<PositionValue<HistoryPoint<T>>>()
|
||||
const xScale = viewXScale ?? props.xScale
|
||||
|
||||
const px = useCallback((p: HistoryPoint<T>) => xScale(p.x), [xScale])
|
||||
|
@ -276,61 +244,45 @@ export const SingleValueHistoryChart = <T,>(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 (
|
||||
<div className="relative">
|
||||
{mouseState && Tooltip && (
|
||||
<TooltipContainer top={mouseState.top} left={mouseState.left}>
|
||||
<Tooltip xScale={xScale} {...mouseState.p} />
|
||||
</TooltipContainer>
|
||||
)}
|
||||
<SVGChart
|
||||
w={w}
|
||||
h={h}
|
||||
xAxis={xAxis}
|
||||
yAxis={yAxis}
|
||||
onSelect={onSelect}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<AreaWithTopStroke
|
||||
color={color}
|
||||
data={data}
|
||||
px={px}
|
||||
py0={py0}
|
||||
py1={py1}
|
||||
curve={curveStepAfter}
|
||||
/>
|
||||
</SVGChart>
|
||||
</div>
|
||||
<SVGChart
|
||||
w={w}
|
||||
h={h}
|
||||
xAxis={xAxis}
|
||||
yAxis={yAxis}
|
||||
onSelect={onSelect}
|
||||
onMouseOver={onMouseOver}
|
||||
Tooltip={Tooltip}
|
||||
>
|
||||
<AreaWithTopStroke
|
||||
color={color}
|
||||
data={data}
|
||||
px={px}
|
||||
py0={py0}
|
||||
py1={py1}
|
||||
curve={curveStepAfter}
|
||||
/>
|
||||
</SVGChart>
|
||||
)
|
||||
}
|
||||
|
||||
export type SingleValueHistoryTooltipProps<T = unknown> = HistoryPoint<T> & {
|
||||
export type SingleValueHistoryTooltipProps<T = unknown> = {
|
||||
p: HistoryPoint<T>
|
||||
xScale: React.ComponentProps<typeof SingleValueHistoryChart<T>>['xScale']
|
||||
}
|
||||
|
|
|
@ -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 = <P,>(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export const SVGChart = <X, Y>(props: {
|
||||
export const SVGChart = <X, Y, P, XS>(props: {
|
||||
children: ReactNode
|
||||
w: number
|
||||
h: number
|
||||
xAxis: Axis<X>
|
||||
yAxis: Axis<Y>
|
||||
onSelect?: (ev: D3BrushEvent<any>) => 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<TooltipPosition & { p: P }>()
|
||||
const overlayRef = useRef<SVGGElement>(null)
|
||||
const innerW = w - MARGIN_X
|
||||
const innerH = h - MARGIN_Y
|
||||
|
@ -139,6 +146,7 @@ export const SVGChart = <X, Y>(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 = <X, Y>(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 (
|
||||
<svg className="w-full" width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
|
||||
<clipPath id={clipPathId}>
|
||||
<rect x={0} y={0} width={innerW} height={innerH} />
|
||||
</clipPath>
|
||||
<g transform={`translate(${MARGIN.left}, ${MARGIN.top})`}>
|
||||
<XAxis axis={xAxis} w={innerW} h={innerH} />
|
||||
<YAxis axis={yAxis} w={innerW} h={innerH} />
|
||||
<g clipPath={`url(#${clipPathId})`}>{children}</g>
|
||||
<g
|
||||
ref={overlayRef}
|
||||
x="0"
|
||||
y="0"
|
||||
width={innerW}
|
||||
height={innerH}
|
||||
fill="none"
|
||||
pointerEvents="all"
|
||||
onPointerEnter={onMouseOver}
|
||||
onPointerMove={onMouseOver}
|
||||
onPointerLeave={onMouseLeave}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<div className="relative">
|
||||
{mouseState && Tooltip && (
|
||||
<TooltipContainer top={mouseState.top} left={mouseState.left}>
|
||||
<Tooltip xScale={xAxis.scale() as XS} p={mouseState.p} />
|
||||
</TooltipContainer>
|
||||
)}
|
||||
<svg className="w-full" width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
|
||||
<clipPath id={clipPathId}>
|
||||
<rect x={0} y={0} width={innerW} height={innerH} />
|
||||
</clipPath>
|
||||
<g transform={`translate(${MARGIN.left}, ${MARGIN.top})`}>
|
||||
<XAxis axis={xAxis} w={innerW} h={innerH} />
|
||||
<YAxis axis={yAxis} w={innerW} h={innerH} />
|
||||
<g clipPath={`url(#${clipPathId})`}>{children}</g>
|
||||
<g
|
||||
ref={overlayRef}
|
||||
x="0"
|
||||
y="0"
|
||||
width={innerW}
|
||||
height={innerH}
|
||||
fill="none"
|
||||
pointerEvents="all"
|
||||
onPointerEnter={onPointerMove}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerLeave={onPointerLeave}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user