2025, Dec 13 03:00

Fix empty legends in GeoPandas by passing a Matplotlib proxy artist (Patch) instead of the Axes

Learn why GeoPandas plots return Axes, not legend handles, and how to fix empty legends with Matplotlib proxy artists (Patch). Step-by-step code and layout tips

Getting a legend out of a GeoPandas plot can be surprisingly confusing. The plot renders fine, but the legend stays empty or throws a warning. The root of the issue is simple: what you pass to the legend must be a drawable artist, not the Axes object returned by GeoPandas.

Minimal example that reproduces the issue

import geopandas as gpd
from shapely.geometry import box, Polygon, LineString
import matplotlib.pyplot as plt
fig_map, ax_map = plt.subplots()
bounds = [9.454, 80.4, 12, 80.88]
rect = box(*bounds)
geo_frame = gpd.GeoDataFrame(geometry=[rect])
drawn = geo_frame.plot(
    ax=ax_map,
    edgecolor='red',
    facecolor='none',
    linewidth=2,
    label="user bbox query"
)
# This will not work: "drawn" is an Axes, not a legend handle
ax_map.legend(handles=[drawn])
plt.xlabel('Longitude')
plt.ylabel('Latitude')
plt.show()

Matplotlib explains why this fails:

Legend does not support handles for Axes instances. A proxy artist may be used instead.

Why this happens

GeoPandas uses Matplotlib under the hood. The call to GeoDataFrame.plot returns an Axes. Legends, however, are built from artists such as patches, lines, or collections that visually represent what appears in the plot. Passing an Axes to legend is not supported and results in an empty legend. The fix is to give the legend a proxy artist that matches the style of what you drew.

Working approach with a proxy artist

Create a Patch with the same style as your polygon, then feed that Patch to the legend. Placing the legend outside the axes avoids overlapping the map and makes the label readable.

import geopandas as gpd
from shapely.geometry import box
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
fig_map, ax_map = plt.subplots()
bounds = [9.454, 80.4, 12, 80.88]
rect = box(*bounds)
geo_frame = gpd.GeoDataFrame(geometry=[rect])
geo_frame.plot(
    ax=ax_map,
    edgecolor='red',
    facecolor='none',
    linewidth=2
)
legend_item = mpatches.Patch(
    facecolor='none',
    edgecolor='red',
    linewidth=2,
    label='user bbox query'
)
ax_map.legend(handles=[legend_item], bbox_to_anchor=(1.05, 1), loc='upper left')
plt.xlabel('Longitude')
plt.ylabel('Latitude')
plt.tight_layout()
plt.show()

Why this is worth remembering

Legends in Matplotlib are handle-driven. If the handle isn’t a drawable artist, the legend won’t display what you expect. In GeoPandas workflows this shows up often, because the convenience plotting API returns an Axes while the legend needs an artist. Using a proxy artist such as Patch keeps the legend accurate and the map readable. Positioning the legend outside the plotting area with bbox_to_anchor helps when map content fills the axes tightly.

Conclusion

When a GeoPandas legend looks empty or refuses to render, don’t pass the Axes to legend. Provide a proxy artist that mirrors the style of the plotted geometry, then place the legend where there is room. The pattern is straightforward: draw your data, build a matching Patch (or other artist), feed it to legend, and let tight_layout handle spacing.