Search code examples
pythonpandasnumpydataframecartopy

How to put a label on a country with Python cartopy?


Using python3 and cartopy, having this code:

import matplotlib.pyplot as plt
import cartopy
import cartopy.io.shapereader as shpreader
import cartopy.crs as ccrs

ax = plt.axes(projection=ccrs.PlateCarree())
ax.add_feature(cartopy.feature.LAND)
ax.add_feature(cartopy.feature.OCEAN)
ax.add_feature(cartopy.feature.COASTLINE)
ax.add_feature(cartopy.feature.BORDERS, linestyle='-', alpha=.5)
ax.add_feature(cartopy.feature.LAKES, alpha=0.95)
ax.add_feature(cartopy.feature.RIVERS)

ax.set_extent([-150, 60, -25, 60])

shpfilename = shpreader.natural_earth(resolution='110m',
                                      category='cultural',
                                      name='admin_0_countries')

reader = shpreader.Reader(shpfilename)
countries = reader.records()

for country in countries:
    if country.attributes['SOVEREIGNT'] == "Bulgaria":
        ax.add_geometries(country.geometry, ccrs.PlateCarree(), facecolor=(0, 1, 0), label = "A")
    else:
        ax.add_geometries(country.geometry, ccrs.PlateCarree(), facecolor=(1, 1, 1), label = country.attributes['SOVEREIGNT'])
plt.rcParams["figure.figsize"] = (50,50)
plt.show()

I get this:

enter image description here

Question: What should I write, in order to get a red "A" over Bulgaria (or any other country, which I refer to in country.attributes['SOVEREIGNT'])? Currently the label is not shown at all and I am not sure how to change the font of the label. Thus, it seems that the following only changes the color, without adding the label:

ax.add_geometries(country.geometry, ccrs.PlateCarree(), facecolor=(0, 1, 0), label = "A")

Solution

  • You can retrieve the centroid of the geometry and plot the text at that location:

    import matplotlib.patheffects as PathEffects
    
    for country in countries:
    
        if country.attributes['SOVEREIGNT'] == "Bulgaria":
            g = ax.add_geometries(country.geometry, ccrs.PlateCarree(), facecolor=(0, 1, 0), label="A")
    
            x = country.geometry.centroid.x        
            y = country.geometry.centroid.y
    
            ax.text(x, y, 'A', color='red', size=15, ha='center', va='center', transform=ccrs.PlateCarree(), 
                    path_effects=[PathEffects.withStroke(linewidth=5, foreground="k", alpha=.8)])
    
        else:
            ax.add_geometries(country.geometry, ccrs.PlateCarree(), facecolor=(1, 1, 1), label = country.attributes['SOVEREIGNT'])
    

    With the extent focused on "Bulgaria" it looks like:

    enter image description here

    edit:

    To get "dependencies" separate, consider using the admin_0_map_units instead of admin_0_map_countries, see the Natural Earth documentation .

    To highlight small countries/regions you could add a buffer to the geometry with something like:

    highlight = ['Singapore', 'Liechtenstein']
    
    for country in countries:
    
        if country.attributes['NAME'] in highlight:
    
            if country.geometry.area < 2:
                geom = [country.geometry.buffer(2)]
            else:
                geom = [country.geometry]
    
            g = ax.add_geometries(geom, ccrs.PlateCarree(), facecolor=(0, 0.5, 0, 0.6), label="A", zorder=99)
    
            x = country.geometry.centroid.x        
            y = country.geometry.centroid.y
    
            ax.text(x, y+5, country.attributes['NAME'], color='red', size=14, ha='center', va='center', transform=ccrs.PlateCarree(), 
                    path_effects=[PathEffects.withStroke(linewidth=3, foreground="k", alpha=.8)])
    
        else:
            ax.add_geometries(country.geometry, ccrs.PlateCarree(), facecolor=(1, 1, 1), label=country.attributes['NAME'])
    

    enter image description here

    You could split a specific country with something like this, It uses Shapely to perform an intersection at the middle of the geometry. Ultimately it might be "cleaner" to separate the plotting and spatial analysis (splitting etc) in to more distinct steps. Mixing it like this probably makes it harder to re-use the code for other cases.

    from shapely.geometry import LineString, MultiLineString
    
    for country in countries:
    
        if country.attributes['NAME'] in 'China':
    
            # line at the centroid y-coord of the country
            l = LineString([(-180, country.geometry.centroid.y), 
                            (180, country.geometry.centroid.y)])
    
            north_poly = MultiLineString([l, north_line]).convex_hull
            south_poly = MultiLineString([l, south_line]).convex_hull
    
            g = ax.add_geometries([country.geometry.intersection(north_poly)], ccrs.PlateCarree(), facecolor=(0.8, 0.0, 0.0, 0.4), zorder=99)
            g = ax.add_geometries([country.geometry.intersection(south_poly)], ccrs.PlateCarree(), facecolor=(0.0, 0.0, 0.8, 0.4), zorder=99)
    
            x = country.geometry.centroid.x        
            y = country.geometry.centroid.y
    
            ax.text(x, y, country.attributes['NAME'], color='k', size=16, ha='center', va='center', transform=ccrs.PlateCarree(), 
                    path_effects=[PathEffects.withStroke(linewidth=5, foreground="w", alpha=1)], zorder=100)
    
        else:
            ax.add_geometries(country.geometry, ccrs.PlateCarree(), facecolor=(1, 1, 1), label=country.attributes['NAME'])
    

    enter image description here