diff --git a/web/components/charts/contract/choice.tsx b/web/components/charts/contract/choice.tsx
index 0d3c457f..635b0475 100644
--- a/web/components/charts/contract/choice.tsx
+++ b/web/components/charts/contract/choice.tsx
@@ -14,27 +14,25 @@ const getMultiChartData = (
contract: FreeResponseContract | MultipleChoiceContract,
bets: Bet[]
) => {
- const { totalBets, outcomeType } = contract
+ const { answers, totalBets, outcomeType } = contract
const sortedBets = sortBy(bets, (b) => b.createdTime)
const betsByOutcome = groupBy(sortedBets, (bet) => bet.outcome)
- const outcomes = Object.keys(betsByOutcome).filter((outcome) => {
+ const validAnswers = answers.filter((answer) => {
const maxProb = Math.max(
- ...betsByOutcome[outcome].map((bet) => bet.probAfter)
+ ...betsByOutcome[answer.id].map((bet) => bet.probAfter)
)
return (
- (outcome !== '0' || outcomeType === 'MULTIPLE_CHOICE') &&
+ (answer.id !== '0' || outcomeType === 'MULTIPLE_CHOICE') &&
maxProb > 0.02 &&
- totalBets[outcome] > 0.000000001
+ totalBets[answer.id] > 0.000000001
)
})
- const trackedOutcomes = sortBy(
- outcomes,
- (outcome) => -1 * getOutcomeProbability(contract, outcome)
- )
- .slice(0, 10)
- .reverse()
+ const trackedAnswers = sortBy(
+ validAnswers,
+ (answer) => -1 * getOutcomeProbability(contract, answer.id)
+ ).slice(0, 10)
const points: MultiPoint[] = []
@@ -51,23 +49,26 @@ const getMultiChartData = (
)
points.push([
new Date(bet.createdTime),
- trackedOutcomes.map(
- (outcome) => sharesByOutcome[outcome] ** 2 / sharesSquared
+ trackedAnswers.map(
+ (answer) => sharesByOutcome[answer.id] ** 2 / sharesSquared
),
])
}
const allPoints: MultiPoint[] = [
- [new Date(contract.createdTime), trackedOutcomes.map((_) => 0)],
+ [new Date(contract.createdTime), trackedAnswers.map((_) => 0)],
...points,
[
new Date(Date.now()),
- trackedOutcomes.map((outcome) =>
- getOutcomeProbability(contract, outcome)
+ trackedAnswers.map((answer) =>
+ getOutcomeProbability(contract, answer.id)
),
],
]
- return { points: allPoints, labels: trackedOutcomes }
+ return {
+ points: allPoints,
+ labels: trackedAnswers.map((answer) => answer.text),
+ }
}
export const ChoiceContractChart = (props: {
diff --git a/web/components/charts/generic-charts.tsx b/web/components/charts/generic-charts.tsx
index bbcb0685..897f2a40 100644
--- a/web/components/charts/generic-charts.tsx
+++ b/web/components/charts/generic-charts.tsx
@@ -1,4 +1,4 @@
-import { useRef, useCallback } from 'react'
+import { useCallback, useMemo, useState } from 'react'
import {
axisBottom,
axisLeft,
@@ -7,6 +7,7 @@ import {
curveStepAfter,
pointer,
stack,
+ stackOrderReverse,
ScaleTime,
ScaleContinuousNumeric,
SeriesPoint,
@@ -14,13 +15,21 @@ import {
import { range } from 'lodash'
import dayjs from 'dayjs'
-import { SVGChart, AreaPath, AreaWithTopStroke } from './helpers'
+import {
+ SVGChart,
+ AreaPath,
+ AreaWithTopStroke,
+ ChartTooltip,
+ TooltipPosition,
+} from './helpers'
import { formatLargeNumber } from 'common/util/format'
import { useEvent } from 'web/hooks/use-event'
+import { Row } from 'web/components/layout/row'
export type MultiPoint = readonly [Date, number[]] // [time, [ordered outcome probs]]
export type HistoryPoint = readonly [Date, number] // [time, number or percentage]
-export type DistributionPoint = readonly [number, number] // [number, prob]
+export type DistributionPoint = readonly [number, number] // [outcome amount, prob]
+export type PositionValue
= TooltipPosition & { p: P }
const formatPct = (n: number, digits?: number) => {
return `${(n * 100).toFixed(digits ?? 0)}%`
@@ -66,6 +75,28 @@ const getTickValues = (min: number, max: number, n: number) => {
return [min, ...range(1, n - 1).map((i) => min + step * i), max]
}
+type LegendItem = { color: string; label: string; value?: string }
+
+const Legend = (props: { className?: string; items: LegendItem[] }) => {
+ const { items, className } = props
+ return (
+
+ {items.map((item) => (
+ -
+
+
+ {item.label}
+
+ {item.value}
+
+ ))}
+
+ )
+}
+
export const SingleValueDistributionChart = (props: {
data: DistributionPoint[]
w: number
@@ -75,46 +106,40 @@ export const SingleValueDistributionChart = (props: {
yScale: ScaleContinuousNumeric
}) => {
const { color, data, xScale, yScale, w, h } = props
- const tooltipRef = useRef(null)
+ const [mouseState, setMouseState] =
+ useState>()
const px = useCallback((p: DistributionPoint) => xScale(p[0]), [xScale])
const py0 = yScale(0)
const py1 = useCallback((p: DistributionPoint) => yScale(p[1]), [yScale])
-
- const formatX = (n: number) => formatLargeNumber(n)
- const formatY = (n: number) => formatPct(n, 2)
-
- const xAxis = axisBottom(xScale).tickFormat(formatX)
- const yAxis = axisLeft(yScale).tickFormat(formatY)
-
const xBisector = bisector((p: DistributionPoint) => p[0])
+
+ const { fmtX, fmtY, xAxis, yAxis } = useMemo(() => {
+ const fmtX = (n: number) => formatLargeNumber(n)
+ const fmtY = (n: number) => formatPct(n, 2)
+ const xAxis = axisBottom(xScale).tickFormat(fmtX)
+ const yAxis = axisLeft(yScale).tickFormat(fmtY)
+ return { fmtX, fmtY, xAxis, yAxis }
+ }, [xScale, yScale])
+
const onMouseOver = useEvent((event: React.PointerEvent) => {
- const tt = tooltipRef.current
- if (tt != null) {
- const [mouseX, mouseY] = pointer(event)
- const queryX = xScale.invert(mouseX)
- const [_x, y] = data[xBisector.center(data, queryX)]
- tt.innerHTML = `${formatY(y)} ${formatX(queryX)}`
- tt.style.display = 'block'
- tt.style.top = mouseY - 10 + 'px'
- tt.style.left = mouseX + 20 + 'px'
- }
+ const [mouseX, mouseY] = pointer(event)
+ const queryX = xScale.invert(mouseX)
+ const [_x, y] = data[xBisector.center(data, queryX)]
+ setMouseState({ top: mouseY - 10, left: mouseX + 60, p: [queryX, y] })
})
const onMouseLeave = useEvent(() => {
- const tt = tooltipRef.current
- if (tt != null) {
- tt.style.display = 'none'
- }
+ setMouseState(undefined)
})
return (
-
+ {mouseState && (
+
+ {fmtY(mouseState.p[1])} {fmtX(mouseState.p[0])}
+
+ )}
{
const { colors, data, xScale, yScale, labels, w, h, pct } = props
- const tooltipRef = useRef(null)
+ const [mouseState, setMouseState] = useState>()
- const px = useCallback(
- (p: SeriesPoint) => xScale(p.data[0]),
- [xScale]
- )
- const py0 = useCallback(
- (p: SeriesPoint) => yScale(p[0]),
- [yScale]
- )
- const py1 = useCallback(
- (p: SeriesPoint) => yScale(p[1]),
- [yScale]
- )
+ type SP = SeriesPoint
+ const px = useCallback((p: SP) => xScale(p.data[0]), [xScale])
+ const py0 = useCallback((p: SP) => yScale(p[0]), [yScale])
+ const py1 = useCallback((p: SP) => yScale(p[1]), [yScale])
+ const xBisector = bisector((p: MultiPoint) => p[0])
- const [xStart, xEnd] = xScale.domain()
- const fmtX = getFormatterForDateRange(xStart, xEnd)
- const fmtY = (n: number) => (pct ? formatPct(n, 0) : formatLargeNumber(n))
+ const { fmtX, fmtY, xAxis, yAxis, series } = useMemo(() => {
+ const [start, end] = xScale.domain()
+ const fmtX = getFormatterForDateRange(start, end)
+ const fmtY = (n: number) => (pct ? formatPct(n, 0) : formatLargeNumber(n))
- const [min, max] = yScale.domain()
- const tickValues = getTickValues(min, max, h < 200 ? 3 : 5)
+ const [min, max] = yScale.domain()
+ const tickValues = getTickValues(min, max, h < 200 ? 3 : 5)
+ const xAxis = axisBottom(xScale).tickFormat(fmtX)
+ const yAxis = axisLeft(yScale)
+ .tickValues(tickValues)
+ .tickFormat(fmtY)
- const xAxis = axisBottom(xScale).tickFormat(fmtX)
- const yAxis = axisLeft(yScale).tickValues(tickValues).tickFormat(fmtY)
+ const d3Stack = stack()
+ .keys(range(0, labels.length))
+ .value(([_date, probs], o) => probs[o])
+ .order(stackOrderReverse)
+ const series = d3Stack(data)
+ return { fmtX, fmtY, xAxis, yAxis, series }
+ }, [h, pct, xScale, yScale, data, labels.length])
- const d3Stack = stack()
- .keys(range(0, labels.length))
- .value(([_date, probs], o) => probs[o])
+ const onMouseOver = useEvent((event: React.PointerEvent) => {
+ const [mouseX, mouseY] = pointer(event)
+ const queryX = xScale.invert(mouseX)
+ const [_x, ys] = data[xBisector.center(data, queryX)]
+ setMouseState({ top: mouseY - 10, left: mouseX + 60, p: [queryX, ys] })
+ })
+
+ const onMouseLeave = useEvent(() => {
+ setMouseState(undefined)
+ })
return (
-
-
- {d3Stack(data).map((s, i) => (
+ {mouseState && (
+
+ {fmtX(mouseState.p[0])}
+
+ )}
+
+ {series.map((s, i) => (
{
const { color, data, xScale, yScale, pct, w, h } = props
- const tooltipRef = useRef(null)
+ const [mouseState, setMouseState] = useState>()
const px = useCallback((p: HistoryPoint) => xScale(p[0]), [xScale])
const py0 = yScale(0)
const py1 = useCallback((p: HistoryPoint) => yScale(p[1]), [yScale])
- const [start, end] = xScale.domain()
- const formatX = getFormatterForDateRange(start, end)
- const formatY = (n: number) => (pct ? formatPct(n, 0) : formatLargeNumber(n))
+ const { fmtX, fmtY, xAxis, yAxis } = useMemo(() => {
+ const [start, end] = xScale.domain()
+ const fmtX = getFormatterForDateRange(start, end)
+ const fmtY = (n: number) => (pct ? formatPct(n, 0) : formatLargeNumber(n))
- const [min, max] = yScale.domain()
- const tickValues = getTickValues(min, max, h < 200 ? 3 : 5)
-
- const xAxis = axisBottom(xScale).tickFormat(formatX)
- const yAxis = axisLeft(yScale)
- .tickValues(tickValues)
- .tickFormat(formatY)
+ const [min, max] = yScale.domain()
+ const tickValues = getTickValues(min, max, h < 200 ? 3 : 5)
+ const xAxis = axisBottom(xScale).tickFormat(fmtX)
+ const yAxis = axisLeft(yScale)
+ .tickValues(tickValues)
+ .tickFormat(fmtY)
+ return { fmtX, fmtY, xAxis, yAxis }
+ }, [h, pct, xScale, yScale])
const xBisector = bisector((p: HistoryPoint) => p[0])
const onMouseOver = useEvent((event: React.PointerEvent) => {
- const tt = tooltipRef.current
- if (tt != null) {
- const [mouseX, mouseY] = pointer(event)
- const queryX = xScale.invert(mouseX)
- const [_x, y] = data[xBisector.center(data, queryX)]
- tt.innerHTML = `${formatY(y)} ${formatX(queryX)}`
- tt.style.display = 'block'
- tt.style.top = mouseY - 10 + 'px'
- tt.style.left = mouseX + 20 + 'px'
- }
+ const [mouseX, mouseY] = pointer(event)
+ const queryX = xScale.invert(mouseX)
+ const [_x, y] = data[xBisector.center(data, queryX)]
+ setMouseState({ top: mouseY - 10, left: mouseX + 60, p: [queryX, y] })
})
const onMouseLeave = useEvent(() => {
- const tt = tooltipRef.current
- if (tt != null) {
- tt.style.display = 'none'
- }
+ setMouseState(undefined)
})
return (
-
+ {mouseState && (
+
+ {fmtY(mouseState.p[1])} {fmtX(mouseState.p[0])}
+
+ )}
(props: {
- w: number
- h: number
- axis: Axis
-}) => {
+export const XAxis = (props: { w: number; h: number; axis: Axis }) => {
const { h, axis } = props
const axisRef = useRef(null)
useEffect(() => {
@@ -33,11 +21,7 @@ export const XAxis = (props: {
return
}
-export const YAxis = (props: {
- w: number
- h: number
- axis: Axis
-}) => {
+export const YAxis = (props: { w: number; h: number; axis: Axis }) => {
const { w, h, axis } = props
const axisRef = useRef(null)
useEffect(() => {
@@ -109,7 +93,7 @@ export const AreaWithTopStroke = (props: {
)
}
-export const SVGChart = (props: {
+export const SVGChart = (props: {
children: ReactNode
w: number
h: number
@@ -131,8 +115,8 @@ export const SVGChart = (props: {
(props: {
)
}
+export type TooltipPosition = { top: number; left: number }
+
+export const ChartTooltip = (
+ props: TooltipPosition & { children: React.ReactNode }
+) => {
+ const { top, left, children } = props
+ return (
+
+ {children}
+
+ )
+}
+
export const getDateRange = (contract: Contract) => {
const { createdTime, closeTime, resolutionTime } = contract
const now = Date.now()