manifold/web/components/charts/contract/choice.tsx
Marshall Polaris e0d9b4d335
Rewrite contract graphs (#935)
* Fiddle around with everything, WIP FR charts

* Implement numeric chart

* Reorganize everything into neat little files

* Add `AreaWithTopStroke` helper

* Tidying, don't gratuitously use d3.format

* Remove duplicate code

* Better tooltip bisection

* `NumericPoint` -> `DistributionPoint`

* Add numeric market tooltip

* Make numeric chart bucket points less wrong

* Clean up numeric bucket computation

* Clean up a bunch of tooltip stuff, add FR legend tooltips

* Fix a dumb bug

* Implement basic time selection

* Fix fishy Date.now inconsistency bugs

* Might as well show all the FR outcomes now

* Make tooltips accurate on curveStepAfter charts

* Make log scale PN charts work properly

* Adjust x-axis tick count

* Display tooltip on charts only for mouse

* Fix up deps

* Tighter chart tooltips

* Adjustments to chart time range management

* Better date formatting

* Continue tweaking time selection handling to be perfect

* Make FR charts taller by default
2022-09-27 20:24:42 -07:00

171 lines
4.2 KiB
TypeScript

import { useMemo, useRef } from 'react'
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'
import { MARGIN_X, MARGIN_Y, MAX_DATE, getDateRange } from '../helpers'
import { MultiPoint, MultiValueHistoryChart } from '../generic-charts'
import { useElementWidth } from 'web/hooks/use-element-width'
// thanks to https://observablehq.com/@jonhelfman/optimal-orders-for-choosing-categorical-colors
const CATEGORY_COLORS = [
'#00b8dd',
'#eecafe',
'#874c62',
'#6457ca',
'#f773ba',
'#9c6bbc',
'#a87744',
'#af8a04',
'#bff9aa',
'#f3d89d',
'#c9a0f5',
'#ff00e5',
'#9dc6f7',
'#824475',
'#d973cc',
'#bc6808',
'#056e70',
'#677932',
'#00b287',
'#c8ab6c',
'#a2fb7a',
'#f8db68',
'#14675a',
'#8288f4',
'#fe1ca0',
'#ad6aff',
'#786306',
'#9bfbaf',
'#b00cf7',
'#2f7ec5',
'#4b998b',
'#42fa0e',
'#5b80a1',
'#962d9d',
'#3385ff',
'#48c5ab',
'#b2c873',
'#4cf9a4',
'#00ffff',
'#3cca73',
'#99ae17',
'#7af5cf',
'#52af45',
'#fbb80f',
'#29971b',
'#187c9a',
'#00d539',
'#bbfa1a',
'#61f55c',
'#cabc03',
'#ff9000',
'#779100',
'#bcfd6f',
'#70a560',
]
const getTrackedAnswers = (
contract: FreeResponseContract | MultipleChoiceContract,
topN: number
) => {
const { answers, outcomeType, totalBets } = contract
const validAnswers = answers.filter((answer) => {
return (
(answer.id !== '0' || outcomeType === 'MULTIPLE_CHOICE') &&
totalBets[answer.id] > 0.000000001
)
})
return sortBy(
validAnswers,
(answer) => -1 * getOutcomeProbability(contract, answer.id)
).slice(0, topN)
}
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
const sharesSquared = sum(
Object.values(sharesByOutcome).map((shares) => shares ** 2)
)
points.push([
new Date(bet.createdTime),
answers.map((answer) => sharesByOutcome[answer.id] ** 2 / sharesSquared),
])
}
return points
}
export const ChoiceContractChart = (props: {
contract: FreeResponseContract | MultipleChoiceContract
bets: Bet[]
height?: number
}) => {
const { contract, bets } = props
const [contractStart, contractEnd] = getDateRange(contract)
const answers = useMemo(
() => getTrackedAnswers(contract, CATEGORY_COLORS.length),
[contract]
)
const betPoints = useMemo(() => getBetPoints(answers, bets), [answers, bets])
const data = useMemo(
() => [
getStartPoint(answers, contractStart),
...betPoints,
getEndPoint(answers, contract, contractEnd ?? MAX_DATE),
],
[answers, contract, betPoints, contractStart, contractEnd]
)
const visibleRange = [contractStart, contractEnd ?? Date.now()]
const isMobile = useIsMobile(800)
const containerRef = useRef<HTMLDivElement>(null)
const width = useElementWidth(containerRef) ?? 0
const height = props.height ?? (isMobile ? 150 : 250)
const xScale = scaleTime(visibleRange, [0, width - MARGIN_X])
const yScale = scaleLinear([0, 1], [height - MARGIN_Y, 0])
return (
<div ref={containerRef}>
{width && (
<MultiValueHistoryChart
w={width}
h={height}
xScale={xScale}
yScale={yScale}
data={data}
colors={CATEGORY_COLORS}
labels={answers.map((answer) => answer.text)}
pct
/>
)}
</div>
)
}