Update FR colors, consolidate non-top answers into "Other" (#1031)
* Update FR colors, consolidate non-top answers into "Other" * Fix answer panel coloration to not be weird and work on Firefox
This commit is contained in:
		
							parent
							
								
									f587e0256d
								
							
						
					
					
						commit
						59cdc9f776
					
				|  | @ -23,7 +23,7 @@ import { Linkify } from 'web/components/linkify' | ||||||
| import { Button } from 'web/components/button' | import { Button } from 'web/components/button' | ||||||
| import { useAdmin } from 'web/hooks/use-admin' | import { useAdmin } from 'web/hooks/use-admin' | ||||||
| import { needsAdminToResolve } from 'web/pages/[username]/[contractSlug]' | 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' | import { useChartAnswers } from '../charts/contract/choice' | ||||||
| 
 | 
 | ||||||
| export function AnswersPanel(props: { | export function AnswersPanel(props: { | ||||||
|  | @ -190,7 +190,10 @@ function OpenAnswer(props: { | ||||||
|   const probPercent = formatPercent(prob) |   const probPercent = formatPercent(prob) | ||||||
|   const [open, setOpen] = useState(false) |   const [open, setOpen] = useState(false) | ||||||
|   const color = |   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 ( |   return ( | ||||||
|     <Col className="my-1 px-2"> |     <Col className="my-1 px-2"> | ||||||
|  | @ -206,9 +209,12 @@ function OpenAnswer(props: { | ||||||
| 
 | 
 | ||||||
|       <Col |       <Col | ||||||
|         className={clsx( |         className={clsx( | ||||||
|           'bg-greyscale-1 relative w-full rounded-lg transition-all', |           'relative w-full rounded-lg transition-all', | ||||||
|           tradingAllowed(contract) ? 'text-greyscale-7' : 'text-greyscale-5' |           tradingAllowed(contract) ? 'text-greyscale-7' : 'text-greyscale-5' | ||||||
|         )} |         )} | ||||||
|  |         style={{ | ||||||
|  |           background: `linear-gradient(to right, ${color} ${colorWidth}%, #FBFBFF ${colorWidth}%)`, | ||||||
|  |         }} | ||||||
|       > |       > | ||||||
|         <Row className="z-20 -mb-1 justify-between gap-2 py-2 px-3"> |         <Row className="z-20 -mb-1 justify-between gap-2 py-2 px-3"> | ||||||
|           <Row> |           <Row> | ||||||
|  | @ -236,11 +242,6 @@ function OpenAnswer(props: { | ||||||
|             )} |             )} | ||||||
|           </Row> |           </Row> | ||||||
|         </Row> |         </Row> | ||||||
|         <hr |  | ||||||
|           color={color} |  | ||||||
|           className="absolute z-0 h-full w-full rounded-l-lg border-none opacity-30" |  | ||||||
|           style={{ width: `${100 * Math.max(prob, 0.01)}%` }} |  | ||||||
|         /> |  | ||||||
|       </Col> |       </Col> | ||||||
|     </Col> |     </Col> | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { useMemo } from 'react' | 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 { scaleTime, scaleLinear } from 'd3-scale' | ||||||
| import { curveStepAfter } from 'd3-shape' | import { curveStepAfter } from 'd3-shape' | ||||||
| 
 | 
 | ||||||
|  | @ -19,83 +19,36 @@ import { MultiPoint, MultiValueHistoryChart } from '../generic-charts' | ||||||
| import { Row } from 'web/components/layout/row' | import { Row } from 'web/components/layout/row' | ||||||
| import { Avatar } from 'web/components/avatar' | import { Avatar } from 'web/components/avatar' | ||||||
| 
 | 
 | ||||||
| export const CATEGORY_COLORS = [ | type ChoiceContract = FreeResponseContract | MultipleChoiceContract | ||||||
|   '#7eb0d5', | 
 | ||||||
|   '#fd7f6f', | export const CHOICE_ANSWER_COLORS = [ | ||||||
|   '#b2e061', |   '#97C1EB', | ||||||
|   '#bd7ebe', |   '#F39F83', | ||||||
|   '#ffb55a', |   '#F9EBA5', | ||||||
|   '#ffee65', |   '#FFC7D2', | ||||||
|   '#beb9db', |   '#C7ECFF', | ||||||
|   '#fdcce5', |   '#8CDEC7', | ||||||
|   '#8bd3c7', |   '#DBE96F', | ||||||
|   '#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', |  | ||||||
| ] | ] | ||||||
|  | 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 = { top: 20, right: 10, bottom: 20, left: 40 } | ||||||
| const MARGIN_X = MARGIN.left + MARGIN.right | const MARGIN_X = MARGIN.left + MARGIN.right | ||||||
| const MARGIN_Y = MARGIN.top + MARGIN.bottom | const MARGIN_Y = MARGIN.top + MARGIN.bottom | ||||||
| 
 | 
 | ||||||
| const getTrackedAnswers = ( | const getAnswers = (contract: ChoiceContract) => { | ||||||
|   contract: FreeResponseContract | MultipleChoiceContract, |   const { answers, outcomeType } = contract | ||||||
|   topN: number |   const validAnswers = answers.filter( | ||||||
| ) => { |     (answer) => answer.id !== '0' || outcomeType === 'MULTIPLE_CHOICE' | ||||||
|   const { answers, outcomeType, totalBets } = contract |  | ||||||
|   const validAnswers = answers.filter((answer) => { |  | ||||||
|     return ( |  | ||||||
|       (answer.id !== '0' || outcomeType === 'MULTIPLE_CHOICE') && |  | ||||||
|       totalBets[answer.id] > 0.000000001 |  | ||||||
|   ) |   ) | ||||||
|   }) |  | ||||||
|   return sortBy( |   return sortBy( | ||||||
|     validAnswers, |     validAnswers, | ||||||
|     (answer) => -1 * getOutcomeProbability(contract, answer.id) |     (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 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( | ||||||
|  | @ -109,11 +62,14 @@ const getBetPoints = (answers: Answer[], bets: Bet[]) => { | ||||||
|     const sharesSquared = sum( |     const sharesSquared = sum( | ||||||
|       Object.values(sharesByOutcome).map((shares) => shares ** 2) |       Object.values(sharesByOutcome).map((shares) => shares ** 2) | ||||||
|     ) |     ) | ||||||
|     points.push({ |     const probs = answers.map((a) => sharesByOutcome[a.id] ** 2 / sharesSquared) | ||||||
|       x: new Date(bet.createdTime), | 
 | ||||||
|       y: answers.map((a) => sharesByOutcome[a.id] ** 2 / sharesSquared), |     if (topN != null && answers.length > topN) { | ||||||
|       obj: bet, |       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 |   return points | ||||||
| } | } | ||||||
|  | @ -141,17 +97,12 @@ const Legend = (props: { className?: string; items: LegendItem[] }) => { | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useChartAnswers( | export function useChartAnswers(contract: ChoiceContract) { | ||||||
|   contract: FreeResponseContract | MultipleChoiceContract |   return useMemo(() => getAnswers(contract), [contract]) | ||||||
| ) { |  | ||||||
|   return useMemo( |  | ||||||
|     () => getTrackedAnswers(contract, CATEGORY_COLORS.length), |  | ||||||
|     [contract] |  | ||||||
|   ) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const ChoiceContractChart = (props: { | export const ChoiceContractChart = (props: { | ||||||
|   contract: FreeResponseContract | MultipleChoiceContract |   contract: ChoiceContract | ||||||
|   bets: Bet[] |   bets: Bet[] | ||||||
|   width: number |   width: number | ||||||
|   height: number |   height: number | ||||||
|  | @ -160,18 +111,33 @@ export const ChoiceContractChart = (props: { | ||||||
|   const { contract, bets, width, height, onMouseOver } = props |   const { contract, bets, width, height, onMouseOver } = props | ||||||
|   const [start, end] = getDateRange(contract) |   const [start, end] = getDateRange(contract) | ||||||
|   const answers = useChartAnswers(contract) |   const answers = useChartAnswers(contract) | ||||||
|   const betPoints = useMemo(() => getBetPoints(answers, bets), [answers, bets]) |   const topN = Math.min(CHOICE_ANSWER_COLORS.length, answers.length) | ||||||
|   const data = useMemo( |   const betPoints = useMemo( | ||||||
|     () => [ |     () => getBetPoints(answers, bets, topN), | ||||||
|       { x: new Date(start), y: answers.map((_) => 0) }, |     [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, |       ...betPoints, | ||||||
|       { |       { | ||||||
|         x: new Date(end ?? Date.now() + DAY_MS), |         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( |   const rightmostDate = getRightmostVisibleDate( | ||||||
|     end, |     end, | ||||||
|     last(betPoints)?.x?.getTime(), |     last(betPoints)?.x?.getTime(), | ||||||
|  | @ -188,8 +154,8 @@ export const ChoiceContractChart = (props: { | ||||||
|       const d = xScale.invert(x) |       const d = xScale.invert(x) | ||||||
|       const legendItems = sortBy( |       const legendItems = sortBy( | ||||||
|         data.y.map((p, i) => ({ |         data.y.map((p, i) => ({ | ||||||
|           color: CATEGORY_COLORS[i], |           color: CHOICE_ALL_COLORS[i], | ||||||
|           label: answers[i].text, |           label: i === CHOICE_ANSWER_COLORS.length ? 'Other' : answers[i].text, | ||||||
|           value: formatPct(p), |           value: formatPct(p), | ||||||
|           p, |           p, | ||||||
|         })), |         })), | ||||||
|  | @ -221,7 +187,7 @@ export const ChoiceContractChart = (props: { | ||||||
|       yScale={yScale} |       yScale={yScale} | ||||||
|       yKind="percent" |       yKind="percent" | ||||||
|       data={data} |       data={data} | ||||||
|       colors={CATEGORY_COLORS} |       colors={CHOICE_ALL_COLORS} | ||||||
|       curve={curveStepAfter} |       curve={curveStepAfter} | ||||||
|       onMouseOver={onMouseOver} |       onMouseOver={onMouseOver} | ||||||
|       Tooltip={ChoiceTooltip} |       Tooltip={ChoiceTooltip} | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user