Clean up a bunch of tooltip stuff, add FR legend tooltips
This commit is contained in:
parent
7fa11bdc4e
commit
b9376a725e
|
@ -14,27 +14,25 @@ const getMultiChartData = (
|
||||||
contract: FreeResponseContract | MultipleChoiceContract,
|
contract: FreeResponseContract | MultipleChoiceContract,
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
) => {
|
) => {
|
||||||
const { totalBets, outcomeType } = contract
|
const { answers, totalBets, outcomeType } = contract
|
||||||
|
|
||||||
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 outcomes = Object.keys(betsByOutcome).filter((outcome) => {
|
const validAnswers = answers.filter((answer) => {
|
||||||
const maxProb = Math.max(
|
const maxProb = Math.max(
|
||||||
...betsByOutcome[outcome].map((bet) => bet.probAfter)
|
...betsByOutcome[answer.id].map((bet) => bet.probAfter)
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
(outcome !== '0' || outcomeType === 'MULTIPLE_CHOICE') &&
|
(answer.id !== '0' || outcomeType === 'MULTIPLE_CHOICE') &&
|
||||||
maxProb > 0.02 &&
|
maxProb > 0.02 &&
|
||||||
totalBets[outcome] > 0.000000001
|
totalBets[answer.id] > 0.000000001
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const trackedOutcomes = sortBy(
|
const trackedAnswers = sortBy(
|
||||||
outcomes,
|
validAnswers,
|
||||||
(outcome) => -1 * getOutcomeProbability(contract, outcome)
|
(answer) => -1 * getOutcomeProbability(contract, answer.id)
|
||||||
)
|
).slice(0, 10)
|
||||||
.slice(0, 10)
|
|
||||||
.reverse()
|
|
||||||
|
|
||||||
const points: MultiPoint[] = []
|
const points: MultiPoint[] = []
|
||||||
|
|
||||||
|
@ -51,23 +49,26 @@ const getMultiChartData = (
|
||||||
)
|
)
|
||||||
points.push([
|
points.push([
|
||||||
new Date(bet.createdTime),
|
new Date(bet.createdTime),
|
||||||
trackedOutcomes.map(
|
trackedAnswers.map(
|
||||||
(outcome) => sharesByOutcome[outcome] ** 2 / sharesSquared
|
(answer) => sharesByOutcome[answer.id] ** 2 / sharesSquared
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const allPoints: MultiPoint[] = [
|
const allPoints: MultiPoint[] = [
|
||||||
[new Date(contract.createdTime), trackedOutcomes.map((_) => 0)],
|
[new Date(contract.createdTime), trackedAnswers.map((_) => 0)],
|
||||||
...points,
|
...points,
|
||||||
[
|
[
|
||||||
new Date(Date.now()),
|
new Date(Date.now()),
|
||||||
trackedOutcomes.map((outcome) =>
|
trackedAnswers.map((answer) =>
|
||||||
getOutcomeProbability(contract, outcome)
|
getOutcomeProbability(contract, answer.id)
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
return { points: allPoints, labels: trackedOutcomes }
|
return {
|
||||||
|
points: allPoints,
|
||||||
|
labels: trackedAnswers.map((answer) => answer.text),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChoiceContractChart = (props: {
|
export const ChoiceContractChart = (props: {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useRef, useCallback } from 'react'
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
axisBottom,
|
axisBottom,
|
||||||
axisLeft,
|
axisLeft,
|
||||||
|
@ -7,6 +7,7 @@ import {
|
||||||
curveStepAfter,
|
curveStepAfter,
|
||||||
pointer,
|
pointer,
|
||||||
stack,
|
stack,
|
||||||
|
stackOrderReverse,
|
||||||
ScaleTime,
|
ScaleTime,
|
||||||
ScaleContinuousNumeric,
|
ScaleContinuousNumeric,
|
||||||
SeriesPoint,
|
SeriesPoint,
|
||||||
|
@ -14,13 +15,21 @@ import {
|
||||||
import { range } from 'lodash'
|
import { range } from 'lodash'
|
||||||
import dayjs from 'dayjs'
|
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 { formatLargeNumber } from 'common/util/format'
|
||||||
import { useEvent } from 'web/hooks/use-event'
|
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 MultiPoint = readonly [Date, number[]] // [time, [ordered outcome probs]]
|
||||||
export type HistoryPoint = readonly [Date, number] // [time, number or percentage]
|
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<P> = TooltipPosition & { p: P }
|
||||||
|
|
||||||
const formatPct = (n: number, digits?: number) => {
|
const formatPct = (n: number, digits?: number) => {
|
||||||
return `${(n * 100).toFixed(digits ?? 0)}%`
|
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]
|
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 (
|
||||||
|
<ol className={className}>
|
||||||
|
{items.map((item) => (
|
||||||
|
<li key={item.label} className="flex flex-row justify-between">
|
||||||
|
<Row className="mr-4 items-center">
|
||||||
|
<span
|
||||||
|
className="mr-2 h-4 w-4"
|
||||||
|
style={{ backgroundColor: item.color }}
|
||||||
|
></span>
|
||||||
|
{item.label}
|
||||||
|
</Row>
|
||||||
|
{item.value}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const SingleValueDistributionChart = (props: {
|
export const SingleValueDistributionChart = (props: {
|
||||||
data: DistributionPoint[]
|
data: DistributionPoint[]
|
||||||
w: number
|
w: number
|
||||||
|
@ -75,46 +106,40 @@ export const SingleValueDistributionChart = (props: {
|
||||||
yScale: ScaleContinuousNumeric<number, number>
|
yScale: ScaleContinuousNumeric<number, number>
|
||||||
}) => {
|
}) => {
|
||||||
const { color, data, xScale, yScale, w, h } = props
|
const { color, data, xScale, yScale, w, h } = props
|
||||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
const [mouseState, setMouseState] =
|
||||||
|
useState<PositionValue<DistributionPoint>>()
|
||||||
|
|
||||||
const px = useCallback((p: DistributionPoint) => xScale(p[0]), [xScale])
|
const px = useCallback((p: DistributionPoint) => xScale(p[0]), [xScale])
|
||||||
const py0 = yScale(0)
|
const py0 = yScale(0)
|
||||||
const py1 = useCallback((p: DistributionPoint) => yScale(p[1]), [yScale])
|
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<number>(xScale).tickFormat(formatX)
|
|
||||||
const yAxis = axisLeft<number>(yScale).tickFormat(formatY)
|
|
||||||
|
|
||||||
const xBisector = bisector((p: DistributionPoint) => p[0])
|
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<number>(xScale).tickFormat(fmtX)
|
||||||
|
const yAxis = axisLeft<number>(yScale).tickFormat(fmtY)
|
||||||
|
return { fmtX, fmtY, xAxis, yAxis }
|
||||||
|
}, [xScale, yScale])
|
||||||
|
|
||||||
const onMouseOver = useEvent((event: React.PointerEvent) => {
|
const onMouseOver = useEvent((event: React.PointerEvent) => {
|
||||||
const tt = tooltipRef.current
|
|
||||||
if (tt != null) {
|
|
||||||
const [mouseX, mouseY] = pointer(event)
|
const [mouseX, mouseY] = pointer(event)
|
||||||
const queryX = xScale.invert(mouseX)
|
const queryX = xScale.invert(mouseX)
|
||||||
const [_x, y] = data[xBisector.center(data, queryX)]
|
const [_x, y] = data[xBisector.center(data, queryX)]
|
||||||
tt.innerHTML = `<strong>${formatY(y)}</strong> ${formatX(queryX)}`
|
setMouseState({ top: mouseY - 10, left: mouseX + 60, p: [queryX, y] })
|
||||||
tt.style.display = 'block'
|
|
||||||
tt.style.top = mouseY - 10 + 'px'
|
|
||||||
tt.style.left = mouseX + 20 + 'px'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const onMouseLeave = useEvent(() => {
|
const onMouseLeave = useEvent(() => {
|
||||||
const tt = tooltipRef.current
|
setMouseState(undefined)
|
||||||
if (tt != null) {
|
|
||||||
tt.style.display = 'none'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
{mouseState && (
|
||||||
ref={tooltipRef}
|
<ChartTooltip {...mouseState}>
|
||||||
style={{ display: 'none' }}
|
<strong>{fmtY(mouseState.p[1])}</strong> {fmtX(mouseState.p[0])}
|
||||||
className="pointer-events-none absolute z-10 whitespace-pre rounded border-2 border-black bg-slate-600/75 p-2 text-white"
|
</ChartTooltip>
|
||||||
/>
|
)}
|
||||||
<SVGChart
|
<SVGChart
|
||||||
w={w}
|
w={w}
|
||||||
h={h}
|
h={h}
|
||||||
|
@ -147,46 +172,71 @@ export const MultiValueHistoryChart = (props: {
|
||||||
pct?: boolean
|
pct?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
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 [mouseState, setMouseState] = useState<PositionValue<MultiPoint>>()
|
||||||
|
|
||||||
const px = useCallback(
|
type SP = SeriesPoint<MultiPoint>
|
||||||
(p: SeriesPoint<MultiPoint>) => xScale(p.data[0]),
|
const px = useCallback((p: SP) => xScale(p.data[0]), [xScale])
|
||||||
[xScale]
|
const py0 = useCallback((p: SP) => yScale(p[0]), [yScale])
|
||||||
)
|
const py1 = useCallback((p: SP) => yScale(p[1]), [yScale])
|
||||||
const py0 = useCallback(
|
const xBisector = bisector((p: MultiPoint) => p[0])
|
||||||
(p: SeriesPoint<MultiPoint>) => yScale(p[0]),
|
|
||||||
[yScale]
|
|
||||||
)
|
|
||||||
const py1 = useCallback(
|
|
||||||
(p: SeriesPoint<MultiPoint>) => yScale(p[1]),
|
|
||||||
[yScale]
|
|
||||||
)
|
|
||||||
|
|
||||||
const [xStart, xEnd] = xScale.domain()
|
const { fmtX, fmtY, xAxis, yAxis, series } = useMemo(() => {
|
||||||
const fmtX = getFormatterForDateRange(xStart, xEnd)
|
const [start, end] = xScale.domain()
|
||||||
|
const fmtX = getFormatterForDateRange(start, end)
|
||||||
const fmtY = (n: number) => (pct ? formatPct(n, 0) : formatLargeNumber(n))
|
const fmtY = (n: number) => (pct ? formatPct(n, 0) : formatLargeNumber(n))
|
||||||
|
|
||||||
const [min, max] = yScale.domain()
|
const [min, max] = yScale.domain()
|
||||||
const tickValues = getTickValues(min, max, h < 200 ? 3 : 5)
|
const tickValues = getTickValues(min, max, h < 200 ? 3 : 5)
|
||||||
|
|
||||||
const xAxis = axisBottom<Date>(xScale).tickFormat(fmtX)
|
const xAxis = axisBottom<Date>(xScale).tickFormat(fmtX)
|
||||||
const yAxis = axisLeft<number>(yScale).tickValues(tickValues).tickFormat(fmtY)
|
const yAxis = axisLeft<number>(yScale)
|
||||||
|
.tickValues(tickValues)
|
||||||
|
.tickFormat(fmtY)
|
||||||
|
|
||||||
const d3Stack = stack<MultiPoint, number>()
|
const d3Stack = stack<MultiPoint, number>()
|
||||||
.keys(range(0, labels.length))
|
.keys(range(0, labels.length))
|
||||||
.value(([_date, probs], o) => probs[o])
|
.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 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 (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
{mouseState && (
|
||||||
ref={tooltipRef}
|
<ChartTooltip {...mouseState}>
|
||||||
style={{ display: 'none' }}
|
{fmtX(mouseState.p[0])}
|
||||||
className="pointer-events-none absolute z-10 whitespace-pre rounded border-2 border-black bg-slate-600/75 p-2 text-white"
|
<Legend
|
||||||
|
className="text-sm"
|
||||||
|
items={mouseState.p[1].map((p, i) => ({
|
||||||
|
color: colors[i],
|
||||||
|
label: labels[i],
|
||||||
|
value: fmtY(p),
|
||||||
|
}))}
|
||||||
/>
|
/>
|
||||||
<SVGChart w={w} h={h} xAxis={xAxis} yAxis={yAxis}>
|
</ChartTooltip>
|
||||||
{d3Stack(data).map((s, i) => (
|
)}
|
||||||
|
<SVGChart
|
||||||
|
w={w}
|
||||||
|
h={h}
|
||||||
|
xAxis={xAxis}
|
||||||
|
yAxis={yAxis}
|
||||||
|
onMouseOver={onMouseOver}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
|
>
|
||||||
|
{series.map((s, i) => (
|
||||||
<AreaPath
|
<AreaPath
|
||||||
key={s.key}
|
key={i}
|
||||||
data={s}
|
data={s}
|
||||||
px={px}
|
px={px}
|
||||||
py0={py0}
|
py0={py0}
|
||||||
|
@ -210,52 +260,45 @@ export const SingleValueHistoryChart = (props: {
|
||||||
pct?: boolean
|
pct?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const { color, data, xScale, yScale, pct, w, h } = props
|
const { color, data, xScale, yScale, pct, w, h } = props
|
||||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
const [mouseState, setMouseState] = useState<PositionValue<HistoryPoint>>()
|
||||||
|
|
||||||
const px = useCallback((p: HistoryPoint) => xScale(p[0]), [xScale])
|
const px = useCallback((p: HistoryPoint) => xScale(p[0]), [xScale])
|
||||||
const py0 = yScale(0)
|
const py0 = yScale(0)
|
||||||
const py1 = useCallback((p: HistoryPoint) => yScale(p[1]), [yScale])
|
const py1 = useCallback((p: HistoryPoint) => yScale(p[1]), [yScale])
|
||||||
|
|
||||||
|
const { fmtX, fmtY, xAxis, yAxis } = useMemo(() => {
|
||||||
const [start, end] = xScale.domain()
|
const [start, end] = xScale.domain()
|
||||||
const formatX = getFormatterForDateRange(start, end)
|
const fmtX = getFormatterForDateRange(start, end)
|
||||||
const formatY = (n: number) => (pct ? formatPct(n, 0) : formatLargeNumber(n))
|
const fmtY = (n: number) => (pct ? formatPct(n, 0) : formatLargeNumber(n))
|
||||||
|
|
||||||
const [min, max] = yScale.domain()
|
const [min, max] = yScale.domain()
|
||||||
const tickValues = getTickValues(min, max, h < 200 ? 3 : 5)
|
const tickValues = getTickValues(min, max, h < 200 ? 3 : 5)
|
||||||
|
const xAxis = axisBottom<Date>(xScale).tickFormat(fmtX)
|
||||||
const xAxis = axisBottom<Date>(xScale).tickFormat(formatX)
|
|
||||||
const yAxis = axisLeft<number>(yScale)
|
const yAxis = axisLeft<number>(yScale)
|
||||||
.tickValues(tickValues)
|
.tickValues(tickValues)
|
||||||
.tickFormat(formatY)
|
.tickFormat(fmtY)
|
||||||
|
return { fmtX, fmtY, xAxis, yAxis }
|
||||||
|
}, [h, pct, xScale, yScale])
|
||||||
|
|
||||||
const xBisector = bisector((p: HistoryPoint) => p[0])
|
const xBisector = bisector((p: HistoryPoint) => p[0])
|
||||||
const onMouseOver = useEvent((event: React.PointerEvent) => {
|
const onMouseOver = useEvent((event: React.PointerEvent) => {
|
||||||
const tt = tooltipRef.current
|
|
||||||
if (tt != null) {
|
|
||||||
const [mouseX, mouseY] = pointer(event)
|
const [mouseX, mouseY] = pointer(event)
|
||||||
const queryX = xScale.invert(mouseX)
|
const queryX = xScale.invert(mouseX)
|
||||||
const [_x, y] = data[xBisector.center(data, queryX)]
|
const [_x, y] = data[xBisector.center(data, queryX)]
|
||||||
tt.innerHTML = `<strong>${formatY(y)}</strong> ${formatX(queryX)}`
|
setMouseState({ top: mouseY - 10, left: mouseX + 60, p: [queryX, y] })
|
||||||
tt.style.display = 'block'
|
|
||||||
tt.style.top = mouseY - 10 + 'px'
|
|
||||||
tt.style.left = mouseX + 20 + 'px'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const onMouseLeave = useEvent(() => {
|
const onMouseLeave = useEvent(() => {
|
||||||
const tt = tooltipRef.current
|
setMouseState(undefined)
|
||||||
if (tt != null) {
|
|
||||||
tt.style.display = 'none'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
{mouseState && (
|
||||||
ref={tooltipRef}
|
<ChartTooltip {...mouseState}>
|
||||||
style={{ display: 'none' }}
|
<strong>{fmtY(mouseState.p[1])}</strong> {fmtX(mouseState.p[0])}
|
||||||
className="pointer-events-none absolute z-10 whitespace-pre rounded border-2 border-black bg-slate-600/75 p-2 text-white"
|
</ChartTooltip>
|
||||||
/>
|
)}
|
||||||
<SVGChart
|
<SVGChart
|
||||||
w={w}
|
w={w}
|
||||||
h={h}
|
h={h}
|
||||||
|
|
|
@ -1,13 +1,5 @@
|
||||||
import { ReactNode, SVGProps, memo, useRef, useEffect } from 'react'
|
import { ReactNode, SVGProps, memo, useRef, useEffect } from 'react'
|
||||||
import {
|
import { Axis, CurveFactory, area, curveStepAfter, line, select } from 'd3'
|
||||||
Axis,
|
|
||||||
AxisDomain,
|
|
||||||
CurveFactory,
|
|
||||||
area,
|
|
||||||
curveStepAfter,
|
|
||||||
line,
|
|
||||||
select,
|
|
||||||
} from 'd3'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
|
@ -16,11 +8,7 @@ export const MARGIN = { top: 20, right: 10, bottom: 20, left: 40 }
|
||||||
export const MARGIN_X = MARGIN.right + MARGIN.left
|
export const MARGIN_X = MARGIN.right + MARGIN.left
|
||||||
export const MARGIN_Y = MARGIN.top + MARGIN.bottom
|
export const MARGIN_Y = MARGIN.top + MARGIN.bottom
|
||||||
|
|
||||||
export const XAxis = <X extends AxisDomain>(props: {
|
export const XAxis = <X,>(props: { w: number; h: number; axis: Axis<X> }) => {
|
||||||
w: number
|
|
||||||
h: number
|
|
||||||
axis: Axis<X>
|
|
||||||
}) => {
|
|
||||||
const { h, axis } = props
|
const { h, axis } = props
|
||||||
const axisRef = useRef<SVGGElement>(null)
|
const axisRef = useRef<SVGGElement>(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -33,11 +21,7 @@ export const XAxis = <X extends AxisDomain>(props: {
|
||||||
return <g ref={axisRef} transform={`translate(0, ${h})`} />
|
return <g ref={axisRef} transform={`translate(0, ${h})`} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export const YAxis = <Y extends AxisDomain>(props: {
|
export const YAxis = <Y,>(props: { w: number; h: number; axis: Axis<Y> }) => {
|
||||||
w: number
|
|
||||||
h: number
|
|
||||||
axis: Axis<Y>
|
|
||||||
}) => {
|
|
||||||
const { w, h, axis } = props
|
const { w, h, axis } = props
|
||||||
const axisRef = useRef<SVGGElement>(null)
|
const axisRef = useRef<SVGGElement>(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -109,7 +93,7 @@ export const AreaWithTopStroke = <P,>(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SVGChart = <X extends AxisDomain, Y extends AxisDomain>(props: {
|
export const SVGChart = <X, Y>(props: {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
w: number
|
w: number
|
||||||
h: number
|
h: number
|
||||||
|
@ -131,8 +115,8 @@ export const SVGChart = <X extends AxisDomain, Y extends AxisDomain>(props: {
|
||||||
<rect
|
<rect
|
||||||
x="0"
|
x="0"
|
||||||
y="0"
|
y="0"
|
||||||
width={w - MARGIN_X}
|
width={innerW}
|
||||||
height={h - MARGIN_Y}
|
height={innerH}
|
||||||
fill="none"
|
fill="none"
|
||||||
pointerEvents="all"
|
pointerEvents="all"
|
||||||
onPointerEnter={onMouseOver}
|
onPointerEnter={onMouseOver}
|
||||||
|
@ -144,6 +128,22 @@ export const SVGChart = <X extends AxisDomain, Y extends AxisDomain>(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TooltipPosition = { top: number; left: number }
|
||||||
|
|
||||||
|
export const ChartTooltip = (
|
||||||
|
props: TooltipPosition & { children: React.ReactNode }
|
||||||
|
) => {
|
||||||
|
const { top, left, children } = props
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute z-10 whitespace-pre rounded border-2 border-black bg-white/90 p-2"
|
||||||
|
style={{ top, left }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const getDateRange = (contract: Contract) => {
|
export const getDateRange = (contract: Contract) => {
|
||||||
const { createdTime, closeTime, resolutionTime } = contract
|
const { createdTime, closeTime, resolutionTime } = contract
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user