diff --git a/src/components/ledger-table.tsx b/src/components/ledger-table.tsx
index ef7729fd3..390cb715d 100644
--- a/src/components/ledger-table.tsx
+++ b/src/components/ledger-table.tsx
@@ -3,6 +3,7 @@
import { useMemo, useState } from "react";
import { rateDecimals, successTitle } from "@/lib/sample-precision";
import { rowHref } from "@/lib/row-link";
+import { barScale, fieldMean, sortableValue } from "@/lib/ledger-value";
import Link from "next/link";
import { ChevronDown, ChevronUp } from "lucide-react";
@@ -230,11 +231,25 @@ export function LedgerTable({
// window's first custom column, or `panel.values[slug]` when a panel
// tab is active. Used for sort, filter, the inline data bar, and the
// displayed value in the headline column.
- const pickValue = (r: ProviderResult): number => {
- if (activePanel) return activePanel.values[r.slug] ?? 0;
- if (customCols) return colValueW(r, customCols[0]) ?? 0;
+ // The honest value, which can be absent. A panel holds one figure per
+ // provider and a provider we could not ask has none; coercing that to 0
+ // publishes a refusal nobody gave. Bench 311 renders its capability
+ // figures as columns for exactly this reason, and said so in its
+ // methodology rather than fixing it here.
+ //
+ // Columns already distinguish the two: a custom cell renders "-" when its
+ // value is null. This brings the panel path to the same convention.
+ const pickValueOrNull = (r: ProviderResult): number | null => {
+ if (activePanel) return activePanel.values[r.slug] ?? null;
+ if (customCols) return colValueW(r, customCols[0]) ?? null;
return r.ms.p50;
};
+ // Numeric form for the places that need a scalar and have no way to
+ // express absence: sort position, the data-bar scale. Missing sorts as
+ // the worst value rather than the best, which is what 0 was doing on a
+ // higher-is-better board.
+ const pickValue = (r: ProviderResult): number =>
+ sortableValue(pickValueOrNull(r), higherIsBetter);
// Detected from the first provider's results — if ANY provider declares
// slot_p50/slot_p99 in its YAML queries, every row gets the column (with
// "-" for providers that don't declare it). Used by Solana-native benches
@@ -263,9 +278,9 @@ export function LedgerTable({
// Field-mean Δ — duplicated here so the sort comparator can rank by it
// without waiting on per-Row computation. Kept aligned with the same
// formula the Row uses for the displayed Δ% (see deltaPct below).
- const fieldMeanRaw =
- results.reduce((s, r) => s + pickValue(r), 0) /
- Math.max(1, results.length);
+ // Rows with no value are excluded rather than counted as zero: a mean
+ // dragged down by absences makes every other row's delta wrong too.
+ const fieldMeanRaw = fieldMean(results.map((r) => pickValueOrNull(r)));
const deltaForSort = (r: ProviderResult): number => {
if (fieldMeanRaw === 0) return 0;
return (pickValue(r) - fieldMeanRaw) / fieldMeanRaw;
@@ -416,7 +431,7 @@ export function LedgerTable({
// Max + field-mean of the active column. Computed off `sorted` (the
// scored set) so the field mean isn't pulled to zero by augmented
// unavailable rows, which would flatten every delta to +999%.
- const maxValue = Math.max(...sorted.map((r) => pickValue(r))) || 1;
+ const maxValue = barScale(sorted.map((r) => pickValueOrNull(r)));
const fieldValue =
sorted.reduce((s, r) => s + pickValue(r), 0) / Math.max(1, sorted.length);
@@ -698,7 +713,7 @@ export function LedgerTable({
r={r}
i={i}
unit={unit}
- value={pickValue(r)}
+ value={pickValueOrNull(r)}
fieldValue={fieldValue}
maxValue={maxValue}
panelActive={panelActive}
@@ -739,7 +754,7 @@ export function LedgerTable({
r={r}
i={-1}
unit={unit}
- value={pickValue(r)}
+ value={pickValueOrNull(r)}
fieldValue={fieldValue}
maxValue={maxValue}
panelActive={panelActive}
@@ -825,7 +840,7 @@ function Row({
r: ProviderResult;
i: number;
unit: string;
- value: number;
+ value: number | null;
fieldValue: number;
maxValue: number;
panelActive: boolean;
@@ -858,9 +873,10 @@ function Row({
const isUnranked = !isUnresponsive && !!r.unrankedLabel;
const isOffline = !isUnresponsive && !isUnranked && r.availability === "unavailable";
const isMuted = isOffline || isUnresponsive;
- const deltaPct = fieldValue > 0 ? ((value - fieldValue) / fieldValue) * 100 : 0;
+ const deltaPct =
+ value != null && fieldValue > 0 ? ((value - fieldValue) / fieldValue) * 100 : 0;
const deltaSign = deltaPct > 0 ? "+" : deltaPct < 0 ? "−" : "±";
- const barPct = Math.max(2, (value / maxValue) * 100);
+ const barPct = value == null ? 0 : Math.max(2, (value / maxValue) * 100);
return (
@@ -1134,9 +1150,11 @@ function Row({
? customCells[0].v != null
? fmtUnit(customCells[0].v, customCells[0].unit)
: "-"
- : panelActive
- ? fmtUnit(value, unit)
- : fmtHeadline(value, { ...benchmark, unit }, r.slug)}
+ : value == null
+ ? "-"
+ : panelActive
+ ? fmtUnit(value, unit)
+ : fmtHeadline(value, { ...benchmark, unit }, r.slug)}
diff --git a/src/lib/ledger-value.test.ts b/src/lib/ledger-value.test.ts
new file mode 100644
index 000000000..3e189eef9
--- /dev/null
+++ b/src/lib/ledger-value.test.ts
@@ -0,0 +1,103 @@
+import { describe, expect, it } from "bun:test";
+
+import { barScale, fieldMean, sortableValue } from "@/lib/ledger-value";
+
+/**
+ * The case these exist for, in full, because it is the reason bench 311 was
+ * built around the problem instead of through it.
+ *
+ * A metric panel holds one figure per provider. A provider we could not ask
+ * has none, and the table read that as `?? 0`. On a higher-is-better board a
+ * zero is not an absence, it is the worst possible reading, printed as a
+ * number with a bar beside it. Bench 311's methodology states the
+ * consequence plainly: "a provider with no reading for it is shown as a zero
+ * rather than left out, which would state a refusal we never received", and
+ * its capability figures were published as columns to avoid it.
+ *
+ * The same `?? 0` was also in the field mean, which is the denominator of
+ * every row's delta, so one unmeasured provider moved the percentage printed
+ * next to all the others.
+ */
+describe("sortableValue", () => {
+ it("sorts a missing value last on a lower-is-better board", () => {
+ // Latency. The worst place is the largest number, and 0 would have put an
+ // unmeasured provider at the top of the table.
+ expect(sortableValue(null, false)).toBe(Number.POSITIVE_INFINITY);
+ expect(sortableValue(null, false)).toBeGreaterThan(sortableValue(99999, false));
+ });
+
+ it("sorts a missing value last on a higher-is-better board", () => {
+ // Capability, in percent. Here the worst place IS zero, which is why one
+ // sentinel cannot serve both directions.
+ expect(sortableValue(null, true)).toBe(0);
+ expect(sortableValue(null, true)).toBeLessThan(sortableValue(0.1, true));
+ });
+
+ it("passes a real zero through unchanged", () => {
+ // A measured 0 is a refusal we did receive, and it must keep its place:
+ // a getLogs window of 0 blocks means the narrowest span was refused,
+ // which is a fact about the provider rather than about our access.
+ expect(sortableValue(0, true)).toBe(0);
+ expect(sortableValue(0, false)).toBe(0);
+ });
+});
+
+describe("fieldMean", () => {
+ it("excludes missing rows instead of counting them as zero", () => {
+ // Four providers, one unmeasured. The mean of what was measured is 100.
+ // Counting the absence drags it to 75 and every other row's delta with it.
+ expect(fieldMean([100, 100, 100, null])).toBe(100);
+ });
+
+ it("is the plain mean when nothing is missing", () => {
+ expect(fieldMean([10, 20, 30])).toBe(20);
+ });
+
+ it("returns zero when nothing was measured", () => {
+ // The caller reads 0 as "no delta to show" rather than dividing by it.
+ expect(fieldMean([null, null])).toBe(0);
+ expect(fieldMean([])).toBe(0);
+ });
+
+ it("ignores the infinite sentinel if one ever reaches it", () => {
+ // sortableValue's lower-is-better sentinel is Infinity. If the two paths
+ // were ever crossed, a single Infinity would make the mean Infinity and
+ // every delta NaN, which renders as nothing at all and looks like a data
+ // outage rather than a bug.
+ expect(fieldMean([10, 20, Number.POSITIVE_INFINITY])).toBe(15);
+ });
+});
+
+describe("barScale", () => {
+ it("scales to the largest measured value", () => {
+ expect(barScale([10, 40, null])).toBe(40);
+ });
+
+ it("never returns zero, so dividing by it stays safe", () => {
+ expect(barScale([])).toBe(1);
+ expect(barScale([null, null])).toBe(1);
+ expect(barScale([0, 0])).toBe(1);
+ });
+
+ it("refuses an infinite sentinel, which would flatten every real bar", () => {
+ // This is the failure that would be hardest to read on the page: every
+ // bar becomes a sliver and nothing errors.
+ expect(barScale([10, 40, Number.POSITIVE_INFINITY])).toBe(40);
+ });
+});
+
+describe("the three together, on the shape that caused this", () => {
+ it("one unasked provider changes neither the mean nor the scale", () => {
+ // Bench 311's robinhood tab: five providers, one of which we could not
+ // ask on that chain. Measured values are the getLogs windows.
+ const measured = [500, 10_000, 10_000, 1_000_000];
+ const withGap: (number | null)[] = [...measured, null];
+ expect(fieldMean(withGap)).toBe(fieldMean(measured));
+ expect(barScale(withGap)).toBe(barScale(measured));
+ // And the unasked row is last rather than first on a board where more
+ // is better.
+ const keys = withGap.map((v) => sortableValue(v, true));
+ expect(Math.min(...keys)).toBe(0);
+ expect(keys[keys.length - 1]).toBe(0);
+ });
+});
diff --git a/src/lib/ledger-value.ts b/src/lib/ledger-value.ts
new file mode 100644
index 000000000..c820dc593
--- /dev/null
+++ b/src/lib/ledger-value.ts
@@ -0,0 +1,58 @@
+/**
+ * What the ledger does with a row that has no value.
+ *
+ * A metric panel holds one figure per provider, and a provider we could not
+ * ask has none. The table read that absence as `?? 0`, which on a
+ * higher-is-better board is not a missing reading, it is the worst possible
+ * one: a refusal nobody gave, ranked last, with a data bar of zero width and
+ * a number printed next to it.
+ *
+ * Bench 311 ran into this and routed around it rather than through it. Its
+ * capability figures are published as columns, and its methodology says why:
+ * "In a chart view the board renders one metric for every row, and a provider
+ * with no reading for it is shown as a zero rather than left out, which would
+ * state a refusal we never received." The columns were the workaround. This is
+ * the fix, and with it a capability or a window can live in the view selector
+ * on any board without lying about the rows it could not measure.
+ *
+ * Columns already had the convention right: a custom cell renders "-" when its
+ * value is null. Everything here brings the panel path to that same rule.
+ */
+
+/** Sort key for a possibly-absent value.
+ *
+ * Absent must sort LAST in both directions, which a single sentinel cannot
+ * do: on a lower-is-better board the worst value is the largest, and on a
+ * higher-is-better board it is the smallest. Using 0 for both is what put an
+ * unmeasured provider at the top of a latency board. */
+export function sortableValue(
+ value: number | null,
+ higherIsBetter: boolean,
+): number {
+ if (value != null) return value;
+ return higherIsBetter ? 0 : Number.POSITIVE_INFINITY;
+}
+
+/** Mean over the rows that have a value.
+ *
+ * Counting an absence as zero does not only misplace that row: the field
+ * mean is the denominator of every other row's delta, so one unmeasured
+ * provider moves the percentage printed beside all the others. Returns 0
+ * when nothing was measured, which the caller already treats as "no delta
+ * to show". */
+export function fieldMean(values: (number | null)[]): number {
+ const present = values.filter((v): v is number => v != null && Number.isFinite(v));
+ if (present.length === 0) return 0;
+ return present.reduce((s, v) => s + v, 0) / present.length;
+}
+
+/** Scale for the inline data bars: the largest value actually measured.
+ *
+ * Infinite sentinels from `sortableValue` must never reach this, or every
+ * real bar collapses to nothing. Returns 1 when there is nothing to scale
+ * against, so a division by it is still safe. */
+export function barScale(values: (number | null)[]): number {
+ const present = values.filter((v): v is number => v != null && Number.isFinite(v));
+ if (present.length === 0) return 1;
+ return Math.max(...present, 0) || 1;
+}