diff --git a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx index 75a18f27a..af7b0ceeb 100644 --- a/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx +++ b/nextjs/src/app/reports/[id]/(components)/MapLayers/PoliticalChoropleths.tsx @@ -53,7 +53,7 @@ const PoliticalChoropleths: React.FC = ({ >({}) const [, setMapZoom] = useMapZoom() - const activeTileset = useActiveTileset(boundaryType) + const activeTileset = useActiveTileset(boundaryType, view) const { data } = useDataByBoundary({ view, tileset: activeTileset, @@ -110,126 +110,135 @@ const PoliticalChoropleths: React.FC = ({ } }, [map.loaded, activeTileset, data, view.mapOptions]) + const [zoom, _] = useMapZoom() + if (!map.loaded) return null if (!tilesets) return null return ( <> - {tilesets.map((tileset) => ( - - - {/* Fill of the boundary */} - - {/* Border of the boundary */} - - {/* Border of the selected boundary */} - - - - { + const isActiveTileset = + activeTileset.analyticalAreaType === tileset.analyticalAreaType + return ( + + + {/* Fill of the boundary */} + + {/* Border of the boundary */} + + {/* Border of the selected boundary */} + + + + type="geojson" + data={getAreaGeoJSON(dataByBoundary)} + // minzoom={tileset.minZoom} + // maxzoom={tileset.maxZoom} + > + - - - - ))} + + + + ) + })} ) } diff --git a/nextjs/src/app/reports/[id]/(components)/MapLayers/ReportMapChoroplethLegend.tsx b/nextjs/src/app/reports/[id]/(components)/MapLayers/ReportMapChoroplethLegend.tsx index e70233dde..802e4e6f4 100644 --- a/nextjs/src/app/reports/[id]/(components)/MapLayers/ReportMapChoroplethLegend.tsx +++ b/nextjs/src/app/reports/[id]/(components)/MapLayers/ReportMapChoroplethLegend.tsx @@ -54,7 +54,8 @@ export default function ReportMapChoroplethLegend() { ) const activeTileset = useActiveTileset( - viewManager.currentViewOfType?.mapOptions.choropleth.boundaryType + viewManager.currentViewOfType?.mapOptions.choropleth.boundaryType, + viewManager.currentViewOfType ) const { loading, data } = useDataByBoundary({ @@ -316,6 +317,8 @@ export default function ReportMapChoroplethLegend() { onChange={(d) => { viewManager.updateView((draft) => { draft.mapOptions.choropleth.boundaryType = d as BoundaryType + // unset the analytical area type + // delete draft.mapOptions.choropleth.lockedOnAnalyticalAreaType }) }} /> diff --git a/nextjs/src/app/reports/[id]/(components)/MapView.tsx b/nextjs/src/app/reports/[id]/(components)/MapView.tsx index e201e6595..e579fc4a1 100644 --- a/nextjs/src/app/reports/[id]/(components)/MapView.tsx +++ b/nextjs/src/app/reports/[id]/(components)/MapView.tsx @@ -25,7 +25,7 @@ export default function MapView({ (boundary) => boundary.boundaryType === boundaryType )?.tilesets - const activeTileset = useActiveTileset(boundaryType) + const activeTileset = useActiveTileset(boundaryType, mapView) const { loading, fetchMore } = useDataByBoundary({ view: mapView, diff --git a/nextjs/src/app/reports/[id]/mapboxStyles.ts b/nextjs/src/app/reports/[id]/mapboxStyles.ts index 3165e180d..e807e834a 100644 --- a/nextjs/src/app/reports/[id]/mapboxStyles.ts +++ b/nextjs/src/app/reports/[id]/mapboxStyles.ts @@ -64,7 +64,8 @@ export function getChoroplethFill( } export function getChoroplethEdge( - visible?: boolean + visible?: boolean, + isFocused?: boolean ): LineLayerSpecification['paint'] { return { 'line-color': 'white', @@ -76,10 +77,10 @@ export function getChoroplethEdge( ['zoom'], // 8, - 0.3, + isFocused ? 0.3 : 0.1, // 12, - 1, + isFocused ? 1 : 0.5, ] : 0, 'line-width': [ @@ -88,10 +89,10 @@ export function getChoroplethEdge( ['zoom'], // 8, - 0.3, + isFocused ? 0.5 : 0.1, // 12, - 2, + isFocused ? 2.5 : 1, ], } } diff --git a/nextjs/src/app/reports/[id]/politicalTilesets.ts b/nextjs/src/app/reports/[id]/politicalTilesets.ts index 4d1470e64..539c2655e 100644 --- a/nextjs/src/app/reports/[id]/politicalTilesets.ts +++ b/nextjs/src/app/reports/[id]/politicalTilesets.ts @@ -42,43 +42,9 @@ export type PoliticalTileset = { tilesets: Tileset[] } -const MAX_VALID_ZOOM = 24 +export const MAX_VALID_ZOOM = 24 const uk: PoliticalTileset[] = [ - { - label: 'Parliamentary Constituencies', - boundaryType: BoundaryType.PARLIAMENTARY_CONSTITUENCIES, - tilesets: [ - { - analyticalAreaType: AnalyticalAreaType.ParliamentaryConstituency_2024, - name: 'Constituencies', - singular: 'constituency', - mapboxSourceId: 'commonknowledge.bhg1h3hj', - sourceLayerId: 'uk_cons_2025', - promoteId: 'gss_code', - labelId: 'name', - minZoom: 0, - maxZoom: MAX_VALID_ZOOM, - }, - ], - }, - { - label: 'Wards', - boundaryType: BoundaryType.WARDS, - tilesets: [ - { - analyticalAreaType: AnalyticalAreaType.AdminWard, - name: 'Wards', - singular: 'ward', - mapboxSourceId: 'commonknowledge.3s92t1yc', - sourceLayerId: 'converted_uk_wards_2025', - promoteId: 'WD24CD', - labelId: 'WD24NM', - minZoom: 0, - maxZoom: MAX_VALID_ZOOM, - }, - ], - }, // { // boundaryType: AnalyticalAreaType.Country, // label: 'Countries', @@ -108,6 +74,34 @@ const uk: PoliticalTileset[] = [ }, ], }, + { + label: 'Parliamentary Constituencies', + boundaryType: BoundaryType.PARLIAMENTARY_CONSTITUENCIES, + tilesets: [ + { + analyticalAreaType: AnalyticalAreaType.ParliamentaryConstituency_2024, + name: 'Constituencies', + singular: 'constituency', + mapboxSourceId: 'commonknowledge.bhg1h3hj', + sourceLayerId: 'uk_cons_2025', + promoteId: 'gss_code', + labelId: 'name', + minZoom: 0, + maxZoom: 10, + }, + { + analyticalAreaType: AnalyticalAreaType.AdminWard, + name: 'Wards', + singular: 'ward', + mapboxSourceId: 'commonknowledge.3s92t1yc', + sourceLayerId: 'converted_uk_wards_2025', + promoteId: 'WD24CD', + labelId: 'WD24NM', + minZoom: 10, + maxZoom: MAX_VALID_ZOOM, + }, + ], + }, { label: 'Local Authority Districts', boundaryType: BoundaryType.LOCAL_AUTHORITIES, @@ -121,6 +115,34 @@ const uk: PoliticalTileset[] = [ promoteId: 'LAD23CD', labelId: 'LAD23NM', minZoom: 0, + maxZoom: 10, + }, + { + analyticalAreaType: AnalyticalAreaType.AdminWard, + name: 'Wards', + singular: 'ward', + mapboxSourceId: 'commonknowledge.3s92t1yc', + sourceLayerId: 'converted_uk_wards_2025', + promoteId: 'WD24CD', + labelId: 'WD24NM', + minZoom: 10, + maxZoom: MAX_VALID_ZOOM, + }, + ], + }, + { + label: 'Wards', + boundaryType: BoundaryType.WARDS, + tilesets: [ + { + analyticalAreaType: AnalyticalAreaType.AdminWard, + name: 'Wards', + singular: 'ward', + mapboxSourceId: 'commonknowledge.3s92t1yc', + sourceLayerId: 'converted_uk_wards_2025', + promoteId: 'WD24CD', + labelId: 'WD24NM', + minZoom: 10, maxZoom: MAX_VALID_ZOOM, }, ], @@ -150,7 +172,7 @@ const uk: PoliticalTileset[] = [ promoteId: 'LSOA21CD', labelId: 'LSOA21NM', minZoom: 10, - maxZoom: 13, + maxZoom: 15, useBoundsInDataQuery: true, }, { @@ -161,7 +183,7 @@ const uk: PoliticalTileset[] = [ sourceLayerId: 'output_areas_latlng-8qk00p', promoteId: 'OA21CD', labelId: 'OA21CD', - minZoom: 13, + minZoom: 15, maxZoom: MAX_VALID_ZOOM, useBoundsInDataQuery: true, }, diff --git a/nextjs/src/app/reports/[id]/reportContext.ts b/nextjs/src/app/reports/[id]/reportContext.ts index bf8f59159..30f92a07c 100644 --- a/nextjs/src/app/reports/[id]/reportContext.ts +++ b/nextjs/src/app/reports/[id]/reportContext.ts @@ -1,4 +1,5 @@ import { + AnalyticalAreaType, AreaQueryMode, ChoroplethMode, DataSourceType, @@ -160,7 +161,12 @@ const mapOptionsSchema = z.object({ .object({ boundaryType: z .nativeEnum(BoundaryType) - .default(BoundaryType.PARLIAMENTARY_CONSTITUENCIES), + .default(BoundaryType.PARLIAMENTARY_CONSTITUENCIES) + .describe('Boundary hierarchy'), + lockedOnAnalyticalAreaType: z + .nativeEnum(AnalyticalAreaType) + .optional() + .describe('Specific boundary'), palette: z.nativeEnum(Palette).default(Palette.Inferno), isPaletteReversed: z.boolean().optional(), layerId: z.string().uuid().optional(), @@ -170,6 +176,22 @@ const mapOptionsSchema = z.object({ }) .optional() .default({}), + // TODO: Validate lockOn + // .transform((data) => { + // // Ensure that analyticalAreaType is a valid key for the boundaryType + // const boundaryHierarchy = POLITICAL_BOUNDARIES.find( + // (b) => b.boundaryType === data.boundaryType + // ) + // if ( + // boundaryHierarchy && + // !boundaryHierarchy.tilesets.some( + // (t) => t.analyticalAreaType === data.analyticalAreaType + // ) + // ) { + // data.analyticalAreaType = undefined + // } + // return data + // }), display: z .object({ choropleth: z.boolean().default(true), diff --git a/nextjs/src/app/reports/[id]/useAreasList.ts b/nextjs/src/app/reports/[id]/useAreasList.ts index 699ea7a69..ec9fdf854 100644 --- a/nextjs/src/app/reports/[id]/useAreasList.ts +++ b/nextjs/src/app/reports/[id]/useAreasList.ts @@ -1,6 +1,8 @@ import { useActiveTileset, useLoadedMap } from '@/lib/map' +import { useView } from '@/lib/map/useView' import { useEffect, useState } from 'react' import { BoundaryType } from './politicalTilesets' +import { ViewType } from './reportContext' interface Area { gss: string @@ -10,7 +12,8 @@ interface Area { export function useAreasList(boundaryType: BoundaryType | undefined) { const map = useLoadedMap() const [areas, setAreas] = useState([]) - const tileset = useActiveTileset(boundaryType) + const view = useView(ViewType.Map) + const tileset = useActiveTileset(boundaryType, view.currentViewOfType) useEffect(() => { if (!map.loaded || !tileset || !map.loadedMap) return diff --git a/nextjs/src/lib/map/state.ts b/nextjs/src/lib/map/state.ts index e44cd32de..aa1e2226d 100644 --- a/nextjs/src/lib/map/state.ts +++ b/nextjs/src/lib/map/state.ts @@ -14,6 +14,7 @@ import { RecordGeometryQuery, RecordGeometryQueryVariables, } from '@/__generated__/graphql' +import { SpecificViewConfig, ViewType } from '@/app/reports/[id]/reportContext' import { gql, useApolloClient } from '@apollo/client' import { useLoadedMap } from '.' import { createNuqsParserFromZodResolver } from '../parsers' @@ -194,13 +195,26 @@ export function useMapZoom() { return useAtom(zoomAtom) } -export function useActiveTileset(boundaryType: BoundaryType | undefined) { +export function useActiveTileset( + boundaryType: BoundaryType | undefined, + view?: SpecificViewConfig +) { const [zoom] = useMapZoom() const politicalTileset = POLITICAL_BOUNDARIES.find((t) => t.boundaryType === boundaryType) || POLITICAL_BOUNDARIES[0] + if (view?.mapOptions.choropleth.lockedOnAnalyticalAreaType) { + return ( + politicalTileset.tilesets.find( + (t) => + t.analyticalAreaType === + view!.mapOptions.choropleth.lockedOnAnalyticalAreaType + ) || politicalTileset.tilesets[0] + ) + } + const tileset = politicalTileset.tilesets.filter( (t) => zoom >= t.minZoom && zoom <= t.maxZoom )[0]