"use client"; import * as React from "react"; import { Bar, BarChart, CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; import { useTheme } from "next-themes"; import { formatCompact, formatUsd } from "@/lib/format"; /** * Categorical palette, validated (dataviz skill) against both surfaces. * Fixed slot order — providers always keep the same hue regardless of which are present. */ const LIGHT = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100"]; const DARK = ["#3987e5", "#d95926", "#199e70", "#c98500"]; const PROVIDER_SLOT: Record = { oxylabs: 0, decodo: 1, soax: 2, direct: 3 }; const noop = () => () => {}; /** false during SSR/hydration, true once on the client — avoids a theme mismatch warning. */ const useMounted = () => React.useSyncExternalStore(noop, () => true, () => false); function usePalette() { const { resolvedTheme } = useTheme(); const mounted = useMounted(); const dark = mounted && resolvedTheme === "dark"; return dark ? DARK : LIGHT; } export function providerColor(provider: string, palette: string[]): string { const slot = PROVIDER_SLOT[provider]; return palette[slot ?? 3] ?? palette[0]!; } const axisStyle = { fontSize: 11, fill: "var(--fg-subtle)", fontFamily: "var(--font-mono)" } as const; const gridStroke = "var(--border)"; function fmtTick(unit: "hour" | "day") { return (t: string) => { const d = new Date(t); return unit === "hour" ? `${String(d.getHours()).padStart(2, "0")}:00` : `${d.getMonth() + 1}/${d.getDate()}`; }; } function TooltipBox({ active, payload, label, unit, money }: { active?: boolean; payload?: Array<{ name?: string; value?: number; color?: string }>; label?: string; unit: "hour" | "day"; money?: boolean }) { if (!active || !payload?.length) return null; const d = label ? new Date(label) : null; return (
{d ? (unit === "hour" ? d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit" }) : d.toLocaleDateString(undefined, { month: "short", day: "numeric" })) : label}
{payload.map((p) => (
{p.name} {money ? formatUsd(p.value ?? 0, true) : formatCompact(p.value ?? 0)}
))}
); } /** Revenue vs upstream cost vs margin over time. One axis (USD), three series, legend + direct table alongside. */ export function EconomicsChart({ data, unit }: { data: Array<{ key: string; revenue: number; cost: number; margin: number }>; unit: "hour" | "day" }) { const pal = usePalette(); if (!data.length) return
No requests in this range.
; return (
(v >= 1 ? `$${formatCompact(v)}` : `$${v.toFixed(2)}`)} /> } cursor={{ fill: "var(--bg-muted)" }} />
); } /** Requests over time, one line per provider (fixed hue per provider). */ export function ProviderSeriesChart({ data, providers, unit, metric = "requests" }: { data: Array>; providers: string[]; unit: "hour" | "day"; metric?: "requests" | "successes" | "cost" }) { const pal = usePalette(); if (!data.length || !providers.length) return
No routing metrics in this range.
; const money = metric === "cost"; return (
(money ? `$${v >= 1 ? formatCompact(v) : v.toFixed(2)}` : formatCompact(v))} /> } cursor={{ stroke: "var(--border-strong)" }} /> {providers.map((p) => ( ))}
); }