Implement numeric chart
This commit is contained in:
parent
b6471a425a
commit
221d2208df
|
@ -4,6 +4,7 @@ import { tradingAllowed } from 'web/lib/firebase/contracts'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import {
|
import {
|
||||||
BinaryContractChart,
|
BinaryContractChart,
|
||||||
|
NumericContractChart,
|
||||||
PseudoNumericContractChart,
|
PseudoNumericContractChart,
|
||||||
ChoiceContractChart,
|
ChoiceContractChart,
|
||||||
} from './contract-prob-graph'
|
} from './contract-prob-graph'
|
||||||
|
@ -28,7 +29,6 @@ import {
|
||||||
BinaryContract,
|
BinaryContract,
|
||||||
} from 'common/contract'
|
} from 'common/contract'
|
||||||
import { ContractDetails } from './contract-details'
|
import { ContractDetails } from './contract-details'
|
||||||
import { NumericGraph } from './numeric-graph'
|
|
||||||
|
|
||||||
const OverviewQuestion = (props: { text: string }) => (
|
const OverviewQuestion = (props: { text: string }) => (
|
||||||
<Linkify className="text-lg text-indigo-700 sm:text-2xl" text={props.text} />
|
<Linkify className="text-lg text-indigo-700 sm:text-2xl" text={props.text} />
|
||||||
|
@ -66,7 +66,7 @@ const NumericOverview = (props: { contract: NumericContract }) => {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<NumericGraph contract={contract} />
|
<NumericContractChart contract={contract} />
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { memo, useEffect, useMemo, useRef, useState } from 'react'
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import {
|
import {
|
||||||
getInitialProbability,
|
getInitialProbability,
|
||||||
getOutcomeProbability,
|
getOutcomeProbability,
|
||||||
getProbability,
|
getProbability,
|
||||||
} from 'common/calculate'
|
} from 'common/calculate'
|
||||||
|
import { getDpmOutcomeProbabilities } from 'common/calculate-dpm'
|
||||||
import {
|
import {
|
||||||
Contract,
|
Contract,
|
||||||
BinaryContract,
|
BinaryContract,
|
||||||
PseudoNumericContract,
|
PseudoNumericContract,
|
||||||
|
NumericContract,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
MultipleChoiceContract,
|
MultipleChoiceContract,
|
||||||
} from 'common/contract'
|
} from 'common/contract'
|
||||||
|
import { NUMERIC_GRAPH_COLOR } from 'common/numeric-constants'
|
||||||
import { useIsMobile } from 'web/hooks/use-is-mobile'
|
import { useIsMobile } from 'web/hooks/use-is-mobile'
|
||||||
import { formatLargeNumber } from 'common/util/format'
|
import { formatLargeNumber } from 'common/util/format'
|
||||||
import { range, sortBy, groupBy, sumBy } from 'lodash'
|
import { max, range, sortBy, groupBy, sum } from 'lodash'
|
||||||
import { useEvent } from 'web/hooks/use-event'
|
import { useEvent } from 'web/hooks/use-event'
|
||||||
|
|
||||||
import * as d3 from 'd3'
|
import * as d3 from 'd3'
|
||||||
|
@ -24,8 +27,9 @@ const MARGIN = { top: 20, right: 10, bottom: 20, left: 40 }
|
||||||
const MARGIN_X = MARGIN.right + MARGIN.left
|
const MARGIN_X = MARGIN.right + MARGIN.left
|
||||||
const MARGIN_Y = MARGIN.top + MARGIN.bottom
|
const MARGIN_Y = MARGIN.top + MARGIN.bottom
|
||||||
|
|
||||||
type MultiPoint = readonly [Date, number[]]
|
type MultiPoint = readonly [Date, number[]] // [time, [ordered outcome probs]]
|
||||||
type Point = readonly [Date, number]
|
type HistoryPoint = readonly [Date, number] // [time, number or percentage]
|
||||||
|
type NumericPoint = readonly [number, number] // [number, prob]
|
||||||
|
|
||||||
const useElementWidth = <T extends Element>(ref: React.RefObject<T>) => {
|
const useElementWidth = <T extends Element>(ref: React.RefObject<T>) => {
|
||||||
const [width, setWidth] = useState<number>()
|
const [width, setWidth] = useState<number>()
|
||||||
|
@ -85,6 +89,17 @@ const getTickValues = (min: number, max: number, n: number) => {
|
||||||
return [min, ...range(1, n - 1).map((i) => min + step * i), max]
|
return [min, ...range(1, n - 1).map((i) => min + step * i), max]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getNumericChartData = (contract: NumericContract) => {
|
||||||
|
const { totalShares, bucketCount, min, max } = contract
|
||||||
|
const bucketProbs = getDpmOutcomeProbabilities(totalShares)
|
||||||
|
|
||||||
|
const xs = range(bucketCount).map(
|
||||||
|
(i) => min + ((max - min) * i) / bucketCount
|
||||||
|
)
|
||||||
|
const probs = range(bucketCount).map((i) => bucketProbs[`${i}`])
|
||||||
|
return probs.map((prob, i) => [xs[i], prob] as const)
|
||||||
|
}
|
||||||
|
|
||||||
const getMultiChartData = (
|
const getMultiChartData = (
|
||||||
contract: FreeResponseContract | MultipleChoiceContract,
|
contract: FreeResponseContract | MultipleChoiceContract,
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
|
@ -121,7 +136,7 @@ const getMultiChartData = (
|
||||||
const { outcome, shares } = bet
|
const { outcome, shares } = bet
|
||||||
sharesByOutcome[outcome] += shares
|
sharesByOutcome[outcome] += shares
|
||||||
|
|
||||||
const sharesSquared = sumBy(
|
const sharesSquared = sum(
|
||||||
Object.values(sharesByOutcome).map((shares) => shares ** 2)
|
Object.values(sharesByOutcome).map((shares) => shares ** 2)
|
||||||
)
|
)
|
||||||
points.push([
|
points.push([
|
||||||
|
@ -148,7 +163,7 @@ const getMultiChartData = (
|
||||||
const getChartData = (
|
const getChartData = (
|
||||||
contract: BinaryContract | PseudoNumericContract,
|
contract: BinaryContract | PseudoNumericContract,
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
): Point[] => {
|
): HistoryPoint[] => {
|
||||||
const getY = (p: number) => {
|
const getY = (p: number) => {
|
||||||
if (contract.outcomeType === 'PSEUDO_NUMERIC') {
|
if (contract.outcomeType === 'PSEUDO_NUMERIC') {
|
||||||
const { min, max } = contract
|
const { min, max } = contract
|
||||||
|
@ -169,36 +184,32 @@ const getChartData = (
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const XAxis = (props: { w: number; h: number; scale: d3.AxisScale<Date> }) => {
|
const XAxis = <X extends d3.AxisDomain>(props: {
|
||||||
const { h, scale } = props
|
w: number
|
||||||
|
h: number
|
||||||
|
axis: d3.Axis<X>
|
||||||
|
}) => {
|
||||||
|
const { h, axis } = props
|
||||||
const axisRef = useRef<SVGGElement>(null)
|
const axisRef = useRef<SVGGElement>(null)
|
||||||
const [start, end] = scale.domain()
|
|
||||||
const fmt = getFormatterForDateRange(start, end)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (axisRef.current != null) {
|
if (axisRef.current != null) {
|
||||||
const axis = d3.axisBottom(scale).tickFormat(fmt)
|
|
||||||
d3.select(axisRef.current)
|
d3.select(axisRef.current)
|
||||||
.call(axis)
|
.call(axis)
|
||||||
.call((g) => g.select('.domain').remove())
|
.call((g) => g.select('.domain').remove())
|
||||||
}
|
}
|
||||||
})
|
}, [h, axis])
|
||||||
return <g ref={axisRef} transform={`translate(0, ${h})`} />
|
return <g ref={axisRef} transform={`translate(0, ${h})`} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const YAxis = (props: {
|
const YAxis = <Y extends d3.AxisDomain>(props: {
|
||||||
w: number
|
w: number
|
||||||
h: number
|
h: number
|
||||||
scale: d3.AxisScale<number>
|
axis: d3.Axis<Y>
|
||||||
pct?: boolean
|
|
||||||
}) => {
|
}) => {
|
||||||
const { w, h, scale, pct } = props
|
const { w, h, axis } = props
|
||||||
const axisRef = useRef<SVGGElement>(null)
|
const axisRef = useRef<SVGGElement>(null)
|
||||||
const [min, max] = scale.domain()
|
|
||||||
const tickValues = getTickValues(min, max, h < 200 ? 3 : 5)
|
|
||||||
const fmt = (n: number) => (pct ? d3.format('.0%')(n) : formatLargeNumber(n))
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (axisRef.current != null) {
|
if (axisRef.current != null) {
|
||||||
const axis = d3.axisLeft(scale).tickValues(tickValues).tickFormat(fmt)
|
|
||||||
d3.select(axisRef.current)
|
d3.select(axisRef.current)
|
||||||
.call(axis)
|
.call(axis)
|
||||||
.call((g) => g.select('.domain').remove())
|
.call((g) => g.select('.domain').remove())
|
||||||
|
@ -206,7 +217,7 @@ const YAxis = (props: {
|
||||||
g.selectAll('.tick line').attr('x2', w).attr('stroke-opacity', 0.1)
|
g.selectAll('.tick line').attr('x2', w).attr('stroke-opacity', 0.1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
}, [w, h, axis])
|
||||||
return <g ref={axisRef} />
|
return <g ref={axisRef} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,10 +226,11 @@ const LinePathInternal = <P,>(
|
||||||
data: P[]
|
data: P[]
|
||||||
px: number | ((p: P) => number)
|
px: number | ((p: P) => number)
|
||||||
py: number | ((p: P) => number)
|
py: number | ((p: P) => number)
|
||||||
|
curve: d3.CurveFactory
|
||||||
} & React.SVGProps<SVGPathElement>
|
} & React.SVGProps<SVGPathElement>
|
||||||
) => {
|
) => {
|
||||||
const { data, px, py, ...rest } = props
|
const { data, px, py, curve, ...rest } = props
|
||||||
const line = d3.line<P>(px, py).curve(d3.curveStepAfter)
|
const line = d3.line<P>(px, py).curve(curve)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
return <path {...rest} fill="none" d={line(data)!} />
|
return <path {...rest} fill="none" d={line(data)!} />
|
||||||
}
|
}
|
||||||
|
@ -230,40 +242,40 @@ const AreaPathInternal = <P,>(
|
||||||
px: number | ((p: P) => number)
|
px: number | ((p: P) => number)
|
||||||
py0: number | ((p: P) => number)
|
py0: number | ((p: P) => number)
|
||||||
py1: number | ((p: P) => number)
|
py1: number | ((p: P) => number)
|
||||||
|
curve: d3.CurveFactory
|
||||||
} & React.SVGProps<SVGPathElement>
|
} & React.SVGProps<SVGPathElement>
|
||||||
) => {
|
) => {
|
||||||
const { data, px, py0, py1, ...rest } = props
|
const { data, px, py0, py1, curve, ...rest } = props
|
||||||
const area = d3.area<P>(px, py0, py1).curve(d3.curveStepAfter)
|
const area = d3.area<P>(px, py0, py1).curve(curve)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
return <path {...rest} d={area(data)!} />
|
return <path {...rest} d={area(data)!} />
|
||||||
}
|
}
|
||||||
const AreaPath = memo(AreaPathInternal) as typeof AreaPathInternal
|
const AreaPath = memo(AreaPathInternal) as typeof AreaPathInternal
|
||||||
|
|
||||||
const TwoAxisChart = (props: {
|
const SVGChart = <X extends d3.AxisDomain, Y extends d3.AxisDomain>(props: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
w: number
|
w: number
|
||||||
h: number
|
h: number
|
||||||
xScale: d3.ScaleTime<number, number>
|
xAxis: d3.Axis<X>
|
||||||
yScale: d3.ScaleContinuousNumeric<number, number>
|
yAxis: d3.Axis<Y>
|
||||||
onMouseOver?: (ev: React.PointerEvent) => void
|
onMouseOver?: (ev: React.PointerEvent) => void
|
||||||
onMouseLeave?: (ev: React.PointerEvent) => void
|
onMouseLeave?: (ev: React.PointerEvent) => void
|
||||||
pct?: boolean
|
pct?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const { children, w, h, xScale, yScale, onMouseOver, onMouseLeave, pct } =
|
const { children, w, h, xAxis, yAxis, onMouseOver, onMouseLeave } = props
|
||||||
props
|
|
||||||
const innerW = w - MARGIN_X
|
const innerW = w - MARGIN_X
|
||||||
const innerH = h - MARGIN_Y
|
const innerH = h - MARGIN_Y
|
||||||
return (
|
return (
|
||||||
<svg className="w-full" width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
|
<svg className="w-full" width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
|
||||||
<g transform={`translate(${MARGIN.left}, ${MARGIN.top})`}>
|
<g transform={`translate(${MARGIN.left}, ${MARGIN.top})`}>
|
||||||
<XAxis scale={xScale} w={innerW} h={innerH} />
|
<XAxis axis={xAxis} w={innerW} h={innerH} />
|
||||||
<YAxis scale={yScale} w={innerW} h={innerH} pct={pct} />
|
<YAxis axis={yAxis} w={innerW} h={innerH} />
|
||||||
{children}
|
{children}
|
||||||
<rect
|
<rect
|
||||||
x="0"
|
x="0"
|
||||||
y="0"
|
y="0"
|
||||||
width={innerW}
|
width={w - MARGIN_X}
|
||||||
height={innerH}
|
height={h - MARGIN_Y}
|
||||||
fill="none"
|
fill="none"
|
||||||
pointerEvents="all"
|
pointerEvents="all"
|
||||||
onPointerEnter={onMouseOver}
|
onPointerEnter={onMouseOver}
|
||||||
|
@ -275,6 +287,54 @@ const TwoAxisChart = (props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const SingleValueDistributionChart = (props: {
|
||||||
|
data: NumericPoint[]
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
color: string
|
||||||
|
xScale: d3.ScaleContinuousNumeric<number, number>
|
||||||
|
yScale: d3.ScaleContinuousNumeric<number, number>
|
||||||
|
}) => {
|
||||||
|
const { color, data, xScale, yScale, w, h } = props
|
||||||
|
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||||
|
const px = useCallback((p: NumericPoint) => xScale(p[0]), [xScale])
|
||||||
|
const py0 = yScale(0)
|
||||||
|
const py1 = useCallback((p: NumericPoint) => yScale(p[1]), [yScale])
|
||||||
|
|
||||||
|
const formatX = (n: number) => formatLargeNumber(n)
|
||||||
|
const formatY = (n: number) => d3.format(',.2%')(n)
|
||||||
|
const xAxis = d3.axisBottom<number>(xScale).tickFormat(formatX)
|
||||||
|
const yAxis = d3.axisLeft<number>(yScale).tickFormat(formatY)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
ref={tooltipRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
className="pointer-events-none absolute z-10 whitespace-pre rounded border-2 border-black bg-slate-600/75 p-2 text-white"
|
||||||
|
/>
|
||||||
|
<SVGChart w={w} h={h} xAxis={xAxis} yAxis={yAxis}>
|
||||||
|
<LinePath
|
||||||
|
data={data}
|
||||||
|
curve={d3.curveLinear}
|
||||||
|
px={px}
|
||||||
|
py={py1}
|
||||||
|
stroke={color}
|
||||||
|
/>
|
||||||
|
<AreaPath
|
||||||
|
data={data}
|
||||||
|
curve={d3.curveLinear}
|
||||||
|
px={px}
|
||||||
|
py0={py0}
|
||||||
|
py1={py1}
|
||||||
|
fill={color}
|
||||||
|
opacity={0.3}
|
||||||
|
/>
|
||||||
|
</SVGChart>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const MultiValueHistoryChart = (props: {
|
export const MultiValueHistoryChart = (props: {
|
||||||
data: MultiPoint[]
|
data: MultiPoint[]
|
||||||
w: number
|
w: number
|
||||||
|
@ -287,10 +347,34 @@ export const MultiValueHistoryChart = (props: {
|
||||||
}) => {
|
}) => {
|
||||||
const { colors, data, xScale, yScale, labels, w, h, pct } = props
|
const { colors, data, xScale, yScale, labels, w, h, pct } = props
|
||||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||||
|
const px = useCallback(
|
||||||
|
(p: d3.SeriesPoint<MultiPoint>) => xScale(p.data[0]),
|
||||||
|
[xScale]
|
||||||
|
)
|
||||||
|
const py0 = useCallback(
|
||||||
|
(p: d3.SeriesPoint<MultiPoint>) => yScale(p[0]),
|
||||||
|
[yScale]
|
||||||
|
)
|
||||||
|
const py1 = useCallback(
|
||||||
|
(p: d3.SeriesPoint<MultiPoint>) => yScale(p[1]),
|
||||||
|
[yScale]
|
||||||
|
)
|
||||||
const stack = d3
|
const stack = d3
|
||||||
.stack<MultiPoint, number>()
|
.stack<MultiPoint, number>()
|
||||||
.keys(range(0, labels.length))
|
.keys(range(0, labels.length))
|
||||||
.value((p, o) => p[1][o])
|
.value(([_date, probs], o) => probs[o])
|
||||||
|
|
||||||
|
const [xStart, xEnd] = xScale.domain()
|
||||||
|
const fmtX = getFormatterForDateRange(xStart, xEnd)
|
||||||
|
const fmtY = (n: number) => (pct ? d3.format('.0%')(n) : formatLargeNumber(n))
|
||||||
|
|
||||||
|
const [min, max] = yScale.domain()
|
||||||
|
const tickValues = getTickValues(min, max, h < 200 ? 3 : 5)
|
||||||
|
const xAxis = d3.axisBottom<Date>(xScale).tickFormat(fmtX)
|
||||||
|
const yAxis = d3
|
||||||
|
.axisLeft<number>(yScale)
|
||||||
|
.tickValues(tickValues)
|
||||||
|
.tickFormat(fmtY)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
@ -299,31 +383,33 @@ export const MultiValueHistoryChart = (props: {
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
className="pointer-events-none absolute z-10 whitespace-pre rounded border-2 border-black bg-slate-600/75 p-2 text-white"
|
className="pointer-events-none absolute z-10 whitespace-pre rounded border-2 border-black bg-slate-600/75 p-2 text-white"
|
||||||
/>
|
/>
|
||||||
<TwoAxisChart w={w} h={h} xScale={xScale} yScale={yScale} pct={pct}>
|
<SVGChart w={w} h={h} xAxis={xAxis} yAxis={yAxis}>
|
||||||
{stack(data).map((s, i) => (
|
{stack(data).map((s, i) => (
|
||||||
<g key={s.key}>
|
<g key={s.key}>
|
||||||
<LinePath
|
<LinePath
|
||||||
data={s}
|
data={s}
|
||||||
px={(p) => xScale(p.data[0])}
|
px={px}
|
||||||
py={(p) => yScale(p[1])}
|
py={py1}
|
||||||
|
curve={d3.curveStepAfter}
|
||||||
stroke={colors[i]}
|
stroke={colors[i]}
|
||||||
/>
|
/>
|
||||||
<AreaPath
|
<AreaPath
|
||||||
data={s}
|
data={s}
|
||||||
px={(p) => xScale(p.data[0])}
|
px={px}
|
||||||
py0={(p) => yScale(p[0])}
|
py0={py0}
|
||||||
py1={(p) => yScale(p[1])}
|
py1={py1}
|
||||||
|
curve={d3.curveStepAfter}
|
||||||
fill={colors[i]}
|
fill={colors[i]}
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
))}
|
))}
|
||||||
</TwoAxisChart>
|
</SVGChart>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SingleValueHistoryChart = (props: {
|
export const SingleValueHistoryChart = (props: {
|
||||||
data: Point[]
|
data: HistoryPoint[]
|
||||||
w: number
|
w: number
|
||||||
h: number
|
h: number
|
||||||
color: string
|
color: string
|
||||||
|
@ -333,6 +419,10 @@ export const SingleValueHistoryChart = (props: {
|
||||||
}) => {
|
}) => {
|
||||||
const { color, data, xScale, yScale, pct, w, h } = props
|
const { color, data, xScale, yScale, pct, w, h } = props
|
||||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||||
|
const px = useCallback((p: HistoryPoint) => xScale(p[0]), [xScale])
|
||||||
|
const py0 = yScale(0)
|
||||||
|
const py1 = useCallback((p: HistoryPoint) => yScale(p[1]), [yScale])
|
||||||
|
|
||||||
const dates = useMemo(() => data.map(([d]) => d), [data])
|
const dates = useMemo(() => data.map(([d]) => d), [data])
|
||||||
const [startDate, endDate] = xScale.domain().map(dayjs)
|
const [startDate, endDate] = xScale.domain().map(dayjs)
|
||||||
const includeYear = !startDate.isSame(endDate, 'year')
|
const includeYear = !startDate.isSame(endDate, 'year')
|
||||||
|
@ -343,6 +433,14 @@ export const SingleValueHistoryChart = (props: {
|
||||||
const formatY = (n: number) =>
|
const formatY = (n: number) =>
|
||||||
pct ? d3.format('.0%')(n) : formatLargeNumber(n)
|
pct ? d3.format('.0%')(n) : formatLargeNumber(n)
|
||||||
|
|
||||||
|
const [min, max] = yScale.domain()
|
||||||
|
const tickValues = getTickValues(min, max, h < 200 ? 3 : 5)
|
||||||
|
const xAxis = d3.axisBottom<Date>(xScale).tickFormat(formatX)
|
||||||
|
const yAxis = d3
|
||||||
|
.axisLeft<number>(yScale)
|
||||||
|
.tickValues(tickValues)
|
||||||
|
.tickFormat(formatY)
|
||||||
|
|
||||||
const onMouseOver = useEvent((event: React.PointerEvent) => {
|
const onMouseOver = useEvent((event: React.PointerEvent) => {
|
||||||
const tt = tooltipRef.current
|
const tt = tooltipRef.current
|
||||||
if (tt != null) {
|
if (tt != null) {
|
||||||
|
@ -370,30 +468,31 @@ export const SingleValueHistoryChart = (props: {
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
className="pointer-events-none absolute z-10 whitespace-pre rounded border-2 border-black bg-slate-600/75 p-2 text-white"
|
className="pointer-events-none absolute z-10 whitespace-pre rounded border-2 border-black bg-slate-600/75 p-2 text-white"
|
||||||
/>
|
/>
|
||||||
<TwoAxisChart
|
<SVGChart
|
||||||
w={w}
|
w={w}
|
||||||
h={h}
|
h={h}
|
||||||
xScale={xScale}
|
xAxis={xAxis}
|
||||||
yScale={yScale}
|
yAxis={yAxis}
|
||||||
pct={pct}
|
|
||||||
onMouseOver={onMouseOver}
|
onMouseOver={onMouseOver}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
>
|
>
|
||||||
<LinePath
|
<LinePath
|
||||||
data={data}
|
data={data}
|
||||||
px={(p) => xScale(p[0])}
|
px={px}
|
||||||
py={(p) => yScale(p[1])}
|
py={py1}
|
||||||
|
curve={d3.curveStepAfter}
|
||||||
stroke={color}
|
stroke={color}
|
||||||
/>
|
/>
|
||||||
<AreaPath
|
<AreaPath
|
||||||
data={data}
|
data={data}
|
||||||
px={(p) => xScale(p[0])}
|
px={px}
|
||||||
py0={yScale(0)}
|
py0={py0}
|
||||||
py1={(p) => yScale(p[1])}
|
py1={py1}
|
||||||
|
curve={d3.curveStepAfter}
|
||||||
fill={color}
|
fill={color}
|
||||||
fillOpacity={0.3}
|
opacity={0.3}
|
||||||
/>
|
/>
|
||||||
</TwoAxisChart>
|
</SVGChart>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -412,6 +511,8 @@ export const ContractChart = (props: {
|
||||||
case 'FREE_RESPONSE':
|
case 'FREE_RESPONSE':
|
||||||
case 'MULTIPLE_CHOICE':
|
case 'MULTIPLE_CHOICE':
|
||||||
return <ChoiceContractChart {...{ ...props, contract }} />
|
return <ChoiceContractChart {...{ ...props, contract }} />
|
||||||
|
case 'NUMERIC':
|
||||||
|
return <NumericContractChart {...{ ...props, contract }} />
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -426,6 +527,38 @@ const getFormatterForDateRange = (start: Date, end: Date) => {
|
||||||
return (d: Date) => formatDate(d, opts)
|
return (d: Date) => formatDate(d, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const NumericContractChart = (props: {
|
||||||
|
contract: NumericContract
|
||||||
|
height?: number
|
||||||
|
}) => {
|
||||||
|
const { contract } = props
|
||||||
|
const data = useMemo(() => getNumericChartData(contract), [contract])
|
||||||
|
const isMobile = useIsMobile(800)
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const width = useElementWidth(containerRef) ?? 0
|
||||||
|
const height = props.height ?? isMobile ? 150 : 250
|
||||||
|
const maxY = max(data.map((d) => d[1])) as number
|
||||||
|
const xScale = d3.scaleLinear(
|
||||||
|
[contract.min, contract.max],
|
||||||
|
[0, width - MARGIN_X]
|
||||||
|
)
|
||||||
|
const yScale = d3.scaleLinear([0, maxY], [height - MARGIN_Y, 0])
|
||||||
|
return (
|
||||||
|
<div ref={containerRef}>
|
||||||
|
{width && (
|
||||||
|
<SingleValueDistributionChart
|
||||||
|
w={width}
|
||||||
|
h={height}
|
||||||
|
xScale={xScale}
|
||||||
|
yScale={yScale}
|
||||||
|
data={data}
|
||||||
|
color={NUMERIC_GRAPH_COLOR}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const PseudoNumericContractChart = (props: {
|
export const PseudoNumericContractChart = (props: {
|
||||||
contract: PseudoNumericContract
|
contract: PseudoNumericContract
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
|
@ -449,7 +582,7 @@ export const PseudoNumericContractChart = (props: {
|
||||||
xScale={xScale}
|
xScale={xScale}
|
||||||
yScale={yScale}
|
yScale={yScale}
|
||||||
data={data}
|
data={data}
|
||||||
color="#5fa5f9"
|
color={NUMERIC_GRAPH_COLOR}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,99 +0,0 @@
|
||||||
import { DatumValue } from '@nivo/core'
|
|
||||||
import { Point, ResponsiveLine } from '@nivo/line'
|
|
||||||
import { NUMERIC_GRAPH_COLOR } from 'common/numeric-constants'
|
|
||||||
import { memo } from 'react'
|
|
||||||
import { range } from 'lodash'
|
|
||||||
import { getDpmOutcomeProbabilities } from '../../../common/calculate-dpm'
|
|
||||||
import { NumericContract } from '../../../common/contract'
|
|
||||||
import { useWindowSize } from '../../hooks/use-window-size'
|
|
||||||
import { Col } from '../layout/col'
|
|
||||||
import { formatLargeNumber } from 'common/util/format'
|
|
||||||
|
|
||||||
export const NumericGraph = memo(function NumericGraph(props: {
|
|
||||||
contract: NumericContract
|
|
||||||
height?: number
|
|
||||||
}) {
|
|
||||||
const { contract, height } = props
|
|
||||||
const { totalShares, bucketCount, min, max } = contract
|
|
||||||
|
|
||||||
const bucketProbs = getDpmOutcomeProbabilities(totalShares)
|
|
||||||
|
|
||||||
const xs = range(bucketCount).map(
|
|
||||||
(i) => min + ((max - min) * i) / bucketCount
|
|
||||||
)
|
|
||||||
const probs = range(bucketCount).map((i) => bucketProbs[`${i}`] * 100)
|
|
||||||
const points = probs.map((prob, i) => ({ x: xs[i], y: prob }))
|
|
||||||
const maxProb = Math.max(...probs)
|
|
||||||
const data = [{ id: 'Probability', data: points, color: NUMERIC_GRAPH_COLOR }]
|
|
||||||
|
|
||||||
const yTickValues = [
|
|
||||||
0,
|
|
||||||
0.25 * maxProb,
|
|
||||||
0.5 & maxProb,
|
|
||||||
0.75 * maxProb,
|
|
||||||
maxProb,
|
|
||||||
]
|
|
||||||
|
|
||||||
const { width } = useWindowSize()
|
|
||||||
|
|
||||||
const numXTickValues = !width || width < 800 ? 2 : 5
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="w-full overflow-hidden"
|
|
||||||
style={{ height: height ?? (!width || width >= 800 ? 350 : 250) }}
|
|
||||||
>
|
|
||||||
<ResponsiveLine
|
|
||||||
data={data}
|
|
||||||
yScale={{ min: 0, max: maxProb, type: 'linear' }}
|
|
||||||
yFormat={formatPercent}
|
|
||||||
axisLeft={{
|
|
||||||
tickValues: yTickValues,
|
|
||||||
format: formatPercent,
|
|
||||||
}}
|
|
||||||
xScale={{
|
|
||||||
type: 'linear',
|
|
||||||
min: min,
|
|
||||||
max: max,
|
|
||||||
}}
|
|
||||||
xFormat={(d) => `${formatLargeNumber(+d, 3)}`}
|
|
||||||
axisBottom={{
|
|
||||||
tickValues: numXTickValues,
|
|
||||||
format: (d) => `${formatLargeNumber(+d, 3)}`,
|
|
||||||
}}
|
|
||||||
colors={{ datum: 'color' }}
|
|
||||||
pointSize={0}
|
|
||||||
enableSlices="x"
|
|
||||||
sliceTooltip={({ slice }) => {
|
|
||||||
const point = slice.points[0]
|
|
||||||
return <Tooltip point={point} />
|
|
||||||
}}
|
|
||||||
enableGridX={!!width && width >= 800}
|
|
||||||
enableArea
|
|
||||||
margin={{ top: 20, right: 28, bottom: 22, left: 50 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
function formatPercent(y: DatumValue) {
|
|
||||||
const p = Math.round(+y * 100) / 100
|
|
||||||
return `${p}%`
|
|
||||||
}
|
|
||||||
|
|
||||||
function Tooltip(props: { point: Point }) {
|
|
||||||
const { point } = props
|
|
||||||
return (
|
|
||||||
<Col className="border border-gray-300 bg-white py-2 px-3">
|
|
||||||
<div
|
|
||||||
className="pb-1"
|
|
||||||
style={{
|
|
||||||
color: point.serieColor,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<strong>{point.serieId}</strong> {point.data.yFormatted}
|
|
||||||
</div>
|
|
||||||
<div>{formatLargeNumber(+point.data.x)}</div>
|
|
||||||
</Col>
|
|
||||||
)
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user