When a user drags a slider, UrbanViable does not call an API, re-fetch tiles, or run a JavaScript loop over features. Instead, theDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/danizd/urbanviable/llms.txt
Use this file to discover all available pages before exploring further.
useMapStyle hook assembles a MapLibre GL JS paint expression — a JSON array of operators — and pushes it to the GPU in a single setPaintProperty call. The GPU then evaluates that expression independently for every visible census section on each frame, producing the colour ramp at up to 60 fps.
MapLibre GL JS expressions are pure JSON arrays evaluated in the WebGL shader pipeline. They have no access to JavaScript scope, closures, or React state. Weight values cannot be referenced by name — they must be embedded as numeric literals inside the expression array at the moment it is constructed. This is why useMapStyle rebuilds the entire expression object on every weight change rather than passing variable references.
Scoring Formula
score is always in [0, 1], regardless of how many sliders are active or what values they hold. A user who enables only one slider at 0.3 gets the same colour scale as a user who enables all seven — the relative emphasis of each active indicator is preserved.
useMapStyle Hook — Full Source
Expression Anatomy
The hook builds the expression in five steps:| Step | What happens |
|---|---|
| 1 — Filter active variables | vars.filter(v => weights[v] > 0) discards any slider set to 0 so it contributes nothing to the sum |
| 2 — Zero-weight fallback | If no variables are active totalWeight === 0, the layer is painted a flat rgba(80, 80, 100, 0.2) — uniform gray |
| 3 — Build weighted sum | Starting from the first active variable, each term is ['*', ['get', key], numericLiteral]; terms are chained with ['+', ...] |
| 4 — Normalise | The whole sum is wrapped in ['/', scoreExpr, totalWeight] when more than one variable is active, keeping the output in [0, 1] |
| 5 — Colour interpolation | An interpolate / linear expression maps the score to the colour ramp stops below |
Colour Ramp
| Score stop | Colour | Meaning |
|---|---|---|
0 | rgba(30, 30, 50, 0.6) | No score — near-invisible dark blue-gray |
0.25 | rgba(50, 120, 220, 0.8) | Low score — blue |
0.5 | rgba(255, 200, 0, 0.9) | Medium score — yellow-orange |
0.75 | rgba(255, 100, 0, 0.95) | High score — deep orange |
1.0 | rgba(220, 20, 60, 1.0) | Maximum score — red |
Applying the Expression
setPaintProperty replaces the GPU-side paint rule for the secciones-fill layer atomically. MapLibre recompiles the affected shader and re-renders all tiles on the next frame — the entire Galicia heatmap updates in under 16 ms on modern hardware without any data movement between CPU and GPU.
DEFAULT_WEIGHTS
constants/variables.js exports the initial slider positions loaded into React state on mount:
densidad_norm starts at 0.0 because population-density data is not yet available for all sections; its ScoreSlider is rendered in a disabled state.
VARIABLES Array Structure
Each entry in VARIABLES describes one indicator. Here is a representative entry:
key field is the string passed to ['get', key] inside the MapLibre expression — it must exactly match the property name baked into the GeoJSON features.
The layer IDs
'secciones-fill' and 'secciones-outline' in MapViewer.jsx must
match the layer IDs passed to map.getLayer() and map.setPaintProperty() in
useMapStyle. The current implementation uses a GeoJSON source — there is no
source-layer field. If you migrate to vector tiles generated by Tippecanoe, add
a source-layer field to each layer definition and ensure its value matches the
--layer argument used when building the MBTiles file.