Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Visualisation

Authors
Affiliations
TU Wien
TU Wien
TU Wien
Binder

This notebook will present you some useful visualisation libraries in Python that can be used for visualising geospatial data. We illustrate them with both OSM intersection features and GTFS stop features for the same Vienna area.

Why visualisation matters

Visualisation is not only useful for presenting final results, but also for validating and debugging spatial data throughout the whole processing pipeline. Interactive maps might help identify errors like incorrect coordinate reference systems (CRS), missing or duplicated geometries, outliers or gaps in coverage.

Therefore, by visually inspecting data often and early, many processing errors can be detected before expensive analysis steps are performed. This makes visualisation a powerful tool that should not be neglected.

Imports and Configuration

import sys
from pathlib import Path

import folium
import geopandas as gpd
import iplotx as ipx
import matplotlib.pyplot as plt
import networkx as nx
import pandas as pd
import requests
import seaborn as sns
from shapely import wkb
from shapely.geometry import box

# ruff: noqa: E501

# Importing custom functions
sys.path.append(str(Path("..").resolve()))
from src.geoai.cookbook_functions import (
    create_hexagonal_grid,
    create_polygon_grid,
    get_base_map,
)

# Configuration
CITY_NAME = "Vienna"
BBOX = "16.335005,48.187854,16.400923,48.209995"
DATA_DIR = "./osm_data"
OUTPUT_DIR = "./output"

# Computing the center of the bounding box
min_lon, min_lat, max_lon, max_lat = map(float, BBOX.split(","))
bounds = [[min_lat, min_lon], [max_lat, max_lon]]
center_lat = (min_lat + max_lat) / 2
center_lon = (min_lon + max_lon) / 2

Now we load both feature tables from the previous notebooks: OSM intersections and GTFS stops.

# Load OSM intersections
csv_url = "https://gitlab.tuwien.ac.at/api/v4/projects/15378/repository/files/intersections.csv/raw?ref=main&lfs=true"
PROJECT_ROOT = Path.cwd()
csv_path = PROJECT_ROOT / "output" / "Vienna" / "intersections.csv"
csv_path.parent.mkdir(parents=True, exist_ok=True)
if not csv_path.exists():
    response = requests.get(csv_url, timeout=30)
    response.raise_for_status()
    csv_path.write_bytes(response.content)

df_int = pd.read_csv(csv_path)
df_int["geometry"] = df_int["geom"].apply(lambda x: wkb.loads(bytes.fromhex(x)))
intersections_gdf = gpd.GeoDataFrame(df_int, geometry="geometry", crs="EPSG:4326")

# Load GTFS stop features from GitLab LFS (same pattern as intersections.csv)
gtfs_csv_url = (
    "https://gitlab.tuwien.ac.at/api/v4/projects/15378/repository/files/"
    "gtfs_stops.csv/raw?ref=main&lfs=true"
)
gtfs_csv = Path(OUTPUT_DIR) / CITY_NAME / "gtfs_stops.csv"
gtfs_csv.parent.mkdir(parents=True, exist_ok=True)
if not gtfs_csv.exists():
    response = requests.get(gtfs_csv_url, timeout=60)
    response.raise_for_status()
    gtfs_csv.write_bytes(response.content)

gtfs_df = pd.read_csv(gtfs_csv)
stops_gdf = gpd.GeoDataFrame(
    gtfs_df,
    geometry=gpd.points_from_xy(gtfs_df["stop_lon"], gtfs_df["stop_lat"]),
    crs="EPSG:4326",
)
print(
    f"Loaded {len(intersections_gdf)} OSM intersections and {len(stops_gdf)} GTFS stops"
)
Loaded 8155 OSM intersections and 395 GTFS stops

Folium

What is Folium?

Folium is a Python library that makes it easy to visualise data on an interactive Leaflet.js map. It allows us to take our spatial Python data and plot it directly on the map as markers, heatmaps, or choropleth grids. Unlike static plots, Folium maps are interactive, allowing users to zoom, pan, and toggle layers to explore complex spatial relationships in detail. Because Folium is built on top of Leaflet.js, maps can also easily be exported as standalone HTML files and shared easily.

There are multiple different tile providers for Folium available (think of it as the background map), like OpenStreetMap or CartoDB. For the visualisations in this cookbook, we have been using “CartoDB positron” tiles because they are simple and minimalist in style and therefore don’t distract visually from the data we’ve been plotting while still giving that visual spatial reference. But for other applications and use-cases, different tiles might be better suited or preferable.

In the next cell we put OSM intersections and GTFS stops on one map as toggleable layers.

m_final = get_base_map(center_lat, center_lon)

# OSM intersections
fg_pts = folium.FeatureGroup(name="OSM intersections", show=True)
for _, row in intersections_gdf.iterrows():
    color = "blue"
    if row.get("i_tpe") == "Car":
        color = "crimson"
    elif row.get("i_tpe") == "Path":
        color = "green"
    popup_html = f"""
    <div style="font-family: sans-serif; font-size: 12px;">
        <b>ID:</b> {row.get("osm_id")}<br>
        <b>Type:</b> {row.get("i_tpe")}<br>
        <b>Arms:</b> {row.get("num_ways")}<br>
        <b>Delta:</b> {row.get("delta"):.2f}<br>
    </div>
    """
    folium.CircleMarker(
        [row.geometry.y, row.geometry.x],
        radius=1,
        color=color,
        fill=True,
        popup=folium.Popup(popup_html, max_width=200),
    ).add_to(fg_pts)
fg_pts.add_to(m_final)

# GTFS stops
mode_colors = {
    "Tram": "#e41a1c",
    "Subway": "#377eb8",
    "Bus": "#4daf4a",
    "Rail": "#984ea3",
    "Unknown": "#999999",
}
fg_stops = folium.FeatureGroup(name="GTFS stops", show=True)
for _, row in stops_gdf.iterrows():
    folium.CircleMarker(
        location=[row.stop_lat, row.stop_lon],
        radius=3,
        color=mode_colors.get(row.primary_mode, "#999999"),
        fill=True,
        fill_opacity=0.75,
        popup=row.stop_name,
    ).add_to(fg_stops)
fg_stops.add_to(m_final)

# Grid layers from OSM extent (shared AOI)
intersection_meters = intersections_gdf.to_crs("EPSG:3857")
minx, miny, maxx, maxy = intersection_meters.total_bounds
total_area = box(minx, miny, maxx, maxy)
hex_grid = create_hexagonal_grid(total_area, side_length=250)
hex_grid.set_crs("EPSG:3857", inplace=True)
hex_grid_geo = hex_grid.to_crs("EPSG:4326")
folium.GeoJson(hex_grid_geo, name="Hex grid 250m", show=False).add_to(m_final)

minx, miny, maxx, maxy = total_area.bounds
rect_grid = create_polygon_grid(
    width=maxx - minx,
    height=maxy - miny,
    cell_size=(500, 500),
    origin=(minx, miny),
    crs="EPSG:3857",
)
rect_grid_geo = rect_grid.to_crs("EPSG:4326")
folium.GeoJson(rect_grid_geo, name="Rect grid 500m", show=False).add_to(m_final)

folium.LayerControl().add_to(m_final)
out_map = Path(OUTPUT_DIR) / CITY_NAME / "analysis_map.html"
out_map.parent.mkdir(parents=True, exist_ok=True)
m_final.save(str(out_map))
print(f"Saved {out_map}")
m_final
/opt/conda/lib/python3.12/site-packages/folium/raster_layers.py:130: UserWarning: CartoDB tiles now require an API key. Please provide one to continue using the tiles. You can request the key at https://carto.com/basemaps/apikey/.
  tiles = tiles.build_url(fill_subdomain=False, scale_factor="{r}")  # type: ignore
Saved output/Vienna/analysis_map.html
Loading...

Matplotlib

What is Matplotlib?

Matplotlib is a foundational plotting library in Python. While Folium answers the “Where?” on an interactive map, Matplotlib is stronger for static statistical charts and publication figures.

Below we summarise OSM intersection types and GTFS stop modes in one figure.

fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# OSM
osm_counts = intersections_gdf["i_tpe"].value_counts()
axes[0].bar(osm_counts.index.astype(str), osm_counts.values, color="#54278f")
axes[0].set_title("OSM intersections by type")
axes[0].set_xlabel("Type")
axes[0].set_ylabel("Count")
axes[0].tick_params(axis="x", rotation=30)

# GTFS
mode_counts = stops_gdf["primary_mode"].value_counts()
axes[1].bar(mode_counts.index.astype(str), mode_counts.values, color="#d94801")
axes[1].set_title("GTFS stops by primary mode")
axes[1].set_xlabel("Mode")
axes[1].set_ylabel("Count")
axes[1].tick_params(axis="x", rotation=30)

fig.suptitle("Feature counts for the same AOI (Matplotlib)", fontsize=14)
fig.tight_layout()
plt.show()
<Figure size 1200x400 with 2 Axes>

Seaborn

What is Seaborn?

Seaborn is a Python data visualisation library built on top of Matplotlib. It is specifically designed for creating attractive and informative statistical graphics with minimal code. Seaborn integrates well with Pandas DataFrames and provides built-in themes and colour palettes, making it especially useful for exploratory data analysis.

Here we compare distributions for OSM (delta) and GTFS (trip_count) side by side.

sns.set_theme(style="whitegrid")
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

sns.boxplot(data=intersections_gdf, x="i_tpe", y="delta", ax=axes[0])
axes[0].set_title("OSM intersection irregularity (delta)")
axes[0].tick_params(axis="x", rotation=30)

sns.boxplot(data=stops_gdf, x="primary_mode", y="trip_count", ax=axes[1])
axes[1].set_title("GTFS trip counts by mode")
axes[1].tick_params(axis="x", rotation=30)

fig.suptitle("Distributions for OSM and GTFS features (Seaborn)", fontsize=14)
fig.tight_layout()
plt.show()
<Figure size 1200x400 with 2 Axes>

Iplotx

What is iplotx?

iplotx is best suited for visualising spatial networks and relationships. The OSM street / intersection graph is a natural fit; GTFS stops are point features, so this example stays on the network side while Folium, Matplotlib, and Seaborn already covered both sources.

# Lightweight OSM neighbourhood graph from intersection points
G = nx.Graph()
sample = intersections_gdf.head(40).copy()
for _, row in sample.iterrows():
    G.add_node(row["osm_id"], pos=(row.geometry.x, row.geometry.y))

coords = sample.set_index("osm_id")
ids = list(coords.index)
for i, a in enumerate(ids):
    for b in ids[i + 1 :]:
        d = coords.loc[a].geometry.distance(coords.loc[b].geometry)
        if d < 0.002:
            G.add_edge(a, b)

fig, ax = plt.subplots(figsize=(8, 8))
layout = {n: G.nodes[n]["pos"] for n in G.nodes}
# Pass layout as the second positional arg (same pattern as feature engineering).
# Do not pass it as pos=...: iplotx would treat that as a style dict and fail on integer node IDs.
ipx.network(
    G,
    layout,
    ax=ax,
    vertex_labels=False,
    vertex_facecolor="lightblue",
    edge_linewidth=1,
)
ax.set_title("OSM intersection neighbourhood graph (iplotx)")
ax.set_aspect("equal")
plt.show()
<Figure size 800x800 with 1 Axes>

How to decide which visualisation library to use?

When choosing a visualisation library for geospatial data, the decision depends on the type of analysis and the level of interactivity required.

  • Folium is best suited for interactive, map-based visualisations — here we layered OSM intersections and GTFS stops together.

  • Matplotlib is appropriate for static, customizable plots, including side-by-side OSM vs GTFS charts.

  • Seaborn is better for statistical and exploratory analysis of feature distributions (again for both sources).

  • iplotx is the better choice for network-based geospatial visualisation (OSM graph).

Therefore, Folium is typically preferred for interactive mapping, while Matplotlib and Seaborn are stronger choices for analytical charts, and iplotx for networks.

Summary

In this notebook you learned:

  • Why visualisation is an important step in a geospatial data handling pipeline

  • How to use Folium with OSM and GTFS layers on one map

  • How to use Matplotlib and Seaborn with paired OSM / GTFS statistical charts

  • When iplotx is useful for network views