{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. */