From 575cdb48a398b5b74356673fbd4e8b0ad842843b Mon Sep 17 00:00:00 2001 From: Flotapponnier Date: Tue, 15 Sep 2026 01:09:50 +0200 Subject: [PATCH] compare: drop the per-card trend charts, the perp volume hero is the chart Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HJgbZCqjR4nvCfcJSzofbw --- src/app/compare/[slug]/page.tsx | 5 - src/components/compare-bench-card.tsx | 18 -- src/components/compare-trend-chart.tsx | 242 ------------------------- 3 files changed, 265 deletions(-) delete mode 100644 src/components/compare-trend-chart.tsx diff --git a/src/app/compare/[slug]/page.tsx b/src/app/compare/[slug]/page.tsx index 37920584b..e0bf7e3d1 100644 --- a/src/app/compare/[slug]/page.tsx +++ b/src/app/compare/[slug]/page.tsx @@ -1004,11 +1004,6 @@ export default async function ComparePage({ bench={s} aName={a.name} bName={b.name} - // Per-card trend charts only for perp venue pairs, where the - // benches are daily or slow-moving series worth a history. - // Latency / fee benches on other pairs read as flat lines. - aSlug={perpPair ? a.slug : undefined} - bSlug={perpPair ? b.slug : undefined} /> ))} diff --git a/src/components/compare-bench-card.tsx b/src/components/compare-bench-card.tsx index 20e820661..d319b6c9e 100644 --- a/src/components/compare-bench-card.tsx +++ b/src/components/compare-bench-card.tsx @@ -1,7 +1,6 @@ "use client"; import { Fragment } from "react"; -import { CompareTrendChart } from "@/components/compare-trend-chart"; import Link from "next/link"; import type { Benchmark } from "@/types/benchmark"; import { fmtUnit, fmtValue, unitSuffix } from "@/lib/format"; @@ -72,16 +71,10 @@ export function CompareBenchCard({ bench, aName, bName, - aSlug, - bSlug, }: { bench: CompareBench; aName: string; bName: string; - /** Provider slugs, for the trend chart's /api/series filter. Optional - * so older call sites keep working without the chart. */ - aSlug?: string; - bSlug?: string; }) { const hasScopes = bench.panelScopes.length > 0; @@ -153,17 +146,6 @@ export function CompareBenchCard({ )} - {aSlug && bSlug && ( - - )} - {bench.note && (

{bench.note} diff --git a/src/components/compare-trend-chart.tsx b/src/components/compare-trend-chart.tsx deleted file mode 100644 index c35c93697..000000000 --- a/src/components/compare-trend-chart.tsx +++ /dev/null @@ -1,242 +0,0 @@ -"use client"; - -import { useEffect, useMemo, useRef, useState } from "react"; -import { fmtUnit } from "@/lib/format"; -import { brandColor } from "@/lib/brand"; -import { lineColor } from "@/lib/series-colors"; - -type Range = "7d" | "30d" | "90d"; - -type SeriesPayload = { - timestamps: number[]; - providers: { slug: string; values: (number | null)[] }[]; -}; - -/** - * Two-line trend for one shared bench on a compare page: the same - * /api/series the bench page reads, filtered to the pair, so every - * compare card carries the history behind its two headline numbers - * rather than a single 24h figure. Fetched when the card scrolls into - * view (a pair shares up to a dozen benches) and cached per range. - * Hidden when neither side has a point in the window. - */ -export function CompareTrendChart({ - benchSlug, - unit, - aSlug, - bSlug, - aName, - bName, -}: { - benchSlug: string; - unit: string; - aSlug: string; - bSlug: string; - aName: string; - bName: string; -}) { - const [range, setRange] = useState("30d"); - const [data, setData] = useState>>({}); - // Starts visible when IntersectionObserver is unavailable (old - // browsers, SSR) so the chart still loads; otherwise waits for the - // card to scroll near the viewport. - const [visible, setVisible] = useState( - () => typeof IntersectionObserver === "undefined", - ); - const [hover, setHover] = useState(null); - const rootRef = useRef(null); - - useEffect(() => { - const el = rootRef.current; - if (!el || visible) return; - const io = new IntersectionObserver( - (entries) => { - if (entries.some((e) => e.isIntersecting)) setVisible(true); - }, - { rootMargin: "200px" }, - ); - io.observe(el); - return () => io.disconnect(); - }, [visible]); - - useEffect(() => { - if (!visible || range in data) return; - let cancelled = false; - const qs = new URLSearchParams({ range, raw: "1", providers: `${aSlug},${bSlug}` }); - fetch(`/api/series/${benchSlug}?${qs.toString()}`) - .then((r) => (r.ok ? r.json() : null)) - .then((json: SeriesPayload | null) => { - if (cancelled) return; - setData((d) => ({ ...d, [range]: json })); - }) - .catch(() => { - if (!cancelled) setData((d) => ({ ...d, [range]: null })); - }); - return () => { - cancelled = true; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [visible, range, benchSlug, aSlug, bSlug]); - - const payload = data[range]; - const series = useMemo(() => { - if (!payload) return null; - const pick = (slug: string) => - payload.providers.find((p) => p.slug === slug)?.values ?? []; - const a = pick(aSlug); - const b = pick(bSlug); - const n = Math.max(a.length, b.length, payload.timestamps.length); - if (n === 0) return null; - const hasA = a.some((v) => v !== null); - const hasB = b.some((v) => v !== null); - if (!hasA && !hasB) return null; - return { a, b, ts: payload.timestamps, n }; - }, [payload, aSlug, bSlug]); - - const aColor = brandColor(aSlug) ?? lineColor(0); - const bColor = brandColor(bSlug) ?? lineColor(1); - - // Nothing yet, or confirmed empty: keep the card's layout stable with - // a slim placeholder only while loading; render nothing on empty. - if (payload === null || (payload && !series)) return

; - if (!series) { - return ( -
- ); - } - - const W = 800; - const H = 170; - const PAD_L = 54; - const PAD_R = 8; - const PAD_T = 10; - const PAD_B = 20; - const plotW = W - PAD_L - PAD_R; - const plotH = H - PAD_T - PAD_B; - const vals = [...series.a, ...series.b].filter((v): v is number => v !== null && Number.isFinite(v)); - const max = Math.max(...vals, 0); - const min = Math.min(...vals, 0); - const span = max - min || 1; - const x = (i: number) => PAD_L + (series.n <= 1 ? plotW / 2 : (i / (series.n - 1)) * plotW); - const y = (v: number) => PAD_T + plotH - ((v - min) / span) * plotH; - const path = (arr: (number | null)[]) => { - const segs: string[] = []; - let cur = ""; - arr.forEach((v, i) => { - if (v === null || !Number.isFinite(v)) { - if (cur) segs.push(cur); - cur = ""; - return; - } - cur += `${cur ? "L" : "M"}${x(i).toFixed(1)},${y(v).toFixed(1)}`; - }); - if (cur) segs.push(cur); - return segs.join(" "); - }; - const hv = hover !== null ? { a: series.a[hover] ?? null, b: series.b[hover] ?? null, t: series.ts[hover] } : null; - const fmt = (v: number | null) => (v === null ? "n/a" : fmtUnit(v, unit)); - const first = series.ts[0]; - const last = series.ts[series.n - 1]; - - return ( -
-
-
- - - {aName} - - - - {bName} - - {hv && ( - - {fmtTs(hv.t, range)} ยท {fmt(hv.a)} vs {fmt(hv.b)} - - )} -
-
- {(["7d", "30d", "90d"] as const).map((r) => ( - - ))} -
-
- setHover(null)} - > - {[0, 0.5, 1].map((f) => { - const v = min + f * span; - return ( - - - - {fmtUnit(v, unit)} - - - ); - })} - - - {hover !== null && ( - - )} - {Array.from({ length: series.n }, (_, i) => ( - setHover(i)} - /> - ))} - - {fmtTs(first, range)} - - - {fmtTs(last, range)} - - -
- ); -} - -function fmtTs(ms: number | undefined, range: Range): string { - if (!ms) return ""; - const d = new Date(ms); - const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - const day = `${months[d.getUTCMonth()]} ${d.getUTCDate()}`; - if (range === "7d") return `${day} ${String(d.getUTCHours()).padStart(2, "0")}:00`; - return day; -}