diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index 9f63b5d5e..3e91dbde4 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -30,6 +30,7 @@ import { import { formatLeftoverMapPlotAxisSingular, leftoverMapComparePlotAxisBadge, + leftoverMapComparePlotTickAxisBadge, leftoverMapPlotTickAxisBadge, leftoverSingularForAxis, LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR, @@ -362,20 +363,23 @@ export function LeftoverMapPlot({ {leftoverMapPlotAxisText(2, leftoverMapAxes, variant)} {layout.ticks.map((tick) => { + const singular = leftoverSingularForAxis(leftoverMapAxes, tick.axis); const reportTickBadge = variant === "report" - ? leftoverMapPlotTickAxisBadge( - tick.axis, - tick.label, - leftoverSingularForAxis(leftoverMapAxes, tick.axis), - ) + ? leftoverMapPlotTickAxisBadge(tick.axis, tick.label, singular) + : null; + const comparisonTickBadge = + variant === "comparison" + ? leftoverMapComparePlotTickAxisBadge(tick.axis, tick.label, singular) : null; const tickAriaLabel = variant === "comparison" - ? `${t(LEFTOVER_MAP_COMPARE_PLOT_LABEL)}: ${tf(LEFTOVER_MAP_PLOT_TICK, { - axis: tick.axis, - value: tick.label, - })}` + ? comparisonTickBadge !== null + ? tf(comparisonTickBadge.template, comparisonTickBadge.values) + : `${t(LEFTOVER_MAP_COMPARE_PLOT_LABEL)}: ${tf(LEFTOVER_MAP_PLOT_TICK, { + axis: tick.axis, + value: tick.label, + })}` : reportTickBadge !== null ? tf(reportTickBadge.template, reportTickBadge.values) : tf(LEFTOVER_MAP_PLOT_TICK, { axis: tick.axis, value: tick.label }); diff --git a/frontend/src/leftoverMapPlotAxisSingular.ts b/frontend/src/leftoverMapPlotAxisSingular.ts index 273b15244..16423ddad 100644 --- a/frontend/src/leftoverMapPlotAxisSingular.ts +++ b/frontend/src/leftoverMapPlotAxisSingular.ts @@ -30,6 +30,9 @@ export const LEFTOVER_MAP_COMPARE_AXIS_SHARE = export const LEFTOVER_MAP_PLOT_TICK_SINGULAR = "leftover-map axis {axis} tick {value} σ {singular}"; +export const LEFTOVER_MAP_COMPARE_PLOT_TICK_SINGULAR = + "leftover map comparison graphic leftover-map axis {axis} tick {value} σ {singular}"; + export type LeftoverMapPlotAxisSingular = { axis_index: LeftoverMapAxis["axis_index"]; leftover_singular_value?: LeftoverMapAxis["leftover_singular_value"] | null; @@ -150,3 +153,19 @@ export function leftoverMapPlotTickAxisBadge( values: { axis: axisIndex, value: tickLabel, singular }, }; } + +/** Project finite persisted σ onto one comparison-graphic tick without inventing share evidence. */ +export function leftoverMapComparePlotTickAxisBadge( + axisIndex: number, + tickLabel: string, + leftoverSingular: number | null | undefined, +): LeftoverMapCompareAxisBadge | null { + const singular = formatLeftoverMapPlotAxisSingular(leftoverSingular); + if (singular === null) { + return null; + } + return { + template: LEFTOVER_MAP_COMPARE_PLOT_TICK_SINGULAR, + values: { axis: axisIndex, value: tickLabel, singular }, + }; +} diff --git a/tests/test_grouping_comparison_graphic_tick_singular_contract.py b/tests/test_grouping_comparison_graphic_tick_singular_contract.py new file mode 100644 index 000000000..da559bf74 --- /dev/null +++ b/tests/test_grouping_comparison_graphic_tick_singular_contract.py @@ -0,0 +1,37 @@ +"""Executable contract for comparison-graphic tick singular-value captions.""" + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SINGULAR_SOURCE = ROOT / "frontend" / "src" / "leftoverMapPlotAxisSingular.ts" +PLOT_COMPONENT_SOURCE = ROOT / "frontend" / "src" / "components" / "LeftoverMapPlot.tsx" + + +def test_comparison_graphic_tick_keeps_persisted_singular_value_without_share() -> None: + """A comparison tick may name valid persisted σ even when axis share is absent.""" + assert SINGULAR_SOURCE.exists(), "singular-value axis helper is missing" + source = SINGULAR_SOURCE.read_text(encoding="utf-8") + + assert "leftoverMapComparePlotTickAxisBadge" in source + assert "leftover map comparison graphic leftover-map axis {axis} tick {value} σ {singular}" in source + + +def test_comparison_graphic_consumes_tick_singular_projection() -> None: + """The buyer-visible comparison graphic must consume its persisted-σ tick projection.""" + source = PLOT_COMPONENT_SOURCE.read_text(encoding="utf-8") + + assert "leftoverMapComparePlotTickAxisBadge" in source + assert "tf(comparisonTickBadge.template, comparisonTickBadge.values)" in source + + +def test_comparison_graphic_tick_does_not_infer_sigma_or_share() -> None: + """Invalid σ falls back to the comparison tick and tick captions never synthesize share.""" + assert SINGULAR_SOURCE.exists(), "singular-value axis helper is missing" + source = SINGULAR_SOURCE.read_text(encoding="utf-8") + + assert "formatLeftoverMapPlotAxisSingular" in source + assert "Number.isFinite" in source + assert "Math.sqrt" not in source + tail = source.split("leftoverMapComparePlotTickAxisBadge", 1)[-1][:1600] + assert "leftover_share" not in tail