From 8929b2e6ba719e31062bf59e12c10cf6118a38c4 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Thu, 29 Sep 2022 12:51:38 -0700 Subject: [PATCH] Improve typing for chart tooltip stuff (#962) --- web/components/charts/contract/binary.tsx | 8 +-- web/components/charts/contract/choice.tsx | 9 +-- web/components/charts/contract/numeric.tsx | 14 ++-- .../charts/contract/pseudo-numeric.tsx | 10 +-- web/components/charts/generic-charts.tsx | 72 ++++++++----------- web/components/charts/helpers.tsx | 17 +++-- 6 files changed, 54 insertions(+), 76 deletions(-) diff --git a/web/components/charts/contract/binary.tsx b/web/components/charts/contract/binary.tsx index 8a378799..55cf4e88 100644 --- a/web/components/charts/contract/binary.tsx +++ b/web/components/charts/contract/binary.tsx @@ -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) => { +const BinaryChartTooltip = (props: TooltipProps>) => { const { p, xScale } = props const { x, y, datum } = p const [start, end] = xScale.domain() diff --git a/web/components/charts/contract/choice.tsx b/web/components/charts/contract/choice.tsx index 0811a2ed..d5d0d09e 100644 --- a/web/components/charts/contract/choice.tsx +++ b/web/components/charts/contract/choice.tsx @@ -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) => { + () => (props: TooltipProps>) => { const { p, xScale } = props const { x, y, datum } = p const [start, end] = xScale.domain() diff --git a/web/components/charts/contract/numeric.tsx b/web/components/charts/contract/numeric.tsx index de1d1a0c..3c14149a 100644 --- a/web/components/charts/contract/numeric.tsx +++ b/web/components/charts/contract/numeric.tsx @@ -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) => { + const { x, y } = props.p return ( {formatPct(y, 2)} {formatLargeNumber(x)} @@ -51,7 +47,7 @@ export const NumericContractChart = (props: { return (
{width > 0 && ( - number) => { })) } -const PseudoNumericChartTooltip = ( - props: SingleValueHistoryTooltipProps -) => { +const PseudoNumericChartTooltip = (props: TooltipProps>) => { const { p, xScale } = props const { x, y, datum } = p const [start, end] = xScale.domain() diff --git a/web/components/charts/generic-charts.tsx b/web/components/charts/generic-charts.tsx index 161721f1..344ae061 100644 --- a/web/components/charts/generic-charts.tsx +++ b/web/components/charts/generic-charts.tsx @@ -16,28 +16,29 @@ import { SVGChart, AreaPath, AreaWithTopStroke, - TooltipContent, + Point, + TooltipComponent, 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 } +export type MultiPoint = Point +export type HistoryPoint = Point +export type DistributionPoint = Point 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 = (props: { - data: DistributionPoint[] +export const DistributionChart =

(props: { + data: P[] w: number h: number color: string xScale: ScaleContinuousNumeric yScale: ScaleContinuousNumeric - Tooltip?: TooltipContent> + Tooltip?: TooltipComponent

}) => { const { color, data, yScale, w, h, Tooltip } = props @@ -45,10 +46,10 @@ export const SingleValueDistributionChart = (props: { useState>() const xScale = viewXScale ?? props.xScale - const px = useCallback((p: DistributionPoint) => xScale(p.x), [xScale]) + const px = useCallback((p: P) => xScale(p.x), [xScale]) const py0 = yScale(yScale.domain()[0]) - const py1 = useCallback((p: DistributionPoint) => yScale(p.y), [yScale]) - const xBisector = bisector((p: DistributionPoint) => 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(xScale).ticks(w / 100) @@ -56,7 +57,7 @@ export const SingleValueDistributionChart = (props: { return { xAxis, yAxis } }, [w, xScale, yScale]) - const onSelect = useEvent((ev: D3BrushEvent>) => { + const onSelect = useEvent((ev: D3BrushEvent

) => { if (ev.selection) { const [mouseX0, mouseX1] = ev.selection as [number, number] setViewXScale(() => @@ -75,7 +76,7 @@ export const SingleValueDistributionChart = (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 = (props: { ) } -export type SingleValueDistributionTooltipProps = { - p: DistributionPoint - xScale: React.ComponentProps>['xScale'] -} - -export const MultiValueHistoryChart = (props: { - data: MultiPoint[] +export const MultiValueHistoryChart =

(props: { + data: P[] w: number h: number colors: readonly string[] xScale: ScaleTime yScale: ScaleContinuousNumeric - Tooltip?: TooltipContent> + Tooltip?: TooltipComponent

pct?: boolean }) => { const { colors, data, yScale, w, h, Tooltip, pct } = props @@ -120,11 +116,11 @@ export const MultiValueHistoryChart = (props: { const [viewXScale, setViewXScale] = useState>() const xScale = viewXScale ?? props.xScale - type SP = SeriesPoint> + type SP = SeriesPoint

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) => 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 = (props: { }, [w, h, pct, xScale, yScale]) const series = useMemo(() => { - const d3Stack = stack, number>() + const d3Stack = stack() .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>) => { + const onSelect = useEvent((ev: D3BrushEvent

) => { if (ev.selection) { const [mouseX0, mouseX1] = ev.selection as [number, number] setViewXScale(() => @@ -165,7 +161,7 @@ export const MultiValueHistoryChart = (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 = (props: { ) } -export type MultiValueHistoryTooltipProps = { - p: MultiPoint - xScale: React.ComponentProps>['xScale'] -} - -export const SingleValueHistoryChart = (props: { - data: HistoryPoint[] +export const SingleValueHistoryChart =

(props: { + data: P[] w: number h: number color: string xScale: ScaleTime yScale: ScaleContinuousNumeric - Tooltip?: TooltipContent> + Tooltip?: TooltipComponent

pct?: boolean }) => { const { color, data, pct, yScale, w, h, Tooltip } = props @@ -213,10 +204,10 @@ export const SingleValueHistoryChart = (props: { const [viewXScale, setViewXScale] = useState>() const xScale = viewXScale ?? props.xScale - const px = useCallback((p: HistoryPoint) => xScale(p.x), [xScale]) + const px = useCallback((p: P) => xScale(p.x), [xScale]) const py0 = yScale(yScale.domain()[0]) - const py1 = useCallback((p: HistoryPoint) => yScale(p.y), [yScale]) - const xBisector = bisector((p: HistoryPoint) => 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 = (props: { return { xAxis, yAxis } }, [w, h, pct, xScale, yScale]) - const onSelect = useEvent((ev: D3BrushEvent>) => { + const onSelect = useEvent((ev: D3BrushEvent

) => { if (ev.selection) { const [mouseX0, mouseX1] = ev.selection as [number, number] setViewXScale(() => @@ -249,7 +240,7 @@ export const SingleValueHistoryChart = (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 = (props: { ) } - -export type SingleValueHistoryTooltipProps = { - p: HistoryPoint - xScale: React.ComponentProps>['xScale'] -} diff --git a/web/components/charts/helpers.tsx b/web/components/charts/helpers.tsx index 236c6e1d..35c8a335 100644 --- a/web/components/charts/helpers.tsx +++ b/web/components/charts/helpers.tsx @@ -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: X; y: Y; datum?: T } +export type XScale

= P extends Point ? AxisScale : never +export type YScale

= P extends Point ? AxisScale : 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 = (props: { ) } -export const SVGChart = (props: { +export const SVGChart = >(props: { children: ReactNode w: number h: number xAxis: Axis - yAxis: Axis + yAxis: Axis onSelect?: (ev: D3BrushEvent) => void onMouseOver?: (mouseX: number, mouseY: number) => P | undefined - Tooltip?: TooltipContent<{ xScale: XS } & { p: P }> + Tooltip?: TooltipComponent

}) => { const { children, w, h, xAxis, yAxis, onMouseOver, onSelect, Tooltip } = props const [mouseState, setMouseState] = useState() @@ -181,7 +185,7 @@ export const SVGChart = (props: {

{mouseState && Tooltip && ( - + )} @@ -210,7 +214,8 @@ export const SVGChart = (props: { ) } -export type TooltipContent

= React.ComponentType

+export type TooltipProps

= { p: P; xScale: XScale

} +export type TooltipComponent

= React.ComponentType> export type TooltipPosition = { top: number; left: number } export const TooltipContainer = ( props: TooltipPosition & { className?: string; children: React.ReactNode }