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
20 changes: 15 additions & 5 deletions src/components/chart-export-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export function ChartExportButton({
filename = "openchainbench-chart",
className = "",
}: Props) {
const [state, setState] = useState<"idle" | "working" | "copied" | "error">("idle");
const [state, setState] = useState<
"idle" | "working" | "copied" | "downloaded" | "error"
>("idle");

const capture = useCallback(async (): Promise<Blob | null> => {
const el = targetRef.current;
Expand Down Expand Up @@ -99,8 +101,8 @@ export function ChartExportButton({
const blob = await capture();
if (!blob) throw new Error("no target");
triggerDownload(blob, filename);
setState("copied");
setTimeout(() => setState("idle"), 1200);
setState("downloaded");
setTimeout(() => setState("idle"), 1600);
} catch (err) {
console.warn("[chart-export] download failed", err);
setState("error");
Expand All @@ -126,7 +128,11 @@ export function ChartExportButton({
<Copy size={11} strokeWidth={2} />
)}
<span className="hidden sm:inline">
{state === "copied" ? "Copied" : state === "error" ? "Failed" : "Copy"}
{state === "copied"
? "Copied"
: state === "error"
? "Failed"
: "Copy"}
</span>
</button>
<span aria-hidden className="h-4 w-px bg-ink/15" />
Expand All @@ -138,7 +144,11 @@ export function ChartExportButton({
title="Download chart as PNG"
aria-label="Download chart as PNG"
>
<Download size={11} strokeWidth={2} />
{state === "downloaded" ? (
<Check size={11} strokeWidth={2.4} />
) : (
<Download size={11} strokeWidth={2} />
)}
</button>
</span>
);
Expand Down
72 changes: 37 additions & 35 deletions src/components/chart-watermark.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,65 @@
/**
* Subtle openchainbench.com attribution that renders in both the on-screen
* chart and inside any PNG capture (see `chart-export-button.tsx`). Kept
* low-opacity so it never competes with the data; the copy is small enough
* to survive Twitter/Slack recompression without pixel loss on the value.
* 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`).
*
* Two variants:
* - `<ChartWatermarkSvg>` → inlined <text> element for SVG charts
* (time-series). Positioned bottom-right of the plot area.
* - `<ChartWatermarkHtml>` → absolutely-positioned HTML span for
* HTML-composed charts (ranked bar). Parent must be `relative`.
* - `<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.
*
* Colour picks `--color-ink-faint` so it inherits the dark/light theme
* variables and stays legible on both without hardcoded hex.
* 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.
*/

export function ChartWatermarkSvg({
x,
y,
anchor = "end",
cx,
cy,
fontSize = 48,
}: {
x: number;
y: number;
anchor?: "start" | "middle" | "end";
/** 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;
}) {
return (
<text
x={x}
y={y}
textAnchor={anchor}
x={cx}
y={cy}
textAnchor="middle"
dominantBaseline="middle"
transform={`rotate(-22 ${cx} ${cy})`}
className="pointer-events-none select-none"
style={{
fontFamily: "var(--font-jetbrains-mono, ui-monospace, monospace)",
fontSize: "11px",
letterSpacing: "0.08em",
fontSize: `${fontSize}px`,
fontWeight: 600,
letterSpacing: "0.06em",
fill: "var(--color-ink)",
fillOpacity: 0.28,
fillOpacity: 0.09,
}}
>
openchainbench.com
</text>
);
}

export function ChartWatermarkHtml({
position = "bottom-right",
}: {
position?: "bottom-right" | "bottom-left" | "top-right";
}) {
const cls =
position === "bottom-right"
? "bottom-1 right-2"
: position === "bottom-left"
? "bottom-1 left-2"
: "top-1 right-2";
export function ChartWatermarkHtml() {
return (
<span
aria-hidden
className={`pointer-events-none absolute ${cls} select-none font-sans font-medium uppercase text-[10px] tracking-[0.1em] text-ink`}
style={{ opacity: 0.28 }}
className="pointer-events-none absolute inset-0 z-0 flex items-center justify-center select-none font-mono font-semibold uppercase text-[clamp(1.4rem,4.2vw,2.6rem)] tracking-[0.08em] text-ink"
style={{ opacity: 0.09, transform: "rotate(-22deg)" }}
>
openchainbench.com
</span>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ranked-bar-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export function RankedBarChart({
<p className="mt-3 text-[11px] font-sans font-medium uppercase tracking-[0.12em] text-ink-faint">
{useLog ? "Log scale · " : ""}p50 · last 24 h · click rows to exclude
</p>
<ChartWatermarkHtml position="bottom-right" />
<ChartWatermarkHtml />
</figure>
);
}
15 changes: 10 additions & 5 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,6 +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. Sized to the plot area's
shorter axis so it scales gracefully with the chart. */}
<ChartWatermarkSvg
cx={padL + innerW / 2}
cy={padT + innerH / 2}
fontSize={Math.round(Math.min(innerW, innerH) * 0.16)}
/>

{/* Y gridlines + tick labels */}
<YAxis
yTicks={yTicks}
Expand Down Expand Up @@ -470,11 +480,6 @@
stroke="var(--color-ink)"
strokeWidth={1}
/>

{/* Attribution watermark. Sits inside the plot's right pad so it
doesn't overlap data lines; captured in any PNG export via
chart-export-button.tsx. Kept low-opacity by ChartWatermarkSvg. */}
<ChartWatermarkSvg x={padL + innerW - 2} y={padT + innerH - 6} anchor="end" />
</svg>

{/* Floating tooltip */}
Expand Down
Loading