Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 17 additions & 21 deletions src/components/benchmark-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@
// they always saw.
const allowedViews = viewsForBenchmark(benchmark);
const defaultView = defaultViewFor(benchmark);
const [view, setView, viewMounted] = useViewPreference(

Check failure on line 183 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook "useViewPreference" is called conditionally. React Hooks must be called in the exact same order in every component render
benchmark.slug,
defaultView,
allowedViews,
Expand All @@ -191,7 +191,7 @@
// hidden when they switch to distribution or donut - the model is
// "this is the field of providers the reader chose to focus on",
// not "what each view chose to drop". Resets on bench navigation.
const [excluded, setExcluded] = useState<Set<string>>(() => new Set());

Check failure on line 194 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render
const toggleExclude = (slug: string) =>
setExcluded((prev) => {
const next = new Set(prev);
Expand All @@ -207,8 +207,8 @@
// being split between the dimension row and the chart toolbar.
const chartRegions = chartOnlyRegions(benchmark);
const showChartRegionRow = regionOptions.length === 0 && chartRegions.length > 1;
const [chartRegion, setChartRegion] = useState<string>("all");

Check failure on line 210 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
const chartRegionOptions: ChainOption[] = useMemo(

Check failure on line 211 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook "useMemo" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
() => [
{ value: "all", label: "All" },
...chartRegions.map((r) => ({ value: r, label: REGION_DISPLAY[r] ?? r })),
Expand Down Expand Up @@ -291,57 +291,53 @@

{!isDraft && (
<>
<div className="mt-8 card-soft rounded-xl p-4 sm:p-6 lg:p-8 relative">
{/* 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. */}
<div className="absolute top-3 right-3 sm:top-4 sm:right-4 z-10">
<ViewSwitcher
allowed={allowedViews}
value={view}
onChange={setView}
/>
</div>
{/* 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. */}
<div className="mt-8 card-soft rounded-xl p-4 sm:p-6 lg:p-8">
{/* 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. */}
<div
className="min-h-[260px] transition-opacity duration-200"
style={{ opacity: viewMounted ? 1 : 0 }}
>
{view === "countLeaderboard" && <CountLeaderboard benchmark={benchmark} />}
{view === "countLeaderboard" && (
<CountLeaderboard
benchmark={benchmark}
headerActions={<ViewSwitcher allowed={allowedViews} value={view} onChange={setView} />}
/>
)}
{view === "rankedBar" && (
<RankedBarChart
benchmark={benchmark}
excluded={excluded}
onToggleExclude={toggleExclude}
onResetExcluded={resetExcluded}
headerActions={<ViewSwitcher allowed={allowedViews} value={view} onChange={setView} />}
/>
)}
{view === "distribution" && (
<DistributionChart
benchmark={benchmark}
excluded={excluded}
onToggleExclude={toggleExclude}
headerActions={<ViewSwitcher allowed={allowedViews} value={view} onChange={setView} />}
/>
)}
{view === "donut" && (
<DonutChart
benchmark={benchmark}
excluded={excluded}
onToggleExclude={toggleExclude}
headerActions={<ViewSwitcher allowed={allowedViews} value={view} onChange={setView} />}
/>
)}
{view === "timeseries" && (
<TimeSeriesChart
benchmark={benchmark}
region={showChartRegionRow ? chartRegion : undefined}
headerActions={<ViewSwitcher allowed={allowedViews} value={view} onChange={setView} />}
/>
)}
</div>
Expand Down
24 changes: 21 additions & 3 deletions src/components/count-leaderboard.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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]);
Expand All @@ -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 && (
<div className="flex items-center justify-between gap-3 mb-3 min-h-7">
<p className="text-[11px] font-sans font-medium uppercase tracking-[0.18em] text-ink-faint">
Leaderboard
</p>
{headerActions}
</div>
)}
{/* Thin summary strip. matches the latency-bench layout. */}
<dl className="mt-10 grid grid-cols-2 sm:flex sm:flex-wrap items-baseline gap-x-8 gap-y-3 border-y border-rule py-4">
<dl className="grid grid-cols-2 sm:flex sm:flex-wrap items-baseline gap-x-8 gap-y-3 border-y border-rule py-4">
<CountStat
label="Leader"
value={fmtValue(leader?.ms.p50 ?? 0, benchmark.unit)}
Expand Down
15 changes: 12 additions & 3 deletions src/components/distribution-chart.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useMemo } from "react";
import { type ReactNode, useMemo } from "react";
import type { Benchmark } from "@/types/benchmark";
import { liveResults } from "@/lib/provider-filters";
import { Hint } from "@/components/hint";
Expand Down Expand Up @@ -29,10 +29,16 @@ export function DistributionChart({
benchmark,
excluded: controlledExcluded,
onToggleExclude,
headerActions,
}: {
benchmark: Benchmark;
excluded?: Set<string>;
onToggleExclude?: (slug: string) => void;
/** Optional slot rendered in the chart's header row, right-aligned.
* BenchmarkBody passes the <ViewSwitcher> 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(
Expand All @@ -59,11 +65,14 @@ export function DistributionChart({

return (
<div className="w-full">
<div className="flex items-baseline justify-between mb-5">
<div className="flex items-center justify-between gap-3 mb-5 min-h-7">
<p className="text-[11px] font-sans font-medium uppercase tracking-[0.18em] text-ink-faint">
Latency spread
</p>
<MarkerLegend />
<div className="flex items-center gap-3">
<MarkerLegend />
{headerActions}
</div>
</div>
<ul className="flex flex-col">
{sorted.map((r) => {
Expand Down
15 changes: 10 additions & 5 deletions src/components/donut-chart.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -36,10 +36,12 @@ export function DonutChart({
benchmark,
excluded: controlledExcluded,
onToggleExclude,
headerActions,
}: {
benchmark: Benchmark;
excluded?: Set<string>;
onToggleExclude?: (slug: string) => void;
headerActions?: ReactNode;
}) {
const { results } = benchmark;
const { excluded, toggle } = useChartExclusion(
Expand Down Expand Up @@ -106,13 +108,16 @@ export function DonutChart({

return (
<div className="w-full">
<div className="flex items-baseline justify-between mb-5">
<div className="flex items-center justify-between gap-3 mb-5 min-h-7">
<p className="text-[11px] font-sans font-medium uppercase tracking-[0.18em] text-ink-faint">
Share by p50
</p>
<p className="text-[11px] font-mono tabular text-ink-faint">
{live.length} of {liveAll.length} live
</p>
<div className="flex items-center gap-3">
<p className="text-[11px] font-mono tabular text-ink-faint">
{live.length} of {liveAll.length} live
</p>
{headerActions}
</div>
</div>
<div className="flex flex-col items-center md:flex-row md:items-center gap-6 md:gap-10">
<div
Expand Down
27 changes: 17 additions & 10 deletions src/components/ranked-bar-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ type Props = {
excluded?: Set<string>;
onToggleExclude?: (slug: string) => void;
onResetExcluded?: () => void;
/** Optional slot rendered in the chart's header row, right-aligned.
* BenchmarkBody passes the <ViewSwitcher> here. */
headerActions?: import("react").ReactNode;
};

/**
Expand All @@ -45,6 +48,7 @@ export function RankedBarChart({
excluded: controlledExcluded,
onToggleExclude,
onResetExcluded,
headerActions,
}: Props) {
const { excluded, toggle, reset } = useChartExclusion(
controlledExcluded,
Expand Down Expand Up @@ -100,20 +104,23 @@ export function RankedBarChart({

return (
<figure className="my-2">
<div className="mb-3 flex flex-wrap items-baseline justify-between gap-3">
<div className="mb-3 flex flex-wrap items-center justify-between gap-3 min-h-7">
<p className="inline-flex items-center gap-2 text-[10px] font-medium uppercase tracking-[0.18em] text-ink-muted">
<LiveDot />
<span>{benchmark.metric} · last 24 hours</span>
</p>
{excludedCount > 0 && (
<button
type="button"
onClick={reset}
className="text-[10px] font-sans font-medium uppercase tracking-[0.16em] text-ink-muted hover:text-ink lnk"
>
Reset · {excludedCount} excluded
</button>
)}
<div className="flex items-center gap-3">
{excludedCount > 0 && (
<button
type="button"
onClick={reset}
className="text-[10px] font-sans font-medium uppercase tracking-[0.16em] text-ink-muted hover:text-ink lnk"
>
Reset · {excludedCount} excluded
</button>
)}
{headerActions}
</div>
</div>
<ul className="space-y-2">
{rows.map((r) => {
Expand Down
19 changes: 12 additions & 7 deletions src/components/time-series-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* 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";
Expand All @@ -40,7 +42,7 @@
global: "Global",
};

export function TimeSeriesChart({ benchmark, region: regionProp }: Props) {
export function TimeSeriesChart({ benchmark, region: regionProp, headerActions }: Props) {
const [range, setRange] = useState<Range>("24h");
const [regionLocal, setRegionLocal] = useState<string>("all");
const region = regionProp ?? regionLocal;
Expand Down Expand Up @@ -88,12 +90,15 @@

return (
<figure className="my-2">
<p className="mb-3 inline-flex items-center gap-2 text-[10px] font-medium uppercase tracking-[0.18em] text-ink-muted">
<LiveDot />
<span>
{benchmark.metric} · {RANGE_LABEL[range]}
</span>
</p>
<div className="mb-3 flex items-center justify-between gap-3 min-h-7">
<p className="inline-flex items-center gap-2 text-[10px] font-medium uppercase tracking-[0.18em] text-ink-muted">
<LiveDot />
<span>
{benchmark.metric} · {RANGE_LABEL[range]}
</span>
</p>
{headerActions}
</div>
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-1">
{RANGES.map((r) => {
Expand Down Expand Up @@ -325,7 +330,7 @@
const lastY = lastDrawn ? lastDrawn.y : padT + innerH;
return { ...l, color, pts, linePath, fillPath, lastX, lastY, last };
});
}, [lines, padL, padR, padT, padB, innerW, innerH, lo, yRange]);

Check warning on line 333 in src/components/time-series-chart.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook useMemo has unnecessary dependencies: 'padB' and 'padR'. Either exclude them or remove the dependency array

const hoverX = hover ? padL + innerW * (hover.idx / Math.max(1, numPoints - 1)) : null;
const hoverFraction = hover ? hover.idx / Math.max(1, numPoints - 1) : 0;
Expand Down
Loading