diff --git a/src/components/benchmark-body.tsx b/src/components/benchmark-body.tsx index 7104bd79..19424e21 100644 --- a/src/components/benchmark-body.tsx +++ b/src/components/benchmark-body.tsx @@ -291,37 +291,30 @@ export function BenchmarkBody({ {!isDraft && ( <> -
- {/* View switcher floats in the card's top-right corner so it - doesn't reserve a vertical band of its own. The chart fills - the card top-to-bottom; the switcher sits in the existing - card padding and never competes with the chart for space. - Popover opens downward (default) since there's room below - the corner before the chart geometry begins. */} -
- -
- {/* Fade-in until the localStorage view preference resolves. - Without this the SSR-default view paints first, then - useViewPreference swaps to the saved view in a visible - second frame ("two charts at once" user report). The - wrapper keeps a min-h so the card doesn't collapse to - zero while the chart is invisible. */} +
+ {/* Each chart owns its header row and accepts a headerActions + slot. We pass the ViewSwitcher there so the control sits + on the same baseline as the chart's own title text - + consistent across all views, no separate row reserved, + no absolute overlay that risks clipping the legend or + action buttons each chart already has on the right. */}
- {view === "countLeaderboard" && } + {view === "countLeaderboard" && ( + } + /> + )} {view === "rankedBar" && ( } /> )} {view === "distribution" && ( @@ -329,6 +322,7 @@ export function BenchmarkBody({ benchmark={benchmark} excluded={excluded} onToggleExclude={toggleExclude} + headerActions={} /> )} {view === "donut" && ( @@ -336,12 +330,14 @@ export function BenchmarkBody({ benchmark={benchmark} excluded={excluded} onToggleExclude={toggleExclude} + headerActions={} /> )} {view === "timeseries" && ( } /> )}
diff --git a/src/components/count-leaderboard.tsx b/src/components/count-leaderboard.tsx index 0b864154..5e94342a 100644 --- a/src/components/count-leaderboard.tsx +++ b/src/components/count-leaderboard.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMemo } from "react"; +import { type ReactNode, useMemo } from "react"; import type { Benchmark } from "@/types/benchmark"; import { fmtValue } from "@/lib/format"; @@ -14,7 +14,13 @@ import { buildProviderColors } from "@/lib/series-colors"; * bar instead. the two visualizations that actually carry information * for this metric shape. */ -export function CountLeaderboard({ benchmark }: { benchmark: Benchmark }) { +export function CountLeaderboard({ + benchmark, + headerActions, +}: { + benchmark: Benchmark; + headerActions?: ReactNode; +}) { const ranked = rankResults(benchmark.results, benchmark.higherIsBetter); const max = Math.max(...ranked.map((r) => r.ms.p50)) || 1; const colors = useMemo(() => buildProviderColors(benchmark.results), [benchmark.results]); @@ -27,8 +33,20 @@ export function CountLeaderboard({ benchmark }: { benchmark: Benchmark }) { return ( <> + {/* Header row: leaderboard label + headerActions slot (view + switcher). Aligned with the same baseline pattern as the + other chart views so the toolbar position is consistent + across views. */} + {headerActions && ( +
+

+ Leaderboard +

+ {headerActions} +
+ )} {/* Thin summary strip. matches the latency-bench layout. */} -
+
; onToggleExclude?: (slug: string) => void; + /** Optional slot rendered in the chart's header row, right-aligned. + * BenchmarkBody passes the here so the control sits + * on the same baseline as the chart title instead of floating in + * the card corner or eating a footer row of its own. */ + headerActions?: ReactNode; }) { const { results, unit, higherIsBetter } = benchmark; const { excluded, toggle } = useChartExclusion( @@ -59,11 +65,14 @@ export function DistributionChart({ return (
-
+

Latency spread

- +
+ + {headerActions} +
    {sorted.map((r) => { diff --git a/src/components/donut-chart.tsx b/src/components/donut-chart.tsx index d0d5a567..1ec44253 100644 --- a/src/components/donut-chart.tsx +++ b/src/components/donut-chart.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMemo, useState } from "react"; +import { type ReactNode, useMemo, useState } from "react"; import type { Benchmark } from "@/types/benchmark"; import { liveResults } from "@/lib/provider-filters"; import { ProviderLogo } from "@/components/provider-logo"; @@ -36,10 +36,12 @@ export function DonutChart({ benchmark, excluded: controlledExcluded, onToggleExclude, + headerActions, }: { benchmark: Benchmark; excluded?: Set; onToggleExclude?: (slug: string) => void; + headerActions?: ReactNode; }) { const { results } = benchmark; const { excluded, toggle } = useChartExclusion( @@ -106,13 +108,16 @@ export function DonutChart({ return (
    -
    +

    Share by p50

    -

    - {live.length} of {liveAll.length} live -

    +
    +

    + {live.length} of {liveAll.length} live +

    + {headerActions} +
    ; onToggleExclude?: (slug: string) => void; onResetExcluded?: () => void; + /** Optional slot rendered in the chart's header row, right-aligned. + * BenchmarkBody passes the here. */ + headerActions?: import("react").ReactNode; }; /** @@ -45,6 +48,7 @@ export function RankedBarChart({ excluded: controlledExcluded, onToggleExclude, onResetExcluded, + headerActions, }: Props) { const { excluded, toggle, reset } = useChartExclusion( controlledExcluded, @@ -100,20 +104,23 @@ export function RankedBarChart({ return (
    -
    +

    {benchmark.metric} · last 24 hours

    - {excludedCount > 0 && ( - - )} +
    + {excludedCount > 0 && ( + + )} + {headerActions} +
      {rows.map((r) => { diff --git a/src/components/time-series-chart.tsx b/src/components/time-series-chart.tsx index 1f3ff18a..402f2a57 100644 --- a/src/components/time-series-chart.tsx +++ b/src/components/time-series-chart.tsx @@ -14,6 +14,8 @@ type Props = { * filters its lines by this value and hides its internal region tabs * (the parent component renders them in a shared dimension row). */ region?: string; + /** Optional slot rendered in the chart's header row, right-aligned. */ + headerActions?: import("react").ReactNode; }; type Range = "1h" | "6h" | "24h" | "7d"; @@ -40,7 +42,7 @@ const REGION_LABEL: Record = { global: "Global", }; -export function TimeSeriesChart({ benchmark, region: regionProp }: Props) { +export function TimeSeriesChart({ benchmark, region: regionProp, headerActions }: Props) { const [range, setRange] = useState("24h"); const [regionLocal, setRegionLocal] = useState("all"); const region = regionProp ?? regionLocal; @@ -88,12 +90,15 @@ export function TimeSeriesChart({ benchmark, region: regionProp }: Props) { return (
      -

      - - - {benchmark.metric} · {RANGE_LABEL[range]} - -

      +
      +

      + + + {benchmark.metric} · {RANGE_LABEL[range]} + +

      + {headerActions} +
      {RANGES.map((r) => {