Improve typing for chart tooltip stuff (#962)

This commit is contained in:
Marshall Polaris 2022-09-29 12:51:38 -07:00 committed by GitHub
parent 9fc1e855ff
commit 8929b2e6ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 54 additions and 76 deletions

View File

@ -8,6 +8,7 @@ import { BinaryContract } from 'common/contract'
import { DAY_MS } from 'common/util/time'
import { useIsMobile } from 'web/hooks/use-is-mobile'
import {
TooltipProps,
MARGIN_X,
MARGIN_Y,
getDateRange,
@ -15,10 +16,7 @@ import {
formatDateInRange,
formatPct,
} from '../helpers'
import {
SingleValueHistoryTooltipProps,
SingleValueHistoryChart,
} from '../generic-charts'
import { HistoryPoint, SingleValueHistoryChart } from '../generic-charts'
import { useElementWidth } from 'web/hooks/use-element-width'
import { Row } from 'web/components/layout/row'
import { Avatar } from 'web/components/avatar'
@ -31,7 +29,7 @@ const getBetPoints = (bets: Bet[]) => {
}))
}
const BinaryChartTooltip = (props: SingleValueHistoryTooltipProps<Bet>) => {
const BinaryChartTooltip = (props: TooltipProps<HistoryPoint<Bet>>) => {
const { p, xScale } = props
const { x, y, datum } = p
const [start, end] = xScale.domain()

View File

@ -10,6 +10,7 @@ import { useIsMobile } from 'web/hooks/use-is-mobile'
import { DAY_MS } from 'common/util/time'
import {
Legend,
TooltipProps,
MARGIN_X,
MARGIN_Y,
getDateRange,
@ -17,11 +18,7 @@ import {
formatPct,
formatDateInRange,
} from '../helpers'
import {
MultiPoint,
MultiValueHistoryChart,
MultiValueHistoryTooltipProps,
} from '../generic-charts'
import { MultiPoint, MultiValueHistoryChart } from '../generic-charts'
import { useElementWidth } from 'web/hooks/use-element-width'
import { Row } from 'web/components/layout/row'
import { Avatar } from 'web/components/avatar'
@ -161,7 +158,7 @@ export const ChoiceContractChart = (props: {
const yScale = scaleLinear([0, 1], [height - MARGIN_Y, 0])
const ChoiceTooltip = useMemo(
() => (props: MultiValueHistoryTooltipProps<Bet>) => {
() => (props: TooltipProps<MultiPoint<Bet>>) => {
const { p, xScale } = props
const { x, y, datum } = p
const [start, end] = xScale.domain()

View File

@ -7,11 +7,8 @@ import { getDpmOutcomeProbabilities } from 'common/calculate-dpm'
import { NumericContract } from 'common/contract'
import { NUMERIC_GRAPH_COLOR } from 'common/numeric-constants'
import { useIsMobile } from 'web/hooks/use-is-mobile'
import { MARGIN_X, MARGIN_Y, formatPct } from '../helpers'
import {
SingleValueDistributionChart,
SingleValueDistributionTooltipProps,
} from '../generic-charts'
import { TooltipProps, MARGIN_X, MARGIN_Y, formatPct } from '../helpers'
import { DistributionPoint, DistributionChart } from '../generic-charts'
import { useElementWidth } from 'web/hooks/use-element-width'
const getNumericChartData = (contract: NumericContract) => {
@ -24,9 +21,8 @@ const getNumericChartData = (contract: NumericContract) => {
}))
}
const NumericChartTooltip = (props: SingleValueDistributionTooltipProps) => {
const { p } = props
const { x, y } = p
const NumericChartTooltip = (props: TooltipProps<DistributionPoint>) => {
const { x, y } = props.p
return (
<span className="text-sm">
<strong>{formatPct(y, 2)}</strong> {formatLargeNumber(x)}
@ -51,7 +47,7 @@ export const NumericContractChart = (props: {
return (
<div ref={containerRef}>
{width > 0 && (
<SingleValueDistributionChart
<DistributionChart
w={width}
h={height}
xScale={xScale}

View File

@ -10,16 +10,14 @@ import { PseudoNumericContract } from 'common/contract'
import { NUMERIC_GRAPH_COLOR } from 'common/numeric-constants'
import { useIsMobile } from 'web/hooks/use-is-mobile'
import {
TooltipProps,
MARGIN_X,
MARGIN_Y,
getDateRange,
getRightmostVisibleDate,
formatDateInRange,
} from '../helpers'
import {
SingleValueHistoryChart,
SingleValueHistoryTooltipProps,
} from '../generic-charts'
import { HistoryPoint, SingleValueHistoryChart } from '../generic-charts'
import { useElementWidth } from 'web/hooks/use-element-width'
import { Row } from 'web/components/layout/row'
import { Avatar } from 'web/components/avatar'
@ -43,9 +41,7 @@ const getBetPoints = (bets: Bet[], scaleP: (p: number) => number) => {
}))
}
const PseudoNumericChartTooltip = (
props: SingleValueHistoryTooltipProps<Bet>
) => {
const PseudoNumericChartTooltip = (props: TooltipProps<HistoryPoint<Bet>>) => {
const { p, xScale } = props
const { x, y, datum } = p
const [start, end] = xScale.domain()

View File

@ -16,28 +16,29 @@ import {
SVGChart,
AreaPath,
AreaWithTopStroke,
TooltipContent,
Point,
TooltipComponent,
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 }
export type MultiPoint<T = unknown> = Point<Date, number[], T>
export type HistoryPoint<T = unknown> = Point<Date, number, T>
export type DistributionPoint<T = unknown> = Point<number, number, T>
const getTickValues = (min: number, max: number, n: number) => {
const step = (max - min) / (n - 1)
return [min, ...range(1, n - 1).map((i) => min + step * i), max]
}
export const SingleValueDistributionChart = <T,>(props: {
data: DistributionPoint<T>[]
export const DistributionChart = <P extends DistributionPoint>(props: {
data: P[]
w: number
h: number
color: string
xScale: ScaleContinuousNumeric<number, number>
yScale: ScaleContinuousNumeric<number, number>
Tooltip?: TooltipContent<SingleValueDistributionTooltipProps<T>>
Tooltip?: TooltipComponent<P>
}) => {
const { color, data, yScale, w, h, Tooltip } = props
@ -45,10 +46,10 @@ export const SingleValueDistributionChart = <T,>(props: {
useState<ScaleContinuousNumeric<number, number>>()
const xScale = viewXScale ?? props.xScale
const px = useCallback((p: DistributionPoint<T>) => xScale(p.x), [xScale])
const px = useCallback((p: P) => xScale(p.x), [xScale])
const py0 = yScale(yScale.domain()[0])
const py1 = useCallback((p: DistributionPoint<T>) => yScale(p.y), [yScale])
const xBisector = bisector((p: DistributionPoint<T>) => p.x)
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)
@ -56,7 +57,7 @@ export const SingleValueDistributionChart = <T,>(props: {
return { xAxis, yAxis }
}, [w, xScale, yScale])
const onSelect = useEvent((ev: D3BrushEvent<DistributionPoint<T>>) => {
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
if (ev.selection) {
const [mouseX0, mouseX1] = ev.selection as [number, number]
setViewXScale(() =>
@ -75,7 +76,7 @@ export const SingleValueDistributionChart = <T,>(props: {
// so your queryX is out of bounds
return
}
return { x: queryX, y: item.y, datum: item.datum }
return { ...item, x: queryX }
})
return (
@ -100,19 +101,14 @@ export const SingleValueDistributionChart = <T,>(props: {
)
}
export type SingleValueDistributionTooltipProps<T = unknown> = {
p: DistributionPoint<T>
xScale: React.ComponentProps<typeof SingleValueDistributionChart<T>>['xScale']
}
export const MultiValueHistoryChart = <T,>(props: {
data: MultiPoint<T>[]
export const MultiValueHistoryChart = <P extends MultiPoint>(props: {
data: P[]
w: number
h: number
colors: readonly string[]
xScale: ScaleTime<number, number>
yScale: ScaleContinuousNumeric<number, number>
Tooltip?: TooltipContent<MultiValueHistoryTooltipProps<T>>
Tooltip?: TooltipComponent<P>
pct?: boolean
}) => {
const { colors, data, yScale, w, h, Tooltip, pct } = props
@ -120,11 +116,11 @@ export const MultiValueHistoryChart = <T,>(props: {
const [viewXScale, setViewXScale] = useState<ScaleTime<number, number>>()
const xScale = viewXScale ?? props.xScale
type SP = SeriesPoint<MultiPoint<T>>
type SP = SeriesPoint<P>
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: MultiPoint<T>) => p.x)
const xBisector = bisector((p: P) => p.x)
const { xAxis, yAxis } = useMemo(() => {
const [min, max] = yScale.domain()
@ -139,14 +135,14 @@ export const MultiValueHistoryChart = <T,>(props: {
}, [w, h, pct, xScale, yScale])
const series = useMemo(() => {
const d3Stack = stack<MultiPoint<T>, number>()
const d3Stack = stack<P, number>()
.keys(range(0, Math.max(...data.map(({ y }) => y.length))))
.value(({ y }, o) => y[o])
.order(stackOrderReverse)
return d3Stack(data)
}, [data])
const onSelect = useEvent((ev: D3BrushEvent<MultiPoint<T>>) => {
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
if (ev.selection) {
const [mouseX0, mouseX1] = ev.selection as [number, number]
setViewXScale(() =>
@ -165,7 +161,7 @@ export const MultiValueHistoryChart = <T,>(props: {
// so your queryX is out of bounds
return
}
return { x: queryX, y: item.y, datum: item.datum }
return { ...item, x: queryX }
})
return (
@ -193,19 +189,14 @@ export const MultiValueHistoryChart = <T,>(props: {
)
}
export type MultiValueHistoryTooltipProps<T = unknown> = {
p: MultiPoint<T>
xScale: React.ComponentProps<typeof MultiValueHistoryChart<T>>['xScale']
}
export const SingleValueHistoryChart = <T,>(props: {
data: HistoryPoint<T>[]
export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
data: P[]
w: number
h: number
color: string
xScale: ScaleTime<number, number>
yScale: ScaleContinuousNumeric<number, number>
Tooltip?: TooltipContent<SingleValueHistoryTooltipProps<T>>
Tooltip?: TooltipComponent<P>
pct?: boolean
}) => {
const { color, data, pct, yScale, w, h, Tooltip } = props
@ -213,10 +204,10 @@ export const SingleValueHistoryChart = <T,>(props: {
const [viewXScale, setViewXScale] = useState<ScaleTime<number, number>>()
const xScale = viewXScale ?? props.xScale
const px = useCallback((p: HistoryPoint<T>) => xScale(p.x), [xScale])
const px = useCallback((p: P) => xScale(p.x), [xScale])
const py0 = yScale(yScale.domain()[0])
const py1 = useCallback((p: HistoryPoint<T>) => yScale(p.y), [yScale])
const xBisector = bisector((p: HistoryPoint<T>) => p.x)
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()
@ -230,7 +221,7 @@ export const SingleValueHistoryChart = <T,>(props: {
return { xAxis, yAxis }
}, [w, h, pct, xScale, yScale])
const onSelect = useEvent((ev: D3BrushEvent<HistoryPoint<T>>) => {
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
if (ev.selection) {
const [mouseX0, mouseX1] = ev.selection as [number, number]
setViewXScale(() =>
@ -249,7 +240,7 @@ export const SingleValueHistoryChart = <T,>(props: {
// so your queryX is out of bounds
return
}
return { x: queryX, y: item.y, datum: item.datum }
return { ...item, x: queryX }
})
return (
@ -273,8 +264,3 @@ export const SingleValueHistoryChart = <T,>(props: {
</SVGChart>
)
}
export type SingleValueHistoryTooltipProps<T = unknown> = {
p: HistoryPoint<T>
xScale: React.ComponentProps<typeof SingleValueHistoryChart<T>>['xScale']
}

View File

@ -8,7 +8,7 @@ import {
useState,
} from 'react'
import { pointer, select } from 'd3-selection'
import { Axis } from 'd3-axis'
import { Axis, AxisScale } from 'd3-axis'
import { brushX, D3BrushEvent } from 'd3-brush'
import { area, line, curveStepAfter, CurveFactory } from 'd3-shape'
import { nanoid } from 'nanoid'
@ -18,6 +18,10 @@ import clsx from 'clsx'
import { Contract } from 'common/contract'
import { Row } from 'web/components/layout/row'
export type Point<X, Y, T = unknown> = { x: X; y: Y; datum?: 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
export const MARGIN = { top: 20, right: 10, bottom: 20, left: 40 }
export const MARGIN_X = MARGIN.right + MARGIN.left
export const MARGIN_Y = MARGIN.top + MARGIN.bottom
@ -113,15 +117,15 @@ export const AreaWithTopStroke = <P,>(props: {
)
}
export const SVGChart = <X, Y, P, XS>(props: {
export const SVGChart = <X, Y, P extends Point<X, Y>>(props: {
children: ReactNode
w: number
h: number
xAxis: Axis<X>
yAxis: Axis<Y>
yAxis: Axis<number>
onSelect?: (ev: D3BrushEvent<any>) => void
onMouseOver?: (mouseX: number, mouseY: number) => P | undefined
Tooltip?: TooltipContent<{ xScale: XS } & { p: P }>
Tooltip?: TooltipComponent<P>
}) => {
const { children, w, h, xAxis, yAxis, onMouseOver, onSelect, Tooltip } = props
const [mouseState, setMouseState] = useState<TooltipPosition & { p: P }>()
@ -181,7 +185,7 @@ export const SVGChart = <X, Y, P, XS>(props: {
<div className="relative">
{mouseState && Tooltip && (
<TooltipContainer top={mouseState.top} left={mouseState.left}>
<Tooltip xScale={xAxis.scale() as XS} p={mouseState.p} />
<Tooltip xScale={xAxis.scale()} p={mouseState.p} />
</TooltipContainer>
)}
<svg className="w-full" width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
@ -210,7 +214,8 @@ export const SVGChart = <X, Y, P, XS>(props: {
)
}
export type TooltipContent<P> = React.ComponentType<P>
export type TooltipProps<P> = { p: P; xScale: XScale<P> }
export type TooltipComponent<P> = React.ComponentType<TooltipProps<P>>
export type TooltipPosition = { top: number; left: number }
export const TooltipContainer = (
props: TooltipPosition & { className?: string; children: React.ReactNode }