Python plot vectors on maps using different projections#

The drawing of vectors on a map is often difficult when you use a map projection different than PlateCarree. In this notebook we give a brief overview and show what needs to be considered.

Software requirements:

  • Python 3

  • numpy

  • xarray

  • matplotlib

  • cartopy

Example script#

vectors_and_map_projections.py

#!/usr/bin/env python
# coding: utf-8
'''
------------------------------------------------------------------------------
DKRZ example

Vectors on different projections

The drawing of vectors on a map is often difficult when you use a map projection
different than PlateCarree. In this notebook we give a brief overview and show
what needs to be considered.

The base plot is a contour fill plot of the surface temperature data over which
the wind vectors are to be plotted.

Content

  - PlateCarree
  - Orthographic
  - NorthPolarStereo/SouthPolarStereo
  - Mollweide
  - Robinson

-------------------------------------------------------------------------------
2024 copyright DKRZ licensed under CC BY-NC-SA 4.0 <br>
               (https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en)
-------------------------------------------------------------------------------
'''
import xarray as xr
import numpy as np

import matplotlib.pyplot as plt
import matplotlib.path as mpath
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import cartopy.util as cutil

def main():
    #-- Open example data file
    infile = '../../data/rectilinear_grid_2D.nc'
    
    ds = xr.open_dataset(infile)
    
    #-- Assign coordinate and data variables for time step _t_.
    lon = ds.lon
    lat = ds.lat
    
    t = 0      #-- time index
    
    var = ds.tsurf.isel(time=t)
    u   = ds.u10.isel(time=t)
    v   = ds.v10.isel(time=t)
    
    #-- Colormap
    cmap = 'Spectral_r'
    
    #-- Coordinate reference systems (CRS)
    #
    # Read more about 'Understanding the transform and projection keywords' in the
    # Cartopy documentation: <br>
    # https://scitools.org.uk/cartopy/docs/latest/tutorials/understanding_transform.html
    #
    # For the data CRS we can use PlateCarree.
    data_crs = ccrs.PlateCarree()
    
    #-- Projections
    #
    # When using map projections the projection CRS has to be used with `plt.subplots()`, `ax.add_subplot()`, `ax.set_global()`, and `ax.set_extent()` (and if needed with the Cartopy features).
    #
    # The vector plots for various map projections are generated below.
    
    #-- PlateCarree
    #
    # Map projection CRS for the cylindrical equidistant map projection.
    projection = ccrs.PlateCarree()
    
    # Create contour fill plot and overlay the vectors
    plt.switch_backend('agg')
    
    fig, ax = plt.subplots(figsize=(8,8), subplot_kw=dict(projection=projection))
    
    ax.add_feature(cfeature.COASTLINE.with_scale('50m'), edgecolor='grey', linewidth=0.5)
    ax.gridlines(draw_labels=True, linewidth=0.5, color='gray',
                 xlocs=range(-180,180,30), ylocs=range(-90,90,30))
    
    ax.set_title('Wind velocity', fontsize=10, fontweight='bold')
    
    cnplot = ax.pcolormesh(lon, lat, var.values,
                           cmap=cmap,
                           transform=data_crs)
    
    incr = 2  #-- increment to thin out the vectors
    
    vplot = ax.quiver(lon[::incr], lat[::incr],
                  u[::incr,::incr].values,
                  v[::incr,::incr].values,
                  scale_units='xy',
                  scale=2,
                  angles='xy',
                  transform=data_crs)
    
    vref = ax.quiverkey(vplot, 0.9, 0.905, 20,
                        r'$20 \frac{m}{s}$',
                        labelpos='E',
                    coordinates='figure', zorder=5)
    
    #plt.savefig('plot_matplotlib_vector_rect.png', bbox_inches='tight', dpi=100)
    
    #-- Orthographic projection
    #
    # To use the Orthographic projection either `ax.set_global()` or `ax.set_extent()` has to be used.
    #
    # Map projection CRS
    projection = ccrs.Orthographic(central_longitude=0.,
                                   central_latitude=60.)
    
    fig, ax = plt.subplots(figsize=(6,6), subplot_kw=dict(projection=projection))
    
    ax.set_global()
    
    ax.set_title('Vectors on Orthographic projection', y=1.08)
    
    ax.add_feature(cfeature.COASTLINE.with_scale('50m'), edgecolor='grey', linewidth=0.5)
    ax.gridlines(draw_labels=True, linewidth=0.5, color='gray',
                 xlocs=range(-180,180,30), ylocs=range(-90,90,30))
    
    cnplot = ax.pcolormesh(lon, lat, var.values,
                           cmap=cmap,
                           transform=data_crs)
    
    incr = 2 #-- increment if you want to thin out the vectors
    
    vplot = ax.quiver(lon[::incr], lat[::incr],
                      u[::incr,::incr].values,
                      v[::incr,::incr].values,
                      angles='xy',
                      scale=200,
                      color='black',
                      transform=data_crs)
    
    vref = ax.quiverkey(vplot, 0.91, 1.06, 20,
                        r'$20 \frac{m}{s}$',
                        labelpos='E',
                        coordinates='axes',
                        color='k')
    
    #-- save to PNG
    plt.savefig('plot_matplotlib_vector_ortho.png', bbox_inches='tight', dpi=100)
    
    
    #-- Change the map projection CRS to zoom into the map.
    projection = ccrs.Orthographic(central_longitude=14.,
                                   central_latitude=68.)
    
    # Create the plot
    fig, ax = plt.subplots(figsize=(6,6), subplot_kw=dict(projection=projection))
    
    ax.set_extent([-10., 30., 50, 80.], crs=data_crs)
    
    ax.set_title('Vectors on Orthographic projection', y=1.08)
    
    ax.add_feature(cfeature.COASTLINE.with_scale('50m'), edgecolor='grey', linewidth=0.5)
    ax.gridlines(draw_labels=True, linewidth=0.5, color='gray',
                 xlocs=range(-180,180,30), ylocs=range(-90,90,30))
    
    cnplot = ax.pcolormesh(lon, lat, var.values,
                           cmap=cmap,
                           transform=data_crs)
    
    incr = 1 #-- increment if you want to thin out the vectors
    
    vplot = ax.quiver(lon[::incr], lat[::incr],
                      u[::incr,::incr].values,
                      v[::incr,::incr].values,
                      angles='xy',
                      scale=200,
                      color='black',
                      transform=data_crs)
    
    vref = ax.quiverkey(vplot, 0.91, 1.06, 20,
                        r'$20 \frac{m}{s}$',
                        labelpos='E',
                        coordinates='axes',
                        color='k')
    
    #-- save to PNG
    plt.savefig('plot_matplotlib_vector_ortho_zoom.png', bbox_inches='tight', dpi=100)
    
    #-- NorthPolarStereo / SouthPolarStereo
    #
    # Create the two plots using the polar projections at once.
    #
    #-- north polar
    
    # map projection CRS
    projection = ccrs.NorthPolarStereo()
    
    fig = plt.figure(figsize=(8,8))
    
    ax1 = fig.add_subplot(1, 2, 1, projection=projection)
    ax1.set_extent([-180., 180., 60., 90.], crs=data_crs)
    
    ax1.set_title('Vectors on NorthPolarStereo projection', y=1.08)
    
    ax1.add_feature(cfeature.COASTLINE.with_scale('50m'), edgecolor='grey', linewidth=0.5)
    ax1.gridlines(draw_labels=True, linewidth=0.5, color='gray',
                 xlocs=range(-180,180,30), ylocs=range(-90,90,30))
    
    cnplot1 = ax1.pcolormesh(lon, lat, var.values,
                           cmap=cmap,
                           transform=data_crs)
    incr = 2
    
    vplot1 = ax1.quiver(lon[::incr], lat[::incr],
                        u[::incr,::incr].values,
                        v[::incr,::incr].values,
                        angles='xy',
                        scale=400,
                        color='black',
                        transform=data_crs)
    
    vref1 = ax1.quiverkey(vplot1, 0.91, 1.06, 20,
                          r'$20 \frac{m}{s}$',
                          labelpos='E',
                          coordinates='axes',
                          color='k')
    
    # circular boundary of the map
    # (see https://scitools.org.uk/cartopy/docs/v0.15/examples/always_circular_stereo.html)
    theta  = np.linspace(0, 2*np.pi, 100)
    center = [0.5, 0.5]
    radius =  0.5
    verts  = np.vstack([np.sin(theta), np.cos(theta)]).T
    circle = mpath.Path(verts * radius + center)
    ax1.set_boundary(circle, transform=ax1.transAxes)
    
    #-- south polar
    
    # map projection CRS
    projection = ccrs.SouthPolarStereo()
    
    ax2 = fig.add_subplot(1, 2, 2, projection=projection)
    ax2.set_extent([-180., 180., -90., -60.], crs=data_crs)
    
    ax2.set_title('Vectors on SouthPolarStereo projection', y=1.08)
    
    ax2.add_feature(cfeature.COASTLINE.with_scale('50m'), edgecolor='grey', linewidth=0.5)
    ax2.gridlines(draw_labels=True, linewidth=0.5, color='gray',
                  xlocs=range(-180,180,30), ylocs=range(-90,90,30))
    
    cnplot2 = ax2.pcolormesh(lon, lat, var.values,
                             cmap=cmap,
                             transform=data_crs)
    
    vplot2 = ax2.quiver(lon[::incr], lat[::incr],
                        u[::incr,::incr].values,
                        v[::incr,::incr].values,
                        angles='xy',
                        scale=400,
                        color='black',
                        transform=data_crs)
    
    vref2 = ax2.quiverkey(vplot2, 0.91, 1.06, 20,
                          r'$20 \frac{m}{s}$',
                          labelpos='E',
                          coordinates='axes',
                          color='k')
    
    # circular boundary of the map
    ax2.set_boundary(circle, transform=ax2.transAxes)
    
    #-- Mollweide
    #
    # Map projection CRS
    projection = ccrs.Mollweide()
    
    # Create the plot
    fig, ax = plt.subplots(figsize=(8,8), subplot_kw=dict(projection=projection))
    
    ax.set_global()
    
    ax.set_title('Vectors on Mollweide projection', y=1.08)
    
    ax.add_feature(cfeature.COASTLINE.with_scale('50m'), edgecolor='grey', linewidth=0.5)
    ax.gridlines(draw_labels=True, linewidth=0.5, color='gray',
                 xlocs=range(-180,180,30), ylocs=range(-90,90,30),
                 x_inline=False, y_inline=False)
    
    cnplot = ax.pcolormesh(lon, lat, var.values,
                           cmap=cmap,
                           transform=data_crs)
    incr = 3
    
    vplot = ax.quiver(lon[::incr], lat[::incr],
                      u[::incr,::incr].values,
                      v[::incr,::incr].values,
                      angles='xy',
                      scale=400,
                      color='black',
                      transform=data_crs)
    
    vref = ax.quiverkey(vplot, 0.78, 1.05, 20,
                        r'$20 \frac{m}{s}$',
                        labelpos='E',
                        coordinates='axes',
                        color='k')
    #-- save to PNG
    plt.savefig('plot_matplotlib_vector_mollweide.png', bbox_inches='tight', dpi=100)
    
    #-- Robinson
    #
    # Map projection CRS
    projection = ccrs.Robinson()
    
    # Create the plot
    fig, ax = plt.subplots(figsize=(8,8), subplot_kw=dict(projection=projection))
    
    ax.set_global()
    
    ax.set_title('Vectors on Robinson projection', y=1.08)
    
    ax.add_feature(cfeature.COASTLINE.with_scale('50m'), edgecolor='grey', linewidth=0.5)
    ax.gridlines(draw_labels=True, linewidth=0.5, color='gray',
                 xlocs=range(-180,180,30), ylocs=range(-90,90,30),
                 x_inline=False, y_inline=False)
    
    cnplot = ax.pcolormesh(lon, lat, var.values,
                           cmap=cmap,
                           transform=data_crs)
    incr = 3
    
    vplot = ax.quiver(lon[::incr], lat[::incr],
                      u[::incr,::incr].values,
                      v[::incr,::incr].values,
                      angles='xy',
                      scale=400,
                      color='black',
                      transform=data_crs)
    
    vref = ax.quiverkey(vplot, 0.78, 1.08, 20,
                        r'$20 \frac{m}{s}$',
                        labelpos='E',
                        coordinates='axes',
                        color='k')
    

if __name__ == '__main__':
    main()

Plot result:

image0

image1