Animating Traffic Flow in Offenbach with HERE Data

1 minute read

Published:

An animation of relative traffic volume on Offenbach’s main roads throughout the day. Yellow = high traffic, blue = low traffic, white = no data available. The traffic data was collected from the HERE Traffic API over several weeks.

HERE Traffic Flow API

The API reports per-segment metrics including speed (capped and uncapped), free flow speed, a jam factor (0–10, where 10 = road closure), and a confidence value. The jam factor is the most useful single number for visualizing congestion.

Matching HERE segments to OSM

HERE delivers traffic data as geographic line segments. To render them on an OpenStreetMap street network, each segment gets clipped to the city boundary and matched to the nearest OSM edge:

import osmnx as ox
from shapely.geometry import LineString, Point, shape
import json

city_boundary = shape(json.load(open("boundaries.json")))

G = ox.graph_from_polygon(
    city_boundary, network_type="drive", truncate_by_edge=True
)

def process_shp(shp):
    edges = []
    for segment in shp:
        points = []
        for coord in segment["value"][0].strip().split():
            lat, lon = coord.split(",")
            point = Point(float(lon), float(lat))
            if city_boundary.contains(point):
                points.append(point)
        if len(points) > 1:
            edges.append(LineString(points))
    return edges

Rendering

Traffic snapshots are aggregated into 15-minute time slots (median jam factor per segment), then rendered frame by frame against the OSM street graph using a plasma colormap. The frames are stitched into a video with FFmpeg.

grouped_df = gdf.dissolve(by=["hour_minute", "edge_hash"], aggfunc="median")

for idx, time_slot in enumerate(grouped_df.index.get_level_values(0).unique()):
    fig, ax = ox.plot.plot_graph(
        G, node_size=0, edge_color='#eeeeee', bgcolor='#111111', show=False
    )
    tmp_gdf = grouped_df.loc[time_slot]
    tmp_gdf.plot(ax=ax, color=tmp_gdf["color"], alpha=0.8)
    fig.savefig(f"frame_{idx:04d}.jpg", dpi=600,
                facecolor='#111111', bbox_inches='tight')

The daily pattern is what you’d expect: quiet early morning, sharp increase around 7–9 AM, moderate midday, second peak at 4–7 PM, then back to quiet.