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 {
|
||||
BinaryContractChart,
|
||||
NumericContractChart,
|
||||
PseudoNumericContractChart,
|
||||
ChoiceContractChart,
|
||||
} from './contract-prob-graph'
|
||||
|
@ -28,7 +29,6 @@ import {
|
|||
BinaryContract,
|
||||
} from 'common/contract'
|
||||
import { ContractDetails } from './contract-details'
|
||||
import { NumericGraph } from './numeric-graph'
|
||||
|
||||
const OverviewQuestion = (props: { text: string }) => (
|
||||
<Linkify className="text-lg text-indigo-700 sm:text-2xl" text={props.text} />
|
||||
|
@ -66,7 +66,7 @@ const NumericOverview = (props: { contract: NumericContract }) => {
|
|||
contract={contract}
|
||||
/>
|
||||
</Col>
|
||||
<NumericGraph contract={contract} />
|
||||
<NumericContractChart contract={contract} />
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
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 {
|
||||
getInitialProbability,
|
||||
getOutcomeProbability,
|
||||
getProbability,
|
||||
} from 'common/calculate'
|
||||
import { getDpmOutcomeProbabilities } from 'common/calculate-dpm'
|
||||
import {
|
||||
Contract,
|
||||
BinaryContract,
|
||||
PseudoNumericContract,
|
||||
NumericContract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
} from 'common/contract'
|
||||
import { NUMERIC_GRAPH_COLOR } from 'common/numeric-constants'
|
||||
import { useIsMobile } from 'web/hooks/use-is-mobile'
|
||||
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 * 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_Y = MARGIN.top + MARGIN.bottom
|
||||
|
||||
type MultiPoint = readonly [Date, number[]]
|
||||
type Point = readonly [Date, number]
|
||||
type MultiPoint = readonly [Date, number[]] // [time, [ordered outcome probs]]
|
||||
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 [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]
|
||||
}
|
||||
|
||||
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 = (
|
||||
contract: FreeResponseContract | MultipleChoiceContract,
|
||||
bets: Bet[]
|
||||
|
@ -121,7 +136,7 @@ const getMultiChartData = (
|
|||
const { outcome, shares } = bet
|
||||
sharesByOutcome[outcome] += shares
|
||||
|
||||
const sharesSquared = sumBy(
|
||||
const sharesSquared = sum(
|
||||
Object.values(sharesByOutcome).map((shares) => shares ** 2)
|
||||
)
|
||||
points.push([
|
||||
|
@ -148,7 +163,7 @@ const getMultiChartData = (
|
|||
const getChartData = (
|
||||
contract: BinaryContract | PseudoNumericContract,
|
||||
bets: Bet[]
|
||||
): Point[] => {
|
||||
): HistoryPoint[] => {
|
||||
const getY = (p: number) => {
|
||||
if (contract.outcomeType === 'PSEUDO_NUMERIC') {
|
||||
const { min, max } = contract
|
||||
|
@ -169,36 +184,32 @@ const getChartData = (
|
|||
]
|
||||
}
|
||||
|
||||
const XAxis = (props: { w: number; h: number; scale: d3.AxisScale<Date> }) => {
|
||||
const { h, scale } = props
|
||||
const XAxis = <X extends d3.AxisDomain>(props: {
|
||||
w: number
|
||||
h: number
|
||||
axis: d3.Axis<X>
|
||||
}) => {
|
||||
const { h, axis } = props
|
||||
const axisRef = useRef<SVGGElement>(null)
|
||||
const [start, end] = scale.domain()
|
||||
const fmt = getFormatterForDateRange(start, end)
|
||||
useEffect(() => {
|
||||
if (axisRef.current != null) {
|
||||
const axis = d3.axisBottom(scale).tickFormat(fmt)
|
||||
d3.select(axisRef.current)
|
||||
.call(axis)
|
||||
.call((g) => g.select('.domain').remove())
|
||||
}
|
||||
})
|
||||
}, [h, axis])
|
||||
return <g ref={axisRef} transform={`translate(0, ${h})`} />
|
||||
}
|
||||
|
||||
const YAxis = (props: {
|
||||
const YAxis = <Y extends d3.AxisDomain>(props: {
|
||||
w: number
|
||||
h: number
|
||||
scale: d3.AxisScale<number>
|
||||
pct?: boolean
|
||||
axis: d3.Axis<Y>
|
||||
}) => {
|
||||
const { w, h, scale, pct } = props
|
||||
const { w, h, axis } = props
|
||||
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(() => {
|
||||
if (axisRef.current != null) {
|
||||
const axis = d3.axisLeft(scale).tickValues(tickValues).tickFormat(fmt)
|
||||
d3.select(axisRef.current)
|
||||
.call(axis)
|
||||
.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)
|
||||
)
|
||||
}
|
||||
})
|
||||
}, [w, h, axis])
|
||||
return <g ref={axisRef} />
|
||||
}
|
||||
|
||||
|
@ -215,10 +226,11 @@ const LinePathInternal = <P,>(
|
|||
data: P[]
|
||||
px: number | ((p: P) => number)
|
||||
py: number | ((p: P) => number)
|
||||
curve: d3.CurveFactory
|
||||
} & React.SVGProps<SVGPathElement>
|
||||
) => {
|
||||
const { data, px, py, ...rest } = props
|
||||
const line = d3.line<P>(px, py).curve(d3.curveStepAfter)
|
||||
const { data, px, py, curve, ...rest } = props
|
||||
const line = d3.line<P>(px, py).curve(curve)
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return <path {...rest} fill="none" d={line(data)!} />
|
||||
}
|
||||
|
@ -230,40 +242,40 @@ const AreaPathInternal = <P,>(
|
|||
px: number | ((p: P) => number)
|
||||
py0: number | ((p: P) => number)
|
||||
py1: number | ((p: P) => number)
|
||||
curve: d3.CurveFactory
|
||||
} & React.SVGProps<SVGPathElement>
|
||||
) => {
|
||||
const { data, px, py0, py1, ...rest } = props
|
||||
const area = d3.area<P>(px, py0, py1).curve(d3.curveStepAfter)
|
||||
const { data, px, py0, py1, curve, ...rest } = props
|
||||
const area = d3.area<P>(px, py0, py1).curve(curve)
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return <path {...rest} d={area(data)!} />
|
||||
}
|
||||
const AreaPath = memo(AreaPathInternal) as typeof AreaPathInternal
|
||||
|
||||
const TwoAxisChart = (props: {
|
||||
const SVGChart = <X extends d3.AxisDomain, Y extends d3.AxisDomain>(props: {
|
||||
children: React.ReactNode
|
||||
w: number
|
||||
h: number
|
||||
xScale: d3.ScaleTime<number, number>
|
||||
yScale: d3.ScaleContinuousNumeric<number, number>
|
||||
xAxis: d3.Axis<X>
|
||||
yAxis: d3.Axis<Y>
|
||||
onMouseOver?: (ev: React.PointerEvent) => void
|
||||
onMouseLeave?: (ev: React.PointerEvent) => void
|
||||
pct?: boolean
|
||||
}) => {
|
||||
const { children, w, h, xScale, yScale, onMouseOver, onMouseLeave, pct } =
|
||||
props
|
||||
const { children, w, h, xAxis, yAxis, onMouseOver, onMouseLeave } = props
|
||||
const innerW = w - MARGIN_X
|
||||
const innerH = h - MARGIN_Y
|
||||
return (
|
||||
<svg className="w-full" width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
|
||||
<g transform={`translate(${MARGIN.left}, ${MARGIN.top})`}>
|
||||
<XAxis scale={xScale} w={innerW} h={innerH} />
|
||||
<YAxis scale={yScale} w={innerW} h={innerH} pct={pct} />
|
||||
<XAxis axis={xAxis} w={innerW} h={innerH} />
|
||||
<YAxis axis={yAxis} w={innerW} h={innerH} />
|
||||
{children}
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width={innerW}
|
||||
height={innerH}
|
||||
width={w - MARGIN_X}
|
||||
height={h - MARGIN_Y}
|
||||
fill="none"
|
||||
pointerEvents="all"
|
||||
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: {
|
||||
data: MultiPoint[]
|
||||
w: number
|
||||
|
@ -287,10 +347,34 @@ export const MultiValueHistoryChart = (props: {
|
|||
}) => {
|
||||
const { colors, data, xScale, yScale, labels, w, h, pct } = props
|
||||
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
|
||||
.stack<MultiPoint, number>()
|
||||
.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 (
|
||||
<div className="relative">
|
||||
|
@ -299,31 +383,33 @@ export const MultiValueHistoryChart = (props: {
|
|||
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"
|
||||
/>
|
||||
<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) => (
|
||||
<g key={s.key}>
|
||||
<LinePath
|
||||
data={s}
|
||||
px={(p) => xScale(p.data[0])}
|
||||
py={(p) => yScale(p[1])}
|
||||
px={px}
|
||||
py={py1}
|
||||
curve={d3.curveStepAfter}
|
||||
stroke={colors[i]}
|
||||
/>
|
||||
<AreaPath
|
||||
data={s}
|
||||
px={(p) => xScale(p.data[0])}
|
||||
py0={(p) => yScale(p[0])}
|
||||
py1={(p) => yScale(p[1])}
|
||||
px={px}
|
||||
py0={py0}
|
||||
py1={py1}
|
||||
curve={d3.curveStepAfter}
|
||||
fill={colors[i]}
|
||||
/>
|
||||
</g>
|
||||
))}
|
||||
</TwoAxisChart>
|
||||
</SVGChart>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SingleValueHistoryChart = (props: {
|
||||
data: Point[]
|
||||
data: HistoryPoint[]
|
||||
w: number
|
||||
h: number
|
||||
color: string
|
||||
|
@ -333,6 +419,10 @@ export const SingleValueHistoryChart = (props: {
|
|||
}) => {
|
||||
const { color, data, xScale, yScale, pct, w, h } = props
|
||||
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 [startDate, endDate] = xScale.domain().map(dayjs)
|
||||
const includeYear = !startDate.isSame(endDate, 'year')
|
||||
|
@ -343,6 +433,14 @@ export const SingleValueHistoryChart = (props: {
|
|||
const formatY = (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(formatX)
|
||||
const yAxis = d3
|
||||
.axisLeft<number>(yScale)
|
||||
.tickValues(tickValues)
|
||||
.tickFormat(formatY)
|
||||
|
||||
const onMouseOver = useEvent((event: React.PointerEvent) => {
|
||||
const tt = tooltipRef.current
|
||||
if (tt != null) {
|
||||
|
@ -370,30 +468,31 @@ export const SingleValueHistoryChart = (props: {
|
|||
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"
|
||||
/>
|
||||
<TwoAxisChart
|
||||
<SVGChart
|
||||
w={w}
|
||||
h={h}
|
||||
xScale={xScale}
|
||||
yScale={yScale}
|
||||
pct={pct}
|
||||
xAxis={xAxis}
|
||||
yAxis={yAxis}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<LinePath
|
||||
data={data}
|
||||
px={(p) => xScale(p[0])}
|
||||
py={(p) => yScale(p[1])}
|
||||
px={px}
|
||||
py={py1}
|
||||
curve={d3.curveStepAfter}
|
||||
stroke={color}
|
||||
/>
|
||||
<AreaPath
|
||||
data={data}
|
||||
px={(p) => xScale(p[0])}
|
||||
py0={yScale(0)}
|
||||
py1={(p) => yScale(p[1])}
|
||||
px={px}
|
||||
py0={py0}
|
||||
py1={py1}
|
||||
curve={d3.curveStepAfter}
|
||||
fill={color}
|
||||
fillOpacity={0.3}
|
||||
opacity={0.3}
|
||||
/>
|
||||
</TwoAxisChart>
|
||||
</SVGChart>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -412,6 +511,8 @@ export const ContractChart = (props: {
|
|||
case 'FREE_RESPONSE':
|
||||
case 'MULTIPLE_CHOICE':
|
||||
return <ChoiceContractChart {...{ ...props, contract }} />
|
||||
case 'NUMERIC':
|
||||
return <NumericContractChart {...{ ...props, contract }} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
@ -426,6 +527,38 @@ const getFormatterForDateRange = (start: Date, end: Date) => {
|
|||
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: {
|
||||
contract: PseudoNumericContract
|
||||
bets: Bet[]
|
||||
|
@ -449,7 +582,7 @@ export const PseudoNumericContractChart = (props: {
|
|||
xScale={xScale}
|
||||
yScale={yScale}
|
||||
data={data}
|
||||
color="#5fa5f9"
|
||||
color={NUMERIC_GRAPH_COLOR}
|
||||
/>
|
||||
)}
|
||||
</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