diff --git a/AGENTS.md b/AGENTS.md index 7ae4a5426..177393c2b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -273,7 +273,7 @@ stops startup instead of leaving a healthy-looking partial schema, and application code must not compensate for a missing table. Period leftover pairs (ADR 0017 / 0018 / 0048 / 0049 / 0119 / 0158 / 0162 / -0163 / 0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280) are computed in `lineageweave/leftover_pairs.py` from the +0163 / 0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281) are computed in `lineageweave/leftover_pairs.py` from the residual after a real GRM/GPCM score, never invented. Distances are Euclidean on the two-dimensional Gabriel leftover map; missing cells stay out of the factorization. Closest and farthest post–criterion pairs @@ -337,7 +337,12 @@ coordinates. ADR 0280 captions leftover-map pair segments with persisted leftover-map rank so the pair-row `rank` badge matches the graphic; a missing, negative, or non-integer rank omits that leftover-map rank caption and does not invent rank from plotted -coordinates, leftover-map distance, or the count of unused axes. When `R`, `R̂`, `U`, `x`, +coordinates, leftover-map distance, or the count of unused axes. ADR 0281 +captions the leftover-map graphic display with persisted leftover-map +complete-case coverage so the pair-list `used N of M scored posts` note +matches the plot; a missing, non-integer, negative-used, non-positive-scored, +or used-greater-than-scored coverage omits that leftover-map coverage caption +and does not invent coverage from plotted marker count. 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 finite, `U + R̂ = R`. When coordinates, @@ -348,8 +353,8 @@ in Post quality (ADR 0158). Leftover-map axis share (ADR 0148) is Gabriel inerti residual SVD axes 1 and 2 and persists to `report_leftover_map_axis`. Rank-0 residuals emit two zero-share axes; the shares are report-level and are not a leftover score. Complete-case coverage (ADR 0168) persists to -`report_leftover_map_coverage` and captions the pair list with how -many scored posts entered the map. +`report_leftover_map_coverage` and captions the pair list and the leftover-map +graphic with how many scored posts entered the map. Authorized occupational construct catalog search (ADR 0257) matches official O*NET preferred labels or descriptions only when a source-eligible, ABAC-visible diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4827ec7df..eb4b5ca60 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -637,7 +637,7 @@ information at the group's mean θ (Lord, 1980 max-info CAT). Rankings persist to `report_item_information`. After those IRT main effects, residual SVD leftover pairs on two Gabriel axes (Jeon et al., 2021; ADR 0017 / 0048 / 0049 / 0119 / 0148 / 0158 / 0162 / 0163 / 0164 / 0168 / -0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280) persist to `report_leftover_pair` with signed residual `R`, +0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281) persist to `report_leftover_pair` with signed residual `R`, observed `Y`, expected `E[Y|θ, item]`, full leftover-map rank, unexplained leftover, ADR 0201 reconstruction evidence, ADR 0185 cross-share evidence, ADR 0233 unexplained leftover share `s`, ADR 0266 explained leftover @@ -658,13 +658,14 @@ unexplained leftover `U`. ADR 0277 captions leftover-map pair segments with pers residual `R`. ADR 0278 captions leftover-map pair segments with persisted leftover observed `Y`. ADR 0279 captions leftover-map pair segments with persisted leftover expected `E`. ADR 0280 captions leftover-map pair segments with persisted leftover-map -rank. Click a post marker or a pair button +rank. ADR 0281 captions the leftover-map graphic display with persisted leftover-map +complete-case coverage. 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 (Gabriel inertia of residual SVD axes 1 and 2; ADR 0148) persists to `report_leftover_map_axis`. Complete-case leftover-map coverage (ADR 0168) persists to `report_leftover_map_coverage` so readers see how -many scored posts entered the factorization. Results persist to +many scored posts entered the factorization, including on the graphic. Results persist to `report_period_score` / `report_member_score`. `GET /api/reports/{grouping}` lists the trend; `GET /api/reports/{grouping}/{period}` is ABAC-filtered; diff --git a/CHANGELOG.d/2.38.0-leftover-map-plot-coverage.md b/CHANGELOG.d/2.38.0-leftover-map-plot-coverage.md new file mode 100644 index 000000000..bc67b96e7 --- /dev/null +++ b/CHANGELOG.d/2.38.0-leftover-map-plot-coverage.md @@ -0,0 +1,23 @@ +## 2.38.0 — Leftover-map complete-case coverage on the graphic display + +- Caption the leftover-map graphic display with persisted leftover-map + complete-case coverage (ADR 0281). After `make seed`, closest and farthest + leftover pairs sit above the member list with the Gabriel biplot of + already-named coordinates, leftover-map axis share when finite, axis + ticks that match `ξ (x, y) ζ (x, y)` on the pair row, pair segments + that match `d` on the pair row, pair segments that match `R̂` on the + pair row, pair segments that match `R̂²/R²` on the pair row, pair + segments that match `U²/R²` on the pair row, pair segments that match + `2R̂U/R²` on the pair row, pair segments that match `U` on the pair + row, pair segments that match `R` on the pair row, pair segments that + match `Y` on the pair row, pair segments that match `E` on the pair + row, pair segments that match leftover-map rank on the pair row, and + a plot caption that matches leftover-map complete-case coverage above + the pair list; click a post marker or a pair button opens that post. + A missing, non-integer, negative-used, non-positive-scored, or + used-greater-than-scored coverage omits that leftover-map coverage + caption. Coverage `0 of M` is shown when that persisted used count is + a non-negative integer. Do not invent coverage from plotted marker + count, leftover-map distance, leftover-map rank, or the count of unused + axes. Never invent a leftover score. Never invent a theta. No new + columns. diff --git a/CHANGELOG.md b/CHANGELOG.md index e35505006..95d4743a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,28 @@ All notable changes to this project are documented here. Format follows ### Added +- Period leftover pairs now caption the leftover-map graphic display with + persisted leftover-map complete-case coverage (ADR 0281 / v2.38.0). After + `make seed`, closest and farthest leftover pairs sit above the member + list with the Gabriel biplot of already-named coordinates, leftover-map + axis share when finite, axis ticks that match `ξ (x, y) ζ (x, y)` on + the pair row, pair segments that match `d` on the pair row, pair + segments that match `R̂` on the pair row, pair segments that match + `R̂²/R²` on the pair row, pair segments that match `U²/R²` on the pair + row, pair segments that match `2R̂U/R²` on the pair row, pair segments + that match `U` on the pair row, pair segments that match `R` on the + pair row, pair segments that match `Y` on the pair row, pair segments + that match `E` on the pair row, pair segments that match leftover-map + rank on the pair row, and a plot caption that matches leftover-map + complete-case coverage above the pair list; click a post marker or a + pair button opens that post. A missing, non-integer, negative-used, + non-positive-scored, or used-greater-than-scored coverage omits that + leftover-map coverage caption. Coverage `0 of M` is shown when that + persisted used count is a non-negative integer. Do not invent coverage + from plotted marker count, leftover-map distance, leftover-map rank, or + the count of unused axes. Never invent a leftover score or a theta. No + new columns. + - Period leftover pairs now caption leftover-map graphic-display pair segments with persisted leftover-map rank (ADR 0280 / v2.37.0). After `make seed`, closest and farthest leftover pairs sit above the member diff --git a/CLAUDE.md b/CLAUDE.md index 75cc2391a..532fcc899 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), 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), 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/0281-leftover-map-plot-coverage.md b/docs/adr/0281-leftover-map-plot-coverage.md new file mode 100644 index 000000000..2d66236b5 --- /dev/null +++ b/docs/adr/0281-leftover-map-plot-coverage.md @@ -0,0 +1,157 @@ +# ADR 0281 — Name leftover-map complete-case coverage on the graphic display + +**Decision status:** Accepted +**Date:** 2026-08-30 + +Amends [ADR 0268](0268-leftover-map-graphic-display.md), +[ADR 0049](0049-leftover-pair-report-ui.md), and leftover-map complete-case +coverage ([ADR 0168](0168-leftover-map-complete-case-coverage.md)). Independent of +leftover-map rank on pair segments +([ADR 0280](0280-leftover-map-segment-rank.md)), leftover expected on pair +segments ([ADR 0279](0279-leftover-map-segment-expected.md)), leftover observed on +pair segments ([ADR 0278](0278-leftover-map-segment-observed.md)), leftover +residual on pair segments ([ADR 0277](0277-leftover-map-segment-residual.md)), +leftover-map unexplained leftover on pair segments +([ADR 0276](0276-leftover-map-segment-unexplained-leftover.md)), leftover-map +cross share on pair segments ([ADR 0275](0275-leftover-map-segment-cross-share.md)), +leftover-map unexplained leftover share on pair segments +([ADR 0274](0274-leftover-map-segment-unexplained-share.md)), leftover-map +explained leftover share on pair segments +([ADR 0273](0273-leftover-map-segment-explained-share.md)), leftover-map +reconstruction on pair segments +([ADR 0272](0272-leftover-map-segment-reconstruction.md)), leftover-map +distance on pair segments ([ADR 0271](0271-leftover-map-segment-distance.md)), +leftover-map coordinate ticks ([ADR 0270](0270-leftover-map-coordinate-ticks.md)), +leftover-map axis share on the graphic display +([ADR 0269](0269-leftover-map-axis-share-plot.md)), leftover residual +disclosure ([ADR 0162](0162-leftover-residual-disclosure.md)), leftover +observed `Y` / expected `E` ([ADR 0163](0163-leftover-observed-expected.md)), +leftover-map explained leftover share persistence +([ADR 0266](0266-leftover-map-explained-share.md)), leftover-map +unexplained leftover share persistence +([ADR 0233](0233-leftover-map-unexplained-share.md)), leftover-map +reconstruction persistence ([ADR 0201](0201-leftover-map-reconstruction.md)), +leftover-map cross share persistence +([ADR 0185](0185-leftover-map-cross-share.md)), leftover-map unexplained leftover +persistence ([ADR 0182](0182-leftover-map-unexplained.md)), leftover-map rank +persistence ([ADR 0164](0164-leftover-map-rank.md)), and leftover-map axis +share persistence ([ADR 0148](0148-leftover-map-axis-share.md)). + +## Context + +ADR 0168 already persists one `report_leftover_map_coverage` row per period +report and captions the pair list with `Leftover map used N of M scored +posts (complete-case)`. ADR 0280 already names persisted leftover-map rank +on leftover-map graphic-display pair segments. The graphic still has no +coverage caption, so a buyer who reads two pair markers can treat those +markers as the scored-post census even after the pair-list note names +`2 of 3`. Incomplete rows are dropped from Gabriel factorization; hiding +coverage on the plot lets a sparse post with one missing criterion vanish +without a next action. + +This increment names leftover-map complete-case coverage on the leftover-map +graphic display. It does not add columns. It does not recompute coverage from +plotted marker count, leftover-map distance, or leftover-map rank. It does +not persist leftover-map inner product, cosine, or length as separate +columns. It does not land Post quality on the leftover criterion. +Leftover-map distance stays two-axis Euclidean. Do not invent a leftover +score. Do not invent a theta. + +The dashboard stack already used neighbouring leftover facts under other +numbers. This protected increment uses **0281** so it does not collide with +leftover-map rank on pair segments (0280), leftover expected on pair segments +(0279), leftover observed on pair segments (0278), leftover residual on pair +segments (0277), leftover-map unexplained leftover on pair segments (0276), +leftover-map cross share on pair segments (0275), leftover-map unexplained +leftover share on pair segments (0274), leftover-map explained leftover share +on pair segments (0273), leftover-map reconstruction on pair segments (0272), +leftover-map distance on pair segments (0271), leftover-map coordinate ticks +(0270), leftover-map axis share on the graphic display (0269), leftover-map +graphic display (0268), leftover-map coordinates (0267 / migration 0245), +leftover-map explained leftover share persistence (0266 / migration 0244), +leftover-map unexplained leftover share persistence (0233 / migration 0233), +leftover-map reconstruction persistence (0201 / migration 0206), leftover-map +cross share persistence (0185), leftover-map unexplained leftover persistence +(0182), leftover residual disclosure (0162), leftover observed `Y` / expected +`E` persistence (0163), leftover-map rank persistence (0164), leftover +coverage persistence (0168), two-axis leftover-map distance persistence, +leftover-map axis share persistence (0148), leftover interaction-map +persistence, occupational construct catalog search (0265), or the dashboard +stacks. + +## Decision + +On the leftover-map graphic display, caption persisted leftover-map +complete-case coverage as `Leftover map used N of M scored posts +(complete-case)`, using the same `map_post_count` / `scored_post_count` +integers the pair-list note already shows. A missing coverage row, a +non-integer count, a negative used count, a non-positive scored count, or +used greater than scored omits that leftover-map coverage caption and keeps +the connecting lines and any leftover-map distance, reconstruction, explained +leftover share, unexplained leftover share, leftover-map cross share, +unexplained leftover, leftover residual, leftover observed, leftover +expected, or leftover-map rank caption. Coverage `0 of M` is shown when that +persisted used count is a non-negative integer and scored is a positive +integer. Do not invent coverage from plotted marker count, leftover-map +distance, leftover-map rank, or the count of unused axes. Click a post +marker to open that post. Criterion markers are not post buttons. The +grouping comparison strip (ADR 0149) stays on its reduced leftover payload +and does not gain this leftover-map coverage caption. + +Do not add SQL. Do not edit shipped migrations. Do not persist inner +product, cosine, or length as separate columns. Do not invent a leftover +score. Do not invent a theta. + +## Consequences + +After `make seed`, closest and farthest leftover pairs sit above the member +list with the leftover-map graphic display of persisted `ξ` and `ζ`, +leftover-map axes name persisted Gabriel inertia share when finite, +leftover-map axis ticks name the same coordinates shown on the pair row, +pair segments name persisted leftover-map distance `d`, persisted leftover-map +reconstruction `R̂`, persisted leftover-map explained leftover share `e`, +persisted leftover-map unexplained leftover share `s`, persisted leftover-map +cross share `x`, persisted leftover-map unexplained leftover `U`, persisted +leftover residual `R`, persisted leftover observed `Y`, persisted leftover +expected `E`, persisted leftover-map rank, and the plot names persisted +leftover-map complete-case coverage; click a post marker or a pair button +opens that post. Hidden posts stay hidden. Rank-0 unused axes still plot at +the origin and still name coverage when that coverage is persisted. + +## Related + +Independent of leftover interaction-map persistence, leftover-criterion +evaluation landing, leftover residual disclosure, leftover-map +complete-case coverage persistence, leftover-map axis share persistence, +leftover pairs on the grouping comparison strip, two-axis leftover-map +distance persistence, leftover-map rank persistence, leftover-map inner +product, leftover-map cosine, leftover-map length, leftover-map +reconstruction persistence, leftover-map unexplained leftover persistence, +leftover-map cross share persistence, leftover-map unexplained leftover +share persistence, leftover-map explained leftover share persistence, +leftover-map coordinate persistence, leftover-map graphic display, +leftover-map axis share on the graphic display, leftover-map coordinate +ticks, leftover-map distance on pair segments, leftover-map reconstruction +on pair segments, leftover-map explained leftover share on pair segments, +leftover-map unexplained leftover share on pair segments, leftover-map +cross share on pair segments, leftover-map unexplained leftover on pair +segments, leftover residual on pair segments, leftover observed on pair +segments, leftover expected on pair segments, and leftover-map rank on pair +segments. + +## References + +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 + +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 +(LSIRM interaction `−γ‖ξ_j − ζ_i‖` after main effects +`α_j − β_i`; typically `p = 2` for the interaction map. Gabriel +reconstruction of the leftover cell is the two-axis inner product +`R̂ = ξ_{1:2} · ζ_{1:2}`. Incomplete rows are dropped from the +complete-case residual rectangle; coverage names how many scored posts +entered that factorization.) diff --git a/frontend/package.json b/frontend/package.json index efb8af9b2..8b742284a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.37.0", + "version": "2.38.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 790c4da69..d8776aab5 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -4154,6 +4154,9 @@ describe("App, authenticated", () => { expect(screen.getByLabelText("Leftover map coverage")).toHaveTextContent( "Leftover map used 2 of 3 scored posts (complete-case)", ); + expect(screen.getByLabelText("Leftover-map graphic coverage")).toHaveTextContent( + "Leftover map used 2 of 3 scored posts (complete-case)", + ); const coverageCaption = screen.getByLabelText("Leftover map coverage"); const closestPair = screen.getByRole("button", { name: /open leftover closest pair: public post/i }); const farthestPair = screen.getByRole("button", { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e3fb6c796..4e9e95a59 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3869,6 +3869,7 @@ function ReportsPanel({ { onSelectPost(pair.post_id, { diff --git a/frontend/src/components/LeftoverMapPlot.css b/frontend/src/components/LeftoverMapPlot.css index 619fddaee..04fd8d23d 100644 --- a/frontend/src/components/LeftoverMapPlot.css +++ b/frontend/src/components/LeftoverMapPlot.css @@ -6,7 +6,8 @@ background: var(--color-background); } -.leftover-map-plot-caption { +.leftover-map-plot-caption, +.leftover-map-plot-coverage { margin: 0 0 var(--space-control-gap); color: var(--color-text); font-size: 0.85rem; diff --git a/frontend/src/components/LeftoverMapPlot.stories.tsx b/frontend/src/components/LeftoverMapPlot.stories.tsx index d79587245..8a1d58015 100644 --- a/frontend/src/components/LeftoverMapPlot.stories.tsx +++ b/frontend/src/components/LeftoverMapPlot.stories.tsx @@ -12,6 +12,14 @@ const meta = { { axis_index: 1, leftover_singular_value: 1.84, leftover_share: 0.82 }, { axis_index: 2, leftover_singular_value: 0.86, leftover_share: 0.18 }, ], + leftoverMapCoverage: { + map_post_count: 2, + scored_post_count: 3, + map_item_count: 2, + scored_item_count: 2, + incomplete_post_count: 1, + incomplete_item_count: 0, + }, pairs: [ { pair_kind: "closest", @@ -69,6 +77,14 @@ export const RankZeroOrigin: Story = { { axis_index: 1, leftover_singular_value: 0, leftover_share: 0 }, { axis_index: 2, leftover_singular_value: 0, leftover_share: 0 }, ], + leftoverMapCoverage: { + map_post_count: 1, + scored_post_count: 1, + map_item_count: 1, + scored_item_count: 1, + incomplete_post_count: 0, + incomplete_item_count: 0, + }, pairs: [ { pair_kind: "closest", diff --git a/frontend/src/components/LeftoverMapPlot.test.tsx b/frontend/src/components/LeftoverMapPlot.test.tsx index 27e0d665e..13fe165fd 100644 --- a/frontend/src/components/LeftoverMapPlot.test.tsx +++ b/frontend/src/components/LeftoverMapPlot.test.tsx @@ -61,7 +61,7 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("Leftover-map graphic display")).toBeInTheDocument(); expect( screen.getByText( - "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, leftover-map unexplained leftover share s, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, leftover expected E, and leftover-map rank. Click a post marker to open that post. The plot does not invent a leftover score.", + "Leftover map after IRT main effects. Axis ticks name persisted leftover-map coordinates. Pair segments name leftover-map distance d, leftover-map reconstruction R̂, leftover-map explained leftover share e, leftover-map unexplained leftover share s, leftover-map cross share x, leftover-map unexplained leftover U, leftover residual R, leftover observed Y, leftover expected E, and leftover-map rank. The plot names leftover-map complete-case coverage when persisted. Click a post marker to open that post. The plot does not invent a leftover score.", ), ).toBeInTheDocument(); expect(screen.getByText("Post ξ")).toBeInTheDocument(); @@ -89,6 +89,8 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover observed Y 0.90")).toBeInTheDocument(); expect(screen.getAllByLabelText("leftover expected E 2.00")).toHaveLength(2); expect(screen.getAllByLabelText("leftover-map rank rank 1")).toHaveLength(2); + expect(screen.queryByLabelText("Leftover-map graphic coverage")).not.toBeInTheDocument(); + const postMarker = screen.getByRole("button", { name: "Open leftover-map post Public post at ξ (+0.50, +0.10)", @@ -522,4 +524,95 @@ describe("LeftoverMapPlot", () => { expect(screen.getByLabelText("leftover expected E 2.00")).toBeInTheDocument(); expect(screen.getByLabelText("leftover observed Y 2.40")).toBeInTheDocument(); }); + + it("names persisted leftover-map complete-case coverage on the graphic without inventing a leftover score", () => { + render( + , + ); + expect(screen.getByLabelText("Leftover-map graphic coverage")).toHaveTextContent( + "Leftover map used 2 of 3 scored posts (complete-case)", + ); + expect(screen.getByLabelText("Leftover-map graphic coverage")).not.toHaveTextContent( + "Leftover map used 2 of 2 scored posts (complete-case)", + ); + }); + + it("omits leftover-map coverage on the graphic when coverage is missing or not usable", () => { + const { rerender } = render( + , + ); + expect(screen.queryByLabelText("Leftover-map graphic coverage")).not.toBeInTheDocument(); + rerender( + , + ); + expect(screen.queryByLabelText("Leftover-map graphic coverage")).not.toBeInTheDocument(); + expect(screen.getAllByLabelText("leftover-map rank rank 1")).toHaveLength(2); + }); + + it("names leftover-map coverage 0 of M on a rank-0 origin when that persisted used count is a non-negative integer", () => { + render( + , + ); + expect(screen.getByLabelText("Leftover-map graphic coverage")).toHaveTextContent( + "Leftover map used 0 of 3 scored posts (complete-case)", + ); + expect(screen.getByLabelText("leftover-map rank rank 0")).toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/LeftoverMapPlot.tsx b/frontend/src/components/LeftoverMapPlot.tsx index 72ea5aae8..99de0b3b1 100644 --- a/frontend/src/components/LeftoverMapPlot.tsx +++ b/frontend/src/components/LeftoverMapPlot.tsx @@ -1,6 +1,11 @@ -import type { LeftoverMapAxis, LeftoverPair } from "../api"; +import type { LeftoverMapAxis, LeftoverMapCoverage, LeftoverPair } from "../api"; import { t, tf } from "../i18n"; import { formatLeftoverMapCoordinatePair } from "../leftoverMapCoordinates"; +import { + leftoverMapCoverageCounts, + LEFTOVER_MAP_PLOT_COVERAGE, + LEFTOVER_MAP_PLOT_COVERAGE_LABEL, +} from "../leftoverMapCoverage"; import { formatLeftoverMapPlotAxisShare, leftoverShareForAxis, @@ -28,6 +33,7 @@ import "./LeftoverMapPlot.css"; export type LeftoverMapPlotProps = { pairs: LeftoverPair[]; leftoverMapAxes?: LeftoverMapAxis[]; + leftoverMapCoverage?: LeftoverMapCoverage | null; criterionLabel: (criterionCode: string) => string; onSelectPost: (pair: LeftoverPair) => void; }; @@ -62,7 +68,8 @@ function leftoverMapPlotAxisText( * share ``s``, leftover-map cross share ``x``, leftover-map * unexplained leftover ``U``, leftover residual ``R``, leftover * observed ``Y``, leftover expected ``E``, and leftover-map rank so the - * pair-row badges match the graphic. + * pair-row badges match the graphic. Name leftover-map complete-case + * coverage on the figure when those persisted post counts are usable. * Omit that distance caption when ``d`` is missing or non-finite. Omit * that reconstruction caption when ``R̂`` is missing or non-finite. Omit * that explained leftover share caption when ``e`` is missing or @@ -73,7 +80,9 @@ function leftoverMapPlotAxisText( * caption when ``R`` is missing or non-finite. Omit that leftover observed * caption when ``Y`` is missing or non-finite. Omit that leftover expected * caption when ``E`` is missing or non-finite. Omit that leftover-map rank - * caption when rank is missing, negative, or not an integer. Omit that axis badge when share is + * caption when rank is missing, negative, or not an integer. Omit that leftover-map + * coverage caption when coverage is missing or not usable complete-case integers. + * Omit that axis badge when share is * missing or non-finite and keep the existing leftover-map axis text. * Omit the plot when no pair has four finite leftover-map coordinates. * Never invent a leftover score. @@ -81,6 +90,7 @@ function leftoverMapPlotAxisText( export function LeftoverMapPlot({ pairs, leftoverMapAxes, + leftoverMapCoverage, criterionLabel, onSelectPost, }: LeftoverMapPlotProps) { @@ -88,6 +98,7 @@ export function LeftoverMapPlot({ if (layout === null) { return null; } + const coverageCounts = leftoverMapCoverageCounts(leftoverMapCoverage); const openPost = (postId: string) => { const pair = firstPlottablePairForPost(pairs, postId); @@ -99,6 +110,15 @@ export function LeftoverMapPlot({ return (
{t(LEFTOVER_MAP_PLOT_CAPTION)}
+ {coverageCounts !== null ? ( +

+ {tf(LEFTOVER_MAP_PLOT_COVERAGE, coverageCounts)} +

+ ) : null}