import { useCallback, useMemo, useState } from 'react' import { bisector } from 'd3-array' import { axisBottom, axisLeft } from 'd3-axis' import { D3BrushEvent } from 'd3-brush' import { ScaleTime, ScaleContinuousNumeric } from 'd3-scale' import { CurveFactory, SeriesPoint, curveLinear, stack, stackOrderReverse, } from 'd3-shape' import { range } from 'lodash' import { ContinuousScale, Margin, SVGChart, AreaPath, AreaWithTopStroke, Point, TooltipComponent, computeColorStops, formatPct, } from './helpers' import { useEvent } from 'web/hooks/use-event' import { formatMoney } from 'common/util/format' import { nanoid } from 'nanoid' export type MultiPoint = Point export type HistoryPoint = Point export type DistributionPoint = Point export type ValueKind = 'm$' | 'percent' | 'amount' 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] } const betAtPointSelector = >( data: P[], xScale: ContinuousScale ) => { 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 } } export const DistributionChart =

(props: { data: P[] w: number h: number color: string margin: Margin xScale: ScaleContinuousNumeric yScale: ScaleContinuousNumeric curve?: CurveFactory onMouseOver?: (p: P | undefined) => void Tooltip?: TooltipComponent }) => { const { data, w, h, color, margin, yScale, curve, Tooltip } = props const [viewXScale, setViewXScale] = useState>() const xScale = viewXScale ?? props.xScale const px = useCallback((p: P) => xScale(p.x), [xScale]) const py0 = yScale(yScale.domain()[0]) const py1 = useCallback((p: P) => yScale(p.y), [yScale]) const { xAxis, yAxis } = useMemo(() => { const xAxis = axisBottom(xScale).ticks(w / 100) const yAxis = axisLeft(yScale).tickFormat((n) => formatPct(n, 2)) return { xAxis, yAxis } }, [w, xScale, yScale]) const selector = betAtPointSelector(data, xScale) const onMouseOver = useEvent((mouseX: number) => { const p = selector(mouseX) props.onMouseOver?.(p) return p }) const onSelect = useEvent((ev: D3BrushEvent

) => { 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 ( ) } export const MultiValueHistoryChart =

(props: { data: P[] w: number h: number colors: readonly string[] margin: Margin xScale: ScaleTime yScale: ScaleContinuousNumeric yKind?: ValueKind curve?: CurveFactory onMouseOver?: (p: P | undefined) => void Tooltip?: TooltipComponent }) => { const { data, w, h, colors, margin, yScale, yKind, curve, Tooltip } = props const [viewXScale, setViewXScale] = useState>() const xScale = viewXScale ?? props.xScale type SP = SeriesPoint

const px = useCallback((p: SP) => xScale(p.data.x), [xScale]) const py0 = useCallback((p: SP) => yScale(p[0]), [yScale]) const py1 = useCallback((p: SP) => yScale(p[1]), [yScale]) const { xAxis, yAxis } = useMemo(() => { const [min, max] = yScale.domain() const nTicks = h < 200 ? 3 : 5 const pctTickValues = getTickValues(min, max, nTicks) const xAxis = axisBottom(xScale).ticks(w / 100) const yAxis = yKind === 'percent' ? axisLeft(yScale) .tickValues(pctTickValues) .tickFormat((n) => formatPct(n)) : yKind === 'm$' ? axisLeft(yScale) .ticks(nTicks) .tickFormat((n) => formatMoney(n)) : axisLeft(yScale).ticks(nTicks) return { xAxis, yAxis } }, [w, h, yKind, xScale, yScale]) const series = useMemo(() => { const d3Stack = stack() .keys(range(0, Math.max(...data.map(({ y }) => y.length)))) .value(({ y }, k) => y[k]) .order(stackOrderReverse) return d3Stack(data) }, [data]) const selector = betAtPointSelector(data, xScale) const onMouseOver = useEvent((mouseX: number) => { const p = selector(mouseX) props.onMouseOver?.(p) return p }) const onSelect = useEvent((ev: D3BrushEvent

) => { 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 ( {series.map((s, i) => ( ))} ) } export const SingleValueHistoryChart =

(props: { data: P[] w: number h: number color: string | ((p: P) => string) margin: Margin xScale: ScaleTime yScale: ScaleContinuousNumeric yKind?: ValueKind curve?: CurveFactory onMouseOver?: (p: P | undefined) => void Tooltip?: TooltipComponent pct?: boolean }) => { const { data, w, h, color, margin, yScale, yKind, curve, Tooltip } = props const [viewXScale, setViewXScale] = useState>() const xScale = viewXScale ?? props.xScale const px = useCallback((p: P) => xScale(p.x), [xScale]) const py0 = yScale(yScale.domain()[0]) const py1 = useCallback((p: P) => yScale(p.y), [yScale]) const { xAxis, yAxis } = useMemo(() => { const [min, max] = yScale.domain() const nTicks = h < 200 ? 3 : 5 const pctTickValues = getTickValues(min, max, nTicks) const xAxis = axisBottom(xScale).ticks(w / 100) const yAxis = yKind === 'percent' ? axisLeft(yScale) .tickValues(pctTickValues) .tickFormat((n) => formatPct(n)) : yKind === 'm$' ? axisLeft(yScale) .ticks(nTicks) .tickFormat((n) => formatMoney(n)) : axisLeft(yScale).ticks(nTicks) return { xAxis, yAxis } }, [w, h, yKind, xScale, yScale]) const selector = betAtPointSelector(data, xScale) const onMouseOver = useEvent((mouseX: number) => { const p = selector(mouseX) props.onMouseOver?.(p) return p }) const onSelect = useEvent((ev: D3BrushEvent

) => { if (ev.selection) { const [mouseX0, mouseX1] = ev.selection as [number, number] setViewXScale(() => xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)]) ) } else { setViewXScale(undefined) } }) const gradientId = useMemo(() => nanoid(), []) const stops = useMemo( () => typeof color !== 'string' ? computeColorStops(data, color, px) : null, [color, data, px] ) return ( {stops && ( {stops.map((s, i) => ( ))} )} ) }