From ca8526e69218af68ac17b6fbd987867be59bcdef Mon Sep 17 00:00:00 2001 From: Flotapponnier Date: Tue, 15 Sep 2026 00:36:19 +0200 Subject: [PATCH] bench spec: optional chart block (min_range, default_range, window_label) for daily-cadence benches perp-daily-volume moves once per UTC day, so 1h/6h/24h drew one point at "now" and the ledger said "(24h)" for a closed-day figure. The chart now hides ranges below min_range, opens on default_range and the ledger's base window reads "last closed day". Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HJgbZCqjR4nvCfcJSzofbw --- benchmarks/perp-daily-volume.yml | 8 ++++++++ src/components/ledger-table.tsx | 8 ++++++-- src/components/time-series-chart/index.tsx | 20 +++++++++++++++----- src/lib/materialize/load.ts | 11 +++++++++++ src/lib/spec-schema.ts | 16 ++++++++++++++++ src/lib/spec.ts | 2 ++ src/types/benchmark.ts | 8 ++++++++ 7 files changed, 66 insertions(+), 7 deletions(-) diff --git a/benchmarks/perp-daily-volume.yml b/benchmarks/perp-daily-volume.yml index 02cbf6a2e..17110ea49 100644 --- a/benchmarks/perp-daily-volume.yml +++ b/benchmarks/perp-daily-volume.yml @@ -127,6 +127,14 @@ ledger_columns: stacked_share: true +# Gauges move once per UTC day: sub-day chart ranges would show one point +# at "now", so the chart opens on 30 days and the ledger's base window is +# the last closed day rather than "24h". +chart: + min_range: 7d + default_range: 30d + window_label: last closed day + faq: - q: "How is this different from bench 041, perp DEX volume share?" a: "Bench 041 polls each venue's live rolling 24 hour figure every five minutes and ranks the time average. This bench counts closed UTC days, which is what DeFiLlama, weekly reports and flip claims use, and keeps a year of daily history per venue. The two agree on quiet days and disagree whenever a big hour sits on one side of a midnight boundary." diff --git a/src/components/ledger-table.tsx b/src/components/ledger-table.tsx index 386d02897..2326a92dc 100644 --- a/src/components/ledger-table.tsx +++ b/src/components/ledger-table.tsx @@ -157,9 +157,12 @@ export function LedgerTable({ } return colValue(r, col); }; + // A daily-cadence bench relabels the base window ("last closed day" + // instead of "24h") via the spec's chart.window_label. + const baseWindowLabel = benchmark.chart?.windowLabel ?? "24h"; const colLabel = (col: LedgerColumn): string => { if (!hasWindows) return col.label; - const w = windowKey !== "24h" && col.windows?.[windowKey] ? windowKey : "24h"; + const w = windowKey !== "24h" && col.windows?.[windowKey] ? windowKey : baseWindowLabel; return `${col.label} (${w})`; }; @@ -361,6 +364,7 @@ export function LedgerTable({ ))} diff --git a/src/components/time-series-chart/index.tsx b/src/components/time-series-chart/index.tsx index fb9b73e60..862c2f8eb 100644 --- a/src/components/time-series-chart/index.tsx +++ b/src/components/time-series-chart/index.tsx @@ -119,7 +119,14 @@ export function TimeSeriesChart({ longRangeDisabledTitle, onAvailableRangesChange, }: Props) { - const [rangeLocal, setRangeLocal] = useState("24h"); + // Daily-cadence benches (spec `chart` block) hide sub-day ranges, + // which would show one point at "now", and open on a longer window. + const minRange: Range = benchmark.chart?.minRange ?? "1h"; + const rangeOrder: Range[] = ["1h", "6h", "24h", "7d", "30d", "90d", "180d", "1y", "all"]; + const rangeAllowed = (r: Range) => rangeOrder.indexOf(r) >= rangeOrder.indexOf(minRange); + const [rangeLocal, setRangeLocal] = useState( + benchmark.chart?.defaultRange ?? (rangeAllowed("24h") ? "24h" : minRange), + ); const range = rangeProp ?? rangeLocal; const setRange = (next: Range) => { if (onRangeChange) onRangeChange(next); @@ -413,12 +420,14 @@ export function TimeSeriesChart({ useEffect(() => { if (panelActive) return; /* eslint-disable react-hooks/set-state-in-effect */ - if (range === "7d" && lazySeries7d !== null && !seriesHasData(lazySeries7d)) setRange("24h"); - if (range === "30d" && lazySeries30d !== null && !seriesHasData(lazySeries30d)) setRange("24h"); + const base: Range = rangeAllowed("24h") ? "24h" : minRange; + if (range === "7d" && lazySeries7d !== null && !seriesHasData(lazySeries7d) && base !== "7d") setRange(base); + if (range === "30d" && lazySeries30d !== null && !seriesHasData(lazySeries30d) && base !== "30d") + setRange(has7d && rangeAllowed("7d") ? "7d" : base); if (range === "90d" && lazySeries90d !== null && Object.keys(lazySeries90d).length === 0) - setRange(has30d ? "30d" : "24h"); + setRange(has30d ? "30d" : base); if (range === "1y" && lazySeries1y !== null && Object.keys(lazySeries1y).length === 0) - setRange(has90d ? "90d" : "24h"); + setRange(has90d ? "90d" : base); /* eslint-enable react-hooks/set-state-in-effect */ // eslint-disable-next-line react-hooks/exhaustive-deps }, [range, panelActive, lazySeries7d, lazySeries30d, lazySeries90d, lazySeries1y]); @@ -621,6 +630,7 @@ export function TimeSeriesChart({
{RANGES.map((r) => { + if (!rangeAllowed(r)) return null; // Hide entirely once we have confirmed there's no data for this range. if (r === "7d" && lazySeries7d !== null && !has7d) return null; if (r === "30d" && lazySeries30d !== null && !has30d) return null; diff --git a/src/lib/materialize/load.ts b/src/lib/materialize/load.ts index 1deb1bde4..a4c8928c1 100644 --- a/src/lib/materialize/load.ts +++ b/src/lib/materialize/load.ts @@ -31,6 +31,16 @@ import { const SPECS_DIR = process.env.OCB_SPECS_DIR ?? path.join(process.cwd(), "benchmarks"); +/** Camel-cased copy of the spec's optional `chart` block. */ +export function chartFromSpec(spec: { chart?: { min_range?: "24h" | "7d" | "30d"; default_range?: "24h" | "7d" | "30d" | "90d" | "1y"; window_label?: string } }): Benchmark["chart"] { + if (!spec.chart) return undefined; + return { + minRange: spec.chart.min_range, + defaultRange: spec.chart.default_range, + windowLabel: spec.chart.window_label, + }; +} + /** Side-effect hooks injected by the caller (site: KV snapshot write; * worker: store publish). Keeps this module free of persistence deps. */ export type LoadHooks = { @@ -138,6 +148,7 @@ export function buildEditorial( metric: spec.metric, panelMainLabel: spec.panel_main_label, stackedShare: spec.stacked_share, + chart: chartFromSpec(spec), unit: spec.unit, higherIsBetter: spec.higher_is_better, abstract: spec.abstract, diff --git a/src/lib/spec-schema.ts b/src/lib/spec-schema.ts index e9691dea3..2c7b9a0f1 100644 --- a/src/lib/spec-schema.ts +++ b/src/lib/spec-schema.ts @@ -486,6 +486,22 @@ export const SpecSchema = z * where the relative contribution of each venue over time is the * primary signal. */ stacked_share: z.boolean().optional(), + /** + * Chart cadence for benches whose gauges move once a day (closed + * UTC-day series such as perp-daily-volume). Sub-day ranges (1h, 6h, + * 24h) would render a single point at "now", so `min_range` hides + * them and `default_range` opens the chart on a window that holds + * data. `window_label` replaces the "24h" suffix on the ledger's + * headline columns ("last closed day"). + */ + chart: z + .object({ + min_range: z.enum(["24h", "7d", "30d"]).optional(), + default_range: z.enum(["24h", "7d", "30d", "90d", "1y"]).optional(), + window_label: z.string().min(1).max(24).optional(), + }) + .strict() + .optional(), metric_panels: z .array( z.object({ diff --git a/src/lib/spec.ts b/src/lib/spec.ts index d417a967f..1134fd8f5 100644 --- a/src/lib/spec.ts +++ b/src/lib/spec.ts @@ -20,6 +20,7 @@ import type { Spec } from "@/lib/spec-schema"; import { canonicalChainSlug } from "@/lib/chain-aliases"; import { renderBenchmarkText } from "@/lib/bench-template"; import { + chartFromSpec, draftPlaceholderForSpec, filterSig, loadSpecsUncached, @@ -137,6 +138,7 @@ export function overlayEditorial(stored: Benchmark, spec: Spec): Benchmark { // worker running a divergent branch (keyed-rpc-robinhood: main is // Singapore-only while dev/worker carries the region dims). dimensions: spec.dimensions, + chart: chartFromSpec(spec) ?? stored.chart, // Same source-of-truth rule as dimensions: the live YAML decides // the aggregate pin, never the snapshot. aggregateFilters: spec.aggregate_filters, diff --git a/src/types/benchmark.ts b/src/types/benchmark.ts index 8fad54379..ee613cf83 100644 --- a/src/types/benchmark.ts +++ b/src/types/benchmark.ts @@ -341,6 +341,14 @@ export type Benchmark = { * `metric_panels` in the YAML. */ metricPanels?: MetricPanel[]; panelMainLabel?: string; + /** Chart cadence hints from the spec's `chart` block: hide sub-day + * ranges, open on a longer window, relabel the "24h" ledger suffix. + * Set for benches whose gauges move once per UTC day. */ + chart?: { + minRange?: "24h" | "7d" | "30d"; + defaultRange?: "24h" | "7d" | "30d" | "90d" | "1y"; + windowLabel?: string; + }; /** When true the bench page renders a stacked bar chart (absolute + % * share over time) below the main chart. Set via `stacked_share: true` * in the bench YAML. Meaningful only for volume-share benches. */