Every spatial dashboard rests on a small set of facts that engineers look up over and over: which EPSG code a region uses, which dtype keeps a column small, which index makes a spatial join finish before a user gives up waiting. These are not glamorous decisions, but they are the ones that decide whether a Streamlit or Panel application renders in 200 milliseconds or stalls, mis-aligns its layers, or gets killed by a container memory limit. This section is a working reference for those constants — the coordinate reference systems, the GeoDataFrame schema conventions, and the spatial index types that sit underneath the map.

The audience here is the engineer mid-task: you have a layer that renders in the wrong place, a GeoDataFrame that eats 2 GB it should not need, or a filter that takes four seconds per click. Rather than re-deriving the answer each time, this reference collects the correct values, explains why they matter for an interactive dashboard specifically, and links to the deep-dive pages that walk through each subsystem with runnable code.


Jump to heading How the data layer fits together

Three concerns interlock beneath the visible map. The CRS metadata on a GeoDataFrame decides where every geometry lands on screen and whether a measurement is meaningful. The schema — column names and dtypes — decides how much memory the frame occupies and how fast it serialises into the cache. The spatial index decides how quickly a filter, join, or point-in-polygon test resolves. Get any one wrong and the dashboard fails in a way the other two cannot compensate for.

The spatial dashboard data layerA raw spatial source feeds a GeoDataFrame in the data layer. Three reference concerns act on that frame: CRS metadata controls alignment and measurement, schema and dtypes control memory and serialisation, and the spatial index controls query speed. The tuned frame then flows to the map and filter widgets in the dashboard UI.Raw spatial source · PostGIS · GeoJSON · Parquet · Shapefilearrives with — or missing — a CRS, in mixed dtypes, unindexedGeoDataFrameCRS metadataEPSG:4326 · 3857 · 27700controls alignmentand measurementSchema & dtypescategory · float32 · WKBcontrols memoryand serialisationSpatial indexR-tree · grid · sindexcontrols queryspeedDashboard UI · map layers · filter widgets · tooltips

The three subsections below each own one of these concerns. Read them in the order your bug points to: alignment problems start with CRS, memory and out-of-memory kills start with schema, and slow interactions start with the index.


Jump to heading Why reference correctness pays off

A dashboard is unusually sensitive to reference errors because it re-runs its data path on every widget interaction. A mistake that a one-off analysis script would survive becomes a recurring tax paid on every click.

CRS mismatches misplace layers silently. When a base tile layer is in Web Mercator and an overlay is left in WGS 84, the overlay can render hundreds of metres — or entire continents — away from where it belongs, and nothing raises an error. The map simply looks wrong. Worse, a distance or area computed in a geographic CRS returns a number in degrees, not metres, which passes type checks and quietly corrupts every downstream aggregate. The CRS & coordinate systems reference documents which code to use for display, distance, and area, and how to detect the axis-order trap that flips latitude and longitude.

Wrong dtypes exhaust memory. A GeoDataFrame of half a million rows with a handful of object-typed string columns and float64 attributes can occupy several times the memory of the same data with category and float32 columns. In a containerised deployment that difference decides whether the pod survives, which is exactly the failure mode covered under memory limit management. The GeoDataFrame schema reference lists the dtype choices that keep frames compact without losing precision that matters.

Unindexed queries stall the UI. A spatial join or point-in-polygon filter without an index tests every geometry against every other, and the cost grows with the product of the two layer sizes. On an interactive map where each pan or dropdown change re-runs the query, that cost is paid repeatedly and the dashboard feels broken. The spatial index types reference explains when an R-tree, a grid index, or a database-side index is the right tool.

These three failure modes are also why the values here are worth memorising rather than guessing. The next table is the one most engineers reach for first.


Jump to heading Reference: the most common EPSG codes

The single most looked-up table in spatial work is the mapping from EPSG code to what it actually represents. These are the systems you will encounter and set most often in a Streamlit or Panel dashboard. Units and area-of-use are the columns that catch people out: a projected metre CRS is safe for measurement only inside its stated extent.

EPSGNameTypeUnitsTypical useArea of use
4326WGS 84GeographicdegreesCanonical storage, GeoJSON, GPS feedsGlobal
3857WGS 84 / Pseudo-MercatorProjectedmetresWeb tile maps, Folium, Deck.gl, LeafletGlobal (±85.06° lat)
4269NAD83GeographicdegreesUS federal and TIGER datasetsNorth America
3395WGS 84 / World MercatorProjectedmetresGlobal Mercator with true ellipsoidGlobal (±80° lat)
32633WGS 84 / UTM zone 33NProjectedmetresDistance and area, central Europe12°E–18°E, northern hemisphere
27700OSGB36 / British National GridProjectedmetresUK national mapping and measurementUnited Kingdom
25832ETRS89 / UTM zone 32NProjectedmetresDistance and area, western/central EU6°E–12°E Europe
2154RGF93 / Lambert-93ProjectedmetresFrance national gridMetropolitan France
6933WGS 84 / NSIDC EASE-Grid 2.0 GlobalProjected (equal-area)metresGlobal area-weighted statisticsGlobal
3035ETRS89-extended / LAEA EuropeProjected (equal-area)metresEuropean area calculations, EurostatEurope
5070NAD83 / Conus AlbersProjected (equal-area)metresContiguous US area calculationsLower 48 US states

The rule this table encodes: store in 4326, display in 3857, measure in a local projected or equal-area code. The full derivation of that rule, plus the axis-order gotcha and datum-shift notes, lives in the CRS & coordinate systems reference.


Jump to heading Subsection 1: CRS & coordinate systems

The CRS & coordinate systems reference is where alignment and measurement bugs get resolved. It separates geographic systems (angular coordinates on the ellipsoid, like 4326) from projected systems (planar coordinates in metres, like 3857 or 27700), and explains why you can never trust a length or area computed in a geographic CRS.

A typical inspection-and-conversion pass in a dashboard load looks like this:

python
import geopandas as gpd

gdf = gpd.read_file("districts.geojson")
print(gdf.crs)                       # EPSG:4326 — check what you actually have

# Measure in a projected CRS, then return to WGS 84 for display
areas_km2 = gdf.to_crs("EPSG:6933").area / 1_000_000
gdf["area_km2"] = areas_km2
gdf_web = gdf.to_crs("EPSG:3857")    # hand this to the tile map

Two failure signatures dominate: calling to_crs on a frame whose .crs is None (GeoPandas cannot transform without a known source), and the latitude/longitude axis-order swap when a code such as 4326 is interpreted in the wrong order. That reference documents both, and drills into two exact tasks engineers repeat — looking up EPSG codes for your region and reprojecting between EPSG:4326 and EPSG:3857 without distortion. When a filter feeds those reprojected coordinates into a live map, the interaction pattern is covered under dynamic spatial filtering.


Jump to heading Subsection 2: GeoDataFrame schema & dtypes

The GeoDataFrame schema reference is where memory problems get resolved. A GeoDataFrame is a pandas DataFrame with one or more geometry columns, and the same dtype discipline that keeps a large tabular frame lean applies — with the added subtlety of how geometry serialises.

The highest-impact change is usually converting repeated-value string columns to category and downcasting numeric attributes:

python
import geopandas as gpd

gdf = gpd.read_parquet("parcels.parquet")
print(gdf.memory_usage(deep=True).sum() / 1_048_576, "MB")

gdf["land_use"] = gdf["land_use"].astype("category")   # repeated strings
gdf["region_code"] = gdf["region_code"].astype("category")
for col in gdf.select_dtypes("float64").columns:
    gdf[col] = gdf[col].astype("float32")               # halve attribute bytes

print(gdf.memory_usage(deep=True).sum() / 1_048_576, "MB")

That reference lists the dtype choices, the trade-offs of float32 precision for coordinates, and how to serialise geometry as Well-Known Binary for a compact cache. It also covers two recurring tasks: choosing dtypes to reduce GeoDataFrame memory and converting Shapely geometries to GeoJSON for web maps. Keeping frames small is the front line of the broader work described in memory limit management.


Jump to heading Subsection 3: Spatial index types

The spatial index types reference is where slow interactions get resolved. It compares the R-tree that GeoPandas exposes through .sindex, grid and hash indexes, and the database-side GiST indexes PostGIS builds, and explains which one fits point-in-polygon lookups, nearest-neighbour searches, and bounding-box filters.

The single most common speed-up is letting the spatial index drive a join instead of a brute-force overlay:

python
import geopandas as gpd

points = gpd.read_parquet("sensors.parquet")     # EPSG:4326
zones = gpd.read_parquet("zones.parquet")        # EPSG:4326

# sjoin builds and uses the R-tree on the right frame automatically
tagged = gpd.sjoin(points, zones, how="left", predicate="within")

Behind sjoin is the R-tree that turns an O(n×m) scan into a bounding-box tree descent. That reference works through the trade-offs and two exact tasks: R-tree vs grid index for point-in-polygon queries and speeding up GeoPandas spatial joins with sindex. Once the index is warm, the results feed directly into the map components documented under spatial component integration & interactive maps.


Jump to heading Where reference metadata is lost between formats

A recurring source of dashboard bugs is that the three reference concerns are carried unevenly by the file formats data passes through. A CRS that is explicit in a GeoPackage can vanish when the same data is exported to a plain CSV of coordinates; a category dtype that keeps a Parquet file small is flattened back to object when the frame is round-tripped through GeoJSON. Knowing which format preserves what tells you where to re-assert a value on load.

FormatCarries CRSPreserves dtypesCarries indexNotes
GeoParquetYesYesNo (rebuild via sindex)Best default for cached layers; WKB geometry
GeoPackage / SpatiaLiteYesPartialYes (RTree tables)Self-describing; good for interchange
ShapefileVia .prj (often missing)No (10-char field cap)Via .qix sidecarLegacy; assert CRS on load
GeoJSONAssumed 4326 onlyNoNoInterchange for web maps; degrees only
PostGIS tableYes (srid)YesYes (GiST)Index lives server-side
Plain CSVNoNoNoCoordinates only; declare everything

The practical rule this table encodes: treat every load boundary as a place where the CRS, dtypes, and index may all have been dropped, and re-assert them explicitly. GeoJSON in particular is defined only for WGS 84, so a GeoJSON file is always EPSG:4326 even when its producer intended otherwise — which is exactly why the reprojecting between EPSG:4326 and EPSG:3857 task exists as a distinct step in most dashboards.

Jump to heading Common reference mistakes and their symptoms

Because the three concerns fail silently, it helps to recognise each by the symptom it produces on screen rather than by an exception in the log.

A layer renders in the ocean, or off the edge of the map. The overlay and the base tiles are in different coordinate systems — usually a 4326 overlay dropped onto a 3857 base without reprojection, or an axis-order swap that put longitude where latitude belonged. Confirm both layers share a CRS and that points were built as (lon, lat).

Area or distance values are absurdly small or grow with latitude. The measurement was taken in a geographic or Web Mercator CRS. A “distance” of 0.42 is degrees, not metres; an area that doubles as you move north is Mercator inflation. Reproject into a projected or equal-area code before measuring, per the CRS & coordinate systems reference.

Memory climbs far faster than row count. Repeated-value string columns are still object, or attributes are float64 where float32 would do. A frame that should be 80 MB sits at 400 MB. Downcast and categorise on load, and lean on memory limit management for the container-side ceiling.

The first filter after load takes several seconds, then subsequent ones are fast. The R-tree was built lazily on first use. Materialise gdf.sindex during the load so the cost is paid once, off the interactive path, as the spatial index types reference recommends.

A cached layer occasionally returns the wrong CRS. Two upstream feeds delivered the same features in different systems, so the cache key — built from geometry rather than scalar metadata — hashed them as distinct. Normalise to one canonical CRS before caching, and key on the CRS string, not the geometry.

Recognising these five signatures turns a vague “the map looks wrong” into a specific reference lookup, which is the whole purpose of collecting these constants in one place.

Jump to heading Putting the three together in a load path

A well-built dashboard load applies all three concerns in one pass, in the right order: normalise the CRS first, tighten the schema second, build the index third. Reversing that order wastes work — indexing a frame you are about to reproject rebuilds the index, and downcasting before you know the CRS can lose coordinate precision you needed.

python
import geopandas as gpd

def prepare_layer(path: str) -> gpd.GeoDataFrame:
    gdf = gpd.read_parquet(path)

    # 1. CRS: fail loudly if unknown, normalise to canonical storage CRS
    if gdf.crs is None:
        raise ValueError(f"{path} has no CRS; set it before loading")
    gdf = gdf.to_crs("EPSG:4326")

    # 2. Schema: shrink repeated strings and oversized floats
    for col in gdf.select_dtypes("object").columns:
        if col != gdf.geometry.name and gdf[col].nunique() / len(gdf) < 0.5:
            gdf[col] = gdf[col].astype("category")

    # 3. Index: materialise the R-tree so the first query is fast
    _ = gdf.sindex
    return gdf

This is the shape every page in this section supports: the CRS reference tells you which code belongs in step 1, the schema reference tunes step 2, and the index reference explains what step 3 buys you.


Jump to heading Defaults worth committing to memory

Most dashboards converge on the same handful of choices, and adopting them as defaults removes a category of decisions from every new page. Store canonical geometry in EPSG:4326 and reproject to EPSG:3857 only where the tile map demands it. Reproject to a local UTM zone or national grid for distance, and to an equal-area code such as EPSG:6933 for area — never measure in the storage or display CRS. Serialise cached frames as GeoParquet with Well-Known Binary geometry rather than pickle, so the payload stays small and version-portable. Convert any string column whose values repeat to category, and downcast attribute floats to float32 unless coordinate precision genuinely requires 64 bits. Materialise the spatial index at load time so the first interaction is not the one that pays for it.

None of these are exotic; they are the settled answers that experienced teams stop re-deriving. Each is documented with its trade-offs in the three subsections, and each corresponds to one column of the interlock diagram at the top of this page: CRS for placement and measurement, schema for footprint, index for speed. When a new dataset arrives, walking those three columns in order — normalise, tighten, index — turns an open-ended “how should I load this” into a short checklist.

Jump to heading Conclusion

The reference values on these pages are small in isolation and decisive in aggregate. A single wrong EPSG code puts a layer in the ocean; a single object column where a category belonged can double a frame’s footprint; a single missing index turns a snappy filter into a four-second stall. Because a dashboard re-runs its data path on every interaction, each of these errors is paid repeatedly, which is why looking up the correct value once and encoding it in the load path is worth the minute it takes.

Use this section the way you would a spec sheet: jump to the CRS & coordinate systems reference when a layer is misplaced or a measurement looks wrong, the GeoDataFrame schema reference when memory climbs faster than row count, and the spatial index types reference when interactions feel sluggish. Each links onward to the exact-task walkthroughs that turn a reference value into working code.


Jump to heading Frequently asked questions

Which CRS should a spatial dashboard store data in?

Store canonical data in EPSG:4326 (WGS 84) because it is the interchange standard for GeoJSON and most upstream feeds. Reproject to EPSG:3857 only at the rendering boundary for web tile maps, and reproject to a local projected or equal-area CRS such as EPSG:27700 or EPSG:6933 whenever you measure distance or area. Keeping one canonical storage CRS prevents the non-deterministic cache keys that arise when the same feature arrives in two different systems. The CRS & coordinate systems reference sets out the full rule.

Why does the wrong dtype inflate GeoDataFrame memory?

Pandas defaults object columns to Python strings and numeric columns to 64-bit types. A category-like string column stored as object can use five to ten times the memory of a pandas category dtype, and float64 coordinates or attributes use twice the bytes of float32. On a frame with hundreds of thousands of rows those defaults are the difference between fitting a container memory limit and being killed by the out-of-memory reaper. The GeoDataFrame schema reference lists the safe dtype choices.

Do I always need a spatial index for point-in-polygon queries?

For anything above a few thousand geometries, yes. A naive point-in-polygon or spatial join tests every point against every polygon, which is O(n×m) and freezes the dashboard for seconds. Building an R-tree via GeoPandas sindex turns each lookup into a bounding-box tree descent that is orders of magnitude faster, and it is the single highest-leverage change for query latency in an interactive map. See the spatial index types reference for when each index type applies.


Back to Spatial Dashboard Home

Related