Skip to content

Show withheld UK areas as not reported on the hex maps, placing v2's hex maps from the layout files - #1448

Merged
anth-volk merged 21 commits into
mainfrom
fix/uk-hex-maps-not-reported
Oct 27, 2026
Merged

anth-volk merged 21 commits into
mainfrom
fix/uk-hex-maps-not-reported

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #1446
Fixes #1432

This PR combines #1433 (including the fix for Pavel's review, which places a map's areas from the layout all or none) and #1447, and supersedes both. Their commits are cherry-picked onto main with -x, in order, so each commit names the one it came from. #1447 was written on top of #1436, which removes the app's handling of the results' x and y. #1436 isn't included, so #1447's commits are adapted to main as listed below. #1436 will be rebuilt on top of this PR.

The original work in both PRs was done red-green: each commit's tests were written first and seen failing before the code.

What changes

v2's hex maps placed from the layout files (#1433)

v2's constituency and local-authority hex maps place each area from the app's layout files, through placeOnUKHexLayout, as v3's do, on the tiled grid. Placement is all or none. If the layout has no position for even one of a result's areas (an area with no code, or a pre-2023 local-authority code), every area keeps its result's x and y, on the untiled grid. This fixes v2's half-hex row offsets for constituencies, and the five local authorities the Public API stacks at (0, 0). A tiled map now also keeps its top and bottom hexagons inside its frame. #1433's description has the details.

UK areas withheld by disclosure control shown as not reported (#1447)

For a level from a local-area breakdown, which is disclosure-controlled, the v3 hex map and the four v2 hex charts now draw every area of the report's region:

  • all 650 constituencies and 361 local authorities for a UK-wide report;
  • the nation's areas for a nation's report.

An area that the result leaves out:

  • is drawn in its place with grey stripes on the map's background and a grey edge, where a valued hexagon's edge is the background colour, so no colour on the scale resembles it;
  • reads "name: Not reported" in its tooltip;
  • has no value, so it isn't in the colour range or in v3's rankings and area finder, and can't be selected.

Withheld areas are drawn only on the tiled grid. A result that keeps its own positions (any area uncoded or missing from the layout) draws only its own areas, as does a saved level.

The note under the maps now reads "N of M constituencies are not reported" (or local authorities), so the map, its tooltips and the note use the same term.

The geographic map

UKAreaChoroplethMap already drew every boundary. A withheld boundary keeps its solid gray[300] fill; its tooltip now reads "Not reported" instead of "Not published". In v3 that text appears only on the level the breakdown supplied, and a saved level's boundary without a result reads "No data". gray[300] sits between two stops on the scale's grey side, so hatching the geographic map too would be a small follow-up.

How

  • HexagonalMap takes missingAreas (areas it places without a value) and missingAreaText. It draws them on the same grid as the valued areas, hatched with an SVG pattern whose stripes are never closer than 4 SVG units, and fits its frame to them as well.
  • withheldUKHexAreas(level, points, region) in ukHexLayout.ts lists the region's layout codes that the result lacks, each at its layout position. It lists none when any area has no code or a code outside the region's layout, or when the region is neither the UK nor a nation.
  • Withheld areas are named from two new files beside the layouts, constituency_names_2024.json and local_authority_names_2023.json, built from the boundary files' PCON24NM and LAD23NM and checked against them by ukBoundaryFiles.test.ts. They add about 34 KB, before compression.

Adapted for main

On main, areas still carry x and y, HexagonalMap still has its untiled grid and tiled option, and #1436's note on areas the layout can't place never existed. Each adapted commit's message says what changed:

  1. Say "not reported"…: the conflicts were test imports of Remove the app's handling of x and y on UK area results #1436's layout-note fixture, and a v3 section test, which keeps main's hexagon check with the new wording.
  2. Draw areas without a value…: missing areas take the same grid as the valued areas, through a positionOnGrid helper shared by both. They are pointy-topped when tiled and fitted with the tiled reach. The tests draw them on the tiled grid, as the UK maps do.
  3. List the areas of a report's region…: withheldUKHexAreas reads only each point's code, since main's points carry x and y. Its documentation says it lists none whenever placeOnUKHexLayout falls back to the result's positions.
  4. Draw UK areas disclosure control withheld…: v3 and the four v2 charts pass hexMap.points as before, and compute withheld areas only when hexMap.tiled is true. v3's test of a breakdown area without a code checks that the map stays untiled with no withheld areas, instead of checking Remove the app's handling of x and y on UK area results #1436's note. Its fixture is added here.
  5. Read "Not reported" only on a breakdown level's v3 geographic map: applied unchanged.
  6. Keep a withheld hexagon's hatching distinct…: its two tests draw on the tiled grid.
  7. Say in the hex map's docs…: the README keeps the sentence that withheld areas are drawn only on the tiled grid.

None of the adaptations needed behaviour beyond the original commits, so there are no new red-green commits.

Commits

From #1433: d2e72f8d (from 49adbc21), d130de1b (8263784e), 3a48e68e (94f42033), c6f816ff (54a674ee), 71dc49ef (d2af1321) and 07d096ca (f4e720cc).

From #1447: 41cbbeb3 (from 67c46a99), b1c66a38 (211701e9), 4dd70841 (bc4dade1), b3f8cd4b (e3ecff7a), f8a4d1f8 (78b1e8ed), 7a10d52d (184178f5) and 9ec3bf81 (dac1a6d9).

Then 08f9cb07 rewraps the documentation on HexagonalMap's missingAreas prop.

Review

A first review-code round on this PR found no bugs. It noted the overlong documentation row fixed in 08f9cb07, and that the tiled-only rule is checked both in withheldUKHexAreas and in the callers, deliberately. #1433 and #1447 were each reviewed before.

Three more review-code rounds then ran on the whole PR at 08f9cb07. None found a bug, so there are no fix commits.

  • Round 1 read the placement, the withheld-area rule and its v2 and v3 callers, nation and UK-wide regions, saved and uncoded results, tooltips, selection, the area finder, the colour range, downloads and performance. Code-quality notes:

    • the four v2 charts repeat the same placement and withheld-area block, which a shared hook could hold;
    • HexMapMissingArea documents its x and y as 0-based, though layout positions are negative;
    • the nation tests use only Wales.
  • Round 2 checked:

    • the hex map's geometry with missing areas: one grid for both sets, the frame fitted to both, no two hexagons in one place, all clear of the colour bar;
    • the export path: html-to-image copies the hatching pattern with the map;
    • zoom, the v2 Migration page's wiring, and every fixture's codes, names and positions against the data files.

    Code-quality notes:

    • ukNationFromRegion accepts only country/* regions, not the older forms that normalizeRegionCode maps (england, uk-wales). A report on such a region gets no hatching and the uncounted note, just as its geographic map already gets no nation focus.
    • A withheld hexagon's edge is gray[400], one of the scale's stops. How it reads on a narrow screen needs a look in a browser.
  • Round 3 rendered realistic breakdowns in a scratch test, not committed:

    • England: 543 constituencies;
    • Scotland: 32 local authorities;
    • Northern Ireland: 18 constituencies and 11 local authorities, under both country/ni and country/northern_ireland;
    • UK-wide: 600 of 650 constituencies, and 361 local authorities.

    Each map drew every area of its region, with as many hatched hexagons as the note counts, no NaN coordinates, and every hexagon inside the frame. A level whose withheld count is unknown still draws its withheld areas, beside the uncounted note. A git range-diff against Show UK areas withheld by disclosure control on the hex maps as not reported #1447's commits shows only the adaptations listed above, and all 60 of Show UK areas withheld by disclosure control on the hex maps as not reported #1447's tests are here. Code-quality note: the UK-wide note counts against the region list by name, and the hex map against the layout by code. They agree now that the Public API lists 361 local authorities, but nothing checks that they stay in step.

Two review points fixed

Two code-quality points from the review rounds are fixed, each red-green: the tests were written first and seen failing, then the code, committed together.

  1. One function places each hex map and its withheld areas (f7575a8d). The four v2 charts and v3's UKLocalAreaMaps each placed their areas from the layout and then worked out the withheld ones in a second step. placeUKHexMap(level, points, { disclosureControlled, region }) in ukHexLayout.ts does both: placeOnUKHexLayout's all-or-none placement and, for a disclosure-controlled level on the tiled grid, the withheld areas. All five maps call it. Its unit tests failed first because it didn't exist. The chart and v3 tests pass unchanged.
  2. The note counts withheld areas the way the map draws them (e83c161e).
    • Before: for a UK-wide report, the note under the maps counted withheld areas by name against the Public API's region list. For a nation's report it counted by code against fixed per-nation totals. The hex map hatched the region's layout codes that the result lacks, so the two could disagree. With Ards and North Down published, the note read "1 of 3 local authorities is not reported" against the tests' trimmed region list while the map hatched 359. A region list from before Request UK local-area breakdowns, fix the Northern Ireland region name, and update .py to 6.10.0 policyengine-api#4024 gave no count at all.
    • Now: withheldUKAreaCodes holds one rule, each published area's ONS code against the layout's areas of the report's region, and both withheldUKHexAreas and countWithheldUKAreas follow it. A result that can't be matched by code (an area without a code, or one outside the region's layout) gives no count, as a nation's report did before.
    • Region list removed: the count no longer reads the region list. useUKLocalAreaOutput no longer calls useRegions. withheldUKAreasOf and countWithheldUKAreas lose their region-list and name arguments, and the tests no longer mock the region list. UK_AREA_COUNTS_BY_NATION had no other use, so it moves into the test fixture that checks the layout's per-nation counts.
    • Tests: new v3 and v2 tests check that, for a UK-wide breakdown that publishes Ards and North Down, the note's count equals the number of hatched local authorities (359). A unit test checks that the count equals withheldUKHexAreas' length. The existing note tests now expect counts against the layout, for example "649 of 650 constituencies" and "360 of 361 local authorities". A local authority published under a name the region list doesn't use is now counted.

One review-code round on these two commits found no bugs, so there was no second round. Its code-quality notes: the layouts README doesn't yet say that the note counts by the map's rule, and withheldUKHexAreas still guards a position lookup that can't be null.

Pavel's review

Pavel reviewed the PR at e83c161e. Five of his points are fixed. The first three are red-green: the test was written first and seen failing for the expected reason, then the code, committed together.

  1. Hatching blends into flat grey on a phone (e4e754d9). The 4-unit minimum gave 4 pixels only on a map drawn at one pixel per SVG unit. v2's charts draw a fixed 600-unit viewBox, so at 375 pixels wide the UK-wide constituency map's stripes sat 2.5 pixels apart. HexagonalMap now measures its rendered width whenever it has missing areas, and sets the minimum at 4 screen pixels. The screen scale is the rendered width over the viewBox width, never more than one, since the SVG is as tall as its viewBox. A v2 chart test and a HexagonalMap test, each at 375 pixels, failed at 2.5 pixels and now pass.
  2. Stripes widen with zoom on v3 (3fbc1ec9). The pattern sits inside the zoomed layer, so its stripes scaled with the zoom: at zoom 20 they sat 80 pixels apart where they sat 4 at zoom 1. The pattern's size is now divided by the zoom factor, so the stripes keep their spacing on screen. A test zooms from 1 to 20 and compares the spacing; it failed at 80 against 4.
  3. Names of withheld hexagons come from the boundary files (4a674688). A test now checks that both name files equal the app's copies of .py 6.10.0's area lists (constituencies_2024.csv, local_authorities_2023.csv), code by code. The names match, so it passes. Run once against copies with one name changed in each file (not committed), it failed and named both differences.
  4. The position ? … : [] guard can't fail (4203dfd7). It is removed: withheldUKHexAreas reads each withheld code's position from the layout directly, through a gridPosition helper that ukHexPosition shares. A refactor; the existing tests pass unchanged.
  5. "0-based" coordinates (867af2c1). HexMapDataPoint's and HexMapMissingArea's x and y are now documented as able to be negative. Documentation only.

Not changed, as Anthony decided:

  • Every absent area reads as withheld (his first point). A follow-up will have the sim API return the codes it withheld, so the app can tell a withheld area from one that is missing for another reason.
  • The name files load with the v2 tabs. They stay a static import.
  • A UK-wide breakdown without area codes. The breakdown contract requires a code on every record, so that case can't arise.

One review-code round on these five commits found no bugs. Its code-quality notes: the tests' on-screen spacing helper uses the same width-over-viewBox rule as the code, so it wouldn't catch a mistake in that rule; and the phone test covers one of the four v2 charts, which all share HexagonalMap's spacing.

Testing

  • Focused vitest runs on Node 26 of all 13 test files the PR touches, plus the migration subpage, the local-area output hook and the breakdown adapter: 340 tests pass. make format, make lint and make typecheck pass.
  • After the three review rounds, at 08f9cb07 on Node 26.10: the 13 touched test files pass (271 tests). make format changes nothing, and make lint and make typecheck pass.
  • After the two fixes, at e83c161e on Node 26.10: the 16 test files the PR touches pass (344 tests). make format changes nothing, and make lint and make typecheck pass.
  • After Pavel's review, at 867af2c1 on Node 26.10: the 16 test files the PR touches pass (349 tests). make format changes nothing, and make lint and make typecheck pass.
  • Not checked in a browser.

To check by hand

  1. On a UK report with constituency results, open the v2 constituency tab and choose Hex grid. The hexagons should be pointy-topped and touching, in the same arrangement as the v3 report's Local areas hex map.
  2. On the v2 local-authority tab, Cumberland, Westmorland and Furness, and North Yorkshire should be three separate hexes.
  3. On a report whose local-area breakdown has run, the hex maps should show every constituency and local authority, with the withheld ones striped, grey-edged and reading "Not reported" on hover. The note should give the same number of areas as are striped.
  4. On a phone, or a narrow window, the v2 hex charts' withheld hexagons should show stripes, not a flat grey. On v3, zooming in should keep the stripes as close together as they are at the start.

🤖 Generated with Claude Code

anth-volk and others added 13 commits October 27, 2026 20:40
v2's absolute and relative constituency hex maps drew each constituency
at the x and y its result gives, on the untiled square grid. Those
positions are the Open Innovations layout's [q, r], but the untiled grid
shifts the even rows where the layout's odd-r rows need the odd ones
shifted, so v2's arrangement differed from v3's in every other row.

Both now place their areas with placeOnUKHexLayout, which holds the
rule v3's local-area map had inline: each area whose ONS code the layout
has takes the layout's position, on the tiled grid. An area the layout
cannot place (no code, as in an older saved result, or a code the layout
lacks) keeps its result's position, and the map then draws the untiled
grid those positions were made for. v3's map now calls the same
function, with no change in what it draws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 49adbc2)
v2's absolute and relative local-authority hex maps drew each authority
at the x and y its result gives, on the untiled square grid. The Public
API's positions differ from the layout for 56 of the 361 authorities,
and put Cumberland, Westmorland and Furness, North Yorkshire, Somerset
and Bournemouth, Christchurch and Poole, created in 2019 and 2023, all
at (0, 0), so v2 drew them on top of one another.

Both maps now place their areas with placeOnUKHexLayout, as v3's
local-area map and v2's constituency maps do: each authority whose ONS
code the layout has takes the layout's position, on the tiled grid. An
area the layout cannot place keeps its result's position, on the
untiled grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 8263784)
The README said only v3's "Local areas" tab draws from the layout
files. v2's constituency and local-authority tabs now do as well,
through placeOnUKHexLayout. The test fixture that compares v2's and
v3's hex maps also takes UKAreaLevel from its adapter instead of
repeating the type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 94f4203)
On the tiled grid a hexagon's radius is 0.55 of the scale, but the map
fitted only the hexagons' centres inside a margin of twice hexSize. A
map of few areas has a large scale, so its top and bottom hexagons ran
past the frame and were clipped, in the map and in its download. v2's
hex map of three Welsh constituencies, now tiled, reached y = -15.6 in
its 380-unit frame. v3's map of a nation had the same problem.

The tiled map's bounds now take in each hexagon's edges, its radius up
and down and √3/2 of it sideways, so the scale leaves room for them.
HexMapConfig's hexSize comment now says what it sets on each grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 54a674e)
The README said the hex maps no longer depend on the x and y the Public
API sends. An area without a code in the layout still falls back to
them, so it now says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d2af132)
When the layout had no position for some of a result's areas,
placeOnUKHexLayout moved the others to their layout positions and left
those at the x and y their results gave, all on the untiled grid. The
two sets of positions are different grids (a row apart for
constituencies, entirely different for local authorities), so the
map's hexagons overlapped or scattered. A saved result from before
April 2023 reaches this: the layout follows the April 2023 boundaries
and has no code for a district abolished then, such as Allerdale
(E07000026), Craven (E07000163) or Mendip (E07000187).

Placement is now all or none. If the layout lacks any point's code,
every point keeps its result's position, on the untiled grid; only when
it has every code do the points take its positions, tiled. v2's and v3's
hex maps both use it. The README and v3's comment say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit f4e720c)
The note under the UK local-area maps now reads "N of M constituencies
are not reported" (or local authorities), and a withheld boundary's
tooltip on the geographic map reads "Not reported", in v3 and in the
four v2 charts. Tests for the three v2 charts that had none check the
note and the tooltip.

Adapted for main: the conflicts were test imports of the layout-note
fixture, which main never had, and a v3 section test that checks the
stored constituency's hexagon as main does, now with "not reported".

(cherry picked from commit 67c46a9)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HexagonalMap takes `missingAreas`: areas it places but has no value
for, such as ones disclosure control withheld. Each is drawn in its
place with grey stripes on the map's background, which no colour on
the scale resembles, and its tooltip reads its name and
`missingAreaText` ("No data" by default). Missing areas take no part in
the colour range, cannot be selected, and the map's frame fits them
with the valued areas.

Adapted for main, where HexagonalMap has an untiled grid beside the
tiled one: missing areas take the same grid as the valued areas
(positionOnGrid, shared by both), are pointy-topped when tiled, and are
fitted with the tiled reach. The tests draw them on the tiled grid, as
the UK hex maps do.

(cherry picked from commit 211701e)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
withheldUKHexAreas gives the constituencies or local authorities of a
report's region that a result does not hold, each at its hex layout
position and named, with its ONS code as its id: every area of the
layout for a UK-wide report, and the nation's for a nation's report.
It lists none when the result cannot be matched to the layout: an area
with no code, a code outside the region's layout, or a region that is
neither the UK nor a nation.

The names come from two new files beside the layouts, built from the
boundary files' PCON24NM and LAD23NM, which a test checks against
them.

Adapted for main, whose hex-map points still carry the result's x and
y: withheldUKHexAreas reads only each point's code, and the Welsh test
points carry the Public API's positions. Its documentation says that
when placeOnUKHexLayout cannot place every area, and the map keeps the
result's own positions, it lists none, so withheld areas are drawn only
on the tiled grid.

(cherry picked from commit bc4dade)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For a level from a local-area breakdown, the v3 and v2 constituency and
local authority hex maps draw every area of the report's region: all
650 constituencies or 361 local authorities for a UK-wide report, and
the nation's for a nation's report. An area the result leaves out is
drawn hatched in its place, outside the colour scale, and its tooltip
reads its name and "Not reported". It has no value, so it is not in the
colour range or v3's rankings, and it cannot be selected.

A saved level, a result with an area that has no code, or one with a
code outside the region's layout still draws only its own areas, and
the note on areas the layout cannot place stays separate from the note
on withheld areas.

Adapted for main, where a result the layout cannot fully place keeps
its own x and y, untiled: the hex maps draw withheld areas only when
placeOnUKHexLayout tiled the map (hexMap.tiled), and pass hexMap.points
as before. v3's test of a breakdown area without a code checks that the
map stays untiled with no withheld areas, instead of the layout note,
which main never had; the fixture for that output is added here.

(cherry picked from commit e3ecff7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v3's geographic map labelled every boundary without a result "Not
reported" whenever the report had a breakdown, including on a saved
level beside it, which disclosure control did not apply to. It now
does so only on a level the breakdown supplied, as the hex map does; a
saved level's boundary without a result reads "No data".

(cherry picked from commit 78b1e8e)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the UK-wide constituency hex map each hexagon's radius is about 4.7
pixels, so the hatching's stripes repeated 2.3 pixels apart. That is
close enough to blend into a flat light grey, which the colour scale's
grey arm holds, so a withheld constituency could read as a small loss.
The stripes now repeat no closer than 4 SVG units (pixels at full
width), and a withheld hexagon has a grey edge where a valued one's is
the background colour, kept at the same screen width when zoomed.

Adapted for main: the two tests draw their maps on the tiled grid, as
the UK hex maps draw withheld areas, since main's map is untiled
unless asked.

(cherry picked from commit 184178f)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adapted for main: the README keeps this branch's sentence that withheld
areas are drawn only on the tiled grid.

(cherry picked from commit dac1a6d)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
policyengine-calculator-next Ready Ready Preview Oct 27, 2026 8:38pm UTC
policyengine-website Ready Ready Preview Oct 27, 2026 8:38pm UTC

Request Review

The cherry-pick's conflict resolution left one overlong row; the
documentation now also says that missing areas take the valued areas'
grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 27, 2026
#1448 (1A · 2) holds #1433's six commits, cherry-picked unchanged, and
#1447's work adapted to main: the hex maps draw the areas a
disclosure-controlled breakdown withheld, hatched, as "Not reported",
but only on the tiled grid, because areas still carry x and y there.
This branch now builds on it. The conflicts keep this branch's
behaviour and #1448's withheld areas:

- The app reads no x or y: placeOnUKHexLayout takes the adapters'
  UKAreaChoroplethDataPoint, leaves out an area the layout cannot
  place, and returns only the placed points. #1448's Welsh fixture
  points lose their positions, and its fixture with every API position
  at the origin is dropped.
- HexagonalMap draws only the tiled grid. #1448's missingAreas are
  positioned as the valued areas are, by a positionOnGrid with no
  coordinate scale or row-spacing argument, and count in the bounds.
  The duplicate "tiled frame" tests and the missing-area tests'
  tiled config are dropped.
- No note says which areas are left off.
- The withheld areas are kept. Under this branch every map tiles, so
  the callers' "only when tiled" checks are gone: v3's UKLocalAreaMaps
  and the four v2 charts ask withheldUKHexAreas for a breakdown level's
  withheld areas whenever the level is disclosure-controlled.

withheldUKHexAreas still lists none when any of the result's areas has
no code or a code outside the region's layout. That check is about
matching the result to the layout, not about the grid: a layout area
the result lacks may be covered by one of its unmatched areas, such as
the districts a 2023 unitary authority replaced, so calling it withheld
could be wrong. A map that leaves out unplaceable areas therefore draws
no withheld areas, and the function's comment says why. #1448's v3 test
for a breakdown area without a code now expects that: Gower left off,
the other constituencies tiled, and no withheld areas.

The README keeps both sides' text, and the tests take #1448's "not
reported" wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 27, 2026
The README, from #1448, said withheld areas are drawn only on the tiled
grid, as a result that falls back to its x and y lists none. On this
branch nothing falls back: it now says that a result with any area the
layout files cannot place lists none, and why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk added this pull request to stack #1449 October 27, 2026 17:00
@anth-volk
anth-volk removed this pull request from stack #1449 October 27, 2026 17:18
The four v2 charts and v3's local-area map each placed their areas from
the layout and then worked out the withheld ones in a second step.
placeUKHexMap does both: placeOnUKHexLayout's all-or-none placement,
and, for a disclosure-controlled level on the tiled grid, the region's
areas the result leaves out. All five maps now call it, with no change
in what they draw; their tests pass unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The note under the UK local-area maps counted a UK-wide report's
withheld areas by name against the Public API's region list, and a
nation's by code against fixed per-nation totals, while the hex map
hatched the region's layout codes the result lacks. The two could
disagree: with Ards and North Down published, the note read "1 of 3
local authorities is not reported" against a trimmed region list while
the map hatched 359, and a region list from before
PolicyEngine/policyengine-api#4024 gave no count at all.

withheldUKAreaCodes now holds the one rule, each published area's ONS
code against the layout's areas of the report's region, and both
withheldUKHexAreas and countWithheldUKAreas follow it. A result that
cannot be matched by code (an area without a code, or one outside the
region's layout) gives no count, as a nation's report did. The count no
longer reads the region list: useUKLocalAreaOutput stops calling
useRegions, withheldUKAreasOf and countWithheldUKAreas lose their
region and name arguments, and UK_AREA_COUNTS_BY_NATION, now unused,
moves into the test fixtures that check the layout's nation counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@PavelMakarchuk PavelMakarchuk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @anth-volk. I reviewed this at e83c161e. CI is green, and I found no blocking bugs. The all-or-none placement fixes the problem from my #1433 review. Placing the hex maps and counting the note with one rule (placeUKHexMap, withheldUKAreaCodes) is a good change.

Three points that I think need a fix or an answer before merge:

1. Every absent area reads as withheld by disclosure control (ukHexLayout.ts:117)
For a disclosure-controlled level, every layout area that the result does not return is hatched "Not reported", and the note says the disclosure rules are the cause. An area can be absent for another reason. For example, if a UK-wide breakdown has no Northern Ireland areas because those runs fail (PolicyEngine/policyengine-sim-api#858), the map labels them as withheld. Before this PR, only the note's count had this problem. Now the map shows it too. Can the breakdown tell the app which areas it withheld, or can we confirm that absent always means withheld?

2. The v2 hatching blends into flat grey on a phone (HexagonalMap.tsx:317)
The 4-unit minimum spacing gives 4 px only when one SVG unit is one pixel. The v2 hex charts don't use fillWidth, so they draw the fixed 600-unit viewBox. At 375 px wide that is about 0.57 px per unit, so the stripes are about 2 px apart. This is the flat grey that the comment wants to prevent, and it is on the scale's grey arm. You could set the minimum in screen pixels (use the measured width ÷ svgWidth), or turn on fillWidth for the v2 charts.

3. The stripes get larger with zoom on v3 (HexagonalMap.tsx:575)
The pattern is userSpaceOnUse inside the zoom layer. At zoom 20, a withheld hexagon shows only one or two wide bands. Dividing the pattern size by zoom.view.k would keep the stripe density the same at all zoom levels.

Smaller points (non-blocking):

  • ukHexLayout.ts:148: a withheld hexagon gets its name from the boundary files, but a valued hexagon, the rankings and the finder get their names from the API. Nothing checks that the two sources stay the same.
  • ukHexLayout.ts:11: the two name files (about 34 KB) now load with the v2 constituency and local-authority tabs, also for saved results that never need them. A dynamic import would prevent this.
  • ukLocalAreaBreakdownAdapter.ts:158: a UK-wide breakdown with no area codes gave a count by name before, and now gives the uncounted note. This is fine if the breakdown always sends codes.
  • ukHexLayout.ts:147: the position ? … : [] guard can't fail, because the codes come from the layout's own keys.
  • HexMapDataPoint.ts:28: the comment says x and y are "0-based", but layout positions can be negative.

I haven't checked it in a browser yet.

anth-volk and others added 2 commits October 28, 2026 00:34
The hatching's minimum spacing was 4 SVG units, which is 4 pixels only
when one unit is one pixel. v2's hex charts draw a fixed 600-unit
viewBox, so at 375 pixels wide one unit is 0.625 pixels and the UK-wide
constituency map's stripes sat 2.5 pixels apart, blending into a flat
grey on the scale's grey arm. HexagonalMap now measures its rendered
width whenever it has missing areas and sets the minimum in screen
pixels: 4 divided by the screen pixels per unit, which is the rendered
width over the viewBox width, never more than one, since the SVG is as
tall as its viewBox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hatching pattern sits inside v3's zoomed layer, in that layer's
user units, so zooming scaled the stripes with the map: at zoom 20 they
sat 80 pixels apart where they sat 4 at zoom 1, and a withheld hexagon
showed one or two wide bands. The pattern's size is now divided by the
zoom factor, so the stripes keep their spacing on screen at every zoom
level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 3 commits October 28, 2026 00:35
A withheld hexagon is named from the name files beside the layouts,
while every valued area, the rankings and the finder take their names
from the results, which come from .py's area lists. A test now checks
that each name file equals the app's copy of .py 6.10.0's list
(constituencies_2024.csv, local_authorities_2023.csv), code by code. It
passes today; run against copies with one name changed in each file, it
failed and named both differences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each withheld code is one of the layout's own keys, so it always has a
position; the null check around ukHexPosition could never take its
other branch. withheldUKHexAreas now reads the position from the layout
directly, through gridPosition, which ukHexPosition shares. A refactor:
the existing tests pass unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HexMapDataPoint's and HexMapMissingArea's x and y were documented as
0-based, but layout positions can be negative, as most of the UK
constituency layout's rows are. Documentation only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk

Copy link
Copy Markdown
Collaborator Author

Thanks @PavelMakarchuk. Changes are at 867af2c1.

1. Absent areas read as withheld. You're right: the breakdown doesn't say which areas it withheld, so the app infers it from absence. We'll fix that in a follow-up, where the sim API returns each level's withheld codes (the areas it estimated minus those it published), and the app hatches only those and shows any other missing area as "No data". The risk is low until then: PolicyEngine/policyengine-sim-api#858 is closed, a breakdown is one run (so a failure fails the whole result rather than dropping part of it), and every record must carry its code.

2. Hatching on a phone. Fixed in e4e754d9: the minimum gap is now 4 screen pixels, from the rendered width over the viewBox width. A test renders a v2 chart 375 px wide.

3. Stripes widening with zoom. Fixed in 3fbc1ec9: the pattern's size is divided by the zoom factor, tested from zoom 1 to 20.

Smaller points:

  • Names from two sources: 4a674688 adds a test that both name files match the app's copies of .py's area lists, code by code.
  • The name files loading with the v2 tabs: left as they are. A dynamic import would make placement asynchronous for a small saving.
  • UK-wide breakdowns without codes: the contract requires constituency_code and local_authority_code on every record, so this can't arise.
  • The guard: removed in 4203dfd7.
  • "0-based": corrected in 867af2c1.

We haven't checked it in a browser yet either.

@anth-volk
anth-volk merged commit 8f61700 into main Oct 27, 2026
10 checks passed
@anth-volk
anth-volk deleted the fix/uk-hex-maps-not-reported branch October 27, 2026 21:05
anth-volk added a commit that referenced this pull request Oct 27, 2026
#1448 (1A · 2) merged into main with eight commits after the one this
branch took: one shared placeUKHexMap for the four v2 charts and v3,
withheld areas counted by code against the layout (withheldUKAreaCodes),
stripe spacing in screen pixels on a phone and at any zoom, a check of
the name files against .py's area lists, a shared gridPosition, and
coordinates documented as possibly negative. The merge keeps all of
them, and this branch's behaviour:

- placeUKHexMap returns placeOnUKHexLayout's placed points and the
  withheld areas; UKHexMap no longer extends the placement result, as
  there is no tiled flag. Its "withheld areas only when tiled" check is
  gone: a map is untiled on main exactly when an area's code is missing
  from the layout, and withheldUKAreaCodes then already lists none, so
  nothing drawn changes. It takes the adapters' UKAreaChoroplethDataPoint.
- The four v2 charts and v3 take main's placeUKHexMap call; v3's comment
  says the map leaves off areas it cannot place instead of keeping the
  API's positions.
- HexagonalMap keeps main's screen-pixel and zoom stripe spacing with
  this branch's tiled hexagon size, scale times HEX_RADIUS.
- withheldUKHexAreas' comment keeps this branch's reason a result the
  layout cannot fully match lists no withheld areas.
- Tests: placeUKHexMap's tests no longer check a tiled flag, and expect
  an area the layout cannot place to be left out rather than every area
  kept at its own position; main's two new stripe tests drop the removed
  tiled config; the Ards and North Down breakdown fixture loses its x
  and y.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

2 active deployments
Preview – policyengine-website — 867af2c1 Deployed Oct 27, 2026 by vercel[bot]
Preview – policyengine-calculator-next — 867af2c1 Deployed Oct 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Show UK areas withheld by disclosure control on the hex maps as not reported v2's UK hex maps place areas differently from v3's

2 participants