From 6375f7cf2cf7c788517ef558b70ef3ed31d040fd Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Tue, 27 Sep 2022 17:18:26 -0700 Subject: [PATCH] Adjustments to chart time range management --- web/components/charts/contract/binary.tsx | 37 +++++----- web/components/charts/contract/choice.tsx | 74 ++++++++++--------- .../charts/contract/pseudo-numeric.tsx | 54 +++++++------- web/components/charts/generic-charts.tsx | 30 ++++---- web/components/charts/helpers.tsx | 7 +- 5 files changed, 104 insertions(+), 98 deletions(-) diff --git a/web/components/charts/contract/binary.tsx b/web/components/charts/contract/binary.tsx index f03ba0b0..27443ffe 100644 --- a/web/components/charts/contract/binary.tsx +++ b/web/components/charts/contract/binary.tsx @@ -10,20 +10,18 @@ import { MARGIN_X, MARGIN_Y, getDateRange } from '../helpers' import { SingleValueHistoryChart } from '../generic-charts' import { useElementWidth } from 'web/hooks/use-element-width' -const getChartData = ( - contract: BinaryContract, - bets: Bet[], - start: Date, - end: Date -) => { - const sortedBets = sortBy(bets, (b) => b.createdTime) - const startProb = getInitialProbability(contract) - const endProb = getProbability(contract) - return [ - [start, startProb] as const, - ...sortedBets.map((b) => [new Date(b.createdTime), b.probAfter] as const), - [end, endProb] as const, - ] +const getBetPoints = (bets: Bet[]) => { + return sortBy(bets, (b) => b.createdTime).map( + (b) => [new Date(b.createdTime), b.probAfter] as const + ) +} + +const getStartPoint = (contract: BinaryContract, start: Date) => { + return [start, getInitialProbability(contract)] as const +} + +const getEndPoint = (contract: BinaryContract, end: Date) => { + return [end, getProbability(contract)] as const } export const BinaryContractChart = (props: { @@ -32,10 +30,15 @@ export const BinaryContractChart = (props: { height?: number }) => { const { contract, bets } = props - const [start, end] = useMemo(() => getDateRange(contract), [contract]) + const [start, end] = getDateRange(contract) + const betPoints = useMemo(() => getBetPoints(bets), [bets]) const data = useMemo( - () => getChartData(contract, bets, start, end), - [contract, bets, start, end] + () => [ + getStartPoint(contract, start), + ...betPoints, + getEndPoint(contract, end), + ], + [contract, betPoints, start, end] ) const isMobile = useIsMobile(800) const containerRef = useRef(null) diff --git a/web/components/charts/contract/choice.tsx b/web/components/charts/contract/choice.tsx index 875adefa..2ad93ed7 100644 --- a/web/components/charts/contract/choice.tsx +++ b/web/components/charts/contract/choice.tsx @@ -3,6 +3,7 @@ import { sum, sortBy, groupBy } from 'lodash' import { scaleTime, scaleLinear } from 'd3' import { Bet } from 'common/bet' +import { Answer } from 'common/answer' import { FreeResponseContract, MultipleChoiceContract } from 'common/contract' import { getOutcomeProbability } from 'common/calculate' import { useIsMobile } from 'web/hooks/use-is-mobile' @@ -68,35 +69,45 @@ const CATEGORY_COLORS = [ '#70a560', ] -const getMultiChartData = ( +const getTrackedAnswers = ( contract: FreeResponseContract | MultipleChoiceContract, - bets: Bet[], - start: Date, - end: Date, topN: number ) => { - const { answers, totalBets, outcomeType } = contract - - const sortedBets = sortBy(bets, (b) => b.createdTime) - const betsByOutcome = groupBy(sortedBets, (bet) => bet.outcome) + const { answers, outcomeType, totalBets } = contract const validAnswers = answers.filter((answer) => { return ( (answer.id !== '0' || outcomeType === 'MULTIPLE_CHOICE') && totalBets[answer.id] > 0.000000001 ) }) - - const trackedAnswers = sortBy( + return sortBy( validAnswers, (answer) => -1 * getOutcomeProbability(contract, answer.id) ).slice(0, topN) +} - const points: MultiPoint[] = [] +const getStartPoint = (answers: Answer[], start: Date) => { + return [start, answers.map((_) => 0)] as const +} +const getEndPoint = ( + answers: Answer[], + contract: FreeResponseContract | MultipleChoiceContract, + end: Date +) => { + return [ + end, + answers.map((a) => getOutcomeProbability(contract, a.id)), + ] as const +} + +const getBetPoints = (answers: Answer[], bets: Bet[]) => { + const sortedBets = sortBy(bets, (b) => b.createdTime) + const betsByOutcome = groupBy(sortedBets, (bet) => bet.outcome) const sharesByOutcome = Object.fromEntries( Object.keys(betsByOutcome).map((outcome) => [outcome, 0]) ) - + const points: MultiPoint[] = [] for (const bet of sortedBets) { const { outcome, shares } = bet sharesByOutcome[outcome] += shares @@ -106,26 +117,10 @@ const getMultiChartData = ( ) points.push([ new Date(bet.createdTime), - trackedAnswers.map( - (answer) => sharesByOutcome[answer.id] ** 2 / sharesSquared - ), + answers.map((answer) => sharesByOutcome[answer.id] ** 2 / sharesSquared), ]) } - - const allPoints: MultiPoint[] = [ - [start, trackedAnswers.map((_) => 0)], - ...points, - [ - end, - trackedAnswers.map((answer) => - getOutcomeProbability(contract, answer.id) - ), - ], - ] - return { - points: allPoints, - labels: trackedAnswers.map((answer) => answer.text), - } + return points } export const ChoiceContractChart = (props: { @@ -134,10 +129,19 @@ export const ChoiceContractChart = (props: { height?: number }) => { const { contract, bets } = props - const [start, end] = useMemo(() => getDateRange(contract), [contract]) + const [start, end] = getDateRange(contract) + const answers = useMemo( + () => getTrackedAnswers(contract, CATEGORY_COLORS.length), + [contract] + ) + const betPoints = useMemo(() => getBetPoints(answers, bets), [answers, bets]) const data = useMemo( - () => getMultiChartData(contract, bets, start, end, CATEGORY_COLORS.length), - [contract, bets, start, end] + () => [ + getStartPoint(answers, start), + ...betPoints, + getEndPoint(answers, contract, end), + ], + [answers, contract, betPoints, start, end] ) const isMobile = useIsMobile(800) const containerRef = useRef(null) @@ -153,9 +157,9 @@ export const ChoiceContractChart = (props: { h={height} xScale={xScale} yScale={yScale} - data={data.points} + data={data} colors={CATEGORY_COLORS} - labels={data.labels} + labels={answers.map((answer) => answer.text)} pct /> )} diff --git a/web/components/charts/contract/pseudo-numeric.tsx b/web/components/charts/contract/pseudo-numeric.tsx index a53edd9b..85898b91 100644 --- a/web/components/charts/contract/pseudo-numeric.tsx +++ b/web/components/charts/contract/pseudo-numeric.tsx @@ -15,29 +15,25 @@ import { useElementWidth } from 'web/hooks/use-element-width' // contracts. the values are stored "linearly" and can include zero. // as a result, we have to do some weird-looking stuff in this code -const getChartData = ( - contract: PseudoNumericContract, - bets: Bet[], - start: Date, - end: Date -) => { +const getY = (p: number, contract: PseudoNumericContract) => { const { min, max, isLogScale } = contract - const getY = (p: number) => - isLogScale - ? 10 ** (p * Math.log10(contract.max - contract.min + 1)) + - contract.min - - 1 - : p * (max - min) + min - const sortedBets = sortBy(bets, (b) => b.createdTime) - const startProb = getInitialProbability(contract) - const endProb = getProbability(contract) - return [ - [start, getY(startProb)] as const, - ...sortedBets.map( - (b) => [new Date(b.createdTime), getY(b.probAfter)] as const - ), - [end, getY(endProb)] as const, - ] + return isLogScale + ? 10 ** (p * Math.log10(max - min + 1)) + min - 1 + : p * (max - min) + min +} + +const getBetPoints = (contract: PseudoNumericContract, bets: Bet[]) => { + return sortBy(bets, (b) => b.createdTime).map( + (b) => [new Date(b.createdTime), getY(b.probAfter, contract)] as const + ) +} + +const getStartPoint = (contract: PseudoNumericContract, start: Date) => { + return [start, getY(getInitialProbability(contract), contract)] as const +} + +const getEndPoint = (contract: PseudoNumericContract, end: Date) => { + return [end, getY(getProbability(contract), contract)] as const } export const PseudoNumericContractChart = (props: { @@ -46,10 +42,18 @@ export const PseudoNumericContractChart = (props: { height?: number }) => { const { contract, bets } = props - const [start, end] = useMemo(() => getDateRange(contract), [contract]) + const [start, end] = getDateRange(contract) + const betPoints = useMemo( + () => getBetPoints(contract, bets), + [contract, bets] + ) const data = useMemo( - () => getChartData(contract, bets, start, end), - [contract, bets, start, end] + () => [ + getStartPoint(contract, start), + ...betPoints, + getEndPoint(contract, end), + ], + [contract, betPoints, start, end] ) const isMobile = useIsMobile(800) const containerRef = useRef(null) diff --git a/web/components/charts/generic-charts.tsx b/web/components/charts/generic-charts.tsx index 80476cb3..b58ee703 100644 --- a/web/components/charts/generic-charts.tsx +++ b/web/components/charts/generic-charts.tsx @@ -110,9 +110,11 @@ export const SingleValueDistributionChart = (props: { // note that we have to type this funkily in order to succesfully store // a function inside of useState - const [xScale, setXScale] = useState(() => props.xScale) + const [viewXScale, setViewXScale] = + useState>() const [mouseState, setMouseState] = useState>() + const xScale = viewXScale ?? props.xScale const px = useCallback((p: DistributionPoint) => xScale(p[0]), [xScale]) const py0 = yScale(yScale.domain()[0]) @@ -130,12 +132,12 @@ export const SingleValueDistributionChart = (props: { const onSelect = useEvent((ev: D3BrushEvent) => { if (ev.selection) { const [mouseX0, mouseX1] = ev.selection as [number, number] - setXScale(() => + setViewXScale(() => xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)]) ) setMouseState(undefined) } else { - setXScale(() => props.xScale) + setViewXScale(undefined) setMouseState(undefined) } }) @@ -194,10 +196,9 @@ export const MultiValueHistoryChart = (props: { }) => { const { colors, data, yScale, labels, w, h, pct } = props - // note that we have to type this funkily in order to succesfully store - // a function inside of useState - const [xScale, setXScale] = useState(() => props.xScale) + const [viewXScale, setViewXScale] = useState>() const [mouseState, setMouseState] = useState>() + const xScale = viewXScale ?? props.xScale type SP = SeriesPoint const px = useCallback((p: SP) => xScale(p.data[0]), [xScale]) @@ -231,12 +232,12 @@ export const MultiValueHistoryChart = (props: { const onSelect = useEvent((ev: D3BrushEvent) => { if (ev.selection) { const [mouseX0, mouseX1] = ev.selection as [number, number] - setXScale(() => + setViewXScale(() => xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)]) ) setMouseState(undefined) } else { - setXScale(() => props.xScale) + setViewXScale(undefined) setMouseState(undefined) } }) @@ -303,16 +304,15 @@ export const SingleValueHistoryChart = (props: { w: number h: number color: string - xScale: d3.ScaleTime - yScale: d3.ScaleContinuousNumeric + xScale: ScaleTime + yScale: ScaleContinuousNumeric pct?: boolean }) => { const { color, data, pct, yScale, w, h } = props - // note that we have to type this funkily in order to succesfully store - // a function inside of useState - const [xScale, setXScale] = useState(() => props.xScale) + const [viewXScale, setViewXScale] = useState>() const [mouseState, setMouseState] = useState>() + const xScale = viewXScale ?? props.xScale const px = useCallback((p: HistoryPoint) => xScale(p[0]), [xScale]) const py0 = yScale(yScale.domain()[0]) @@ -336,12 +336,12 @@ export const SingleValueHistoryChart = (props: { const onSelect = useEvent((ev: D3BrushEvent) => { if (ev.selection) { const [mouseX0, mouseX1] = ev.selection as [number, number] - setXScale(() => + setViewXScale(() => xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)]) ) setMouseState(undefined) } else { - setXScale(() => props.xScale) + setViewXScale(undefined) setMouseState(undefined) } }) diff --git a/web/components/charts/helpers.tsx b/web/components/charts/helpers.tsx index 22327742..1cb20ca3 100644 --- a/web/components/charts/helpers.tsx +++ b/web/components/charts/helpers.tsx @@ -10,7 +10,6 @@ import { select, } from 'd3' import { nanoid } from 'nanoid' -import dayjs from 'dayjs' import clsx from 'clsx' import { Contract } from 'common/contract' @@ -202,9 +201,5 @@ export const getDateRange = (contract: Contract) => { const now = Date.now() const isClosed = !!closeTime && now > closeTime const endDate = resolutionTime ?? (isClosed ? closeTime : now) - // the graph should be minimum an hour wide - const adjustedEndDate = dayjs(createdTime).add(1, 'hour').isAfter(endDate) - ? dayjs(endDate).add(1, 'hours') - : dayjs(endDate) - return [new Date(createdTime), adjustedEndDate.toDate()] as const + return [new Date(createdTime), new Date(endDate)] as const }