Set z-order of Edges in Pandas Dataframe

Question:

I’m using OSMnx to create some plots of road networks with data shown by colour, below is an example which produces an image with colour showing betweenness centrality. The problem I’m having is that, due to the zordering, some of the lighter regions are covered by other edges, especially around junctions with lots of nodes.

The OSMnx.plot_graph method calls gdf.plot from GeoPandas, which in turn uses PlotAccessor from Pandas. I’ve been reading the OSMnx documentation, and can’t seem to find a way to pass a z-ordering, so my question is: is there a way I can directly plot the graph, either with GeoPandas, pandas, or even matplotlib directly, such that I can specify a z-ordering of the edges?

import networkx as nx
import osmnx as ox

place = 'Hornsea'
G = ox.graph_from_place(place, network_type="drive")

# Digraph removes parallel edges
# Line graph swaps nodes and edges
line_graph = nx.line_graph(ox.get_digraph(G))

betweenness_centrality = nx.betweenness_centrality(line_graph, weight='travel_time')

for edge in G.edges:
    if edge not in betweenness_centrality:
        nx.set_edge_attributes(G, {edge: 0}, 'betweenness_centrality')

betweenness_centrality = {(k[0], k[1], 0): v for k, v in betweenness_centrality.items()}
nx.set_edge_attributes(G, betweenness_centrality, "betweenness_centrality")

ec = ox.plot.get_edge_colors_by_attr(G, 'betweenness_centrality', cmap='plasma')  # "RdYlGn_r"
ew = [G.get_edge_data(*edge).get('betweenness_centrality', 0) * 10 + 0.25 for edge in G.edges]
fig, ax = ox.plot_graph(G, edge_color=ec, edge_linewidth=ew, node_size=0)
fig.savefig(f"images/{place}_betweenness_centrality.png", facecolor="w", dpi=1000, bbox_inches="tight")

hornsea_betweenness_centrality.png

Asked By: George Willcox

||

Answers:

Took a little exploring the source code… not sure if this is exactly right, since I’m not sure how the desired outcome is supposed to look, but hopefully this gets you most of the way there… had to do some poking in the source code….

from pathlib import Path

import geopandas as gpd
import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
import osmnx as ox
from shapely.geometry import LineString
from shapely.geometry import Point

# From your example:
place = 'Hornsea'
G = ox.graph_from_place(place, network_type='drive')
line_graph = nx.line_graph(ox.get_digraph(G))
betweenness_centrality = nx.betweenness_centrality(
    line_graph, weight='travel_time')
for edge in G.edges:
    if edge not in betweenness_centrality:
        nx.set_edge_attributes(G, {edge: 0}, 'betweenness_centrality')
betweenness_centrality = {
    (k[0], k[1], 0): v for k, v in betweenness_centrality.items()}
nx.set_edge_attributes(
    G, betweenness_centrality, 'betweenness_centrality')
ec = ox.plot.get_edge_colors_by_attr(
    G, 'betweenness_centrality', cmap='plasma')

From digging here: https://github.com/gboeing/osmnx/blob/main/osmnx/plot.py#L126

u, v, k, data = zip(*G.edges(keys=True, data=True))
x_lookup, y_lookup = [
    nx.get_node_attributes(G, xy) for xy in ['x', 'y']]

def make_geom(u, v, data, x=x_lookup, y=y_lookup):
    if 'geometry' in data:
        return data['geometry']
    else:
        return LineString((Point((x[u], y[u])), Point((x[v], y[v]))))

crs = G.graph['crs']
geom = map(make_geom, u, v, data)
gdf_edges = gpd.GeoDataFrame(data, crs=crs, geometry=list(geom))

Extract centrality metric in the same order

# get centrality metric in the same order
centralities = [
    G._adj[x][y][0]['betweenness_centrality'] 
    for x, y in zip(u, v)] 

Continue from the source code:

gdf_edges['u'] = u
gdf_edges['v'] = v
gdf_edges['key'] = k
gdf_edges['centralities'] = centralities  # add this in too
gdf_edges.set_index(['u', 'v', 'key'], inplace=True)

# This was the trick: just order the data in desired draw order:
gdf_edges.sort_values('centralities', ascending=False, inplace=True)

Reorder your line widths and colors:

ew2 = gdf_edges.centralities * 10 + 0.25  # same as your ew, but now in the new order
ec2 = ec[gdf_edges.index]                 # also reordered

# plotting
gdf_edges = gdf_edges['geometry']
edge_alpha = None # ...
fig, ax = plt.subplots(
    figsize=(8, 8), facecolor='#111111', frameon=False)
ax.set_facecolor('#111111')
ax = gdf_edges.plot(
    ax=ax, color=ec2, lw=ew2, alpha=edge_alpha)

To save:

west, south, east, north = gdf_edges.total_bounds
bbox = north, south, east, west
padding = 0.02
ax = _config_ax(ax, crs, bbox, padding)  # from source code (see below)
path='my_path/test_map.png'
dpi=1000
fig, ax = _save_and_show(   # from source code (see below)
    fig, ax, save=True, show=True, close=True, filepath=path, dpi=dpi)

Using _config_ax() and _save_and_show() from the source code. (I commented out the if projection.is_projected(crs): clause starting on line 805, and just used the else, but you can dig into that if you need a projected map).enter image description here

Thanks to Damian Satterthwaite-Phillips, I was able to find a solution to the problem. His answer can be cut down a lot, so I thought I’d share what I ended up using in case anyone else has the same issue. However, I have made a pull request to have edge_zorder be an argument for osmnx.plot_graph, so hopefully soon this won’t be necessary.

def plot_graph(G, edge_colour=None, edge_linewidth=None, edge_zorder=None, edge_alpha=None, show=True, save=False, close=False, filepath=None, dpi=300):
    fig, ax = plt.subplots(figsize=(8, 8), facecolor='#111111', frameon=False)
    ax.set_facecolor('#111111')
    
    gs_edges = ox.utils_graph.graph_to_gdfs(G, nodes=False)["geometry"]
    gdf_edges = gpd.GeoDataFrame(gs_edges)
    gdf_edges['zorder'] = edge_zorder
    gdf_edges['colour'] = edge_colour
    gdf_edges['linewidth'] = edge_linewidth
    gdf_edges['alpha'] = edge_alpha
    gdf_edges.sort_values('zorder', inplace=True)
    
    colour = gdf_edges.colour if edge_colour is not None else None
    linewidth = gdf_edges.linewidth if edge_linewidth is not None else None
    alpha = gdf_edges.alpha if edge_alpha is not None else None
    
    ax = gdf_edges.plot(ax=ax, color=colour, lw=linewidth, alpha=alpha, zorder=1)
    west, south, east, north = gdf_edges.total_bounds
    bbox = north, south, east, west
    ax = ox.plot._config_ax(ax, G.graph["crs"], bbox, 0.02)
    return ox.plot._save_and_show(fig, ax, save, show, close, filepath, dpi)

Now this doesn’t plot nodes, but it would be easily modified by looking at the plot_graph method in osmnx.plot.py

Answered By: George Willcox