Tutorial

How to Plot Maps in Python with GeoPandas and Matplotlib

A common GIS task is to display spatial data quickly so you can confirm that it loaded correctly, inspect feature locations, or create a simple static map for a report. In Python, the standard workflow for vector data is to load the data in

Problem statement

A common GIS task is to display spatial data quickly so you can confirm that it loaded correctly, inspect feature locations, or create a simple static map for a report. In Python, the standard workflow for vector data is to load the data into a GeoDataFrame and plot it with GeoPandas.

This page shows how to plot maps in Python with GeoPandas and Matplotlib. It covers common vector inputs such as shapefiles and GeoJSON, and shows how to:

  • check imported GIS data visually
  • plot points, lines, and polygons
  • create a simple thematic map
  • save the result to an image file

If you need a fast way to visualize vector spatial data in Python, this is the usual approach.

Quick answer

To plot spatial data with GeoPandas, read the file into a GeoDataFrame and call .plot():

import geopandas as gpd
import matplotlib.pyplot as plt

gdf = gpd.read_file("data/cities.geojson")
gdf.plot()

plt.show()

This works well for:

  • quick static maps
  • exploratory GIS analysis
  • simple output for reports or automation scripts

For more control, pass styling arguments to .plot() and use Matplotlib to adjust the figure.

Step-by-step solution

Load spatial data into a GeoDataFrame

GeoPandas can read common vector GIS formats including:

  • shapefile
  • GeoJSON
  • GeoPackage

Use gpd.read_file() to load the data:

import geopandas as gpd

gdf = gpd.read_file("data/admin_boundaries.shp")
print(gdf.head())
print(gdf.crs)

To plot correctly, the data must contain a geometry column. You should also confirm that the file loaded with the expected coordinate reference system.

You can do the same with GeoJSON:

gdf = gpd.read_file("data/roads.geojson")

Plot the map with GeoPandas

The simplest way to create a map is with the GeoDataFrame .plot() method:

import matplotlib.pyplot as plt

gdf.plot()
plt.show()

Default behavior depends on geometry type:

  • points are drawn as markers
  • lines are drawn as line paths
  • polygons are drawn as filled shapes

This is the fastest way to verify that the data loaded and that the geometry is where you expect it to be.

Customize the map appearance with Matplotlib

For clearer output, create a Matplotlib figure and axis first, then pass the axis to GeoPandas:

fig, ax = plt.subplots(figsize=(8, 6))

gdf.plot(
    ax=ax,
    color="lightblue",
    edgecolor="black",
    linewidth=0.8
)

ax.set_title("Administrative Boundaries")
plt.show()

Useful options include:

  • figsize for map size
  • color for fill or marker color
  • edgecolor for polygon borders
  • linewidth for line or border thickness
  • markersize for point layers

Plot a column as a thematic map

To create a choropleth map, pass a numeric column to column=:

fig, ax = plt.subplots(figsize=(10, 8))

gdf.plot(
    column="population",
    ax=ax,
    cmap="OrRd",
    legend=True,
    edgecolor="black",
    linewidth=0.5
)

ax.set_title("Population by District")
plt.show()

This is useful when you want to visualize an attribute such as:

  • population
  • area
  • density
  • land value
  • any other numeric field

For categorical fields, you can also use column=, though the output is usually clearer when the number of classes is small.

Plot multiple layers on the same map

A common GIS workflow is to plot one layer on top of another, such as roads over administrative boundaries or points over polygons. The main requirement is that both layers use the same CRS.

import geopandas as gpd
import matplotlib.pyplot as plt

boundaries = gpd.read_file("data/admin_boundaries.shp")
schools = gpd.read_file("data/schools.geojson")

schools = schools.to_crs(boundaries.crs)

fig, ax = plt.subplots(figsize=(10, 8))

boundaries.plot(
    ax=ax,
    color="white",
    edgecolor="black",
    linewidth=0.8
)

schools.plot(
    ax=ax,
    color="red",
    markersize=20
)

ax.set_title("Schools Within Administrative Boundaries")
ax.set_axis_off()
plt.show()

Use this pattern when you need to check alignment between layers or produce a simple reference map.

Remove axes and improve map readability

Map axes are often unnecessary in a simple static output. You can hide them:

fig, ax = plt.subplots(figsize=(8, 8))

gdf.plot(ax=ax, color="lightgreen", edgecolor="gray")
ax.set_title("Park Boundaries")
ax.set_axis_off()

plt.show()

For thematic maps, keep legend=True if the attribute values need explanation.

Save the map to an image file

To export the map for a report or automated workflow, save the figure with Matplotlib:

fig, ax = plt.subplots(figsize=(10, 8))

gdf.plot(
    column="population",
    ax=ax,
    cmap="Blues",
    legend=True,
    edgecolor="black"
)

ax.set_title("Population Map")
ax.set_axis_off()

plt.savefig("output/population_map.png", dpi=300, bbox_inches="tight")
plt.close(fig)

This is the standard pattern for repeatable GIS reporting scripts.

Code examples

Example 1: Plot a shapefile as a basic map

Use this when you want to confirm that a shapefile loaded correctly.

import geopandas as gpd
import matplotlib.pyplot as plt

gdf = gpd.read_file("data/parcels.shp")

gdf.plot()
plt.show()

Example 2: Style polygon boundaries and fill colors

This is a better choice for a simple reporting map.

import geopandas as gpd
import matplotlib.pyplot as plt

gdf = gpd.read_file("data/landuse.shp")

fig, ax = plt.subplots(figsize=(9, 7))
gdf.plot(
    ax=ax,
    color="beige",
    edgecolor="darkslategray",
    linewidth=0.6
)

ax.set_title("Land Use Areas")
ax.set_axis_off()
plt.show()

Example 3: Create a choropleth map from an attribute column

Use a numeric field to color polygons.

import geopandas as gpd
import matplotlib.pyplot as plt

gdf = gpd.read_file("data/districts.geojson")

fig, ax = plt.subplots(figsize=(10, 8))
gdf.plot(
    column="pop_density",
    ax=ax,
    cmap="YlGnBu",
    legend=True,
    edgecolor="black",
    linewidth=0.4
)

ax.set_title("Population Density")
ax.set_axis_off()
plt.show()

Example 4: Plot point data on a clean map layout

Point datasets often need marker styling to be readable.

import geopandas as gpd
import matplotlib.pyplot as plt

gdf = gpd.read_file("data/schools.geojson")

fig, ax = plt.subplots(figsize=(8, 8))
gdf.plot(
    ax=ax,
    color="red",
    markersize=20
)

ax.set_title("School Locations")
ax.set_axis_off()
plt.show()

Example 5: Save the plotted map to a PNG file

Use this pattern in automated GIS workflows.

import geopandas as gpd
import matplotlib.pyplot as plt

gdf = gpd.read_file("data/admin_boundaries.shp")

fig, ax = plt.subplots(figsize=(10, 8))
gdf.plot(ax=ax, color="lightgray", edgecolor="black", linewidth=0.5)

ax.set_title("Boundary Map")
ax.set_axis_off()

plt.savefig("output/boundary_map.png", dpi=300, bbox_inches="tight")
plt.close(fig)

Explanation

GeoPandas and Matplotlib work together:

  • GeoPandas reads and stores spatial data in a GeoDataFrame
  • Matplotlib renders the figure and handles styling

When you call gdf.plot(), GeoPandas converts the geometry into a Matplotlib plot. In many GIS workflows, this is enough for quick visualization.

This method is best for static maps. It is not intended for interactive web maps with pan, zoom, popups, or tiled basemaps.

Geometry type affects the output automatically:

  • Point geometries become markers
  • LineString geometries become lines
  • Polygon geometries become filled areas with optional borders

For many tasks, GeoPandas plotting is the fastest way to inspect data before moving on to reprojection, clipping, spatial joins, or export.

Edge cases and notes

Coordinate reference systems can affect map output

If a layer appears in the wrong place or looks distorted, check its CRS:

print(gdf.crs)

If you are plotting multiple layers together, make sure they use the same CRS before plotting:

roads = roads.to_crs(boundaries.crs)

For single-layer plotting, reprojection is only necessary if your workflow requires a different CRS.

Missing or invalid geometry can affect plotting

Null or invalid geometries may cause blank output, incomplete plots, unexpected rendering, or errors.

Check for missing geometry:

print(gdf.geometry.isna().sum())

Check validity:

print(gdf.geometry.is_valid.value_counts())

If necessary, filter or repair problematic rows before plotting.

Large datasets may plot slowly

Very large shapefiles or GeoJSON files can be slow to render. Common fixes include:

  • selecting fewer columns
  • filtering rows
  • plotting a sample
  • simplifying geometry before plotting

This is common with parcel, road, or building datasets.

GeoPandas plots static maps only

If you need interactive maps, GeoPandas .plot() is not the right tool. This page covers static plotting for analysis, reporting, and script-based GIS workflows.

Internal links

If you need more context before plotting, see Working with GeoDataFrames in GeoPandas.

Related tasks:

If your output is blank, misplaced, or distorted, check Why GeoPandas Plot Is Not Showing Correctly.

FAQ

How do I plot a shapefile in Python with GeoPandas?

Read the shapefile with gpd.read_file() and call .plot():

import geopandas as gpd
import matplotlib.pyplot as plt

gdf = gpd.read_file("data/boundaries.shp")
gdf.plot()
plt.show()

Can GeoPandas plot GeoJSON files directly?

Yes. GeoPandas can read GeoJSON with read_file() the same way as a shapefile:

gdf = gpd.read_file("data/points.geojson")
gdf.plot()

How do I color a map by an attribute column in GeoPandas?

Use the column argument and optionally add a legend:

gdf.plot(column="population", cmap="OrRd", legend=True)

This is the standard way to create a choropleth map in GeoPandas.

How do I save a GeoPandas plot as a PNG image?

Create the plot, then save it with Matplotlib:

fig, ax = plt.subplots()
gdf.plot(ax=ax)
plt.savefig("map.png", dpi=300, bbox_inches="tight")
plt.close(fig)

This is useful for reports, dashboards, and batch GIS scripts.

Keep exploring with more guides in this category.