diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index 6b35f74e..a665a921 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -23,7 +23,7 @@ import { Linkify } from 'web/components/linkify' import { Button } from 'web/components/button' import { useAdmin } from 'web/hooks/use-admin' import { needsAdminToResolve } from 'web/pages/[username]/[contractSlug]' -import { CATEGORY_COLORS } from '../charts/contract/choice' +import { CHOICE_ANSWER_COLORS } from '../charts/contract/choice' import { useChartAnswers } from '../charts/contract/choice' export function AnswersPanel(props: { @@ -190,7 +190,10 @@ function OpenAnswer(props: { const probPercent = formatPercent(prob) const [open, setOpen] = useState(false) const color = - colorIndex != undefined ? CATEGORY_COLORS[colorIndex] : '#B1B1C7' + colorIndex != undefined && colorIndex < CHOICE_ANSWER_COLORS.length + ? CHOICE_ANSWER_COLORS[colorIndex] + '55' // semi-transparent + : '#B1B1C755' + const colorWidth = 100 * Math.max(prob, 0.01) return ( @@ -206,9 +209,12 @@ function OpenAnswer(props: { @@ -236,11 +242,6 @@ function OpenAnswer(props: { )} -
) diff --git a/web/components/charts/contract/choice.tsx b/web/components/charts/contract/choice.tsx index 0355b4b5..31ca9b47 100644 --- a/web/components/charts/contract/choice.tsx +++ b/web/components/charts/contract/choice.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import { last, sum, sortBy, groupBy } from 'lodash' +import { last, range, sum, sortBy, groupBy } from 'lodash' import { scaleTime, scaleLinear } from 'd3-scale' import { curveStepAfter } from 'd3-shape' @@ -19,83 +19,36 @@ import { MultiPoint, MultiValueHistoryChart } from '../generic-charts' import { Row } from 'web/components/layout/row' import { Avatar } from 'web/components/avatar' -export const CATEGORY_COLORS = [ - '#7eb0d5', - '#fd7f6f', - '#b2e061', - '#bd7ebe', - '#ffb55a', - '#ffee65', - '#beb9db', - '#fdcce5', - '#8bd3c7', - '#bddfb7', - '#e2e3f3', - '#fafafa', - '#9fcdeb', - '#d3d3d3', - '#b1a296', - '#e1bdb6', - '#f2dbc0', - '#fae5d3', - '#c5e0ec', - '#e0f0ff', - '#ffddcd', - '#fbd5e2', - '#f2e7e5', - '#ffe7ba', - '#eed9c4', - '#ea9999', - '#f9cb9c', - '#ffe599', - '#b6d7a8', - '#a2c4c9', - '#9fc5e8', - '#b4a7d6', - '#d5a6bd', - '#e06666', - '#f6b26b', - '#ffd966', - '#93c47d', - '#76a5af', - '#6fa8dc', - '#8e7cc3', - '#c27ba0', - '#cc0000', - '#e69138', - '#f1c232', - '#6aa84f', - '#45818e', - '#3d85c6', - '#674ea7', - '#a64d79', - '#990000', - '#b45f06', - '#bf9000', +type ChoiceContract = FreeResponseContract | MultipleChoiceContract + +export const CHOICE_ANSWER_COLORS = [ + '#97C1EB', + '#F39F83', + '#F9EBA5', + '#FFC7D2', + '#C7ECFF', + '#8CDEC7', + '#DBE96F', ] +export const CHOICE_OTHER_COLOR = '#CCC' +export const CHOICE_ALL_COLORS = [...CHOICE_ANSWER_COLORS, CHOICE_OTHER_COLOR] const MARGIN = { top: 20, right: 10, bottom: 20, left: 40 } const MARGIN_X = MARGIN.left + MARGIN.right const MARGIN_Y = MARGIN.top + MARGIN.bottom -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 - ) - }) +const getAnswers = (contract: ChoiceContract) => { + const { answers, outcomeType } = contract + const validAnswers = answers.filter( + (answer) => answer.id !== '0' || outcomeType === 'MULTIPLE_CHOICE' + ) return sortBy( validAnswers, (answer) => -1 * getOutcomeProbability(contract, answer.id) - ).slice(0, topN) + ) } -const getBetPoints = (answers: Answer[], bets: Bet[]) => { +const getBetPoints = (answers: Answer[], bets: Bet[], topN?: number) => { const sortedBets = sortBy(bets, (b) => b.createdTime) const betsByOutcome = groupBy(sortedBets, (bet) => bet.outcome) const sharesByOutcome = Object.fromEntries( @@ -109,11 +62,14 @@ const getBetPoints = (answers: Answer[], bets: Bet[]) => { const sharesSquared = sum( Object.values(sharesByOutcome).map((shares) => shares ** 2) ) - points.push({ - x: new Date(bet.createdTime), - y: answers.map((a) => sharesByOutcome[a.id] ** 2 / sharesSquared), - obj: bet, - }) + const probs = answers.map((a) => sharesByOutcome[a.id] ** 2 / sharesSquared) + + if (topN != null && answers.length > topN) { + const y = [...probs.slice(0, topN), sum(probs.slice(topN))] + points.push({ x: new Date(bet.createdTime), y, obj: bet }) + } else { + points.push({ x: new Date(bet.createdTime), y: probs, obj: bet }) + } } return points } @@ -141,17 +97,12 @@ const Legend = (props: { className?: string; items: LegendItem[] }) => { ) } -export function useChartAnswers( - contract: FreeResponseContract | MultipleChoiceContract -) { - return useMemo( - () => getTrackedAnswers(contract, CATEGORY_COLORS.length), - [contract] - ) +export function useChartAnswers(contract: ChoiceContract) { + return useMemo(() => getAnswers(contract), [contract]) } export const ChoiceContractChart = (props: { - contract: FreeResponseContract | MultipleChoiceContract + contract: ChoiceContract bets: Bet[] width: number height: number @@ -160,18 +111,33 @@ export const ChoiceContractChart = (props: { const { contract, bets, width, height, onMouseOver } = props const [start, end] = getDateRange(contract) const answers = useChartAnswers(contract) - const betPoints = useMemo(() => getBetPoints(answers, bets), [answers, bets]) - const data = useMemo( - () => [ - { x: new Date(start), y: answers.map((_) => 0) }, + const topN = Math.min(CHOICE_ANSWER_COLORS.length, answers.length) + const betPoints = useMemo( + () => getBetPoints(answers, bets, topN), + [answers, bets, topN] + ) + const endProbs = useMemo( + () => answers.map((a) => getOutcomeProbability(contract, a.id)), + [answers, contract] + ) + + const data = useMemo(() => { + const yCount = answers.length > topN ? topN + 1 : topN + const startY = range(0, yCount).map((_) => 0) + const endY = + answers.length > topN + ? [...endProbs.slice(0, topN), sum(endProbs.slice(topN))] + : endProbs + return [ + { x: new Date(start), y: startY }, ...betPoints, { x: new Date(end ?? Date.now() + DAY_MS), - y: answers.map((a) => getOutcomeProbability(contract, a.id)), + y: endY, }, - ], - [answers, contract, betPoints, start, end] - ) + ] + }, [answers.length, topN, betPoints, endProbs, start, end]) + const rightmostDate = getRightmostVisibleDate( end, last(betPoints)?.x?.getTime(), @@ -188,8 +154,8 @@ export const ChoiceContractChart = (props: { const d = xScale.invert(x) const legendItems = sortBy( data.y.map((p, i) => ({ - color: CATEGORY_COLORS[i], - label: answers[i].text, + color: CHOICE_ALL_COLORS[i], + label: i === CHOICE_ANSWER_COLORS.length ? 'Other' : answers[i].text, value: formatPct(p), p, })), @@ -221,7 +187,7 @@ export const ChoiceContractChart = (props: { yScale={yScale} yKind="percent" data={data} - colors={CATEGORY_COLORS} + colors={CHOICE_ALL_COLORS} curve={curveStepAfter} onMouseOver={onMouseOver} Tooltip={ChoiceTooltip}