name: learn-map-layers description: Layer stack, sources, map types (district vs COI), style expressions, shatter filters user-invocable: false
Map Layers
Map layer rendering architecture: layer stack, source configuration, map type rendering differences (district vs community/COI), style expressions, shatter filters, overlays, and basemap composition.
When To Use
- You are adding, removing, or reordering map layers.
- You are changing how zones or communities are colored or styled.
- You are modifying tile source configuration or PMTiles loading.
- You are changing basemap, overlay, or county layer behavior.
- You are working on community (COI) vs district rendering differences.
- You are changing shatter-related layer filtering.
Map Types
The app supports three map types (DistrictrMap.map_type): "default", "local", and "community".
| Aspect | District (default/local) |
Community (community) |
|---|---|---|
| Route | /map/edit/[map_id] |
/coi/edit/[document_id] |
| Page component | MapPage -> MainMap |
CoiMapPage -> CoiMap |
| Layer component | BlockLayers -> ZoneLayerGroup |
CoiBlockLayers -> CoiAssignmentLayers |
| Assignment store | assignmentsStore |
coiAssignmentsStore |
| Default basemap | MINIMAL | STREETS |
| Zone numbers | Shown | Hidden |
| Feature-state key | zone (1-indexed integer) |
community + per-community flags (community_1, community_2, ...) |
| Color source | Color scheme array -> zone index | Per-community color from community.color |
| Visibility control | Global showPaintedDistricts |
Per-community communityVisibility map |
| Render ordering | Single layer per scope | One layer per community, selected community on top |
Mode initialization
Map mode ('districts' | 'coi') is set via useInitializeMapMode hook, which applies mode-specific defaults from mapModeDefaults.ts before document loading begins.
Canonical Files
Layer definitions & ordering
app/src/app/constants/map/layerIds.ts- canonical layer ID constants (BLOCK,ZONE_LABELS,OVERLAY,COUNTIES)app/src/app/constants/map/layerRenderConfig.ts- anchor layer order and defaultbeforeIdmappings
Style expressions
app/src/app/constants/map/layerStyle.ts-ZONE_ASSIGNMENT_STYLE,COMMUNITY_ASSIGNMENT_STYLE,getLayerFill, basemap IDs, opacity constantsapp/src/app/constants/map/overlayLayerStyles.ts- overlay-specific styling
Sources
app/src/app/components/Map/GeoSources/BlockSource.tsx- PMTiles vector source (blocks), registerspmtiles://protocolapp/src/app/components/Map/GeoSources/PointSource.tsx- selection point sources (parent + child)
District layer components
app/src/app/components/Map/PolygonLayers/BlockLayers.tsx- orchestrates parent/childZoneLayerGroupinstancesapp/src/app/components/Map/PolygonLayers/ZoneLayers/ZoneLayerGroup.tsx- composes assignment + highlight + hover layersapp/src/app/components/Map/PolygonLayers/ZoneLayers/ZoneAssignmentLayer.tsx- fill layer with zone color expressionapp/src/app/components/Map/PolygonLayers/ZoneLayers/ZoneHighlightLayer.tsx- outline layer for focus/highlight/broken states
Community (COI) layer components
app/src/app/components/Map/PolygonLayers/CoiBlockLayers.tsx- orchestrates per-community layers with visibility + render orderapp/src/app/components/Map/PolygonLayers/CoiAssignmentLayers.tsx- creates one layer per community, manages selected-on-top orderingapp/src/app/components/Map/PolygonLayers/CoiAssignmentLayer.tsx- individual community fill layer
Map containers
app/src/app/components/Map/MainMap.tsx- district map shell (usesBlockLayers)app/src/app/components/Map/CoiMap.tsx- community map shell (usesCoiBlockLayers)app/src/app/components/Map/MapContainer.tsx- shared map shell (events, basemap, locking, cursor)
Supporting
app/src/app/components/Map/MapLayerAnchors.tsx- creates invisible anchor layers that define render orderapp/src/app/components/Map/PolygonLayers/CountyLayers.tsx- county boundary + label layersapp/src/app/components/Map/PolygonLayers/OverlayLayers.tsx- user-provided overlay layersapp/src/app/hooks/useLayerFilter.ts- shatter-aware layer filter expressionsapp/src/app/hooks/useInitializeMapMode.ts- mode initialization hookapp/src/app/constants/map/mapModeDefaults.ts- per-mode default optionsapp/src/app/constants/map/mapDefaults.ts- numeric limits (districts, communities)app/src/app/utils/map/mapRenderSubs.ts- render subscriber that applies feature-state to layers
Layer Stack
Layers are ordered via invisible anchor layers created by MapLayerAnchors. From top to bottom:
anchor-hover <- Hover/tooltip layers
anchor-overlays <- User overlay layers
anchor-demography <- Demographic choropleth
anchor-assignments <- Zone/community fill + highlight layers
anchor-geometry-outline <- Geometry outlines
anchor-counties <- County boundaries + labels
[basemap layers] <- Basemap (MINIMAL, STREETS, SATELLITE)
Block layers (both parent and child scopes) position themselves relative to these anchors via DEFAULT_BLOCK_LAYER_ORDER:
- Background fill ->
anchor-assignments - Zone/community fill ->
anchor-assignments - Demography fill ->
anchor-demography - Hover layer ->
anchor-hover - Outline layer ->
anchor-geometry-outline
Tile Source Configuration
All map geometries come from a single PMTiles vector source:
- Source ID:
'blocks'(constant:CANONICAL_LAYER_IDS.SOURCES.BLOCK) - URL pattern:
pmtiles://{TILESET_URL}/{mapDocument.tiles_s3_path} - Feature ID property:
promoteId="path"- thepathproperty becomes the feature ID forsetFeatureState - Source layers: A single PMTiles file may contain multiple source-layers:
mapDocument.parent_layer- parent geography (e.g., VTDs, precincts)mapDocument.child_layer- child geography for shatter (e.g., census blocks), nullable
Style Expressions
Zone coloring (districts)
ZONE_ASSIGNMENT_STYLE(colorScheme) builds a case expression:
['case',
['==', ['feature-state', 'zone'], 1], colorScheme[0],
['==', ['feature-state', 'zone'], 2], colorScheme[1],
...
'#cecece'] // fallback for unassigned
Community coloring (COI)
Each community gets its own layer with a single fill color. Membership is determined by the feature-state flag community_{id}. The COMMUNITY_ASSIGNMENT_STYLE builds a similar case expression but is used for the shared rendering path.
Fill opacity
getLayerFill(captiveIds?, isDemographic?) builds a case expression controlling opacity:
broken: true-> 0 (hidden shattered parent)- Assigned + hovered -> base + 0.3
- Assigned -> base + 0.1
- Unassigned -> 0
Highlight/focus outlines
ZoneHighlightLayer uses feature-state to control outline color and width:
focused: true-> black, 3.5pxhighlighted: true-> yellow (#e5ff00), 3.5px- Unassigned (when highlight enabled) -> red, 3.5px
Shatter Layer Filtering
Parent and child scopes share the same vector source but use different source-layers and filters:
- Parent layer filter: Excludes shattered parent IDs ->
['!', ['match', ['get', 'path'], [...parentIds], true, false]] - Child layer filter: Includes only child IDs ->
['match', ['get', 'path'], [...childIds], true, false]
When a parent is shattered:
- Parent feature-state gets
broken: true(hides via opacity expression) - Parent ID is added to the exclusion filter
- Child features appear via the inclusion filter
- Assignments transfer from parent to children
Filter construction lives in useLayerFilter(child: boolean).
Basemaps
Three basemap options defined in BASEMAP_IDS:
MINIMAL- default for district modeSTREETS- default for community modeSATELLITE- available in both modes
Basemap switching is handled in MapContainer via the map style URL.
Overlays
Overlay layers are positioned at anchor-overlays and support both PMTiles and GeoJSON sources. Overlay constraints can restrict painting (managed by overlayStore). Layer IDs use the OVERLAY prefix constants.
Hard Invariants
- Layer ordering must be maintained via anchor layers (
layerRenderConfig.ts). Never use hardcodedbeforeIdvalues that bypass the anchor system. - The
blockssource ID andpromoteId="path"are load-bearing contracts - feature-state, filters, and event queries all depend on them. - District mode uses a single layer per scope (parent/child). COI mode uses one layer per community per scope. Do not conflate these patterns.
- Community layers must maintain render-order sorting with the selected community on top.
- Shatter filter expressions must stay in sync with
shatterIdsin the assignment store. A mismatch causes ghost features or missing geometry. parent_layerandchild_layersource-layer names come frommapDocument(set during map creation). Never hardcode source-layer names.- Feature-state keys differ by mode:
zonefor districts,community+community_{id}flags for COI. Layer components must use the correct key for their mode. - Basemap defaults are mode-dependent (MINIMAL for districts, STREETS for COI). Preserve this mapping in
mapModeDefaults.ts.
Anti-Patterns
Creating zone color expressions that assume a fixed number of zones.
Mixing district and community feature-state keys in the same layer component.
Bypassing
useLayerFilterto build custom shatter filter expressions.Rendering community layers without respecting
communityVisibilitystate.
Change Checklist
- Verify layer ordering is correct by checking anchor layer positions.
- Test both district and community map types - they use different layer components and feature-state keys.
- Confirm shatter transitions: parent layers hide, child layers appear, no ghost features.
- Validate overlay layers render above assignments but below hover.
- Test basemap switching in both map modes.
- Verify community visibility toggling hides/shows individual community layers.
- Confirm zone coloring works across the full range of zones (up to 538 for districts, up to 8 for communities).
Validation Commands
cd app && bun run buildcd app && bun run ts- Manual flows: switch basemaps, toggle overlays, paint zones in district mode, paint communities in COI mode, shatter/heal, toggle community visibility.
See Also
- learn-map-runtime - interaction events, feature-state mutation, paint tools
- learn-frontend - store architecture, subscription model, worker offloading
- learn-state-sync - assignment persistence and conflict resolution
- learn-map-lifecycle - how maps and tilesets are created upstream
Common Failure Modes
- Ghost features from shatter filter mismatch (parent visible when it should be hidden, or child missing).
- Wrong colors in COI mode from using
zonefeature-state key instead ofcommunity_{id}. - Layer z-order bugs from adding layers without correct
beforeIdanchor. - Community layer ordering bugs from not sorting by render order or not bringing selected community to top.
- Basemap switch losing custom layers because they weren't re-added after style change.
- Overlay layers obscuring assignments due to incorrect anchor positioning.