2022-09-28 03:24:42 +00:00
|
|
|
import { useCallback, useMemo, useState } from 'react'
|
2022-09-28 08:00:39 +00:00
|
|
|
import { bisector } from 'd3-array'
|
|
|
|
import { axisBottom, axisLeft } from 'd3-axis'
|
|
|
|
import { D3BrushEvent } from 'd3-brush'
|
|
|
|
import { ScaleTime, ScaleContinuousNumeric } from 'd3-scale'
|
2022-09-28 03:24:42 +00:00
|
|
|
import {
|
2022-10-03 16:31:07 +00:00
|
|
|
CurveFactory,
|
|
|
|
SeriesPoint,
|
2022-09-28 03:24:42 +00:00
|
|
|
curveLinear,
|
|
|
|
stack,
|
|
|
|
stackOrderReverse,
|
2022-09-28 08:00:39 +00:00
|
|
|
} from 'd3-shape'
|
2022-09-29 04:14:34 +00:00
|
|
|
import { range } from 'lodash'
|
2022-09-28 03:24:42 +00:00
|
|
|
|
|
|
|
import {
|
2022-09-30 23:16:04 +00:00
|
|
|
ContinuousScale,
|
2022-09-28 03:24:42 +00:00
|
|
|
SVGChart,
|
|
|
|
AreaPath,
|
|
|
|
AreaWithTopStroke,
|
2022-09-29 19:51:38 +00:00
|
|
|
Point,
|
|
|
|
TooltipComponent,
|
2022-09-29 04:14:34 +00:00
|
|
|
formatPct,
|
2022-09-28 03:24:42 +00:00
|
|
|
} from './helpers'
|
|
|
|
import { useEvent } from 'web/hooks/use-event'
|
|
|
|
|
2022-09-29 19:51:38 +00:00
|
|
|
export type MultiPoint<T = unknown> = Point<Date, number[], T>
|
|
|
|
export type HistoryPoint<T = unknown> = Point<Date, number, T>
|
|
|
|
export type DistributionPoint<T = unknown> = Point<number, number, T>
|
2022-09-28 03:24:42 +00:00
|
|
|
|
|
|
|
const getTickValues = (min: number, max: number, n: number) => {
|
|
|
|
const step = (max - min) / (n - 1)
|
|
|
|
return [min, ...range(1, n - 1).map((i) => min + step * i), max]
|
|
|
|
}
|
|
|
|
|
2022-09-30 23:16:04 +00:00
|
|
|
const betAtPointSelector = <X, Y, P extends Point<X, Y>>(
|
|
|
|
data: P[],
|
|
|
|
xScale: ContinuousScale<X>
|
|
|
|
) => {
|
|
|
|
const bisect = bisector((p: P) => p.x)
|
|
|
|
return (posX: number) => {
|
|
|
|
const x = xScale.invert(posX)
|
|
|
|
const item = data[bisect.left(data, x) - 1]
|
|
|
|
const result = item ? { ...item, x: posX } : undefined
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-29 19:51:38 +00:00
|
|
|
export const DistributionChart = <P extends DistributionPoint>(props: {
|
|
|
|
data: P[]
|
2022-09-28 03:24:42 +00:00
|
|
|
w: number
|
|
|
|
h: number
|
|
|
|
color: string
|
|
|
|
xScale: ScaleContinuousNumeric<number, number>
|
|
|
|
yScale: ScaleContinuousNumeric<number, number>
|
2022-10-03 16:31:07 +00:00
|
|
|
curve?: CurveFactory
|
2022-09-30 03:18:33 +00:00
|
|
|
onMouseOver?: (p: P | undefined) => void
|
2022-09-30 23:16:04 +00:00
|
|
|
Tooltip?: TooltipComponent<number, P>
|
2022-09-28 03:24:42 +00:00
|
|
|
}) => {
|
2022-10-03 16:31:07 +00:00
|
|
|
const { color, data, yScale, w, h, curve, Tooltip } = props
|
2022-09-28 03:24:42 +00:00
|
|
|
|
|
|
|
const [viewXScale, setViewXScale] =
|
|
|
|
useState<ScaleContinuousNumeric<number, number>>()
|
|
|
|
const xScale = viewXScale ?? props.xScale
|
|
|
|
|
2022-09-29 19:51:38 +00:00
|
|
|
const px = useCallback((p: P) => xScale(p.x), [xScale])
|
2022-09-28 03:24:42 +00:00
|
|
|
const py0 = yScale(yScale.domain()[0])
|
2022-09-29 19:51:38 +00:00
|
|
|
const py1 = useCallback((p: P) => yScale(p.y), [yScale])
|
2022-09-28 03:24:42 +00:00
|
|
|
|
2022-09-29 04:14:34 +00:00
|
|
|
const { xAxis, yAxis } = useMemo(() => {
|
2022-09-28 03:24:42 +00:00
|
|
|
const xAxis = axisBottom<number>(xScale).ticks(w / 100)
|
2022-09-29 04:14:34 +00:00
|
|
|
const yAxis = axisLeft<number>(yScale).tickFormat((n) => formatPct(n, 2))
|
|
|
|
return { xAxis, yAxis }
|
2022-09-28 03:24:42 +00:00
|
|
|
}, [w, xScale, yScale])
|
|
|
|
|
2022-09-30 23:16:04 +00:00
|
|
|
const onMouseOver = useEvent(betAtPointSelector(data, xScale))
|
|
|
|
|
2022-09-29 19:51:38 +00:00
|
|
|
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
|
2022-09-28 03:24:42 +00:00
|
|
|
if (ev.selection) {
|
|
|
|
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
|
|
|
setViewXScale(() =>
|
|
|
|
xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)])
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
setViewXScale(undefined)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return (
|
2022-09-29 04:43:04 +00:00
|
|
|
<SVGChart
|
|
|
|
w={w}
|
|
|
|
h={h}
|
|
|
|
xAxis={xAxis}
|
|
|
|
yAxis={yAxis}
|
|
|
|
onSelect={onSelect}
|
|
|
|
onMouseOver={onMouseOver}
|
|
|
|
Tooltip={Tooltip}
|
|
|
|
>
|
|
|
|
<AreaWithTopStroke
|
|
|
|
color={color}
|
|
|
|
data={data}
|
|
|
|
px={px}
|
|
|
|
py0={py0}
|
|
|
|
py1={py1}
|
2022-10-03 16:31:07 +00:00
|
|
|
curve={curve ?? curveLinear}
|
2022-09-29 04:43:04 +00:00
|
|
|
/>
|
|
|
|
</SVGChart>
|
2022-09-28 03:24:42 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-09-29 19:51:38 +00:00
|
|
|
export const MultiValueHistoryChart = <P extends MultiPoint>(props: {
|
|
|
|
data: P[]
|
2022-09-28 03:24:42 +00:00
|
|
|
w: number
|
|
|
|
h: number
|
|
|
|
colors: readonly string[]
|
|
|
|
xScale: ScaleTime<number, number>
|
|
|
|
yScale: ScaleContinuousNumeric<number, number>
|
2022-10-03 16:31:07 +00:00
|
|
|
curve?: CurveFactory
|
2022-09-30 03:18:33 +00:00
|
|
|
onMouseOver?: (p: P | undefined) => void
|
2022-09-30 23:16:04 +00:00
|
|
|
Tooltip?: TooltipComponent<Date, P>
|
2022-09-28 03:24:42 +00:00
|
|
|
pct?: boolean
|
|
|
|
}) => {
|
2022-10-03 16:31:07 +00:00
|
|
|
const { colors, data, yScale, w, h, curve, Tooltip, pct } = props
|
2022-09-28 03:24:42 +00:00
|
|
|
|
|
|
|
const [viewXScale, setViewXScale] = useState<ScaleTime<number, number>>()
|
|
|
|
const xScale = viewXScale ?? props.xScale
|
|
|
|
|
2022-09-29 19:51:38 +00:00
|
|
|
type SP = SeriesPoint<P>
|
2022-09-29 04:14:34 +00:00
|
|
|
const px = useCallback((p: SP) => xScale(p.data.x), [xScale])
|
2022-09-28 03:24:42 +00:00
|
|
|
const py0 = useCallback((p: SP) => yScale(p[0]), [yScale])
|
|
|
|
const py1 = useCallback((p: SP) => yScale(p[1]), [yScale])
|
|
|
|
|
2022-09-29 04:14:34 +00:00
|
|
|
const { xAxis, yAxis } = useMemo(() => {
|
2022-09-28 03:24:42 +00:00
|
|
|
const [min, max] = yScale.domain()
|
|
|
|
const pctTickValues = getTickValues(min, max, h < 200 ? 3 : 5)
|
|
|
|
const xAxis = axisBottom<Date>(xScale).ticks(w / 100)
|
|
|
|
const yAxis = pct
|
2022-09-29 06:27:42 +00:00
|
|
|
? axisLeft<number>(yScale)
|
|
|
|
.tickValues(pctTickValues)
|
|
|
|
.tickFormat((n) => formatPct(n))
|
2022-09-28 03:24:42 +00:00
|
|
|
: axisLeft<number>(yScale)
|
2022-09-29 04:14:34 +00:00
|
|
|
return { xAxis, yAxis }
|
2022-09-28 03:24:42 +00:00
|
|
|
}, [w, h, pct, xScale, yScale])
|
|
|
|
|
|
|
|
const series = useMemo(() => {
|
2022-09-29 19:51:38 +00:00
|
|
|
const d3Stack = stack<P, number>()
|
2022-09-29 04:14:34 +00:00
|
|
|
.keys(range(0, Math.max(...data.map(({ y }) => y.length))))
|
|
|
|
.value(({ y }, o) => y[o])
|
2022-09-28 03:24:42 +00:00
|
|
|
.order(stackOrderReverse)
|
|
|
|
return d3Stack(data)
|
2022-09-29 04:14:34 +00:00
|
|
|
}, [data])
|
2022-09-28 03:24:42 +00:00
|
|
|
|
2022-09-30 23:16:04 +00:00
|
|
|
const onMouseOver = useEvent(betAtPointSelector(data, xScale))
|
|
|
|
|
2022-09-29 19:51:38 +00:00
|
|
|
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
|
2022-09-28 03:24:42 +00:00
|
|
|
if (ev.selection) {
|
|
|
|
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
|
|
|
setViewXScale(() =>
|
|
|
|
xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)])
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
setViewXScale(undefined)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return (
|
2022-09-29 04:43:04 +00:00
|
|
|
<SVGChart
|
|
|
|
w={w}
|
|
|
|
h={h}
|
|
|
|
xAxis={xAxis}
|
|
|
|
yAxis={yAxis}
|
|
|
|
onSelect={onSelect}
|
|
|
|
onMouseOver={onMouseOver}
|
|
|
|
Tooltip={Tooltip}
|
|
|
|
>
|
|
|
|
{series.map((s, i) => (
|
|
|
|
<AreaPath
|
|
|
|
key={i}
|
|
|
|
data={s}
|
|
|
|
px={px}
|
|
|
|
py0={py0}
|
|
|
|
py1={py1}
|
2022-10-03 16:31:07 +00:00
|
|
|
curve={curve ?? curveLinear}
|
2022-09-29 04:43:04 +00:00
|
|
|
fill={colors[i]}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</SVGChart>
|
2022-09-28 03:24:42 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-09-29 19:51:38 +00:00
|
|
|
export const SingleValueHistoryChart = <P extends HistoryPoint>(props: {
|
|
|
|
data: P[]
|
2022-09-28 03:24:42 +00:00
|
|
|
w: number
|
|
|
|
h: number
|
|
|
|
color: string
|
|
|
|
xScale: ScaleTime<number, number>
|
|
|
|
yScale: ScaleContinuousNumeric<number, number>
|
2022-10-03 16:31:07 +00:00
|
|
|
curve?: CurveFactory
|
2022-09-30 03:18:33 +00:00
|
|
|
onMouseOver?: (p: P | undefined) => void
|
2022-09-30 23:16:04 +00:00
|
|
|
Tooltip?: TooltipComponent<Date, P>
|
2022-09-28 03:24:42 +00:00
|
|
|
pct?: boolean
|
|
|
|
}) => {
|
2022-10-03 16:31:07 +00:00
|
|
|
const { color, data, yScale, w, h, curve, Tooltip, pct } = props
|
2022-09-28 03:24:42 +00:00
|
|
|
|
|
|
|
const [viewXScale, setViewXScale] = useState<ScaleTime<number, number>>()
|
|
|
|
const xScale = viewXScale ?? props.xScale
|
|
|
|
|
2022-09-29 19:51:38 +00:00
|
|
|
const px = useCallback((p: P) => xScale(p.x), [xScale])
|
2022-09-28 03:24:42 +00:00
|
|
|
const py0 = yScale(yScale.domain()[0])
|
2022-09-29 19:51:38 +00:00
|
|
|
const py1 = useCallback((p: P) => yScale(p.y), [yScale])
|
2022-09-28 03:24:42 +00:00
|
|
|
|
2022-09-29 04:14:34 +00:00
|
|
|
const { xAxis, yAxis } = useMemo(() => {
|
2022-09-28 03:24:42 +00:00
|
|
|
const [min, max] = yScale.domain()
|
|
|
|
const pctTickValues = getTickValues(min, max, h < 200 ? 3 : 5)
|
|
|
|
const xAxis = axisBottom<Date>(xScale).ticks(w / 100)
|
|
|
|
const yAxis = pct
|
2022-09-29 06:27:42 +00:00
|
|
|
? axisLeft<number>(yScale)
|
|
|
|
.tickValues(pctTickValues)
|
|
|
|
.tickFormat((n) => formatPct(n))
|
2022-09-28 03:24:42 +00:00
|
|
|
: axisLeft<number>(yScale)
|
2022-09-29 04:14:34 +00:00
|
|
|
return { xAxis, yAxis }
|
2022-09-28 03:24:42 +00:00
|
|
|
}, [w, h, pct, xScale, yScale])
|
|
|
|
|
2022-09-30 23:16:04 +00:00
|
|
|
const onMouseOver = useEvent(betAtPointSelector(data, xScale))
|
|
|
|
|
2022-09-29 19:51:38 +00:00
|
|
|
const onSelect = useEvent((ev: D3BrushEvent<P>) => {
|
2022-09-28 03:24:42 +00:00
|
|
|
if (ev.selection) {
|
|
|
|
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
|
|
|
setViewXScale(() =>
|
|
|
|
xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)])
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
setViewXScale(undefined)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return (
|
2022-09-29 04:43:04 +00:00
|
|
|
<SVGChart
|
|
|
|
w={w}
|
|
|
|
h={h}
|
|
|
|
xAxis={xAxis}
|
|
|
|
yAxis={yAxis}
|
|
|
|
onSelect={onSelect}
|
|
|
|
onMouseOver={onMouseOver}
|
|
|
|
Tooltip={Tooltip}
|
|
|
|
>
|
|
|
|
<AreaWithTopStroke
|
|
|
|
color={color}
|
|
|
|
data={data}
|
|
|
|
px={px}
|
|
|
|
py0={py0}
|
|
|
|
py1={py1}
|
2022-10-03 16:31:07 +00:00
|
|
|
curve={curve ?? curveLinear}
|
2022-09-29 04:43:04 +00:00
|
|
|
/>
|
|
|
|
</SVGChart>
|
2022-09-28 03:24:42 +00:00
|
|
|
)
|
|
|
|
}
|