'use client' import { useMemo } from 'react' import { ComposedChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, Cell, Customized, } from 'recharts' import type { ChartPoint, TimeRangeConfig } from './types' const GREEN = 'oklch(0.65 0.18 142)' const RED = 'oklch(0.55 0.22 25)' const GREEN_V = 'rgba(70, 180, 80, 0.45)' const RED_V = 'rgba(220, 60, 50, 0.45)' interface TooltipPayloadItem { value: number | null payload: ChartPoint } interface CustomTooltipProps { active?: boolean payload?: TooltipPayloadItem[] label?: string } function formatVolume(v: number | null): string { if (v === null) return '-' if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M` if (v >= 1_000) return `${(v / 1_000).toFixed(0)}K` return v.toString() } function formatLocalTimestamp(ts: string): string { const date = new Date(ts) if (Number.isNaN(date.getTime())) return ts return new Intl.DateTimeFormat(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false, }).format(date) } interface SegmentedLineProps { xAxisMap?: Record number }> yAxisMap?: Record number }> data?: ChartPoint[] yAxisId?: string } function SegmentedLine({ xAxisMap, yAxisMap, data, yAxisId = 'pct' }: SegmentedLineProps) { if (!xAxisMap || !yAxisMap || !data?.length) return null const xScale = Object.values(xAxisMap)[0]?.scale const yScale = yAxisMap[yAxisId]?.scale if (!xScale || !yScale) return null const segments: { color: string; d: string }[] = [] let currentColor: string | null = null let currentPath = '' for (let i = 0; i < data.length; i++) { const pt = data[i] if (pt.percentChange === null) { if (currentPath) { segments.push({ color: currentColor!, d: currentPath }) currentPath = '' currentColor = null } continue } const x = xScale(pt.index) const y = yScale(pt.percentChange) const color = pt.percentChange >= 0 ? GREEN : RED if (i === 0 || !currentPath) { currentColor = color currentPath = `M ${x} ${y}` continue } const prev = data[i - 1] if (prev.percentChange === null) { currentColor = color currentPath = `M ${x} ${y}` continue } if ((prev.percentChange >= 0) !== (pt.percentChange >= 0)) { const pct0 = prev.percentChange const pct1 = pt.percentChange const t = pct0 / (pct0 - pct1) const x0 = xScale(prev.index) const x1 = xScale(pt.index) const xCross = x0 + t * (x1 - x0) const yCross = yScale(0) currentPath += ` L ${xCross} ${yCross}` segments.push({ color: currentColor!, d: currentPath }) currentColor = color currentPath = `M ${xCross} ${yCross} L ${x} ${y}` } else { currentPath += ` L ${x} ${y}` } } if (currentPath) { segments.push({ color: currentColor!, d: currentPath }) } return ( {segments.map((seg, i) => ( ))} ) } function CustomTooltip({ active, payload }: CustomTooltipProps) { if (!active || !payload?.length) return null const d = payload[0].payload as ChartPoint if (d.price === null || d.percentChange === null) return null const isUp = d.percentChange >= 0 return (

{formatLocalTimestamp(d.timestamp)}

${d.price.toFixed(2)}

{isUp ? '+' : ''}{d.percentChange.toFixed(2)}%

{formatVolume(d.volume)} vol

) } interface StockChartCoreProps { data: ChartPoint[] config: TimeRangeConfig } export function StockChartCore({ data, config }: StockChartCoreProps) { const tickIndices = useMemo(() => new Set(config.tickInterval(data)), [data, config]) const ticks = useMemo( () => data.filter((_, i) => tickIndices.has(i)).map((d) => d.index), [data, tickIndices] ) const priceValues = useMemo( () => data.map((d) => d.price).filter((v): v is number => v !== null), [data] ) const pctValues = useMemo( () => data.map((d) => d.percentChange).filter((v): v is number => v !== null), [data] ) const volValues = useMemo( () => data.map((d) => d.volume).filter((v): v is number => v !== null), [data] ) const priceMin = priceValues.length ? Math.min(...priceValues) : 0 const priceMax = priceValues.length ? Math.max(...priceValues) : 0 const pricePad = (priceMax - priceMin) * 0.08 || 1 const pctMin = pctValues.length ? Math.min(...pctValues) : 0 const pctMax = pctValues.length ? Math.max(...pctValues) : 0 const pctPad = Math.max(Math.abs(pctMax - pctMin) * 0.08, 0.05) const volMax = volValues.length ? Math.max(...volValues) : 0 return (
{ const pt = data[v] return pt ? config.formatLabel(pt.timestamp) : '' }} tick={{ fill: 'oklch(0.55 0 0)', fontSize: 11, fontFamily: 'var(--font-mono)' }} axisLine={false} tickLine={false} tickMargin={8} interval={0} /> `${v >= 0 ? '+' : ''}${v.toFixed(2)}%`} tick={{ fill: 'oklch(0.55 0 0)', fontSize: 10, fontFamily: 'var(--font-mono)' }} axisLine={false} tickLine={false} width={70} /> `$${v.toFixed(2)}`} tick={{ fill: 'oklch(0.55 0 0)', fontSize: 10, fontFamily: 'var(--font-mono)' }} axisLine={false} tickLine={false} width={72} /> } cursor={{ stroke: 'oklch(0.4 0 0)', strokeWidth: 1 }} /> } /> } cursor={{ stroke: 'oklch(0.4 0 0)', strokeWidth: 1 }} /> {data.map((entry, index) => ( = 0 ? GREEN_V : RED_V} fillOpacity={entry.volume === null ? 0 : 1} /> ))}
) }