Search code examples
pythonmatplotlibgeospatialspatial

How can I add arrows in maps using geopandas and matplotlib with the arrows having color gradients?


I want to show trade flow between countries in a map. For this, I want to add 1 or 2 arrows to and from country as shown below. The arrow should have color gradients, which represent certain numeric values.

1.How can I add arrows on top of countries in maps using geopandas or other packages?

  1. How do I add color gradients to those arrows using color maps? I also need the legend for the color values.

enter image description here


Solution

  • Here's a basic implementation using cartopy (and based on, e.g., Cartopy - multiple arrows using annotate):

    import numpy as np
    
    from matplotlib import pyplot as plt
    from matplotlib import colormaps
    from matplotlib.colors import Normalize
    from matplotlib.cm import ScalarMappable
    
    import cartopy.io.shapereader as shpreader
    import cartopy.crs as ccrs
    
    # required countries
    required = ["Egypt", "Sudan", "South Sudan"]
    
    # get the country border file (10m resolution) and extract
    shpfilename = shpreader.natural_earth(
        resolution="10m",
        category="cultural",
        name="admin_0_countries",
    )
    reader = shpreader.Reader(shpfilename)
    countries = reader.records()
    
    # extract the specific country information
    c = {
        co.attributes["ADMIN"]: co
        for co in countries if co.attributes["ADMIN"] in required
    }
    
    # get overall boundary box from country bounds
    extents = np.array([c[cn].bounds for cn in c])
    lon = [extents.min(0)[0], extents.max(0)[2]]
    lat = [extents.min(0)[1], extents.max(0)[3]]
    
    # get country centroids
    centroids = {
        cn: [c[cn].geometry.centroid.x, c[cn].geometry.centroid.y] for cn in c
    }
    
    # plot the countries
    ax = plt.axes(projection=ccrs.PlateCarree())
    
    for cn in c.values():
        ax.add_geometries(cn.geometry, crs=ccrs.PlateCarree(), edgecolor="white", facecolor="lightgray")
    
    ax.set_extent([lon[0] - 1, lon[1] + 1, lat[0] - 1, lat[1] + 1])
    
    # flows in and out of country pairs
    transfers = {"Egypt,Sudan": [2.3, 8.9], "Sudan,South Sudan": [6.5, 0.9]}
    
    # set up a colormap
    cmap = colormaps.get_cmap("Greens")
    tmax = np.array([v for v in transfers.values()]).max()
    tmin = np.array([v for v in transfers.values()]).min()
    norm = Normalize(tmin, tmax)
    
    offset = 1.0
    
    for tr in transfers:
        c1, c2 = tr.split(",")
        cent1 = centroids[c1]
        cent2 = centroids[c2]
    
        # one way
        t1 = transfers[tr][0]
        col = cmap(norm(t1))
    
        ax.annotate(
            "",
            xy=(cent2[0] - offset, cent2[1] - offset),
            xytext=(cent1[0] - offset, cent1[1] - offset),
            xycoords="data",
            arrowprops={"facecolor": col},
        )
    
        # other way
        t2 = transfers[tr][1]
        col = cmap(norm(t2))
    
        ax.annotate(
            "",
            xy=(cent1[0] + offset, cent1[1] + offset),
            xytext=(cent2[0] + offset, cent2[1] + offset),
            xycoords="data",
            arrowprops={"facecolor": col},
        )
    
    # set the colorbar
    sm = ScalarMappable(norm, cmap)
    
    fig = plt.gcf()
    fig.colorbar(sm, ax=ax)
    
    plt.show()
    

    which produces:

    enter image description here

    You'll likely want to play about with the actual positioning of the arrow start and end points.