UrbanViable’s map coloring is driven entirely by a weighted sum computed on the GPU for each of Galicia’s ~3,800 census sections. The formula combines up to seven pre-normalized variables, each multiplied by a user-controlled weight, and divides by the sum of active weights to keep the result in a fixedDocumentation 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.
[0, 1] range. The score is never transmitted over the network — it is computed in real time by MapLibre GL JS’s paint expression engine from properties already embedded in the vector tiles.
Core Formula
0.5 and one is turned off, the remaining variable would only contribute up to 0.5 — and the entire color ramp would compress into the bottom half of the gradient. Dividing by the sum of active weights re-normalizes the score to the full [0, 1] range regardless of how many sliders are active or what their individual values are. A single slider at any non-zero value produces a map that uses the full color ramp from gray to deep red.
Min-Max Normalization
All variables except commercial activity use standard Min-Max normalization:min and max are computed over all ~3,800 sections in Galicia — not over the currently visible viewport and not clipped to a single province. This means a section’s normalized value is stable regardless of zoom level or map position: a section that scores 0.72 on renta_norm will always score 0.72 whether the user is zoomed to Santiago de Compostela or viewing all of Galicia at once.
NaN treatment. Source datasets do not cover 100% of sections. The INE suppresses income data for sections with fewer than 100 households. IGE may lack population figures for very small sections. Before normalization, all NaN values are filled with 0. This means data-absent sections are treated as the minimum value — they appear at the cold end of the color ramp for that variable, which is conservative and visually honest.
Logarithmic Normalization for Activity
Theactividad_norm column uses a two-step normalization:
density_activity = actividad_abs / area_km².
The Python implementation:
Weight Semantics
Weights are dimensionless values in[0, 1] controlled by the sidebar sliders. They represent the relative importance the user assigns to each variable when scouting a location.
Default weights (from src/constants/variables.js):
- A weight of
0excludes the variable entirely from the score. The variable’s_normcolumn is ignored in the sum and its weight does not count toward the denominator. Settingdensidad_norm: 0.0means density has no influence on the map, as in the default configuration. - All weights at
0→totalWeight === 0→ the formula is undefined.useMapStylehandles this by applying a flat neutral color (rgba(80, 80, 100, 0.2)) rather than attempting a division by zero. The map appears uniformly gray. - Slider values are not automatically normalized to sum to 1. The user can set all sliders to
1.0or any arbitrary combination. The division in the formula absorbs whatever the total weight happens to be.
MapLibre GL JS Expression
MapLibre paint expressions are JSON arrays evaluated by the GPU. They have no access to the JavaScript scope — weights must be embedded as numeric literals at the timesetPaintProperty is called.
The useMapStyle hook in src/hooks/useMapStyle.js constructs the expression dynamically from the current weights object:
scoreExpr is then passed to the color interpolation ramp:
Why numeric literals matter. When
setPaintProperty is called, MapLibre serializes the expression array to JSON and sends it to the WebGL shader. At that point, there is no JavaScript runtime — weights.renta_norm would serialize as undefined. The hook therefore reads each weight value from the weights object before building the array, embedding the resolved number directly. This is why the expression must be rebuilt from scratch on every slider change rather than referencing a live JS variable.Data Quality and Scoring Behavior
Sections that are missing data for a variable receive0.0 for that _norm column (per the NaN fill described in the normalization section above). This has a predictable and documented effect on scoring:
- A section with
actividad_norm: 0.0because OSM data was unavailable will score the same as a section with genuinely zero commercial activity. - If
actividad_normhas a non-zero weight, these sections will appear cooler (bluer or grayer) on the map, which may understate their actual commercial potential.
0 for each variable, and warn if that percentage exceeds 20% for any column.