Adjustments to chart time range management

This commit is contained in:
Marshall Polaris 2022-09-27 17:18:26 -07:00
parent 7718c8b910
commit 6375f7cf2c
5 changed files with 104 additions and 98 deletions

View File

@ -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<HTMLDivElement>(null)

View File

@ -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<HTMLDivElement>(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
/>
)}

View File

@ -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<HTMLDivElement>(null)

View File

@ -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<ScaleContinuousNumeric<number, number>>()
const [mouseState, setMouseState] =
useState<PositionValue<DistributionPoint>>()
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<DistributionPoint>) => {
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<ScaleTime<number, number>>()
const [mouseState, setMouseState] = useState<PositionValue<MultiPoint>>()
const xScale = viewXScale ?? props.xScale
type SP = SeriesPoint<MultiPoint>
const px = useCallback((p: SP) => xScale(p.data[0]), [xScale])
@ -231,12 +232,12 @@ export const MultiValueHistoryChart = (props: {
const onSelect = useEvent((ev: D3BrushEvent<MultiPoint>) => {
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<number, number>
yScale: d3.ScaleContinuousNumeric<number, number>
xScale: ScaleTime<number, number>
yScale: ScaleContinuousNumeric<number, number>
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<ScaleTime<number, number>>()
const [mouseState, setMouseState] = useState<PositionValue<HistoryPoint>>()
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<HistoryPoint>) => {
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)
}
})

View File

@ -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
}