TypeScript 97.5%
SQL 1.4%
Python 0.8%
1"use client";2import * as React from "react";3import { Bar, BarChart, CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";4import { useTheme } from "next-themes";5import { formatCompact, formatUsd } from "@/lib/format";67/**8 * Categorical palette, validated (dataviz skill) against both surfaces.9 * Fixed slot order — providers always keep the same hue regardless of which are present.10 */11const LIGHT = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100"];12const DARK = ["#3987e5", "#d95926", "#199e70", "#c98500"];13const PROVIDER_SLOT: Record<string, number> = { oxylabs: 0, decodo: 1, soax: 2, direct: 3 };1415const noop = () => () => {};16/** false during SSR/hydration, true once on the client — avoids a theme mismatch warning. */17const useMounted = () => React.useSyncExternalStore(noop, () => true, () => false);1819function usePalette() {20 const { resolvedTheme } = useTheme();21 const mounted = useMounted();22 const dark = mounted && resolvedTheme === "dark";23 return dark ? DARK : LIGHT;24}2526export function providerColor(provider: string, palette: string[]): string {27 const slot = PROVIDER_SLOT[provider];28 return palette[slot ?? 3] ?? palette[0]!;29}3031const axisStyle = { fontSize: 11, fill: "var(--fg-subtle)", fontFamily: "var(--font-mono)" } as const;32const gridStroke = "var(--border)";3334function fmtTick(unit: "hour" | "day") {35 return (t: string) => {36 const d = new Date(t);37 return unit === "hour" ? `${String(d.getHours()).padStart(2, "0")}:00` : `${d.getMonth() + 1}/${d.getDate()}`;38 };39}4041function TooltipBox({ active, payload, label, unit, money }: { active?: boolean; payload?: Array<{ name?: string; value?: number; color?: string }>; label?: string; unit: "hour" | "day"; money?: boolean }) {42 if (!active || !payload?.length) return null;43 const d = label ? new Date(label) : null;44 return (45 <div className="rounded-md border border-border bg-bg-elevated px-3 py-2 text-[12px] shadow-md">46 <div className="mb-1 font-medium text-fg">{d ? (unit === "hour" ? d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit" }) : d.toLocaleDateString(undefined, { month: "short", day: "numeric" })) : label}</div>47 <div className="space-y-0.5">48 {payload.map((p) => (49 <div key={p.name} className="flex items-center justify-between gap-4">50 <span className="flex items-center gap-1.5 text-fg-muted">51 <span className="size-2 rounded-full" style={{ background: p.color }} aria-hidden />52 {p.name}53 </span>54 <span className="tabular font-mono text-fg">{money ? formatUsd(p.value ?? 0, true) : formatCompact(p.value ?? 0)}</span>55 </div>56 ))}57 </div>58 </div>59 );60}6162/** Revenue vs upstream cost vs margin over time. One axis (USD), three series, legend + direct table alongside. */63export function EconomicsChart({ data, unit }: { data: Array<{ key: string; revenue: number; cost: number; margin: number }>; unit: "hour" | "day" }) {64 const pal = usePalette();65 if (!data.length) return <div className="flex h-[260px] items-center justify-center text-[13px] text-fg-muted">No requests in this range.</div>;66 return (67 <div className="h-[260px] w-full" role="img" aria-label="Revenue, upstream cost and margin over time">68 <ResponsiveContainer width="100%" height="100%">69 <BarChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }} barGap={2} barCategoryGap="20%">70 <CartesianGrid vertical={false} stroke={gridStroke} strokeDasharray="0" />71 <XAxis dataKey="key" tickFormatter={fmtTick(unit)} tick={axisStyle} axisLine={false} tickLine={false} minTickGap={24} />72 <YAxis tick={axisStyle} axisLine={false} tickLine={false} width={56} tickFormatter={(v: number) => (v >= 1 ? `$${formatCompact(v)}` : `$${v.toFixed(2)}`)} />73 <Tooltip content={<TooltipBox unit={unit} money />} cursor={{ fill: "var(--bg-muted)" }} />74 <Legend iconType="circle" iconSize={8} wrapperStyle={{ fontSize: 12, color: "var(--fg-muted)" }} />75 <Bar dataKey="revenue" name="Revenue" fill={pal[0]} radius={[3, 3, 0, 0]} maxBarSize={28} />76 <Bar dataKey="cost" name="Upstream cost" fill={pal[1]} radius={[3, 3, 0, 0]} maxBarSize={28} />77 <Bar dataKey="margin" name="Margin" fill={pal[2]} radius={[3, 3, 0, 0]} maxBarSize={28} />78 </BarChart>79 </ResponsiveContainer>80 </div>81 );82}8384/** Requests over time, one line per provider (fixed hue per provider). */85export function ProviderSeriesChart({ data, providers, unit, metric = "requests" }: { data: Array<Record<string, number | string>>; providers: string[]; unit: "hour" | "day"; metric?: "requests" | "successes" | "cost" }) {86 const pal = usePalette();87 if (!data.length || !providers.length) return <div className="flex h-[260px] items-center justify-center text-[13px] text-fg-muted">No routing metrics in this range.</div>;88 const money = metric === "cost";89 return (90 <div className="h-[260px] w-full" role="img" aria-label={`${metric} over time per provider`}>91 <ResponsiveContainer width="100%" height="100%">92 <LineChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>93 <CartesianGrid vertical={false} stroke={gridStroke} />94 <XAxis dataKey="t" tickFormatter={fmtTick(unit)} tick={axisStyle} axisLine={false} tickLine={false} minTickGap={24} />95 <YAxis tick={axisStyle} axisLine={false} tickLine={false} width={56} tickFormatter={(v: number) => (money ? `$${v >= 1 ? formatCompact(v) : v.toFixed(2)}` : formatCompact(v))} />96 <Tooltip content={<TooltipBox unit={unit} money={money} />} cursor={{ stroke: "var(--border-strong)" }} />97 <Legend iconType="circle" iconSize={8} wrapperStyle={{ fontSize: 12, color: "var(--fg-muted)" }} />98 {providers.map((p) => (99 <Line key={p} type="monotone" dataKey={`${p}:${metric}`} name={p} stroke={providerColor(p, pal)} strokeWidth={2} dot={false} activeDot={{ r: 4, strokeWidth: 2, stroke: "var(--bg-elevated)" }} connectNulls />100 ))}101 </LineChart>102 </ResponsiveContainer>103 </div>104 );105}106