Behnam Analytics

Work Healthcare BI

Swansea and Neath Port Talbot boundary map

An interactive LSOA and MSOA map built from ONS Open Geography, with a fetch script that refuses truncated or failed downloads.

LSOAs mapped
240
MSOAs mapped
48
Boundary data shipped
536 KB

Local health and care reporting keeps landing on the same question: where. Deprivation, emergency admissions, screening uptake, and service demand are all published by small-area geography, and people have to see those areas before a table of codes means anything to them.

This project is the geography layer for the Swansea Bay area: every 2021 Census Lower layer Super Output Area (LSOA) and Middle layer Super Output Area (MSOA) in the City and County of Swansea and in Neath Port Talbot, on a map you can open here.

What it does

  • Draws 240 LSOAs (150 Swansea, 90 Neath Port Talbot) and 48 MSOAs (30 and 18) on an OpenStreetMap basemap.
  • Lets you toggle either layer and either local authority.
  • Shows the area name and ONS code when you click a shape, which is the join key you need to attach any published statistic.

How the data gets there

A small Python script queries the ONS Open Geography ArcGIS service for the December 2021 generalised (BGC) boundaries, filters to the two authorities by area name, and writes two slim GeoJSON files.

Three decisions keep those files small and trustworthy:

  1. Only the fields the map needs. Code, name, and a derived local authority label. Everything else is dropped.
  2. Coordinates rounded to five decimal places, roughly a metre on the ground, which is finer than the generalised boundaries themselves, so nothing visible is lost.
  3. Fail loudly. ArcGIS reports errors and truncated results inside an HTTP 200 response. The script checks for an error body, for exceededTransferLimit, and for an empty feature list, and stops rather than writing a partial map.
def check_response(raw: dict, url: str) -> None:
    """ArcGIS reports errors and truncation inside an HTTP 200 body."""
    if "error" in raw:
        raise RuntimeError(f"ArcGIS error from {url}: {raw['error']}")
    if raw.get("exceededTransferLimit"):
        raise RuntimeError(f"ArcGIS truncated the result from {url}; paginate with resultOffset")
    if not raw.get("features"):
        raise RuntimeError(f"ArcGIS returned no features from {url}")

That last check exists because the first version of the script trusted the status code. A silently truncated response would have produced a map with missing neighbourhoods and no error anywhere.

The map page

The page is static HTML with Leaflet and the two GeoJSON files, served by the same nginx container as the rest of this site. No framework, no tile server of my own, no tracking. The only third-party request is the OpenStreetMap basemap, which the privacy page names.

Two details took more care than expected:

  • Click targets. MSOAs contain LSOAs and sit on top of them. In Leaflet even an invisible fill takes clicks, so while the LSOA layer is on, MSOAs are drawn with no fill at all and a click inside one reaches the LSOA underneath. Turn LSOAs off and the MSOAs become clickable again.
  • The detail panel is built with DOM methods, not innerHTML. Area names come from an external service. Treating them as text rather than markup closes off an injection route at no cost.

What I’d do next

  • Join a published indicator, such as the Welsh Index of Multiple Deprivation, to colour the LSOAs, with a legend and a data table view.
  • Add a search box for postcode or area name.
  • Publish the slim GeoJSON as a download for anyone building the same view in Power BI’s shape map.

Boundaries are ONS Open Geography, Open Government Licence v3.0; contains OS data © Crown copyright and database right.

Built with

  • Python
  • ArcGIS REST
  • GeoJSON
  • Leaflet
  • OpenStreetMap