-
- Δ p50
-
-
- {fmtUnit(Math.abs(delta), benchmark.unit)}
-
-
- {deltaPct >= 0 ? "+" : ""}{deltaPct.toFixed(0)}%
-
+ {benchmark.title} · top 2
- {/* Right column */}
+ {/* Left */}
+
+ {/* Center divider */}
- Rank 2
-
-
- {b.name}
-
-
-
- {fmtValue(b.ms.p50, benchmark.unit)}
-
-
- {unitSuffix(benchmark.unit).trim()}
-
-
-
- p50
-
-
-
-
-
- P99
-
- {fmtUnit(b.ms.p99, benchmark.unit)}
+ Δ p50
-
-
- SUCCESS
-
-
{b.successRate.toFixed(2)}%
+
+ {fmtUnit(Math.abs(delta), benchmark.unit)}
-
-
- N · 24H
-
-
{Math.round(b.sampleSize ?? 0).toLocaleString()}
+
+ {deltaPct >= 0 ? "+" : ""}
+ {deltaPct.toFixed(0)}%
+ {/* Right */}
+
+
+ ),
+ { ...SIZE }
+ );
+}
- {/* Footer */}
-
+
+ Rank {String(rank).padStart(2, "0")}
+
+
+ {name}
+
+
+
- openchainbench.xyz
- {benchmark.category}
-
+ {p50}
+
+
+ {unit}
+
+
+
+ p50
- ),
- { ...SIZE }
- );
-}
-
-// ─── Leaderboard · ranked rows with mini bars ──────────────────────────
-async function renderLeaderboard(
- benchmark: NonNullable
>>
-) {
- const sorted = [...benchmark.results].sort((a, b) => a.ms.p50 - b.ms.p50);
- const colors = buildProviderColors(benchmark.results);
- const maxP50 = Math.max(...sorted.map((r) => r.ms.p50)) || 1;
- return new ImageResponse(
- (
- {/* Header */}
-
-
+
-
- {benchmark.category}
-
-
+ {p99}
+
+
+
- {benchmark.title}
-
-
+ {successPct.toFixed(2)}%
+
+
+
- Ranked by p50 · ascending
-
-
-
- {/* Rows */}
-
- {sorted.map((r, i) => {
- const color = colors.get(r.slug) ?? INK_SOFT;
- const widthPct = Math.max(8, (r.ms.p50 / maxP50) * 100);
- return (
-
-
- {String(i + 1).padStart(2, "0")}
-
-
-
-
- {r.name}
-
-
-
- {fmtValue(r.ms.p50, benchmark.unit)}
-
-
- {unitSuffix(benchmark.unit).trim()}
-
-
-
-
-
-
- );
- })}
-
-
- {/* Footer */}
-
- openchainbench.xyz
-
- {Math.round(benchmark.sampleSize).toLocaleString()} samples · 24h
+ N · 24H
+ {Math.round(n).toLocaleString()}
- ),
- { ...SIZE }
+
);
}
diff --git a/src/components/share-section.tsx b/src/components/share-section.tsx
index e45588c4..ad9219a0 100644
--- a/src/components/share-section.tsx
+++ b/src/components/share-section.tsx
@@ -1,12 +1,15 @@
"use client";
-import { useState } from "react";
-import { Download, ChevronDown } from "lucide-react";
+import { useEffect, useMemo, useState } from "react";
+import { Download, ChevronDown, Loader2 } from "lucide-react";
+import type { Benchmark } from "@/types/benchmark";
type Template = {
id: string;
label: string;
description: string;
+ /** Whether this template lets the reader filter providers. */
+ filterable: boolean;
};
const TEMPLATES: Template[] = [
@@ -14,42 +17,87 @@ const TEMPLATES: Template[] = [
id: "ranking",
label: "Ranking",
description: "Vertical bars sorted ascending by p50, with provider names and p99 tails.",
+ filterable: false,
},
{
id: "leaderboard",
label: "Leaderboard",
description: "Ranked rows with horizontal mini-bars in each provider's signature color.",
+ filterable: false,
},
{
id: "snapshot",
label: "Snapshot",
description: "Full 24-hour multi-line chart with per-provider legend at the bottom.",
+ filterable: true,
},
{
id: "headline",
label: "Headline",
description: "Big-number poster — the field's fastest p50 in the winner's color.",
+ filterable: true,
},
{
id: "compare",
label: "Compare",
description: "Top-2 head-to-head with p50 / p99 / success / sample-size and the delta between them.",
+ filterable: true,
},
];
type Props = {
slug: string;
title: string;
+ benchmark: Benchmark;
};
-export function ShareSection({ slug, title }: Props) {
+export function ShareSection({ slug, title, benchmark }: Props) {
const [activeId, setActiveId] = useState
("ranking");
- const cardSrc = (templateId: string) =>
- `/benchmarks/${slug}/share-card?template=${templateId}`;
+ // Sort providers ascending p50 for the toggle UI — same order as the
+ // legend / ledger.
+ const orderedProviders = useMemo(
+ () =>
+ [...benchmark.results].sort((a, b) => a.ms.p50 - b.ms.p50).map((r) => ({
+ slug: r.slug,
+ name: r.name,
+ })),
+ [benchmark]
+ );
+
+ // Default: all providers selected.
+ const [selected, setSelected] = useState>(
+ () => new Set(orderedProviders.map((p) => p.slug))
+ );
+
+ function toggleProvider(slug: string) {
+ setSelected((prev) => {
+ const next = new Set(prev);
+ if (next.has(slug)) next.delete(slug);
+ else next.add(slug);
+ return next;
+ });
+ }
+
+ const activeTemplate = TEMPLATES.find((t) => t.id === activeId);
+ const showFilter = activeTemplate?.filterable ?? false;
+
+ // Build the URL with optional providers filter.
+ const cardSrc = (templateId: string, applyFilter: boolean) => {
+ const tpl = TEMPLATES.find((t) => t.id === templateId);
+ const base = `/benchmarks/${slug}/share-card?template=${templateId}`;
+ if (!applyFilter || !tpl?.filterable) return base;
+ if (selected.size === orderedProviders.length) return base; // all selected ⇒ no param
+ if (selected.size === 0) return base; // nothing selected ⇒ no param (server falls back)
+ const list = orderedProviders
+ .filter((p) => selected.has(p.slug))
+ .map((p) => p.slug)
+ .join(",");
+ return `${base}&providers=${encodeURIComponent(list)}`;
+ };
async function handleDownload(templateId: string) {
- const url = cardSrc(templateId);
+ const url = cardSrc(templateId, true);
try {
const res = await fetch(url);
const blob = await res.blob();
@@ -105,17 +153,44 @@ export function ShareSection({ slug, title }: Props) {
return (
{t.description}
-
- {/* eslint-disable-next-line @next/next/no-img-element */}
-
})
-
+
+ {/* Provider filter (only on filterable templates) */}
+ {showFilter && (
+
+
+ Providers
+
+ {orderedProviders.map((p) => {
+ const isOn = selected.has(p.slug);
+ return (
+
+ );
+ })}
+
+
+ )}
+
+