From 3aac452eef781f3c1a5da258cd1c0c548e4930e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 01:22:03 +0000 Subject: [PATCH 1/7] feat(reports): leftover-map singular values on grouping comparison graphic (v2.78.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caption persisted leftover-map singular values σ_k on the grouping comparison leftover-map graphic from already-named leftover-map axes (ADR 0321). Share and singular value omit independently. Rank-0 unused axes still name σ 0.00. Do not invent σ from leftover-map axis share. Synthetic fixtures only. --- AGENTS.md | 12 ++ ARCHITECTURE.md | 5 +- ...78.0-leftover-map-compare-plot-singular.md | 19 ++ CHANGELOG.md | 23 ++- CLAUDE.md | 2 +- .../0320-leftover-map-compare-plot-ticks.md | 7 +- ...0321-leftover-map-compare-plot-singular.md | 111 +++++++++++ docs/product-technical-gap-baseline.md | 18 ++ frontend/package.json | 2 +- frontend/src/App.test.tsx | 8 +- .../src/components/LeftoverMapPlot.test.tsx | 183 +++++++++++++----- frontend/src/components/LeftoverMapPlot.tsx | 32 ++- frontend/src/i18n.test.ts | 39 ++++ frontend/src/i18n.ts | 13 ++ frontend/src/leftoverMapPlotAxisShare.test.ts | 8 + .../src/leftoverMapPlotAxisSingular.test.ts | 93 +++++++++ frontend/src/leftoverMapPlotAxisSingular.ts | 44 +++++ frontend/src/leftoverMapPlotLayout.test.ts | 3 + frontend/src/leftoverMapPlotLayout.ts | 5 +- pyproject.toml | 2 +- 20 files changed, 561 insertions(+), 68 deletions(-) create mode 100644 CHANGELOG.d/2.78.0-leftover-map-compare-plot-singular.md create mode 100644 docs/adr/0321-leftover-map-compare-plot-singular.md create mode 100644 frontend/src/leftoverMapPlotAxisSingular.test.ts create mode 100644 frontend/src/leftoverMapPlotAxisSingular.ts diff --git a/AGENTS.md b/AGENTS.md index 424852d97..076369b1a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -637,6 +637,15 @@ that no persisted coordinate occupies. Do not invent leftover-map coordinate tic distance, leftover-map rank, leftover expected, leftover observed, leftover residual, leftover-map unexplained leftover, leftover-map reconstruction, leftover-map axis share, leftover-map post coverage, leftover-map item coverage, leftover-map incomplete post coverage, leftover-map incomplete item coverage, leftover pair count, or the count of unused axes. +ADR 0321 captions leftover-map singular values on that grouping comparison leftover-map +graphic from already-named leftover-map axes when that persisted `σ_k` is finite and +non-negative. Rank-0 unused axes still name leftover-map comparison graphic leftover-map +axis `σ 0.00`. Share and singular value omit independently. Do not invent leftover-map +singular values from leftover-map axis share, leftover-map coordinate ticks, leftover-map +distance, leftover-map rank, leftover expected, leftover observed, leftover residual, +leftover-map unexplained leftover, leftover-map reconstruction, leftover-map post coverage, +leftover-map item coverage, leftover-map incomplete post coverage, leftover-map incomplete +item coverage, leftover pair count, or the count of unused axes. When `R`, `R̂`, `U`, `x`, `s`, and `e` are finite, `e + s + x = 1`. When `Y`, `E`, and `R` are finite, `Y − E = R`. When `R`, `R̂`, and `U` are @@ -732,6 +741,9 @@ that grouping comparison leftover-map graphic with persisted leftover-map distan Grouping comparison leftover-map coordinate ticks (ADR 0320) captions that grouping comparison leftover-map graphic with persisted leftover-map coordinate ticks at the origin and at each unique finite `ξ` / `ζ` projection. +Grouping comparison leftover-map singular values (ADR 0321) captions +that grouping comparison leftover-map graphic with persisted leftover-map singular +values `σ_k` when leftoverSingularForAxis returns a usable finite non-negative value. Item complete-case coverage (ADR 0282) captions the leftover-map graphic with how many scored criteria entered the map. Item complete-case coverage on the pair list (ADR 0285) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 39c96edb6..d84a65f84 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -748,6 +748,8 @@ comparison leftover-map graphic when formatLeftoverMapDistance returns a usable badge. ADR 0320 captions leftover-map coordinate ticks on that grouping comparison leftover-map graphic from already-named leftover-map coordinates. +ADR 0321 captions leftover-map singular values on that grouping +comparison leftover-map graphic from already-named leftover-map axes. Click a post marker or a pair button opens that post. Those ADRs are the normative mathematical, storage, and display contracts. Leftover-map axis share @@ -797,7 +799,8 @@ graphic (ADR 0316), leftover expected on the grouping comparison leftover-map graphic (ADR 0317), leftover-map rank on the grouping comparison leftover-map graphic (ADR 0318), leftover-map distance on the grouping comparison leftover-map graphic (ADR 0319), leftover-map coordinate ticks on the grouping comparison leftover-map -graphic (ADR 0320); +graphic (ADR 0320), leftover-map singular values on the grouping comparison leftover-map +graphic (ADR 0321); `POST .../rebuild` scores every grouping kind (post_admin). `make seed` folds A-100/B-200 Event Lineage fixtures (and the Riverbend calendar post) that already have constructed IRT cells into the same shared diff --git a/CHANGELOG.d/2.78.0-leftover-map-compare-plot-singular.md b/CHANGELOG.d/2.78.0-leftover-map-compare-plot-singular.md new file mode 100644 index 000000000..14046ef95 --- /dev/null +++ b/CHANGELOG.d/2.78.0-leftover-map-compare-plot-singular.md @@ -0,0 +1,19 @@ +## 2.78.0 — Leftover-map singular values on grouping comparison leftover-map graphic + +- Caption leftover-map singular values on the grouping comparison leftover-map + graphic (ADR 0321). After `make seed`, closest and farthest leftover pairs + sit above the member list with leftover-map singular values `σ_k` on that + grouping comparison leftover-map graphic when those persisted singular values + are finite and non-negative; click a post marker or a pair button opens that + post. Rank-0 origin cells still name leftover-map comparison graphic leftover-map + axis `σ 0.00`. Comparison copy uses + `leftover map comparison graphic leftover-map axis {axis} σ {value}` so it + stays distinct from `leftover-map axis {axis} σ {value}`, from + `leftover axis {axis} σ {value}`, from + `leftover map comparison axis {axis} ({share}%)`, and from + `leftover map comparison graphic leftover-map axis {axis} tick {value}`. + Share and singular value omit independently. Do not invent `σ_k` from + leftover-map axis share. This increment does not caption leftover-map + singular values on leftover-axis report badges with a distinct name. + A finite negative leftover is shown, never clamped. Never invent a leftover + score. Never invent a theta. No new columns. diff --git a/CHANGELOG.md b/CHANGELOG.md index c1acf56f5..bd2315ce4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,24 @@ All notable changes to this project are documented here. Format follows ### Added +- Period leftover pairs now name leftover-map singular values on the grouping + comparison leftover-map graphic (ADR 0321 / v2.78.0). After `make seed`, + closest and farthest leftover pairs sit above the member list with leftover-map + singular values `σ_k` on that grouping comparison leftover-map graphic when + those persisted singular values are finite and non-negative; click a post + marker or a pair button opens that post. Rank-0 origin cells still name + leftover-map comparison graphic leftover-map axis `σ 0.00`. Comparison copy + uses `leftover map comparison graphic leftover-map axis {axis} σ {value}` so + it stays distinct from `leftover-map axis {axis} σ {value}`, from + `leftover axis {axis} σ {value}`, from + `leftover map comparison axis {axis} ({share}%)`, and from + `leftover map comparison graphic leftover-map axis {axis} tick {value}`. + Share and singular value omit independently. Do not invent `σ_k` from + leftover-map axis share. This increment does not caption leftover-map + singular values on leftover-axis report badges with a distinct name. + A finite negative leftover is shown, never clamped. Never invent a leftover + score or a theta. No new columns. + - Period leftover pairs now name leftover-map coordinate ticks on the grouping comparison leftover-map graphic (ADR 0320 / v2.77.0). After `make seed`, closest and farthest leftover pairs sit above the member list with leftover-map @@ -18,9 +36,8 @@ All notable changes to this project are documented here. Format follows `leftover map comparison graphic leftover-map axis {axis} tick {value}` so it stays distinct from `leftover-map axis {axis} tick {value}` and from `leftover map comparison axis {axis} ({share}%)`. Do not invent evenly spaced - ticks that no persisted coordinate occupies. This increment does not caption - leftover-map singular values on the comparison graphic with a distinct name. - Do not invent leftover-map coordinate ticks from leftover-map distance. + ticks that no persisted coordinate occupies. Leftover-map singular values on + the comparison graphic are ADR 0321. Do not invent leftover-map coordinate ticks from leftover-map distance. A finite negative leftover is shown, never clamped. Never invent a leftover score or a theta. No new columns. diff --git a/CLAUDE.md b/CLAUDE.md index cd8a6d9bf..8aa35a2b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,7 +49,7 @@ cutoff. Global Ask optional `knowledge_cutoff` uses the same cover Create/start endpoint rules (ADR 0017 / 0021), tie-vs-miss similarity (ADR 0026), R&R catalog ids (ADR 0019 / 0027), leftover pairs -(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281 / 0282 / 0283 / 0284 / 0285 / 0286 / 0287 / 0288 / 0289 / 0290 / 0291 / 0292 / 0293 / 0294 / 0295 / 0296 / 0297 / 0298 / 0299 / 0300 / 0301 / 0302 / 0303 / 0304 / 0305 / 0306 / 0307 / 0308 / 0309 / 0310 / 0311 / 0312 / 0313 / 0314 / 0315 / 0316 / 0317 / 0318 / 0319 / 0320), occupational construct catalog search +(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281 / 0282 / 0283 / 0284 / 0285 / 0286 / 0287 / 0288 / 0289 / 0290 / 0291 / 0292 / 0293 / 0294 / 0295 / 0296 / 0297 / 0298 / 0299 / 0300 / 0301 / 0302 / 0303 / 0304 / 0305 / 0306 / 0307 / 0308 / 0309 / 0310 / 0311 / 0312 / 0313 / 0314 / 0315 / 0316 / 0317 / 0318 / 0319 / 0320 / 0321), occupational construct catalog search (ADR 0257), the text-channel embedding swap and cosine clamp (ADR 0190), per-edge channel-score persistence (ADR 0195), token-backed status notices (ADR 0220), diff --git a/docs/adr/0320-leftover-map-compare-plot-ticks.md b/docs/adr/0320-leftover-map-compare-plot-ticks.md index b4ba99385..0ead801f4 100644 --- a/docs/adr/0320-leftover-map-compare-plot-ticks.md +++ b/docs/adr/0320-leftover-map-compare-plot-ticks.md @@ -3,6 +3,9 @@ **Decision status:** Accepted **Date:** 2026-08-31 +**Amended by:** [ADR 0321](0321-leftover-map-compare-plot-singular.md) +(leftover-map singular values on the grouping comparison leftover-map graphic) + Amends leftover-map coordinate ticks on graphic-display axes ([ADR 0270](0270-leftover-map-coordinate-ticks.md)), leftover-map distance on the grouping comparison leftover-map graphic @@ -12,7 +15,7 @@ on the grouping comparison leftover-map graphic display on the grouping comparison strip ([ADR 0304](0304-leftover-map-compare-graphic.md)), leftover-map coordinates ([ADR 0267](0267-leftover-map-coordinates.md)). Independent of leftover-map -singular values on the grouping comparison leftover-map graphic. +singular values on leftover-axis report badges. ## Context @@ -95,7 +98,7 @@ and distance are finite, `R̂ = ξ · ζ` and `d = ‖ξ − ζ‖`. ## Related -Independent of leftover-map singular values on the grouping comparison leftover-map graphic. +Independent of leftover-map singular values on leftover-axis report badges. ## References diff --git a/docs/adr/0321-leftover-map-compare-plot-singular.md b/docs/adr/0321-leftover-map-compare-plot-singular.md new file mode 100644 index 000000000..50d096056 --- /dev/null +++ b/docs/adr/0321-leftover-map-compare-plot-singular.md @@ -0,0 +1,111 @@ +# ADR 0321 — Name leftover-map singular values on the grouping comparison leftover-map graphic + +**Decision status:** Accepted +**Date:** 2026-08-31 + +Amends leftover-map coordinate ticks on the grouping comparison leftover-map +graphic ([ADR 0320](0320-leftover-map-compare-plot-ticks.md)), leftover-map +axis share on that comparison graphic +([ADR 0305](0305-leftover-map-compare-plot-axis-share.md)), leftover-map +graphic display on the grouping comparison strip +([ADR 0304](0304-leftover-map-compare-graphic.md)), leftover-map axis share +persistence ([ADR 0148](0148-leftover-map-axis-share.md)), leftover-map +coordinates ([ADR 0267](0267-leftover-map-coordinates.md)). Independent of +leftover-map singular values on leftover-axis report badges. + +## Context + +ADR 0148 already persists leftover-map singular values `σ_k` on +`report_leftover_map_axis` together with leftover-map axis share +`σ_k² / Σ_j σ_j²`. ADR 0305 already captions the grouping comparison +leftover-map graphic with `leftover map comparison axis {k} ({share}%)` +when that share is finite. ADR 0320 already ticks those comparison-graphic +axes at persisted `ξ` / `ζ` projections. The comparison graphic still +omits `σ_k`, so a buyer who compares leftover pairs can treat leftover-map +axis share, leftover-map distance `d`, leftover-map rank, or leftover-map +coordinate ticks as leftover-map structure without a next action. Gabriel +inertia share is `σ_k² / Σ_j σ_j²`; hiding the persisted singular value +lets share be read as a leftover score. A missing, non-finite, or negative +singular value is not a leftover score and must omit independently of +leftover-map axis share. Rank-0 unused axes still persist `σ_k = 0`. +A finite negative leftover is shown, never clamped. Do not invent `σ_k` +from leftover-map axis share. + +This increment captions leftover-map singular values on the grouping +comparison leftover-map graphic from already-named leftover-map axes. +Comparison copy uses the accessible names +`leftover map comparison graphic leftover-map axis {axis} σ {value}` and +`leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)` +so they stay distinct from hyphen `leftover-map axis {axis} σ {value}`, +from pair-list `leftover axis {axis} σ {value}`, from comparison axis-share +`leftover map comparison axis {axis} ({share}%)`, and from comparison +coordinate ticks +`leftover map comparison graphic leftover-map axis {axis} tick {value}`. +It does not add columns. Do not invent a leftover score. Do not invent a +theta. + +This protected increment uses **0321** so it does not collide with leftover-map +coordinate ticks on the grouping comparison leftover-map graphic (0320), +leftover-map distance on that graphic (0319), leftover-map axis share on that +graphic (0305), leftover-map singular values on leftover-axis report badges, +or the dashboard stacks. + +## Decision + +On the grouping comparison leftover-map graphic, caption leftover-map axis 1 +and leftover-map axis 2 with persisted leftover-map singular values `σ_k` +when that singular value is finite and non-negative. Share and singular +value omit independently: no share and no `σ_k` keeps +`leftover map comparison axis {k}`; share only stays +`leftover map comparison axis {k} ({share}%)`; `σ_k` only is +`leftover map comparison graphic leftover-map axis {k} σ {value}`; both is +`leftover map comparison graphic leftover-map axis {k} σ {value} ({share}%)`. +A missing, non-finite, or negative singular value omits that `σ` badge and +does not invent `σ_k` from leftover-map axis share. Rank-0 unused axes +still name `σ 0.00`. Click a post marker to open that post. Criterion +markers are not post buttons. + +Leftover-map comparison graphic leftover-map singular values omit +independently of leftover-map comparison graphic leftover-map axis ticks +and leftover-map axis share. A missing singular value omits that leftover-map +comparison graphic leftover-map axis `σ` caption and keeps a usable +leftover-map comparison axis share caption, leftover-map comparison graphic +leftover-map axis ticks, leftover-map distance caption, leftover-map rank +caption, leftover expected `E`, leftover observed `Y`, leftover residual `R`, +leftover-map unexplained leftover `U`, leftover-map reconstruction `R̂`, +and leftover-map comparison graphic coverage notes. + +This increment does not caption leftover-map singular values on leftover-axis +report badges with a distinct comparison-strip name. Those leftover singular +values already persist on leftover-map axes through ADR 0148. This increment +does not persist leftover-map inner product, cosine, or length. + +Do not add SQL migrations. Do not edit shipped migrations. Do not invent a leftover +score. Do not invent a theta. + +## Consequences + +After `make seed`, grouping comparison leftover pairs that already show +the leftover-map graphic of persisted `ξ` / `ζ` also name leftover-map +singular values on that graphic when those persisted `σ_k` are finite and +non-negative. Rank-0 unused axes still plot at the origin and still name +leftover-map comparison graphic leftover-map axis `σ 0.00`. Click a post +marker or a pair button opens that post. When coordinates, reconstruction, +and distance are finite, `R̂ = ξ · ζ` and `d = ‖ξ − ζ‖`. When leftover-map +singular values and leftover-map axis share are finite, +leftover-map axis share is `σ_k² / Σ_j σ_j²`. + +## Related + +Independent of leftover-map singular values on leftover-axis report badges. + +## References + +Jeon, M., Jin, I. H., Schweinberger, M., & Baugh, S. (2021). Mapping +unobserved item–respondent interactions: A latent space item response +model with interaction map. *Psychometrika, 86*(2), 378–403. +https://doi.org/10.1007/s11336-021-09762-5 + +Gabriel, K. R. (1971). The biplot graphic display of matrices with +application to principal component analysis. *Biometrika, 58*(3), +453–467. https://doi.org/10.1093/biomet/58.3.453 diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 75be1a0d4..4b02b8eca 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,23 @@ # Product & Technical Gap Baseline +> Exact-head loop overlay: 2026-08-31 10:20 KST. Protected `main` is +> `cb187cadee5fb6c46d8a944815ccc154a1e028d1`. Writes work. Do not +> self-approve. #860 leftover-map coordinate ticks on the grouping comparison leftover-map +> graphic (v2.77.0 / ADR 0320) is `40fa5bd`, stacked on #859 @ `9ba8535`. Independent +> APPROVE is missing. Next buyer increment on this +> cycle: leftover-map singular values on the grouping comparison leftover-map +> graphic (ADR 0321 / v2.78.0) delivered locally on +> `feat/leftover-map-compare-plot-singular-v2780`. Caption persisted leftover-map +> singular values `σ_k` on that comparison graphic from already-named leftover-map +> axes. Distinct accessible name `leftover map comparison graphic leftover-map +> axis {axis} σ {value}`. Rank-0 unused axes still name `σ 0.00`. Share and +> singular value omit independently. Do not invent `σ_k` from leftover-map axis +> share. Do not caption leftover-map singular values on leftover-axis report +> badges with a distinct name this increment. Stack onto #860. +> Leave open for independent review. Do not squash-merge. Do not self-approve. +> Issues #79 and #87 stay open. Do not merge #808 without independent APPROVE. +> Never self-approve. + > Exact-head loop overlay: 2026-08-31 09:28 KST. Protected `main` is > `cb187cadee5fb6c46d8a944815ccc154a1e028d1`. Writes work. Do not > self-approve. #859 leftover-map distance on the grouping comparison leftover-map diff --git a/frontend/package.json b/frontend/package.json index 2909efd65..336c33357 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.77.0", + "version": "2.78.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 313feda5b..4f3f774cb 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -4197,8 +4197,8 @@ describe("App, authenticated", () => { expect(screen.getByLabelText("Leftover map comparison graphic")).toBeInTheDocument(); expect(screen.getByText("leftover-map axis 1 (82%)")).toBeInTheDocument(); expect(screen.getByText("leftover-map axis 2 (18%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (18%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 2 σ 0.86 (18%)")).toBeInTheDocument(); expect( screen.getAllByRole("button", { name: /open leftover-map post public post at ξ \(\+0\.50, \+0\.10\)/i, @@ -4322,12 +4322,12 @@ describe("App, authenticated", () => { ).not.toBeInTheDocument(); expect( within(screen.getByLabelText("Grouping comparison")).getByText( - "leftover map comparison axis 1 (82%)", + "leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)", ), ).toBeInTheDocument(); expect( within(screen.getByLabelText("Grouping comparison")).getByText( - "leftover map comparison axis 2 (18%)", + "leftover map comparison graphic leftover-map axis 2 σ 0.86 (18%)", ), ).toBeInTheDocument(); expect( diff --git a/frontend/src/components/LeftoverMapPlot.test.tsx b/frontend/src/components/LeftoverMapPlot.test.tsx index e3717a014..40162a36b 100644 --- a/frontend/src/components/LeftoverMapPlot.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.test.tsx @@ -843,8 +843,8 @@ describe("LeftoverMapPlot", () => { "Leftover map comparison graphic of already-named coordinates. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (18%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 2 σ 0.86 (18%)")).toBeInTheDocument(); expect(screen.queryByText("leftover-map axis 1 (82%)")).not.toBeInTheDocument(); expect(screen.queryByText("leftover-map axis 2 (18%)")).not.toBeInTheDocument(); }); @@ -861,12 +861,95 @@ describe("LeftoverMapPlot", () => { variant="comparison" />, ); - expect(screen.getByText("leftover map comparison axis 1")).toBeInTheDocument(); + expect( + screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84"), + ).toBeInTheDocument(); expect(screen.getByText("leftover map comparison axis 2")).toBeInTheDocument(); + expect(screen.queryByText("leftover map comparison axis 1")).not.toBeInTheDocument(); expect(screen.queryByText(/leftover map comparison axis 1 \(/)).not.toBeInTheDocument(); expect(screen.queryByText(/leftover map comparison axis 2 \(/)).not.toBeInTheDocument(); expect(screen.queryByText("leftover-map axis 1")).not.toBeInTheDocument(); expect(screen.queryByText("leftover-map axis 2")).not.toBeInTheDocument(); + expect(screen.queryByText("leftover-map axis 1 σ 1.84")).not.toBeInTheDocument(); + }); + + it("names leftover-map singular values on the comparison graphic with a distinct name", () => { + render( + , + ); + expect( + screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)"), + ).toBeInTheDocument(); + expect( + screen.getByText("leftover map comparison graphic leftover-map axis 2 σ 0.86 (18%)"), + ).toBeInTheDocument(); + expect(screen.queryByText("leftover map comparison axis 1 (82%)")).not.toBeInTheDocument(); + expect(screen.queryByText("leftover-map axis 1 σ 1.84 (82%)")).not.toBeInTheDocument(); + expect(screen.queryByText("leftover axis 1 σ 1.84 82%")).not.toBeInTheDocument(); + expect(screen.queryByText("leftover-map axis 1 (82%)")).not.toBeInTheDocument(); + }); + + it("omits leftover-map comparison graphic singular values independently of leftover-map axis share", () => { + render( + , + ); + expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 2 (18%)")).toBeInTheDocument(); + expect( + screen.queryByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)"), + ).not.toBeInTheDocument(); + expect( + screen.queryByText(/leftover map comparison graphic leftover-map axis 2 σ/), + ).not.toBeInTheDocument(); + }); + + it("names rank-0 zero leftover-map singular values on the comparison graphic", () => { + render( + , + ); + expect( + screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 0.00 (0%)"), + ).toBeInTheDocument(); + expect( + screen.getByText("leftover map comparison graphic leftover-map axis 2 σ 0.00 (0%)"), + ).toBeInTheDocument(); + expect(screen.queryByText("leftover map comparison axis 1 (0%)")).not.toBeInTheDocument(); + expect(screen.queryByText("leftover-map axis 1 σ 0.00 (0%)")).not.toBeInTheDocument(); }); it("names rank-0 zero leftover-map axis share on the comparison graphic", () => { @@ -890,8 +973,8 @@ describe("LeftoverMapPlot", () => { variant="comparison" />, ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 0.00 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 2 σ 0.00 (0%)")).toBeInTheDocument(); }); it("names leftover-map coverage on the comparison graphic with a distinct accessible name", () => { @@ -935,8 +1018,8 @@ describe("LeftoverMapPlot", () => { expect(screen.queryByLabelText("Leftover map comparison incomplete posts")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete items")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover map comparison incomplete items")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (18%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 2 σ 0.86 (18%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic coverage when coverage is missing or not usable", () => { @@ -954,7 +1037,7 @@ describe("LeftoverMapPlot", () => { ); expect(screen.queryByLabelText("Leftover map comparison graphic coverage")).not.toBeInTheDocument(); expect(screen.getByLabelText("Leftover map comparison graphic")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); rerender( { "Leftover map used 2 of 2 scored criteria (complete-case)", ); expect(screen.queryByLabelText("Leftover-map graphic coverage")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic item coverage when item coverage is missing or not usable", () => { @@ -1009,7 +1092,7 @@ describe("LeftoverMapPlot", () => { ); expect(screen.queryByLabelText("Leftover map comparison graphic item coverage")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic item coverage")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); rerender( { ); expect(screen.queryByLabelText("Leftover map comparison graphic item coverage")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic item coverage")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic incomplete posts when incomplete post coverage is missing or not usable", () => { @@ -1067,7 +1150,7 @@ describe("LeftoverMapPlot", () => { ); expect(screen.queryByLabelText("Leftover map comparison graphic incomplete posts")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete posts")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); rerender( { ); expect(screen.queryByLabelText("Leftover map comparison graphic incomplete posts")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete posts")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic incomplete items when incomplete item coverage is missing or not usable", () => { @@ -1128,7 +1211,7 @@ describe("LeftoverMapPlot", () => { ); expect(screen.queryByLabelText("Leftover map comparison graphic incomplete items")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete items")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); rerender( { ); expect(screen.queryByLabelText("Leftover map comparison graphic incomplete items")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete items")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("names leftover-map comparison graphic coverage 0 of M on a rank-0 origin when that persisted used count is a non-negative integer", () => { @@ -1209,8 +1292,8 @@ describe("LeftoverMapPlot", () => { expect(screen.queryByLabelText("Leftover-map graphic item coverage")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete posts")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete items")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 0.00 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 2 σ 0.00 (0%)")).toBeInTheDocument(); }); it("names leftover-map reconstruction on the comparison graphic with a distinct accessible name", () => { @@ -1246,7 +1329,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic incomplete items")).toHaveTextContent( "Leftover map dropped 0 incomplete criteria", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic reconstruction when R̂ is missing or not usable", () => { @@ -1280,7 +1363,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("names rank-0 origin reconstruction on the comparison graphic when that persisted value is finite", () => { @@ -1323,7 +1406,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 0.00 (0%)")).toBeInTheDocument(); }); it("names leftover-map explained leftover share on the comparison graphic with a distinct accessible name", () => { @@ -1359,7 +1442,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic explained leftover share when e is missing or not usable", () => { @@ -1396,7 +1479,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("names rank-0 origin explained leftover share on the comparison graphic when that persisted value is finite", () => { @@ -1443,7 +1526,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 0.00 (0%)")).toBeInTheDocument(); }); it("names leftover-map unexplained leftover share on the comparison graphic with a distinct accessible name", () => { @@ -1482,7 +1565,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic unexplained leftover share when s is missing or not usable", () => { @@ -1522,7 +1605,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("names rank-0 origin unexplained leftover share on the comparison graphic when that persisted value is finite", () => { @@ -1573,7 +1656,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 0.00 (0%)")).toBeInTheDocument(); }); it("names leftover-map cross share on the comparison graphic with a distinct accessible name", () => { @@ -1615,7 +1698,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic cross share when x is missing or not usable", () => { @@ -1658,7 +1741,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover-map cross share on the comparison graphic when that persisted value is finite", () => { @@ -1713,7 +1796,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 0.00 (0%)")).toBeInTheDocument(); }); it("names leftover-map unexplained leftover on the comparison graphic with a distinct accessible name", () => { @@ -1758,7 +1841,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic unexplained leftover when U is missing or not usable", () => { @@ -1804,7 +1887,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover-map unexplained leftover on the comparison graphic when that persisted value is finite", () => { @@ -1863,7 +1946,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 0.00 (0%)")).toBeInTheDocument(); }); it("names leftover residual on the comparison graphic with a distinct accessible name", () => { @@ -1911,7 +1994,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic leftover residual when R is missing or not usable", () => { @@ -1960,7 +2043,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover residual on the comparison graphic when that persisted value is finite", () => { @@ -2022,7 +2105,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 0.00 (0%)")).toBeInTheDocument(); }); it("names leftover observed on the comparison graphic with a distinct accessible name", () => { @@ -2064,7 +2147,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic leftover observed when Y is missing or not usable", () => { @@ -2106,7 +2189,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover observed on the comparison graphic when that persisted value is finite", () => { @@ -2163,7 +2246,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 0.00 (0%)")).toBeInTheDocument(); }); it("names leftover expected on the comparison graphic with a distinct accessible name", () => { @@ -2198,7 +2281,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic leftover residual R +0.40"), ).toHaveTextContent("R +0.40"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic leftover expected when E is missing or not usable", () => { @@ -2234,7 +2317,7 @@ describe("LeftoverMapPlot", () => { screen.getByLabelText("leftover map comparison graphic leftover residual R +0.40"), ).toHaveTextContent("R +0.40"); expect(screen.getByLabelText("leftover map comparison graphic leftover-map distance d 0.12")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover expected on the comparison graphic when that persisted value is finite", () => { @@ -2286,7 +2369,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic leftover residual R 0.00"), ).toHaveTextContent("R 0.00"); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 0.00 (0%)")).toBeInTheDocument(); }); it("names leftover-map rank on the comparison graphic with a distinct accessible name", () => { @@ -2324,7 +2407,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic leftover residual R +0.40"), ).toHaveTextContent("R +0.40"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic leftover-map rank when rank is missing or not usable", () => { @@ -2363,7 +2446,7 @@ describe("LeftoverMapPlot", () => { screen.getByLabelText("leftover map comparison graphic leftover residual R +0.40"), ).toHaveTextContent("R +0.40"); expect(screen.getByLabelText("leftover map comparison graphic leftover-map distance d 0.12")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover-map rank on the comparison graphic when that persisted value is a non-negative integer", () => { @@ -2418,7 +2501,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic leftover residual R 0.00"), ).toHaveTextContent("R 0.00"); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 0.00 (0%)")).toBeInTheDocument(); }); it("names leftover-map distance on the comparison graphic with a distinct accessible name", () => { @@ -2456,7 +2539,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic leftover observed Y 2.40"), ).toHaveTextContent("Y 2.40"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic leftover-map distance when d is missing or not usable", () => { @@ -2494,7 +2577,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic leftover residual R +0.40"), ).toHaveTextContent("R +0.40"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover-map distance on the comparison graphic when that persisted value is finite", () => { @@ -2546,7 +2629,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic leftover expected E 0.00"), ).toHaveTextContent("E 0.00"); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 0.00 (0%)")).toBeInTheDocument(); }); it("names leftover-map coordinate ticks on the comparison graphic with a distinct accessible name", () => { @@ -2588,7 +2671,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getAllByLabelText("leftover map comparison graphic leftover-map rank rank 1"), ).toHaveLength(2); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 1.84 (82%)")).toBeInTheDocument(); }); it("omits invented leftover-map comparison graphic leftover-map axis ticks that no persisted coordinate occupies", () => { @@ -2644,7 +2727,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic leftover-map distance d 0.00"), ).toHaveTextContent("d 0.00"); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 0.00 (0%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover-map coordinate ticks on the comparison graphic", () => { @@ -2700,6 +2783,6 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic leftover expected E 0.00"), ).toHaveTextContent("E 0.00"); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison graphic leftover-map axis 1 σ 0.00 (0%)")).toBeInTheDocument(); }); }); diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index c0636c65c..a93f6eae8 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -27,6 +27,12 @@ import { LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE, LEFTOVER_MAP_PLOT_AXIS_SHARE, } from "../leftoverMapPlotAxisShare"; +import { + formatLeftoverMapPlotAxisSingular, + leftoverSingularForAxis, + LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR, + LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE, +} from "../leftoverMapPlotAxisSingular"; import { firstPlottablePairForPost, layoutLeftoverMapPlot, @@ -84,10 +90,23 @@ function leftoverMapPlotAxisText( leftoverShareForAxis(leftoverMapAxes, axisIndex), ); if (variant === "comparison") { - if (percent === null) { + const singular = formatLeftoverMapPlotAxisSingular( + leftoverSingularForAxis(leftoverMapAxes, axisIndex), + ); + if (singular === null && percent === null) { return t(axisIndex === 1 ? LEFTOVER_MAP_COMPARE_PLOT_AXIS_1 : LEFTOVER_MAP_COMPARE_PLOT_AXIS_2); } - return tf(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE, { axis: axisIndex, share: percent }); + if (singular === null) { + return tf(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE, { axis: axisIndex, share: percent }); + } + if (percent === null) { + return tf(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR, { axis: axisIndex, value: singular }); + } + return tf(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE, { + axis: axisIndex, + value: singular, + share: percent, + }); } if (percent === null) { return t(axisIndex === 1 ? "leftover-map axis 1" : "leftover-map axis 2"); @@ -129,8 +148,10 @@ function leftoverMapPlotAxisText( * caption when incomplete post coverage is missing or not a usable integer. * Omit that leftover-map incomplete item caption when incomplete item * coverage is missing or not a usable integer. - * Omit that axis badge when share is - * missing or non-finite and keep the existing leftover-map axis text. + * Omit that axis singular-value badge when ``σ_k`` is missing, non-finite, + * or negative, independently of leftover-map axis share. Omit that axis badge when share is + * missing or non-finite and keep the existing leftover-map axis text, + * including any leftover-map comparison graphic leftover-map axis singular value. * Omit the plot when no pair has four finite leftover-map coordinates. * ADR 0304 reuses this graphic on the grouping comparison strip from * already-named leftover-map coordinates. ADR 0305 captions leftover-map axis @@ -181,6 +202,9 @@ function leftoverMapPlotAxisText( * labels. ADR 0320 captions leftover-map coordinate ticks on that * comparison graphic from already-named leftover-map coordinates * with distinct leftover map comparison graphic leftover-map axis tick + * labels. ADR 0321 captions leftover-map singular values on that + * comparison graphic from already-named leftover-map axes + * with distinct leftover map comparison graphic leftover-map axis σ * labels. * Never invent a leftover score. */ diff --git a/frontend/src/i18n.test.ts b/frontend/src/i18n.test.ts index d7ee7ef32..57b911fb0 100644 --- a/frontend/src/i18n.test.ts +++ b/frontend/src/i18n.test.ts @@ -105,6 +105,8 @@ describe("i18n", () => { "Leftover map comparison rank", "leftover map comparison graphic leftover-map distance {label}", "leftover map comparison graphic leftover-map axis {axis} tick {value}", + "leftover map comparison graphic leftover-map axis {axis} σ {value}", + "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)", "Leftover map comparison coordinates", "Leftover map comparison graphic", "Leftover map comparison", @@ -688,6 +690,43 @@ describe("i18n", () => { }, ); + it.each([ + ["ko", "잔여 지도 비교 그림 축 1 σ 1.84"], + ["zh", "残差地图比较图形轴 1 σ 1.84"], + ["ja", "残差マップの比較図軸 1 σ 1.84"], + ["vi", "trục đồ họa so sánh bản đồ phần dư 1 σ 1.84"], + ] as const)( + "formats leftover map comparison graphic leftover-map axis singular values in %s", + (locale, expected) => { + setLocale(locale); + expect( + tf("leftover map comparison graphic leftover-map axis {axis} σ {value}", { + axis: 1, + value: "1.84", + }), + ).toBe(expected); + }, + ); + + it.each([ + ["ko", "잔여 지도 비교 그림 축 1 σ 1.84 (82%)"], + ["zh", "残差地图比较图形轴 1 σ 1.84 (82%)"], + ["ja", "残差マップの比較図軸 1 σ 1.84 (82%)"], + ["vi", "trục đồ họa so sánh bản đồ phần dư 1 σ 1.84 (82%)"], + ] as const)( + "formats leftover map comparison graphic leftover-map axis singular share in %s", + (locale, expected) => { + setLocale(locale); + expect( + tf("leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)", { + axis: 1, + value: "1.84", + share: "82", + }), + ).toBe(expected); + }, + ); + it.each([ ["ko", "잔여 지도 비교 설명 잔여 점유율"], ["zh", "残差地图比较已解释残差份额"], diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index e1fa05892..203113342 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -297,6 +297,9 @@ const TRANSLATIONS: Partial>> = { "Leftover map comparison rank": "잔여 지도 비교 순위", "leftover map comparison graphic leftover-map distance {label}": "잔여 지도 비교 그림 거리 {label}", "leftover map comparison graphic leftover-map axis {axis} tick {value}": "잔여 지도 비교 그림 축 {axis} 눈금 {value}", + "leftover map comparison graphic leftover-map axis {axis} σ {value}": "잔여 지도 비교 그림 축 {axis} σ {value}", + "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)": + "잔여 지도 비교 그림 축 {axis} σ {value} ({share}%)", "Leftover map comparison coordinates": "잔여 지도 비교 좌표", "Leftover map comparison graphic": "잔여 지도 비교 그림", "Leftover map comparison": "잔여 지도 비교", @@ -940,6 +943,9 @@ const TRANSLATIONS: Partial>> = { "Leftover map comparison rank": "残差地图比较秩", "leftover map comparison graphic leftover-map distance {label}": "残差地图比较图形距离 {label}", "leftover map comparison graphic leftover-map axis {axis} tick {value}": "残差地图比较图形轴 {axis} 刻度 {value}", + "leftover map comparison graphic leftover-map axis {axis} σ {value}": "残差地图比较图形轴 {axis} σ {value}", + "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)": + "残差地图比较图形轴 {axis} σ {value} ({share}%)", "Leftover map comparison coordinates": "残差地图比较坐标", "Leftover map comparison graphic": "残差地图比较图形", "Leftover map comparison": "残差地图比较", @@ -1604,6 +1610,9 @@ const TRANSLATIONS: Partial>> = { "Leftover map comparison rank": "残差マップの比較階数", "leftover map comparison graphic leftover-map distance {label}": "残差マップの比較図距離 {label}", "leftover map comparison graphic leftover-map axis {axis} tick {value}": "残差マップの比較図軸 {axis} 目盛 {value}", + "leftover map comparison graphic leftover-map axis {axis} σ {value}": "残差マップの比較図軸 {axis} σ {value}", + "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)": + "残差マップの比較図軸 {axis} σ {value} ({share}%)", "Leftover map comparison coordinates": "残差マップの比較座標", "Leftover map comparison graphic": "残差マップの比較図", "Leftover map comparison": "残差マップの比較", @@ -2247,6 +2256,10 @@ const TRANSLATIONS: Partial>> = { "Leftover map comparison rank": "Hạng so sánh bản đồ phần dư", "leftover map comparison graphic leftover-map distance {label}": "khoảng cách đồ họa so sánh bản đồ phần dư {label}", "leftover map comparison graphic leftover-map axis {axis} tick {value}": "vạch trục đồ họa so sánh bản đồ phần dư {axis} {value}", + "leftover map comparison graphic leftover-map axis {axis} σ {value}": + "trục đồ họa so sánh bản đồ phần dư {axis} σ {value}", + "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)": + "trục đồ họa so sánh bản đồ phần dư {axis} σ {value} ({share}%)", "Leftover map comparison coordinates": "Tọa độ so sánh bản đồ phần dư", "Leftover map comparison graphic": "Đồ họa so sánh bản đồ phần dư", "Leftover map comparison": "So sánh bản đồ phần dư", diff --git a/frontend/src/leftoverMapPlotAxisShare.test.ts b/frontend/src/leftoverMapPlotAxisShare.test.ts index 6c99dac66..7315be18d 100644 --- a/frontend/src/leftoverMapPlotAxisShare.test.ts +++ b/frontend/src/leftoverMapPlotAxisShare.test.ts @@ -7,6 +7,10 @@ import { LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE, LEFTOVER_MAP_PLOT_AXIS_SHARE, } from "./leftoverMapPlotAxisShare"; +import { + LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR, + LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE, +} from "./leftoverMapPlotAxisSingular"; describe("leftoverShareForAxis", () => { const axes = [ @@ -63,5 +67,9 @@ describe("leftover map comparison axis share labels", () => { expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE).not.toBe(LEFTOVER_MAP_PLOT_AXIS_SHARE); expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_1).not.toBe("leftover-map axis 1"); expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_2).not.toBe("leftover-map axis 2"); + expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE).not.toBe( + LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE, + ); + expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_1).not.toBe(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR); }); }); diff --git a/frontend/src/leftoverMapPlotAxisSingular.test.ts b/frontend/src/leftoverMapPlotAxisSingular.test.ts new file mode 100644 index 000000000..77cb55e73 --- /dev/null +++ b/frontend/src/leftoverMapPlotAxisSingular.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "vitest"; +import { + formatLeftoverMapPlotAxisSingular, + leftoverSingularForAxis, + LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR, + LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE, + LEFTOVER_MAP_PLOT_AXIS_SINGULAR, + LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE, +} from "./leftoverMapPlotAxisSingular"; +import { LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE } from "./leftoverMapPlotAxisShare"; +import { LEFTOVER_MAP_COMPARE_PLOT_TICK } from "./leftoverMapPlotLayout"; + +describe("leftoverSingularForAxis", () => { + const axes = [ + { axis_index: 1, leftover_singular_value: 1.84 }, + { axis_index: 2, leftover_singular_value: 0.86 }, + ]; + + it("reads persisted leftover-map singular values without inventing a leftover score", () => { + expect(leftoverSingularForAxis(axes, 1)).toBe(1.84); + expect(leftoverSingularForAxis(axes, 2)).toBe(0.86); + }); + + it("names a rank-0 zero leftover-map singular value", () => { + expect( + leftoverSingularForAxis( + [ + { axis_index: 1, leftover_singular_value: 0 }, + { axis_index: 2, leftover_singular_value: 0 }, + ], + 1, + ), + ).toBe(0); + }); + + it("omits a leftover-map axis when singular value is missing, non-finite, or negative", () => { + expect(leftoverSingularForAxis(undefined, 1)).toBeNull(); + expect(leftoverSingularForAxis([], 1)).toBeNull(); + expect(leftoverSingularForAxis([{ axis_index: 1, leftover_singular_value: null }], 1)).toBeNull(); + expect( + leftoverSingularForAxis([{ axis_index: 1, leftover_singular_value: Number.NaN }], 1), + ).toBeNull(); + expect( + leftoverSingularForAxis( + [{ axis_index: 1, leftover_singular_value: Number.POSITIVE_INFINITY }], + 1, + ), + ).toBeNull(); + expect(leftoverSingularForAxis([{ axis_index: 1, leftover_singular_value: -0.01 }], 1)).toBeNull(); + expect(leftoverSingularForAxis(axes, 3)).toBeNull(); + }); + + it("does not invent leftover-map singular values from leftover-map axis share", () => { + expect(leftoverSingularForAxis([{ axis_index: 1 }], 1)).toBeNull(); + }); +}); + +describe("formatLeftoverMapPlotAxisSingular", () => { + it("formats persisted leftover-map singular values without inventing a leftover score", () => { + expect(formatLeftoverMapPlotAxisSingular(1.84)).toBe("1.84"); + expect(formatLeftoverMapPlotAxisSingular(0.86)).toBe("0.86"); + expect(formatLeftoverMapPlotAxisSingular(0)).toBe("0.00"); + }); + + it("omits leftover-map singular values when the value is missing, non-finite, or negative", () => { + expect(formatLeftoverMapPlotAxisSingular(null)).toBeNull(); + expect(formatLeftoverMapPlotAxisSingular(undefined)).toBeNull(); + expect(formatLeftoverMapPlotAxisSingular(Number.NaN)).toBeNull(); + expect(formatLeftoverMapPlotAxisSingular(Number.POSITIVE_INFINITY)).toBeNull(); + expect(formatLeftoverMapPlotAxisSingular(Number.NEGATIVE_INFINITY)).toBeNull(); + expect(formatLeftoverMapPlotAxisSingular(-0.01)).toBeNull(); + }); +}); + +describe("leftover map comparison graphic leftover-map axis singular labels", () => { + it("stays distinct from leftover-map graphic axis singular copy and comparison axis share", () => { + expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR).toBe( + "leftover map comparison graphic leftover-map axis {axis} σ {value}", + ); + expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE).toBe( + "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)", + ); + expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR).not.toBe(LEFTOVER_MAP_PLOT_AXIS_SINGULAR); + expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE).not.toBe( + LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE, + ); + expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE).not.toBe( + LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE, + ); + expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR).not.toBe(LEFTOVER_MAP_COMPARE_PLOT_TICK); + expect(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR).not.toBe("leftover axis {axis} σ {value}"); + }); +}); diff --git a/frontend/src/leftoverMapPlotAxisSingular.ts b/frontend/src/leftoverMapPlotAxisSingular.ts new file mode 100644 index 000000000..a621fe351 --- /dev/null +++ b/frontend/src/leftoverMapPlotAxisSingular.ts @@ -0,0 +1,44 @@ +/** Caption leftover-map plot axes with persisted Gabriel singular values. */ + +import type { LeftoverMapAxis } from "./api"; + +export const LEFTOVER_MAP_PLOT_AXIS_SINGULAR = "leftover-map axis {axis} σ {value}"; + +export const LEFTOVER_MAP_PLOT_AXIS_SINGULAR_SHARE = + "leftover-map axis {axis} σ {value} ({share}%)"; + +export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR = + "leftover map comparison graphic leftover-map axis {axis} σ {value}"; + +export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE = + "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)"; + +export type LeftoverMapPlotAxisSingular = { + axis_index: LeftoverMapAxis["axis_index"]; + leftover_singular_value?: LeftoverMapAxis["leftover_singular_value"] | null; +}; + +export function leftoverSingularForAxis( + axes: ReadonlyArray | null | undefined, + axisIndex: number, +): number | null { + const axis = axes?.find((candidate) => candidate.axis_index === axisIndex); + if ( + axis == null || + axis.leftover_singular_value == null || + !Number.isFinite(axis.leftover_singular_value) || + axis.leftover_singular_value < 0 + ) { + return null; + } + return axis.leftover_singular_value; +} + +export function formatLeftoverMapPlotAxisSingular( + leftoverSingular: number | null | undefined, +): string | null { + if (leftoverSingular == null || !Number.isFinite(leftoverSingular) || leftoverSingular < 0) { + return null; + } + return leftoverSingular.toFixed(2); +} diff --git a/frontend/src/leftoverMapPlotLayout.test.ts b/frontend/src/leftoverMapPlotLayout.test.ts index d3d5481e0..6787cd828 100644 --- a/frontend/src/leftoverMapPlotLayout.test.ts +++ b/frontend/src/leftoverMapPlotLayout.test.ts @@ -1366,6 +1366,9 @@ describe("leftover map comparison graphic labels", () => { expect(LEFTOVER_MAP_COMPARE_PLOT_TICK).not.toBe( "leftover map comparison axis {axis} ({share}%)", ); + expect(LEFTOVER_MAP_COMPARE_PLOT_TICK).not.toBe( + "leftover map comparison graphic leftover-map axis {axis} σ {value}", + ); expect(LEFTOVER_MAP_COMPARE_PLOT_TICK).not.toBe( LEFTOVER_MAP_COMPARE_PLOT_SEGMENT_DISTANCE, ); diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts index 2c86013e5..c0dac3c23 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -48,7 +48,8 @@ * expected on that graphic (ADR 0317), and grouping comparison leftover-map * rank on that graphic (ADR 0318), grouping comparison leftover-map * distance on that graphic (ADR 0319), and grouping comparison leftover-map - * coordinate ticks on that graphic (ADR 0320) caption the pair list or the grouping comparison + * coordinate ticks on that graphic (ADR 0320), and grouping comparison leftover-map + * singular values on that graphic (ADR 0321) caption the pair list or the grouping comparison * strip. ADR 0304 reuses this graphic layout on the grouping comparison * strip. ADR 0305 captions leftover-map axis share on that comparison * graphic from already-named leftover-map axes. ADR 0306 captions leftover-map @@ -78,6 +79,8 @@ * already-named leftover-map distance. * ADR 0320 captions leftover-map coordinate ticks on that comparison graphic from * already-named leftover-map coordinates. + * ADR 0321 captions leftover-map singular values on that comparison graphic from + * already-named leftover-map axes. */ import { formatLeftoverMapCoordinatePair } from "./leftoverMapCoordinates"; diff --git a/pyproject.toml b/pyproject.toml index c1f5188d8..703556def 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.77.0" +version = "2.78.0" description = "Reconstructs git-branch-style lineage DAGs from scattered short records using multi-channel score fusion and LLM adjudication." readme = "README.md" license = { text = "MIT" } From 6950ffafc5602f7c03ebc4e8e7e39241ce2712a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 14:26:13 +0900 Subject: [PATCH 2/7] fix(ui): project persisted comparison singular evidence --- frontend/src/leftoverMapPlotAxisSingular.ts | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 frontend/src/leftoverMapPlotAxisSingular.ts diff --git a/frontend/src/leftoverMapPlotAxisSingular.ts b/frontend/src/leftoverMapPlotAxisSingular.ts new file mode 100644 index 000000000..dad7c12c4 --- /dev/null +++ b/frontend/src/leftoverMapPlotAxisSingular.ts @@ -0,0 +1,39 @@ +/** Project persisted leftover-map singular-value evidence into buyer-visible axis captions. */ + +import type { LeftoverMapAxis } from "./api"; + +export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR = + "leftover map comparison graphic leftover-map axis {axis} σ {value}"; + +export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE = + "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)"; + +export type LeftoverMapPlotAxisSingular = { + axis_index: LeftoverMapAxis["axis_index"]; + leftover_singular_value?: LeftoverMapAxis["leftover_singular_value"] | null; +}; + +export function leftoverSingularForAxis( + axes: ReadonlyArray | null | undefined, + axisIndex: number, +): number | null { + const axis = axes?.find((candidate) => candidate.axis_index === axisIndex); + if ( + axis == null || + axis.leftover_singular_value == null || + !Number.isFinite(axis.leftover_singular_value) || + axis.leftover_singular_value < 0 + ) { + return null; + } + return axis.leftover_singular_value; +} + +export function formatLeftoverMapPlotAxisSingular( + leftoverSingular: number | null | undefined, +): string | null { + if (leftoverSingular == null || !Number.isFinite(leftoverSingular) || leftoverSingular < 0) { + return null; + } + return leftoverSingular.toFixed(2); +} From 496b2f9361f6e6f09c2137e5c00e79d6324041ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 14:27:03 +0900 Subject: [PATCH 3/7] fix(ui): render persisted comparison singular evidence --- frontend/src/components/LeftoverMapPlot.tsx | 33 +++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index aa5440d58..e68507c5b 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -27,6 +27,12 @@ import { LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE, LEFTOVER_MAP_PLOT_AXIS_SHARE, } from "../leftoverMapPlotAxisShare"; +import { + formatLeftoverMapPlotAxisSingular, + leftoverSingularForAxis, + LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR, + LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE, +} from "../leftoverMapPlotAxisSingular"; import { firstPlottablePairForPost, layoutLeftoverMapPlot, @@ -80,11 +86,24 @@ function leftoverMapPlotAxisText( const percent = formatLeftoverMapPlotAxisShare( leftoverShareForAxis(leftoverMapAxes, axisIndex), ); + const singular = formatLeftoverMapPlotAxisSingular( + leftoverSingularForAxis(leftoverMapAxes, axisIndex), + ); if (variant === "comparison") { + if (singular === null) { + if (percent === null) { + return t(axisIndex === 1 ? LEFTOVER_MAP_COMPARE_PLOT_AXIS_1 : LEFTOVER_MAP_COMPARE_PLOT_AXIS_2); + } + return tf(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE, { axis: axisIndex, share: percent }); + } if (percent === null) { - return t(axisIndex === 1 ? LEFTOVER_MAP_COMPARE_PLOT_AXIS_1 : LEFTOVER_MAP_COMPARE_PLOT_AXIS_2); + return tf(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR, { axis: axisIndex, value: singular }); } - return tf(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SHARE, { axis: axisIndex, share: percent }); + return tf(LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE, { + axis: axisIndex, + value: singular, + share: percent, + }); } if (percent === null) { return t(axisIndex === 1 ? "leftover-map axis 1" : "leftover-map axis 2"); @@ -98,6 +117,9 @@ function leftoverMapPlotAxisText( * Person markers are posts; item markers are leftover criteria. Click a * post marker to open that post. Caption leftover-map axes with persisted * Gabriel inertia share when finite, including rank-0 zero-share axes. + * Comparison graphic axes additionally name finite, non-negative persisted + * Gabriel singular values independently of axis share; never derive one + * measurement from the other. * Axis ticks name persisted leftover-map coordinates so ξ / ζ on the * pair row match the plot. Pair segments name persisted leftover-map * distance ``d``, leftover-map reconstruction ``R̂``, leftover-map @@ -126,8 +148,8 @@ function leftoverMapPlotAxisText( * caption when incomplete post coverage is missing or not a usable integer. * Omit that leftover-map incomplete item caption when incomplete item * coverage is missing or not a usable integer. - * Omit that axis badge when share is - * missing or non-finite and keep the existing leftover-map axis text. + * Omit each axis evidence badge independently when its persisted evidence is + * unusable and keep the remaining buyer-visible axis evidence intact. * Omit the plot when no pair has four finite leftover-map coordinates. * ADR 0304 reuses this graphic on the grouping comparison strip from * already-named leftover-map coordinates. ADR 0305 captions leftover-map axis @@ -174,7 +196,8 @@ function leftoverMapPlotAxisText( * comparison-graphic and rank labels rather than adding SPA translation debt. * ADR 0319 captions leftover-map distance on that comparison graphic using * the same localized composition boundary rather than adding another static - * comparison-only translation key. + * comparison-only translation key. ADR 0321 adds persisted comparison-axis + * singular evidence without deriving it from axis share. * Never invent a leftover score. */ export function LeftoverMapPlot({ From 289527e54e919d649d8f6db6ee7f81e4cd5e39be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 00:55:32 +0900 Subject: [PATCH 4/7] chore(stack): adopt #860 comparison tick contract --- ..._grouping_comparison_graphic_tick_contract.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/test_grouping_comparison_graphic_tick_contract.py b/tests/test_grouping_comparison_graphic_tick_contract.py index 92ddc2fbd..d0c3af61e 100644 --- a/tests/test_grouping_comparison_graphic_tick_contract.py +++ b/tests/test_grouping_comparison_graphic_tick_contract.py @@ -10,23 +10,17 @@ def test_comparison_graphic_names_ticks_with_distinct_accessible_copy() -> None: - """Comparison tick names must not reuse the report-graphic tick key.""" + """Comparison ticks compose existing localized comparison and tick copy.""" plot_source = PLOT_SOURCE.read_text(encoding="utf-8") - layout_source = LAYOUT_SOURCE.read_text(encoding="utf-8") - assert ( - 'export const LEFTOVER_MAP_COMPARE_PLOT_TICK =\n' - ' "leftover map comparison graphic leftover-map axis {axis} tick {value}";' - in layout_source - ) - assert "LEFTOVER_MAP_COMPARE_PLOT_TICK" in plot_source + assert "LEFTOVER_MAP_COMPARE_PLOT_LABEL" in plot_source + assert "LEFTOVER_MAP_PLOT_TICK" in plot_source assert re.search( - r'variant\s*===\s*"comparison"\s*\?\s*LEFTOVER_MAP_COMPARE_PLOT_TICK\s*:\s*LEFTOVER_MAP_PLOT_TICK', + r'variant\s*===\s*"comparison"\s*\?\s*`\$\{t\(LEFTOVER_MAP_COMPARE_PLOT_LABEL\)\}:\s*\$\{tf\(LEFTOVER_MAP_PLOT_TICK,\s*\{\s*axis:\s*tick\.axis,\s*value:\s*tick\.label,?\s*\}\)\}`\s*:\s*tf\(LEFTOVER_MAP_PLOT_TICK,\s*\{\s*axis:\s*tick\.axis,\s*value:\s*tick\.label,?\s*\}\)', plot_source, re.DOTALL, ) - assert "axis: tick.axis" in plot_source - assert "value: tick.label" in plot_source + assert "LEFTOVER_MAP_COMPARE_PLOT_TICK" not in plot_source def test_tick_positions_come_from_persisted_coordinates_not_distance() -> None: From 26f652baeeaa71c40877f3d0d673fb204bbef06c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 00:56:40 +0900 Subject: [PATCH 5/7] chore(stack): adopt #860 comparison tick accessibility repair --- frontend/src/components/LeftoverMapPlot.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index e68507c5b..e46d3b6bd 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -196,8 +196,10 @@ function leftoverMapPlotAxisText( * comparison-graphic and rank labels rather than adding SPA translation debt. * ADR 0319 captions leftover-map distance on that comparison graphic using * the same localized composition boundary rather than adding another static - * comparison-only translation key. ADR 0321 adds persisted comparison-axis - * singular evidence without deriving it from axis share. + * comparison-only translation key. ADR 0320 names comparison coordinate ticks + * through the same composition boundary, preserving localized generic tick copy. + * ADR 0321 adds persisted comparison-axis singular evidence without deriving it + * from axis share. * Never invent a leftover score. */ export function LeftoverMapPlot({ @@ -325,7 +327,14 @@ export function LeftoverMapPlot({ Date: Sun, 20 Sep 2026 22:53:20 +0900 Subject: [PATCH 6/7] test(reports): pin exact comparison sigma acceptance RED --- ...ng_comparison_graphic_singular_contract.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_grouping_comparison_graphic_singular_contract.py b/tests/test_grouping_comparison_graphic_singular_contract.py index 7de47f808..bda9a58fa 100644 --- a/tests/test_grouping_comparison_graphic_singular_contract.py +++ b/tests/test_grouping_comparison_graphic_singular_contract.py @@ -6,6 +6,7 @@ ROOT = Path(__file__).resolve().parents[1] PLOT_SOURCE = ROOT / "frontend" / "src" / "components" / "LeftoverMapPlot.tsx" SINGULAR_SOURCE = ROOT / "frontend" / "src" / "leftoverMapPlotAxisSingular.ts" +APP_TEST_SOURCE = ROOT / "frontend" / "src" / "App.test.tsx" def test_comparison_graphic_has_distinct_persisted_singular_value_copy() -> None: @@ -43,3 +44,27 @@ def test_singular_value_is_read_from_axis_evidence_and_fails_closed() -> None: assert "leftover_share" not in singular_source assert "Math.max" not in singular_source assert "Math.min" not in singular_source + + +def test_app_acceptance_requires_exact_comparison_sigma_and_share_copy() -> None: + """App acceptance must assert the persisted comparison σ and share together, exactly.""" + app_test_source = APP_TEST_SOURCE.read_text(encoding="utf-8") + + stale_share_only = ( + '"leftover map comparison axis 1 (82%)"', + '"leftover map comparison axis 2 (18%)"', + ) + expected_sigma_share = ( + '"leftover map comparison axis 1 (σ 1.84, 82%)"', + '"leftover map comparison axis 2 (σ 0.86, 18%)"', + ) + + for stale_copy in stale_share_only: + assert stale_copy not in app_test_source, ( + "comparison-axis App acceptance still asserts pre-ADR-0321 share-only copy" + ) + for expected_copy in expected_sigma_share: + assert app_test_source.count(expected_copy) >= 2, ( + "comparison-axis App acceptance must cover exact σ+share copy in both report and " + "grouping-comparison integration paths" + ) From d572f65187f1148c3c7ebcdcbad3361d0c19fd9f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 21 Sep 2026 05:37:09 +0900 Subject: [PATCH 7/7] fix(reports): keep comparison axis evidence visible Preserve exact persisted sigma and share copy, right-bound both SVG axis captions, and retain desktop and mobile Storybook evidence. Signed-off-by: Seongho Bae --- ...ftover-map-comparison-singular-desktop.png | Bin 0 -> 64460 bytes ...eftover-map-comparison-singular-mobile.png | Bin 0 -> 3810 bytes docs/storybook-inventory.md | 2 +- frontend/src/App.test.tsx | 8 +- .../src/components/LeftoverMapPlot.test.tsx | 98 ++++++++++-------- frontend/src/components/LeftoverMapPlot.tsx | 7 +- frontend/src/leftoverMapPlotAxisSingular.ts | 4 +- ...ng_comparison_graphic_singular_contract.py | 4 +- 8 files changed, 71 insertions(+), 52 deletions(-) create mode 100644 docs/screenshots/leftover-map-comparison-singular-desktop.png create mode 100644 docs/screenshots/leftover-map-comparison-singular-mobile.png diff --git a/docs/screenshots/leftover-map-comparison-singular-desktop.png b/docs/screenshots/leftover-map-comparison-singular-desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..8e1fac9c4276f0d6c62729373ea975ed56192cf4 GIT binary patch literal 64460 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/leftover-map-comparison-singular-mobile.png b/docs/screenshots/leftover-map-comparison-singular-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..7f9bac1552e994c4d9e96b659a214479af489017 GIT binary patch literal 3810 zcmeHK|5MU;7{{_%R+zZUYmvD8+;zJptka!eQn57g<0f`}xoiozx*)a$wWb*=eqkDS z8K+hzq-?9rwN{!GiXEv7|2oDhLP&?Bo83-R;J2@8|V?KkxTFujlnV z_kP%W@woMo??u93u=QYm?mieStOW*J`RVEv5Tb$ifG`-m3(Vbh;LKIal!${o9@p?< zaEE1bvS9~4m31x^5ud|4C*_`pMOTI+qvOL7gCd!nJAa1BMHt%krzYhCm~oy-2#NY-x1ErM{L*%W}gcgArAfx9cJ=0yJI_I;t%_)IVN?@MEK$i^9J>6fF>5wOJGF_7^#;eROt8T6ST2YqgCFHU6 zrjLK2b~=HRVYg3Uu)!tAT4Xa&L6-!)Gu4KgSK}yVNzkpO@at@%S7?&t{ymBWuhZ$0 z2;P8i7VEPJFp|Zibz1o)BEx#Gk6(YbF$YA9-xG^{Ht}rz(){>@;jQJ;A!5youVdqC z=VUpCN{3ldg!M1@-Oovy(~!Gqre9ibmUpj$$Aid;O2Ctb%k2_;*>>{3}o=rZO z9$@qppz0PL-8wQvZhq62Vn_`zJbmrJ1))ITkVT;GyA6z}%!P+My2SIcvk2{`ZLFY6 z{Vq9=2+^S>q*p5+MQ+YV;KWtdKjo)ylcZU?JWiJPb*0qIvN=>(ris0ctf2~rxY8~(flIhP&64j;GWN)p;p`Neeh~_p^1E>3%Wy?Ql4V^$N0%lJ zaDa+$NKF4%TJe(iK|jC^qfB}@hb7A!ceFN}4LQlrWVl|mqKJ6ex}D7`oIE0NE&@C% zIJ{3mwcH>+NrkXt@f|*NTuw62s@3YsFVr#$j@pPFZn~_XO^tlaj0&#N@$bSV^&5T$ z*a~%GZ(8gTydr%V_{UADRNA`$sz1IMMDRS{M+aksLs|=__~5N({@dL1(k;+y((NEc zb3prPuE_j|yt; z_IgzXbMxaUO#*x?sBd=e=$tU93ekj-WZ$k<><6V1g2S6eIx;Is*6kClD>0UKJQ(eE zYFKZSVR}!dVB=0PMazq6b2jEz+iqrJt|$_GS|r#pj1olhM5+p#q{^ljbrxu4nno? rfA0sLC}= { expect(screen.getByLabelText("Leftover map comparison graphic")).toBeInTheDocument(); expect(screen.getByText("leftover-map axis 1 (82%)")).toBeInTheDocument(); expect(screen.getByText("leftover-map axis 2 (18%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (18%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 2 (σ 0.86, 18%)")).toBeInTheDocument(); expect( screen.getAllByRole("button", { name: /open leftover-map post public post at ξ \(\+0\.50, \+0\.10\)/i, @@ -4322,12 +4322,12 @@ describe("App, authenticated", () => { ).not.toBeInTheDocument(); expect( within(screen.getByLabelText("Grouping comparison")).getByText( - "leftover map comparison axis 1 (82%)", + "leftover map comparison axis 1 (σ 1.84, 82%)", ), ).toBeInTheDocument(); expect( within(screen.getByLabelText("Grouping comparison")).getByText( - "leftover map comparison axis 2 (18%)", + "leftover map comparison axis 2 (σ 0.86, 18%)", ), ).toBeInTheDocument(); expect( diff --git a/frontend/src/components/LeftoverMapPlot.test.tsx b/frontend/src/components/LeftoverMapPlot.test.tsx index 56258c883..42e6bf0f6 100644 --- a/frontend/src/components/LeftoverMapPlot.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.test.tsx @@ -843,8 +843,16 @@ describe("LeftoverMapPlot", () => { "Leftover map comparison graphic of already-named coordinates. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (18%)")).toBeInTheDocument(); + const axis1 = screen.getByText( + "leftover map comparison axis 1 (σ 1.84, 82%)", + ); + const axis2 = screen.getByText( + "leftover map comparison axis 2 (σ 0.86, 18%)", + ); + expect(axis1).toHaveAttribute("x", "472"); + expect(axis1).toHaveAttribute("text-anchor", "end"); + expect(axis2).toHaveAttribute("x", "472"); + expect(axis2).toHaveAttribute("text-anchor", "end"); expect(screen.queryByText("leftover-map axis 1 (82%)")).not.toBeInTheDocument(); expect(screen.queryByText("leftover-map axis 2 (18%)")).not.toBeInTheDocument(); }); @@ -861,9 +869,11 @@ describe("LeftoverMapPlot", () => { variant="comparison" />, ); - expect(screen.getByText("leftover map comparison axis 1")).toBeInTheDocument(); + expect( + screen.getByText("leftover map comparison axis 1 (σ 1.84)"), + ).toBeInTheDocument(); expect(screen.getByText("leftover map comparison axis 2")).toBeInTheDocument(); - expect(screen.queryByText(/leftover map comparison axis 1 \(/)).not.toBeInTheDocument(); + expect(screen.queryByText(/leftover map comparison axis 1 \(σ 1\.84,/)).not.toBeInTheDocument(); expect(screen.queryByText(/leftover map comparison axis 2 \(/)).not.toBeInTheDocument(); expect(screen.queryByText("leftover-map axis 1")).not.toBeInTheDocument(); expect(screen.queryByText("leftover-map axis 2")).not.toBeInTheDocument(); @@ -890,8 +900,12 @@ describe("LeftoverMapPlot", () => { variant="comparison" />, ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (0%)")).toBeInTheDocument(); + expect( + screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)"), + ).toBeInTheDocument(); + expect( + screen.getByText("leftover map comparison axis 2 (σ 0.00, 0%)"), + ).toBeInTheDocument(); }); it("names leftover-map coverage on the comparison graphic with a distinct accessible name", () => { @@ -935,8 +949,8 @@ describe("LeftoverMapPlot", () => { expect(screen.queryByLabelText("Leftover map comparison incomplete posts")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete items")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover map comparison incomplete items")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (18%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 2 (σ 0.86, 18%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic coverage when coverage is missing or not usable", () => { @@ -954,7 +968,7 @@ describe("LeftoverMapPlot", () => { ); expect(screen.queryByLabelText("Leftover map comparison graphic coverage")).not.toBeInTheDocument(); expect(screen.getByLabelText("Leftover map comparison graphic")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); rerender( { "Leftover map used 2 of 2 scored criteria (complete-case)", ); expect(screen.queryByLabelText("Leftover-map graphic coverage")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic item coverage when item coverage is missing or not usable", () => { @@ -1009,7 +1023,7 @@ describe("LeftoverMapPlot", () => { ); expect(screen.queryByLabelText("Leftover map comparison graphic item coverage")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic item coverage")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); rerender( { ); expect(screen.queryByLabelText("Leftover map comparison graphic item coverage")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic item coverage")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic incomplete posts when incomplete post coverage is missing or not usable", () => { @@ -1067,7 +1081,7 @@ describe("LeftoverMapPlot", () => { ); expect(screen.queryByLabelText("Leftover map comparison graphic incomplete posts")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete posts")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); rerender( { ); expect(screen.queryByLabelText("Leftover map comparison graphic incomplete posts")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete posts")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic incomplete items when incomplete item coverage is missing or not usable", () => { @@ -1128,7 +1142,7 @@ describe("LeftoverMapPlot", () => { ); expect(screen.queryByLabelText("Leftover map comparison graphic incomplete items")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete items")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); rerender( { ); expect(screen.queryByLabelText("Leftover map comparison graphic incomplete items")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete items")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names leftover-map comparison graphic coverage 0 of M on a rank-0 origin when that persisted used count is a non-negative integer", () => { @@ -1209,8 +1223,8 @@ describe("LeftoverMapPlot", () => { expect(screen.queryByLabelText("Leftover-map graphic item coverage")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete posts")).not.toBeInTheDocument(); expect(screen.queryByLabelText("Leftover-map graphic incomplete items")).not.toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); - expect(screen.getByText("leftover map comparison axis 2 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 2 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover-map reconstruction on the comparison graphic with a distinct accessible name", () => { @@ -1246,7 +1260,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic incomplete items")).toHaveTextContent( "Leftover map dropped 0 incomplete criteria", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic reconstruction when R̂ is missing or not usable", () => { @@ -1280,7 +1294,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin reconstruction on the comparison graphic when that persisted value is finite", () => { @@ -1323,7 +1337,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover-map explained leftover share on the comparison graphic with a distinct accessible name", () => { @@ -1359,7 +1373,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic explained leftover share when e is missing or not usable", () => { @@ -1396,7 +1410,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin explained leftover share on the comparison graphic when that persisted value is finite", () => { @@ -1443,7 +1457,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover-map unexplained leftover share on the comparison graphic with a distinct accessible name", () => { @@ -1482,7 +1496,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic unexplained leftover share when s is missing or not usable", () => { @@ -1522,7 +1536,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin unexplained leftover share on the comparison graphic when that persisted value is finite", () => { @@ -1573,7 +1587,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover-map cross share on the comparison graphic with a distinct accessible name", () => { @@ -1615,7 +1629,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic cross share when x is missing or not usable", () => { @@ -1658,7 +1672,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover-map cross share on the comparison graphic when that persisted value is finite", () => { @@ -1713,7 +1727,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover-map unexplained leftover on the comparison graphic with a distinct accessible name", () => { @@ -1758,7 +1772,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic unexplained leftover when U is missing or not usable", () => { @@ -1804,7 +1818,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover-map unexplained leftover on the comparison graphic when that persisted value is finite", () => { @@ -1863,7 +1877,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover residual on the comparison graphic with a distinct accessible name", () => { @@ -1911,7 +1925,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic leftover residual when R is missing or not usable", () => { @@ -1960,7 +1974,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover residual on the comparison graphic when that persisted value is finite", () => { @@ -2022,7 +2036,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover observed on the comparison graphic with a distinct accessible name", () => { @@ -2073,7 +2087,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic leftover observed when Y is missing or not usable", () => { @@ -2125,7 +2139,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover observed on the comparison graphic when that persisted value is finite", () => { @@ -2191,7 +2205,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); it("names leftover expected on the comparison graphic with a distinct accessible name", () => { @@ -2241,7 +2255,7 @@ describe("LeftoverMapPlot", () => { expect( screen.getByLabelText("leftover map comparison graphic reconstruction R̂ +0.25"), ).toHaveTextContent("R̂ +0.25"); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("omits leftover-map comparison graphic leftover expected when E is missing or not usable", () => { @@ -2296,7 +2310,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (82%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 1.84, 82%)")).toBeInTheDocument(); }); it("names rank-0 origin leftover expected on the comparison graphic when that persisted value is finite", () => { @@ -2366,6 +2380,6 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover map comparison graphic coverage")).toHaveTextContent( "Leftover map used 0 of 3 scored posts (complete-case)", ); - expect(screen.getByText("leftover map comparison axis 1 (0%)")).toBeInTheDocument(); + expect(screen.getByText("leftover map comparison axis 1 (σ 0.00, 0%)")).toBeInTheDocument(); }); }); diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index e46d3b6bd..4825fafc1 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -320,7 +320,12 @@ export function LeftoverMapPlot({ {leftoverMapPlotAxisText(1, leftoverMapAxes, variant)} - + {leftoverMapPlotAxisText(2, leftoverMapAxes, variant)} {layout.ticks.map((tick) => ( diff --git a/frontend/src/leftoverMapPlotAxisSingular.ts b/frontend/src/leftoverMapPlotAxisSingular.ts index dad7c12c4..267522cbf 100644 --- a/frontend/src/leftoverMapPlotAxisSingular.ts +++ b/frontend/src/leftoverMapPlotAxisSingular.ts @@ -3,10 +3,10 @@ import type { LeftoverMapAxis } from "./api"; export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR = - "leftover map comparison graphic leftover-map axis {axis} σ {value}"; + "leftover map comparison axis {axis} (σ {value})"; export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE = - "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)"; + "leftover map comparison axis {axis} (σ {value}, {share}%)"; export type LeftoverMapPlotAxisSingular = { axis_index: LeftoverMapAxis["axis_index"]; diff --git a/tests/test_grouping_comparison_graphic_singular_contract.py b/tests/test_grouping_comparison_graphic_singular_contract.py index bda9a58fa..d153db856 100644 --- a/tests/test_grouping_comparison_graphic_singular_contract.py +++ b/tests/test_grouping_comparison_graphic_singular_contract.py @@ -19,12 +19,12 @@ def test_comparison_graphic_has_distinct_persisted_singular_value_copy() -> None assert ( 'export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR =\n' - ' "leftover map comparison graphic leftover-map axis {axis} σ {value}";' + ' "leftover map comparison axis {axis} (σ {value})";' in singular_source ) assert ( 'export const LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR_SHARE =\n' - ' "leftover map comparison graphic leftover-map axis {axis} σ {value} ({share}%)";' + ' "leftover map comparison axis {axis} (σ {value}, {share}%)";' in singular_source ) assert "LEFTOVER_MAP_COMPARE_PLOT_AXIS_SINGULAR" in plot_source