Repository navigation
Conversation
…t area data National UK results from .py 6.10.0 on leave out constituency_impact and local_authority_impact (absent, or null). Their local areas come from a separate run of the same economy endpoint with breakdown=local_areas. - SocietyWideCalculationParams takes an optional breakdown, sent after the region and year only when it is set. - fetchLocalAreaBreakdown asks for that run and types its result as LocalAreaBreakdown, keyed by area name like the national lists. - The UK output type's area fields are optional and nullable, and hasUKLocalAreaImpacts says whether a result holds either map. - Area records carry the optional disclosure_tier and the 90% interval fields, which the normalizer copies from the list rows. Tests first: the new societyWideCalculation tests failed (9) because the breakdown parameter was dropped from the URL and fetchLocalAreaBreakdown and hasUKLocalAreaImpacts did not exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A UK report's constituency and local authority impacts, calculated on request by a separate run of the economy endpoint (breakdown=local_areas) for the report's region and year. - Nothing is sent until start(). The run is then polled every 2 s while it computes; retry() sends the request again after an error. - While it computes, the hook gives a progress estimate (the run's clock against an estimated duration, held inside the stage the API reports, and never moving backwards), the stage and the queue position. - The react-query key holds the country, both policies, the region and the year, so every view of a report shares one run. A view opened while another view's run computes, or after it completes, follows that run without sending its own request. Results stay for the session (staleTime and gcTime Infinity). - It calls fetchLocalAreaBreakdown directly, not CalcOrchestrator, so the report's saved national output is not overwritten. UK_LOCAL_AREA_BREAKDOWN_DURATION_MS is an unmeasured 3-minute estimate: no production breakdown runs exist yet. Tests first: before the hook existed the test file failed to import it; against an idle stub, 14 of the 16 tests failed on their assertions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel a UK report shows in place of its local-area maps until the breakdown is in, modelled on the v2 congressional district section (efc3914): - idle: a sentence saying the impacts come from a separate run that takes a few minutes, and the "Compute local-area impacts" button; - computing: "Computing local-area impacts…" with a progress bar, and the queue position or the API's stage when given; - error: a message and Retry; - complete: nothing, so the maps take its place. UKLocalAreaComputeButton is the same button on its own, for a v2 section header's right-hand slot; the panel can then leave it out. Tests first: before the component existed the test file failed to import it; against an empty stub, 6 of the 11 tests failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UKLocalAreaSection is the Local areas tab's content. A result that holds its area impacts (an older saved report) shows the maps at once, as before. A national result without them (.py 6.10.0 on) shows the compute panel in their place: its button sends one breakdown request for the report's region and year, progress shows while it runs, Retry follows an error, and the maps take the panel's place when the run completes. The hex layout comes with the results, so nothing is drawn before then. UKLocalAreaMaps is unchanged and still only draws what it is given; localAreaOutput puts a completed breakdown's areas into the result it draws. The overview's local areas card says the impacts are calculated on request when the results lack them. Tests first: the new ReportView tests failed (3: the card copy, and no compute button in the tab), and against a stub that rendered the maps alone, as ReportView did, 5 of the 6 UKLocalAreaSection tests failed. The saved-results tests passed before and after, as intended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "Constituency impact" and "Local authority impact" sections of MigrationSubPage, and the constituency and local-authority tabs, now handle a national UK result without area impacts (.py 6.10.0 on). - Sections: when the result has no area impacts, each opened section's header holds the "Compute local-area impacts" button (the CollapsibleSection right-hand slot), and its body holds the compute panel's description, progress or error. A click in either section sends the report's one breakdown request: the reform (simulations[1]) over the baseline (simulations[0]), for simulations[0].populationId and report.year. Both sections show the run's progress, and both show the maps when it completes, with no second request. - Tabs: ConstituencySubPage and LocalAuthoritySubPage take the report's policies, region and year from SocietyWideReportOutput. Without their level's impacts (missing or null), they show the compute panel instead of "not available for this region", and pass the computed areas to their charts. Without the request fields (outside the UK, or on one constituency or local authority) they still say it is not available. - Every view shares one run through useUKLocalAreaBreakdown's query key, so one click fills them all. The sections' hook lives in a UK-only child component, so US reports render as before. - Saved results that hold area impacts show them directly, with no button. hasConstituencyImpacts and hasLocalAuthorityImpacts check one level each, for the tabs; hasUKLocalAreaImpacts checks either, for the sections and v3. When a report has one simulation, the request falls back to the baseline as the reform, as the national run does. Tests first: 12 new tests failed (6 MigrationSubPage, 3 per subpage tab) because no compute button existed, and the 2 new SocietyWideReportOutput tests failed against the previous file because the tabs got only the output. The new chart tests for null and missing area data passed before this change: the charts already showed "No ... data available". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A local-area breakdown is disclosure-controlled: .py leaves out the areas
whose estimates rest on too few surveyed households or could reveal one
household's income, and marks those with a low effective sample as
approximate, whose 90% intervals understate the uncertainty.
- Under each map (v3 UKLocalAreaMaps; the four v2 constituency and local
authority charts), UKAreaDisclosureNotes says how many of the level's
areas were not published and why, and how many shown areas are
approximate. Saved results, which are not disclosure-controlled, get
no notes.
- Withheld areas are counted against the UK region list in the country
metadata (useRegions), and only where the two lists agree:
constituencies in UK-wide reports, whose 650 names API v1 and .py
share. Local authorities get the note without a count, because API v1
lists 360 and .py 361 ("Ards and North Down" is only in .py's list).
Nation reports get it without a count too, because the metadata does
not say which nation an area is in. A published name missing from the
list also drops the count.
- Map tooltips (v2 and v3, through generateHoverText) mark approximate
areas and give each value's 90% range; the hex points carry them from
the constituency and local authority adapters, and from UKLocalAreaMaps.
- The v3 area details (ukAreaDetailFigures) add both changes' 90% ranges
and an "Estimate: Approximate" row.
Tests first: 22 new tests failed (hover text, adapters, withheld counts,
v3 maps and details, v3 section, v2 tabs, charts and sections) because
none of this existed, and the UKAreaDisclosureNotes test file failed to
import the component. A further test, for a level the API returns as
null, failed with a TypeError before withheldUKAreasOf guarded it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
useUKLocalAreaBreakdown kept start() as one boolean, which stayed true when the query key changed. v3 keeps the Local areas tab mounted while a budget-window report switches year, so after one Compute click, opening another year sent that year's breakdown request at once, without a click. The hook now stores the hashed query key that start() or retry() was called for, and sends requests only for that key. A year already computed still shows its cached result without a click. Tests first: 3 new tests failed. In the hook, after start() and a switch to another year, the status was "complete" instead of "idle", and the request mock was called twice instead of once. In ReportView, the Compute button did not return after the year changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eakdown results - SocietyWideCalculationParams no longer has a breakdown field. Only fetchLocalAreaBreakdown sends breakdown=local_areas, so a breakdown's result can't be typed as national results. fetchSocietyWideCalculation also forwards only the region and year, so a breakdown passed in anyway is not sent. - useUKLocalAreaBreakdown accepts an ok result as a breakdown only when result.kind is "local_areas". Any other ok result, such as the national results a Public API deployment that ignores the parameter returns, becomes an error that says so. - The compute panel's error state now shows the error's reason under its message, above Retry. Tests first: 3 new tests failed. The national request sent breakdown when given one, the hook reported a national result as "complete" instead of "error", and the panel did not show the error's reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v2 sections and the v3 tab decided on "either level present", while the v2 tabs decided on their own level. So a saved result with constituencies but no local authority key offered Compute in the v2 local authority tab, but the v2 section and the v3 map just said there was no local authority data. Every view now decides on its own level: - localAreaOutput keeps each level the result holds and fills only the missing levels from a completed breakdown, so a saved level is never replaced. - breakdownLevelsWithheld gives withheld counts only for levels that came from the breakdown; a saved level gets no withheld note. - MigrationSubPage shows the button and the compute panel in a section only when that section's level is missing. - UKLocalAreaMaps takes optional missingLevelContent, shown in place of a level's map when the result does not hold that level at all (absent or null). A level computed with no areas left still says there is no data. UKLocalAreaSection passes the compute panel there. - withLocalAreaBreakdown, now unused, is removed. Tests first: 7 new tests failed (the per-level merge, the withheld filter, both v2 section cases, the v3 section case and the maps' slot). The two new v2 tab tests, which check that each tab agrees with its section, passed before: the tabs already decided per level. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v3 Local areas tab (UKLocalAreaSection), the v2 sections
(UKLocalAreaSections in MigrationSubPage) and the v2 tabs
(UKLocalAreaBreakdownView) each called the breakdown hook, merged its
areas into the result and counted the withheld areas. useUKLocalAreaOutput
now does all three and returns { breakdown, areaOutput, withheld }. It
reads the region metadata itself, so useUKWithheldAreas is removed.
The four v2 constituency and local authority charts now look up their
level's areas once, and build the disclosure notes element once for both
the empty and the map branches.
No behaviour change: the existing tests pass unchanged. A new
useUKLocalAreaOutput test covers a result without areas and a saved
level kept beside a computed one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stored v3 outputs from before December 2025 hold constituencies but no
local authorities, and outputs from before February 2025 hold neither.
The local-area views offered "Compute local-area impacts" for their
missing levels, and until the Public API serves breakdowns that request
gets a 400.
A missing level is now offered a breakdown only when the national result
came from a .py release that serves breakdowns (6.10.0 or later):
- servesLocalAreaBreakdowns compares a version with 6.10.0;
outputServesLocalAreaBreakdowns reads the result's policyengine_version,
which fetchSocietyWideCalculation fills from the response's
policyengine_bundle and saved results keep. A result with no version
counts as an older release.
- useUKLocalAreaOutput returns canCompute, and sends no request (even
with a cached run) when it is false.
- For an older or unversioned result, the v3 tab, the v2 sections and the
v2 tabs show what they showed before this PR ("No ... data available",
or "... not available for this region"), with no button, and the
overview card keeps its previous text.
6.10.0 national results hold no area levels, so a saved level can no
longer sit beside a breakdown level from another model and data
version.
Tests first: 24 tests failed. 15 were the new version helpers, which
did not exist yet. The other 9 were the v3 tab, the v2 sections and
tabs, ReportView and the hook, which still offered compute (or did not
show the old text) for older and unversioned results. The round-1 tests
for a result with one saved level now use a result from a breakdown
release.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed breakdown request reused the national request's error and log label: "Society-wide calculation failed (400): <raw body>", logged as [fetchSocietyWideCalculation]. fetchLocalAreaBreakdown's errors now read "Local-area breakdown failed (<status>): ..." and are logged as [fetchLocalAreaBreakdown]. When the error body is JSON with a "message" field, the error gives that message; otherwise it gives the body, or the status text when the body is empty. The national request's errors are unchanged. Tests first: the 2 new tests failed because the error still read "Society-wide calculation failed (...)". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LocalAreaBreakdownResponse's result is now LocalAreaBreakdown | SocietyWideReportOutput, because a Public API deployment that ignores breakdown=local_areas answers with national results. isLocalAreaBreakdown tells them apart, so the hook's check for national results is a case the compiler checks rather than a comparison on a field the type said was always "local_areas". Typing only, no behaviour change. The hook tests' mocks of the API module now keep its real exports, so they get isLocalAreaBreakdown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… stops
- The new UK tests in the MigrationSubPage and ReportView suites now have
Given, When and Then sections, as testing.md asks.
- Literal values moved into fixtures: the report title and source note,
the other budget-window year, the breakdown's local authority name,
and the withheld counts the notes are tested with.
- v2: a section and a tab each click Retry after a failed breakdown,
and the maps or charts then show, with a second request.
- The hook stops polling after an error, and when its view unmounts.
Test-only commit. The new tests passed on the first run, because they
cover existing behaviour. As a check, changing the hook to poll at
every interval made the after-error test fail ("expected 'computing' to
be 'error'"). The unmount test still passed with that change, since
react-query clears a removed observer's polling; it guards against
polling outside react-query later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Remove isUKSocietyWideOutput from the v2 report page. Nothing called it, and it identified UK results by a non-null constituency_impact, which national UK results from .py 6.10.0 on do not have. - Drop ReportLocalAreaRequest. UKLocalAreaBreakdownRequest's fields are now string or undefined, and UKReportLocalAreaRequest is that type without countryId, used by the adapter, useUKLocalAreaOutput and the v2 tab view. - UK_AREA_NOUNS holds each level's singular and plural names once, for UKLocalAreaMaps and UKAreaDisclosureNotes. No behaviour change; the tests pass unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
marked this pull request as ready for review
October 22, 2026 21:13
…ncies
The withheld note under a local-authority map now gives an exact count
in UK-wide reports ("2 of 361 local authorities are not shown."), as the
constituency note does. The count is the number of local authorities in
the country metadata's region list that the breakdown's
local_authority_impact list leaves out.
Local authorities were not counted before because the Public API listed
360 and .py 6.10.0 lists 361 (Ards and North Down was missing).
PolicyEngine/policyengine-api#4024 adds it, so the two lists name the
same 361 areas. Breakdowns are offered only for results from .py 6.10.0
on, which that deployment serves.
Both levels now follow one rule: a level gets a count only for a UK-wide
report, and only when every area the breakdown returned is in the
level's list. A breakdown that returns an unlisted area, for example
Ards and North Down against an earlier Public API deployment, gets the
note without a count. Nation reports still get no count: the metadata
gives each area's name, label and type, not its code or nation.
Tests: 9 failed before the change, each because local authorities got
null instead of a count (adapter, output hook, v3 Local areas tab, v2
local authority tab, and v2 sections).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Relates to #1361
Summary
From .py 6.10.0 on, a national UK result (
region=ukorcountry/*) no longer containsconstituency_impactorlocal_authority_impact: the keys are absent, ornull. Those impacts come from a separate run instead, which the app requests with the same economy endpoint plusbreakdown=local_areas:The response uses the usual
statusenvelope. When it isok,resultis{ kind: "local_areas", model_version, policyengine_version, dataset, data_version, constituency_impact: [...], local_authority_impact: [...] }. Its rows have the shape the normalizer already handles, plus an optionaldisclosure_tierand optional 90% bounds. Areas withheld by disclosure control are left out, and a list may be empty.What this PR changes, in v2 and v3:
UKLocalAreaSection, rendered byReportView).UKLocalAreaMapsstill only draws what it's given.MigrationSubPage(button in the section header's right-hand slot), and theconstituencyandlocal-authoritytabs (ConstituencySubPage,LocalAuthoritySubPage, wired fromSocietyWideReportOutput).useUKLocalAreaBreakdown's react-query key (country, both policies, region, year), so one click fills them all. Results stay for the browser session. The hook calls the endpoint directly, not throughCalcOrchestrator, so the report's saved national output isn't overwritten.policyengine_version. The app fills that field from the response'spolicyengine_bundle, and saved results keep it. A result from an older release, or with no version, shows its local areas as it did before this PR ("No … data available" or "… not available for this region" for a missing level), with no button and no request.Merge order
Merge after PolicyEngine/policyengine-sim-api#881 is deployed, and before the Public API PR that adds
breakdownand moves to .py 6.10.0.On its own, this change does nothing visible: the Public API still runs .py 6.7.5, whose UK results contain area data, so no Compute button appears. The buttons appear for new UK reports once the Public API moves to 6.10.0.
Commits
Each commit was written test-first: the tests were added, run and seen failing for the expected reason, then the code was written, and both went into the same commit.
9a58e92a)breakdownquery parameter,fetchLocalAreaBreakdown,LocalAreaBreakdowntype, area fields made optional and nullable,hasUKLocalAreaImpacts, disclosure fields copied by the normalizer.societyWideCalculationtests failed.breakdownwas dropped from the URL, andfetchLocalAreaBreakdownandhasUKLocalAreaImpactsdidn't exist.f7a7aaef)8a132622)952d5f52)ReportViewtests failed (the card text, and no compute button in the tab). Against a stub that rendered the maps alone, asReportViewdid before, 5 of 6UKLocalAreaSectiontests failed.f38064f4)MigrationSubPage, 3 per tab), and 2SocietyWideReportOutputtests failed against the previous file.nulland missing area data passed before the change: the charts already showed "No … data available".7934ed97)UKAreaDisclosureNotestest file couldn't import the component.null, failed with aTypeErroruntilwithheldUKAreasOfwas guarded.Fixes from the first code review:
199b54db)start()orretry()was called for.b549d95d)SocietyWideCalculationParamsno longer hasbreakdown; onlyfetchLocalAreaBreakdownsends it.fetchSocietyWideCalculationforwards only the region and year.okresult counts as a breakdown only whenresult.kind === "local_areas". Otherwise it's an error that says the API returned national results, for example from a Public API deployment that ignores the parameter. The panel shows the error's reason above Retry.breakdownwhen given one, the hook reported a national result as "complete", and the panel didn't show the reason.c92f31c0)UKLocalAreaMapstakes an optionalmissingLevelContent, shown when the selected level is absent ornull. A level computed with no areas left still says there is no data.254a933e)useUKLocalAreaOutputreturns{ breakdown, areaOutput, withheld }for the v3 tab, the v2 sections and the v2 tabs, and replacesuseUKWithheldAreas.Fixes from the second code review:
bba136fc)servesLocalAreaBreakdownscompares the result'spolicyengine_versionwith 6.10.0. Older and unversioned results show what they showed before this PR, in v3 and in the v2 sections and tabs, with no button and no request.b466e826)[fetchLocalAreaBreakdown]. They give the error body'smessagefield when there is one.6abd5ec9)LocalAreaBreakdownResponse's result isLocalAreaBreakdown | SocietyWideReportOutput.isLocalAreaBreakdownmakes the national-result check a compiler-checked case. Typing only.d995306e)a4ec6750)isUKSocietyWideOutputis removed.UKReportLocalAreaRequestis derived fromUKLocalAreaBreakdownRequest.UK_AREA_NOUNSdefines the area names once. No behaviour change.Withheld local-authority counts:
37d24dad)local_authority_impactlist leaves out.countWithheldUKAreas: a count only for a UK-wide report, and only when every area the breakdown returned is in the level's list. Otherwise the note has no count, as before.nullinstead of a count (the adapter,useUKLocalAreaOutput, the v3 Local areas tab, the v2 local authority tab and the v2 sections).Withheld-area counts
Withheld areas are counted by comparing the breakdown's list with the region list in the country metadata (
useRegions('uk')). Both levels follow one rule (countWithheldUKAreas): a level gets a count only for a UK-wide report, and only when every area the breakdown returned is in that level's list.country/*): the note is shown without a count, for both levels. The metadata gives onlyname,labelandtypefor each area, not its code or nation, so the app can't pick out a nation's areas from the list. The breakdown's rows carry ONS codes, but those cover only the published areas, not the full list.Differences from the plan
hasConstituencyImpacts,hasLocalAuthorityImpacts), since commit 9. Since commit 11, a missing level is offered the breakdown only for results from .py 6.10.0 on. Those results have no saved levels, so a saved level never sits beside a breakdown level from another model or data version.hasUKLocalAreaImpacts(either level): it says the impacts are calculated on request when neither level is saved and the result is from .py 6.10.0 on.SocietyWideCalculationParamshas nobreakdownfield at all, rather thanfetchSocietyWideCalculationtakingOmit<SocietyWideCalculationParams, 'breakdown'>. The effect is the same, and the existing callers are unchanged.SocietyWideCalcStrategy). The plan namedsimulations[1]only.UK_LOCAL_AREA_BREAKDOWN_DURATION_MS, 3 minutes) is unmeasured, because no production breakdown runs exist yet.data-area-id(they don't passonSelect), so the v2 tests check the hovered hexagon's tooltip instead.Verification
All on Node 26.10.0, the version CI uses:
make format,make lintandmake typecheck: clean.make teston the final commit (37d24dad):make format,make lint,make typecheckandmake testalso passed on each review-fix commit, and ona4ec6750(app: 5,667 passed, 10 skipped).NavItem.test.tsx(the header dropdown, which this PR doesn't touch). It passed alone and on the immediate full rerun.Not done here: a local build against staging. That needs the Public API's staging deployment with
breakdown, which isn't available yet.Environment variables
None added, removed or changed. Every change is under
app/src; no CI/CD workflow, Docker image, Terraform or deployment configuration is touched. The breakdown request goes to the existingBASE_URL(fromapp/src/constants.ts), which reads the existingVITE_API_URLand defaults tohttps://api.policyengine.org. This PR doesn't change it.🤖 Generated with Claude Code