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
5 changes: 4 additions & 1 deletion src/app/alternatives/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,10 @@ export default async function AlternativePage({
...(citableAsOf(bench) ? { dateModified: bench.lastRunAt } : {}),
author: { "@id": `${SITE.url}/#org` },
publisher: { "@id": `${SITE.url}/#org` },
about: { "@id": `${url}#dataset` },
// `about` describes the topic. Use a Thing rather than @id-refing
// the same-page Dataset (Google validates @id-only refs as
// standalone incomplete Datasets even within the same @graph).
about: { "@type": "Thing", name: bench.metric },
},
buildBreadcrumbJsonLd([
{ name: "Home", item: SITE.url },
Expand Down
16 changes: 14 additions & 2 deletions src/app/benchmarks/[slug]/[chain]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,20 @@ export default async function BenchmarkChainPage({
author: [{ "@id": PERSON_ID }, { "@id": `${SITE.url}/#org` }],
reviewedBy: { "@id": PERSON_ID },
publisher: { "@id": `${SITE.url}/#org` },
about: { "@id": `${benchmarkUrl}#dataset` },
isPartOf: { "@id": `${benchmarkUrl}#article` },
// `about` describes the topic. Point at the metric as a Thing
// rather than @id-referencing the parent bench Dataset (cross-doc
// @id refs get validated as standalone incomplete Datasets by
// Google, same class of issue as the isPartOf/isBasedOn fixes).
about: { "@type": "Thing", name: benchmark.metric },
// isPartOf: parent bench article. Inline the required fields so
// the reference validates on its own (Google does not stitch
// cross-page @id refs).
isPartOf: {
"@type": "TechArticle",
"@id": `${benchmarkUrl}#article`,
name: benchmark.title,
url: benchmarkUrl,
},
},
buildBreadcrumbJsonLd([
{ name: "Home", item: SITE.url },
Expand Down
7 changes: 6 additions & 1 deletion src/app/benchmarks/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,12 @@ export default async function BenchmarkPage({
author: [{ "@id": PERSON_ID }, { "@id": `${SITE.url}/#org` }],
reviewedBy: { "@id": PERSON_ID },
publisher: { "@id": `${SITE.url}/#org` },
about: { "@id": `${benchmarkUrl}#dataset` },
// `about` describes the topic. Use a Thing (matches the
// StatisticalReport `about` pattern above) rather than @id-refing
// the same-page Dataset. Google's Dataset validator was flagging
// @id-only refs as standalone incomplete Datasets even within
// the same @graph (see the isBasedOn fix in dataset-jsonld.ts).
about: { "@type": "Thing", name: benchmark.metric },
},
buildBreadcrumbJsonLd([
{ name: "Home", item: SITE.url },
Expand Down
152 changes: 112 additions & 40 deletions src/components/chart-watermark.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,139 @@
/**
* DefiLlama-style diagonal attribution that lives BEHIND the chart data.
* Large, rotated, very low opacity — reads as "openchainbench.com"
* without competing with the lines/bars, and can't be crop-shaved because
* it sits at the middle of the frame. Captured in every PNG export
* (see `chart-export-button.tsx`).
* Centered branded watermark: two horizontal rules flanking the OCB "C"
* mark, with the openchainbench.com wordmark below. Discreet enough to
* disappear during normal chart reading, visible enough to attribute
* the graphic when it gets screenshot-shared. Baked inside the chart
* frame so it survives any crop.
*
* Two variants:
* - `<ChartWatermarkSvg>` → inlined <text> at plot center (rotated
* -22°) for SVG charts (time-series). Must be rendered BEFORE the
* data paths so it sits behind the lines.
* - `<ChartWatermarkHtml>` → absolutely-positioned centered span for
* HTML-composed charts (ranked bar). Parent must be `relative` and
* the watermark must be rendered first (z-index 0) so bars stack on top.
* - `<ChartWatermarkSvg>` → self-contained SVG group (time-series).
* Rendered inside the plot area BEFORE the data paths so the lines
* stack on top and the watermark reads as a background stamp.
* - `<ChartWatermarkHtml>` → absolutely-positioned centered block for
* HTML-composed charts (ranked bar). Parent must be `relative`.
*
* Opacity is intentionally lower than the bottom-right badge it replaced
* (0.10 vs 0.28) because the diagonal glyph is much larger and would
* otherwise dominate visually. Trade-off: still perfectly readable in a
* screenshot recompressed by Twitter/Slack; disappears entirely under
* fine-grained chart lines during normal reading.
* The C-mark geometry mirrors the masthead <SiteLogo> (site-logo.tsx)
* so the brand reads identically across screen, favicon and screenshot.
*/

export function ChartWatermarkSvg({
cx,
cy,
fontSize = 34,
scale = 1,
}: {
/** Center X of the plot area (padL + innerW/2). */
cx: number;
/** Center Y of the plot area (padT + innerH/2). */
cy: number;
/** Font size in SVG user units — scale up to fill larger charts. */
fontSize?: number;
/** Overall scale multiplier. Default 1 = ~140px wide composition. */
scale?: number;
}) {
const opacity = 0.18;
const barW = 40 * scale;
const gap = 32 * scale;
const stroke = 0.9 * scale;
const logoR = 8 * scale;
const textY = 20 * scale;
const fontSize = 8 * scale;
return (
<text
x={cx}
y={cy}
textAnchor="middle"
dominantBaseline="middle"
transform={`rotate(-22 ${cx} ${cy})`}
<g
transform={`translate(${cx} ${cy})`}
className="pointer-events-none select-none"
style={{
fontFamily: "var(--font-jetbrains-mono, ui-monospace, monospace)",
fontSize: `${fontSize}px`,
fontWeight: 500,
letterSpacing: "0.06em",
fill: "var(--color-ink)",
fillOpacity: 0.04,
}}
style={{ opacity }}
>
openchainbench.com
</text>
{/* Left + right hairlines — the "two bars" flanking the mark. */}
<line
x1={-(gap / 2 + barW)}
y1={0}
x2={-(gap / 2)}
y2={0}
stroke="var(--color-ink)"
strokeWidth={stroke}
strokeLinecap="round"
/>
<line
x1={gap / 2}
y1={0}
x2={gap / 2 + barW}
y2={0}
stroke="var(--color-ink)"
strokeWidth={stroke}
strokeLinecap="round"
/>
{/* Center C-mark — inline copy of <SiteLogo> geometry so the SVG
stays self-contained and can serialise cleanly in html-to-image. */}
<g transform={`translate(${-logoR} ${-logoR}) scale(${(logoR * 2) / 100})`}>
<mask id="cwmark-mask-svg">
<rect width="100" height="100" fill="white" />
<ellipse cx="45" cy="50" rx="22" ry="40" fill="black" />
<rect x="45" y="38" width="55" height="24" fill="black" />
</mask>
<circle cx="45" cy="50" r="45" fill="var(--color-ink)" mask="url(#cwmark-mask-svg)" />
<path d="M 65 0 L 100 0 L 100 35 Z" fill="var(--color-ink)" opacity="0.5" />
<path d="M 65 100 L 100 100 L 100 65 Z" fill="var(--color-ink)" opacity="0.5" />
</g>
{/* Wordmark. Kept short + wide-tracked for a "stamped" look. */}
<text
x={0}
y={textY}
textAnchor="middle"
dominantBaseline="middle"
style={{
fontFamily: "var(--font-jetbrains-mono, ui-monospace, monospace)",
fontSize: `${fontSize}px`,
fontWeight: 500,
letterSpacing: "0.24em",
fill: "var(--color-ink)",
textTransform: "uppercase",
}}
>
openchainbench.com
</text>
</g>
);
}

export function ChartWatermarkHtml() {
return (
<span
<div
aria-hidden
className="pointer-events-none absolute inset-0 z-0 flex items-center justify-center select-none font-mono font-medium uppercase text-[clamp(1rem,3vw,1.9rem)] tracking-[0.08em] text-ink"
style={{ opacity: 0.045, transform: "rotate(-22deg)" }}
className="pointer-events-none absolute inset-0 z-0 flex flex-col items-center justify-center select-none"
style={{ opacity: 0.18 }}
>
openchainbench.com
</span>
<div className="flex items-center gap-3">
<span
aria-hidden
className="h-px w-10 bg-ink"
style={{ opacity: 0.9 }}
/>
<svg
width={20}
height={20}
viewBox="0 0 100 100"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
>
<mask id="cwmark-mask-html">
<rect width="100" height="100" fill="white" />
<ellipse cx="45" cy="50" rx="22" ry="40" fill="black" />
<rect x="45" y="38" width="55" height="24" fill="black" />
</mask>
<circle cx="45" cy="50" r="45" fill="currentColor" mask="url(#cwmark-mask-html)" className="text-ink" />
<path d="M 65 0 L 100 0 L 100 35 Z" fill="currentColor" className="text-ink" opacity="0.5" />
<path d="M 65 100 L 100 100 L 100 65 Z" fill="currentColor" className="text-ink" opacity="0.5" />
</svg>
<span
aria-hidden
className="h-px w-10 bg-ink"
style={{ opacity: 0.9 }}
/>
</div>
<span
className="mt-1.5 font-mono font-medium uppercase text-[9px] tracking-[0.24em] text-ink"
>
openchainbench.com
</span>
</div>
);
}
13 changes: 7 additions & 6 deletions src/components/time-series-chart/chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@
// line draws a break but the dot keeps rendering at 0 / NaN.
return { ...l, color, pts, linePath, fillPath, lastX, lastY, last, isGap };
});
}, [slicedLines, padL, padT, innerW, innerH, lo, yRange, expectedPoints]);

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

View workflow job for this annotation

GitHub Actions / check

React Hook useMemo has a missing dependency: 'zoom'. Either include it or remove the dependency array

// Hover line position: mirror the right-anchored placement used by drawn().
// hover.idx is in [0, numPoints-1]; we map it to the visible chart via the
Expand Down Expand Up @@ -371,15 +371,16 @@
<SeriesGradients drawn={drawn} />
</defs>

{/* Diagonal attribution watermark. Rendered BEFORE the series
paths so the data lines stack on top of it — reads as a
background stamp, not overlay. Kept intentionally small +
near-transparent so it's discernible in screenshots but
never competes with the data during normal reading. */}
{/* Branded attribution stamp — C-mark between two hairlines
with the openchainbench.com wordmark below. Rendered BEFORE
the series paths so the lines pass over the top and the
composition sits in the background of the plot. Scale
follows the plot's shorter axis so the mark stays visually
consistent across the range of chart heights we render. */}
<ChartWatermarkSvg
cx={padL + innerW / 2}
cy={padT + innerH / 2}
fontSize={Math.round(Math.min(innerW, innerH) * 0.1)}
scale={Math.max(1, Math.min(innerW, innerH) / 260)}
/>

{/* Y gridlines + tick labels */}
Expand Down
Loading