Carry bet data down into charts

This commit is contained in:
Marshall Polaris 2022-09-28 19:35:59 -07:00
parent c7221d1026
commit 0df34473fe
4 changed files with 43 additions and 51 deletions

View File

@ -20,6 +20,7 @@ const getBetPoints = (bets: Bet[]) => {
return sortBy(bets, (b) => b.createdTime).map((b) => ({ return sortBy(bets, (b) => b.createdTime).map((b) => ({
x: new Date(b.createdTime), x: new Date(b.createdTime),
y: b.probAfter, y: b.probAfter,
datum: b,
})) }))
} }

View File

@ -92,28 +92,13 @@ const getTrackedAnswers = (
).slice(0, topN) ).slice(0, topN)
} }
const getStartPoint = (answers: Answer[], start: Date) => {
return { x: start, y: answers.map((_) => 0) }
}
const getEndPoint = (
answers: Answer[],
contract: FreeResponseContract | MultipleChoiceContract,
end: Date
) => {
return {
x: end,
y: answers.map((a) => getOutcomeProbability(contract, a.id)),
}
}
const getBetPoints = (answers: Answer[], bets: Bet[]) => { const getBetPoints = (answers: Answer[], bets: Bet[]) => {
const sortedBets = sortBy(bets, (b) => b.createdTime) const sortedBets = sortBy(bets, (b) => b.createdTime)
const betsByOutcome = groupBy(sortedBets, (bet) => bet.outcome) const betsByOutcome = groupBy(sortedBets, (bet) => bet.outcome)
const sharesByOutcome = Object.fromEntries( const sharesByOutcome = Object.fromEntries(
Object.keys(betsByOutcome).map((outcome) => [outcome, 0]) Object.keys(betsByOutcome).map((outcome) => [outcome, 0])
) )
const points: MultiPoint[] = [] const points: MultiPoint<Bet>[] = []
for (const bet of sortedBets) { for (const bet of sortedBets) {
const { outcome, shares } = bet const { outcome, shares } = bet
sharesByOutcome[outcome] += shares sharesByOutcome[outcome] += shares
@ -124,6 +109,7 @@ const getBetPoints = (answers: Answer[], bets: Bet[]) => {
points.push({ points.push({
x: new Date(bet.createdTime), x: new Date(bet.createdTime),
y: answers.map((a) => sharesByOutcome[a.id] ** 2 / sharesSquared), y: answers.map((a) => sharesByOutcome[a.id] ** 2 / sharesSquared),
datum: bet,
}) })
} }
return points return points
@ -135,7 +121,7 @@ export const ChoiceContractChart = (props: {
height?: number height?: number
}) => { }) => {
const { contract, bets } = props const { contract, bets } = props
const [contractStart, contractEnd] = getDateRange(contract) const [start, end] = getDateRange(contract)
const answers = useMemo( const answers = useMemo(
() => getTrackedAnswers(contract, CATEGORY_COLORS.length), () => getTrackedAnswers(contract, CATEGORY_COLORS.length),
[contract] [contract]
@ -143,18 +129,21 @@ export const ChoiceContractChart = (props: {
const betPoints = useMemo(() => getBetPoints(answers, bets), [answers, bets]) const betPoints = useMemo(() => getBetPoints(answers, bets), [answers, bets])
const data = useMemo( const data = useMemo(
() => [ () => [
getStartPoint(answers, contractStart), { x: start, y: answers.map((_) => 0) },
...betPoints, ...betPoints,
getEndPoint(answers, contract, contractEnd ?? MAX_DATE), {
x: end ?? MAX_DATE,
y: answers.map((a) => getOutcomeProbability(contract, a.id)),
},
], ],
[answers, contract, betPoints, contractStart, contractEnd] [answers, contract, betPoints, start, end]
) )
const rightmostDate = getRightmostVisibleDate( const rightmostDate = getRightmostVisibleDate(
contractEnd, end,
last(betPoints)?.x, last(betPoints)?.x,
new Date(Date.now()) new Date(Date.now())
) )
const visibleRange = [contractStart, rightmostDate] const visibleRange = [start, rightmostDate]
const isMobile = useIsMobile(800) const isMobile = useIsMobile(800)
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const width = useElementWidth(containerRef) ?? 0 const width = useElementWidth(containerRef) ?? 0

View File

@ -32,6 +32,7 @@ const getBetPoints = (bets: Bet[], scaleP: (p: number) => number) => {
return sortBy(bets, (b) => b.createdTime).map((b) => ({ return sortBy(bets, (b) => b.createdTime).map((b) => ({
x: new Date(b.createdTime), x: new Date(b.createdTime),
y: scaleP(b.probAfter), y: scaleP(b.probAfter),
datum: b,
})) }))
} }

View File

@ -25,10 +25,11 @@ import { formatLargeNumber } from 'common/util/format'
import { useEvent } from 'web/hooks/use-event' import { useEvent } from 'web/hooks/use-event'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
export type MultiPoint = { x: Date; y: number[] } export type MultiPoint<T = unknown> = { x: Date; y: number[]; datum?: T }
export type HistoryPoint = { x: Date; y: number } export type HistoryPoint<T = unknown> = { x: Date; y: number; datum?: T }
export type DistributionPoint = { x: number; y: number } export type DistributionPoint<T = unknown> = { x: number; y: number; datum?: T }
export type PositionValue<P> = TooltipPosition & { p: P }
type PositionValue<P> = TooltipPosition & { p: P }
const formatPct = (n: number, digits?: number) => { const formatPct = (n: number, digits?: number) => {
return `${(n * 100).toFixed(digits ?? 0)}%` return `${(n * 100).toFixed(digits ?? 0)}%`
@ -100,8 +101,8 @@ const Legend = (props: { className?: string; items: LegendItem[] }) => {
) )
} }
export const SingleValueDistributionChart = (props: { export const SingleValueDistributionChart = <T = unknown,>(props: {
data: DistributionPoint[] data: DistributionPoint<T>[]
w: number w: number
h: number h: number
color: string color: string
@ -115,13 +116,13 @@ export const SingleValueDistributionChart = (props: {
const [viewXScale, setViewXScale] = const [viewXScale, setViewXScale] =
useState<ScaleContinuousNumeric<number, number>>() useState<ScaleContinuousNumeric<number, number>>()
const [mouseState, setMouseState] = const [mouseState, setMouseState] =
useState<PositionValue<DistributionPoint>>() useState<PositionValue<DistributionPoint<T>>>()
const xScale = viewXScale ?? props.xScale const xScale = viewXScale ?? props.xScale
const px = useCallback((p: DistributionPoint) => xScale(p.x), [xScale]) const px = useCallback((p: DistributionPoint<T>) => xScale(p.x), [xScale])
const py0 = yScale(yScale.domain()[0]) const py0 = yScale(yScale.domain()[0])
const py1 = useCallback((p: DistributionPoint) => yScale(p.y), [yScale]) const py1 = useCallback((p: DistributionPoint<T>) => yScale(p.y), [yScale])
const xBisector = bisector((p: DistributionPoint) => p.x) const xBisector = bisector((p: DistributionPoint<T>) => p.x)
const { fmtX, fmtY, xAxis, yAxis } = useMemo(() => { const { fmtX, fmtY, xAxis, yAxis } = useMemo(() => {
const fmtX = (n: number) => formatLargeNumber(n) const fmtX = (n: number) => formatLargeNumber(n)
@ -131,7 +132,7 @@ export const SingleValueDistributionChart = (props: {
return { fmtX, fmtY, xAxis, yAxis } return { fmtX, fmtY, xAxis, yAxis }
}, [w, xScale, yScale]) }, [w, xScale, yScale])
const onSelect = useEvent((ev: D3BrushEvent<DistributionPoint>) => { const onSelect = useEvent((ev: D3BrushEvent<DistributionPoint<T>>) => {
if (ev.selection) { if (ev.selection) {
const [mouseX0, mouseX1] = ev.selection as [number, number] const [mouseX0, mouseX1] = ev.selection as [number, number]
setViewXScale(() => setViewXScale(() =>
@ -154,7 +155,7 @@ export const SingleValueDistributionChart = (props: {
// so your queryX is out of bounds // so your queryX is out of bounds
return return
} }
const p = { x: queryX, y: item.y } const p = { x: queryX, y: item.y, datum: item.datum }
setMouseState({ top: mouseY - 10, left: mouseX + 60, p }) setMouseState({ top: mouseY - 10, left: mouseX + 60, p })
} }
}) })
@ -192,8 +193,8 @@ export const SingleValueDistributionChart = (props: {
) )
} }
export const MultiValueHistoryChart = (props: { export const MultiValueHistoryChart = <T = unknown,>(props: {
data: MultiPoint[] data: MultiPoint<T>[]
w: number w: number
h: number h: number
labels: readonly string[] labels: readonly string[]
@ -205,14 +206,14 @@ export const MultiValueHistoryChart = (props: {
const { colors, data, yScale, labels, w, h, pct } = props const { colors, data, yScale, labels, w, h, pct } = props
const [viewXScale, setViewXScale] = useState<ScaleTime<number, number>>() const [viewXScale, setViewXScale] = useState<ScaleTime<number, number>>()
const [mouseState, setMouseState] = useState<PositionValue<MultiPoint>>() const [mouseState, setMouseState] = useState<PositionValue<MultiPoint<T>>>()
const xScale = viewXScale ?? props.xScale const xScale = viewXScale ?? props.xScale
type SP = SeriesPoint<MultiPoint> type SP = SeriesPoint<MultiPoint<T>>
const px = useCallback((p: SP) => xScale(p.data.x), [xScale]) const px = useCallback((p: SP) => xScale(p.data.x), [xScale])
const py0 = useCallback((p: SP) => yScale(p[0]), [yScale]) const py0 = useCallback((p: SP) => yScale(p[0]), [yScale])
const py1 = useCallback((p: SP) => yScale(p[1]), [yScale]) const py1 = useCallback((p: SP) => yScale(p[1]), [yScale])
const xBisector = bisector((p: MultiPoint) => p.x) const xBisector = bisector((p: MultiPoint<T>) => p.x)
const { fmtX, fmtY, xAxis, yAxis } = useMemo(() => { const { fmtX, fmtY, xAxis, yAxis } = useMemo(() => {
const [start, end] = xScale.domain() const [start, end] = xScale.domain()
@ -230,14 +231,14 @@ export const MultiValueHistoryChart = (props: {
}, [w, h, pct, xScale, yScale]) }, [w, h, pct, xScale, yScale])
const series = useMemo(() => { const series = useMemo(() => {
const d3Stack = stack<MultiPoint, number>() const d3Stack = stack<MultiPoint<T>, number>()
.keys(range(0, labels.length)) .keys(range(0, labels.length))
.value(({ y }, o) => y[o]) .value(({ y }, o) => y[o])
.order(stackOrderReverse) .order(stackOrderReverse)
return d3Stack(data) return d3Stack(data)
}, [data, labels.length]) }, [data, labels.length])
const onSelect = useEvent((ev: D3BrushEvent<MultiPoint>) => { const onSelect = useEvent((ev: D3BrushEvent<MultiPoint<T>>) => {
if (ev.selection) { if (ev.selection) {
const [mouseX0, mouseX1] = ev.selection as [number, number] const [mouseX0, mouseX1] = ev.selection as [number, number]
setViewXScale(() => setViewXScale(() =>
@ -260,7 +261,7 @@ export const MultiValueHistoryChart = (props: {
// so your queryX is out of bounds // so your queryX is out of bounds
return return
} }
const p = { x: queryX, y: item.y } const p = { x: queryX, y: item.y, datum: item.datum }
setMouseState({ top: mouseY - 10, left: mouseX + 60, p }) setMouseState({ top: mouseY - 10, left: mouseX + 60, p })
} }
}) })
@ -313,8 +314,8 @@ export const MultiValueHistoryChart = (props: {
) )
} }
export const SingleValueHistoryChart = (props: { export const SingleValueHistoryChart = <T = unknown,>(props: {
data: HistoryPoint[] data: HistoryPoint<T>[]
w: number w: number
h: number h: number
color: string color: string
@ -325,13 +326,13 @@ export const SingleValueHistoryChart = (props: {
const { color, data, pct, yScale, w, h } = props const { color, data, pct, yScale, w, h } = props
const [viewXScale, setViewXScale] = useState<ScaleTime<number, number>>() const [viewXScale, setViewXScale] = useState<ScaleTime<number, number>>()
const [mouseState, setMouseState] = useState<PositionValue<HistoryPoint>>() const [mouseState, setMouseState] = useState<PositionValue<HistoryPoint<T>>>()
const xScale = viewXScale ?? props.xScale const xScale = viewXScale ?? props.xScale
const px = useCallback((p: HistoryPoint) => xScale(p.x), [xScale]) const px = useCallback((p: HistoryPoint<T>) => xScale(p.x), [xScale])
const py0 = yScale(yScale.domain()[0]) const py0 = yScale(yScale.domain()[0])
const py1 = useCallback((p: HistoryPoint) => yScale(p.y), [yScale]) const py1 = useCallback((p: HistoryPoint<T>) => yScale(p.y), [yScale])
const xBisector = bisector((p: HistoryPoint) => p.x) const xBisector = bisector((p: HistoryPoint<T>) => p.x)
const { fmtX, fmtY, xAxis, yAxis } = useMemo(() => { const { fmtX, fmtY, xAxis, yAxis } = useMemo(() => {
const [start, end] = xScale.domain() const [start, end] = xScale.domain()
@ -347,7 +348,7 @@ export const SingleValueHistoryChart = (props: {
return { fmtX, fmtY, xAxis, yAxis } return { fmtX, fmtY, xAxis, yAxis }
}, [w, h, pct, xScale, yScale]) }, [w, h, pct, xScale, yScale])
const onSelect = useEvent((ev: D3BrushEvent<HistoryPoint>) => { const onSelect = useEvent((ev: D3BrushEvent<HistoryPoint<T>>) => {
if (ev.selection) { if (ev.selection) {
const [mouseX0, mouseX1] = ev.selection as [number, number] const [mouseX0, mouseX1] = ev.selection as [number, number]
setViewXScale(() => setViewXScale(() =>
@ -370,7 +371,7 @@ export const SingleValueHistoryChart = (props: {
// so your queryX is out of bounds // so your queryX is out of bounds
return return
} }
const p = { x: queryX, y: item.y } const p = { x: queryX, y: item.y, datum: item.datum }
setMouseState({ top: mouseY - 10, left: mouseX + 60, p }) setMouseState({ top: mouseY - 10, left: mouseX + 60, p })
} }
}) })
@ -383,7 +384,7 @@ export const SingleValueHistoryChart = (props: {
<div className="relative"> <div className="relative">
{mouseState && ( {mouseState && (
<ChartTooltip className="text-sm" {...mouseState}> <ChartTooltip className="text-sm" {...mouseState}>
<strong>{fmtY(mouseState.p.y)}</strong> {fmtX(mouseState.p.x)} <strong>{fmtY(mouseState.p.y)}</strong> {fmtX(mouseState.p.x)}{' '}
</ChartTooltip> </ChartTooltip>
)} )}
<SVGChart <SVGChart