© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
A. DanialPython for MATLAB Developmenthttps://doi.org/10.1007/978-1-4842-7223-7_12

12. Plotting

Albert Danial1  
(1)
Redondo Beach, CA, USA
 

MATLAB is well known for the ease with which one can visualize numeric data. Similar capability is available for Python, but before starting one must choose which among many excellent plotting modules to use. Standouts are

  • Altair

  • Bokeh

  • matplotlib

  • Plotly

  • Seaborn

Altair allows one to produce complex plots with relatively little code; Bokeh and Plotly excel at creating interactive, web-based plots; Seaborn is built on matplotlib and has a refined visual style and shortcut functions that create relatively complex plots, notably for statistics work. I chose the oldest among these, matplotlib, for several reasons: its commands and plotting paradigm most closely resemble MATLAB’s; it is the underlying plotting package for Cartopy (Section 12.4) and Pandas (Chapter 13); and Seaborn enhancements can be added at any time.

12.1 Point and Line Plots

Aside from the additional imports , commands to plot a parabola in Python look similar to doing the same in MATLAB. The result looks about the same, too:

MATLAB:

Python:

x=-10:10;

y= x .^2 - 36;

plot(x,y)

import numpy as np

import matplotlib.pyplot as plt

x = np.arange(-10,11)

y = x**2 – 36

plt.plot(x,y)

plt.show()

A notable difference between the two is that, by default, MATLAB automatically opens a display window to show the plot being created, while Python shows nothing unless an explicit plt.show() is invoked. MATLAB’s behavior is convenient when working interactively. However, if you run a MATLAB m-file with long computations, the plot window that does eventually appear may “click-jack” your mouse if you are actively working in another window. This is especially onerous when your code creates plots in a loop causing a long stream of windows to keep popping up. A Python program, on the other hand, will not show the plot if you forget the final plt.show() (or plt.savefig() if you write an image file rather than display it). The same is true when working interactively with ipython. However, IDEs and other graphical Python work environments like PyCharm, Spyder, and Jupyter notebooks work like the MATLAB IDE and will show plots immediately.
Figure 12-1

MATLAB

Figure 12-2

Python

matplotlib commands to add titles, axis labels, and legends closely resemble their MATLAB counterparts. Additionally, for simple plots, one need only reference matplotlib.pyplot, usually imported as plt, commands directly:

MATLAB:

Python:

t=linspace(0,10*pi,300);

decay = exp(-t/10);

osc = decay .* sin(t);

pts = -decay(1:10:end);

plot(t,osc,'g')

hold on

plot(t,decay,'r')

scatter(t(1:10:end),pts)

grid on

hold off

title('Decaying sinusoid')

xlabel('Time [seconds]')

ylabel('exp(-t/10)*sin(t)')

saveas(1,"decay.png")

import numpy as np

import matplotlib.pyplot as plt

t = np.linspace(0,10*np.pi,num=300)

decay = np.exp(-t/10)

osc = decay*np.sin(t)

pts = -decay[::10]

plt.plot(t,osc,'g')

plt.plot(t,decay,'r')

plt.scatter(t[::10],pts)

plt.grid()

plt.title('Decaying sinusoid')

plt.xlabel('Time [seconds]')

plt.ylabel('exp(-t/10)*sin(t)')

plt.savefig('decay.png')

plt.show() # optional

12.1.1 Saving Plots to Files

Matplotlib’s function to save plots to PNG, JPEG, and other formats is plt.savefig(). It resembles MATLAB’s saveas(), print(), and exportgraphics() functions but has an advantage in that it supports transparent backgrounds for PNG files—a desirable property for inclusion in PowerPoint presentations, Word documents, websites, ebooks, and PDF. MATLAB only supports transparency for plots saved as PDF files, a much slower process than writing PNG or JPEG files.

The image file formats supported by matplotlib can be displayed with plt.gcf().canvas.get_supported_filetypes():
In : plt.gcf().canvas.get_supported_filetypes()
Out:
Figure 12-3

MATLAB

Figure 12-4

Python

{'ps': 'Postscript',
 'eps': 'Encapsulated Postscript',
 'pdf': 'Portable Document Format',
 'pgf': 'PGF code for LaTeX',
 'png': 'Portable Network Graphics',
 'raw': 'Raw RGBA bitmap',
 'rgba': 'Raw RGBA bitmap',
 'svg': 'Scalable Vector Graphics',
 'svgz': 'Scalable Vector Graphics',
 'jpg': 'Joint Photographic Experts Group',
 'jpeg': 'Joint Photographic Experts Group',
 'tif': 'Tagged Image File Format',
 'tiff': 'Tagged Image File Format'}
Most of the images in this book were saved with a command such as
plt.savefig('plot.png', dpi=200, bbox_inches='tight',
               pad_inches=0.1, transparent=True)
Tip

If you want your Python program to display a plot to the screen and save it to a file, call plt.savefig() before plt.show(); otherwise, the image file will be empty.

12.1.2 Multiple Plots per Figure

Matplotlib’s subplots() function is similar to MATLAB’s subplot() since both take the desired number of rows and columns of smaller plots as the first two arguments. The MATLAB function’s third argument is the row-major offset to the plot being drawn, while matplotlib’s function returns an array of axis handles:

MATLAB:

Python:

t = -10*pi:0.1:10*pi;

C = {{'blue' , 'red',}, ...

     {'green', 'black'}};

figure

i = 1;

for r = 0:1

  for c = 0:1

    subplot(2, 2, i)

    i = i+1;

    k = 1 + (1.1*r + 1.5*c)/10;

    y = sin(t) + sin(k*t);

    plot(t,y,C{r+1}{c+1})

    xlabel('t')

    ylabel('y')

    title(sprintf(...

                'r=%d, c=%d',r,c))

    grid on

  end

end

import numpy as np

import matplotlib.pyplot as plt

t = np.arange(-10*np.pi, 10*np.pi, 0.1)

C = [['blue' , 'red',],

     ['green', 'black']]

fig, ax = plt.subplots(nrows=2,

    ncols=2, constrained_layout=True)

for r in [0, 1]:

  for c in [0, 1]:

    k = 1 + (1.1*r + 1.5*c)/10

    y = np.sin(t) + np.sin(k*t)

    ax[r,c].plot(t,y,color=C[r][c])

    ax[r,c].set_xlabel('t')

    ax[r,c].set_ylabel('y')

    ax[r,c].set_title(

       f'r={r}, c={c}', fontsize=14)

    ax[r,c].grid(True)

plt.show()

Figure 12-5

MATLAB

Figure 12-6

Python

12.1.3 Date and Time on the X Axis

Date and time annotations on the x axis can be tricky because they take so much space. MATLAB and matplotlib in Python allow fine control over the date string formatting as well as rotated axis labels to prevent overlaps. MATLAB’s time markers are aligned to noon, while matplotlib’s are aligned to midnight.

MATLAB:

Python:

t = .1:.01:.8;

y = sin(3./t).*sin(5./(1-t));

S = 30*24*60*60*t + 1620000000;

dates = arrayfun(@(x) datetime(...

    x,'ConvertFrom','posix'), S);

scatter(dates,y,'v','filled')

grid()

xtickformat('yyyy-MM-dd hh:mm')

xtickangle(30)

import numpy as np

import matplotlib.pyplot as plt

import matplotlib.dates as mdates

from datetime import datetime

t = np.arange(.1, .8, .01)

y = np.sin(3/t)*np.sin(5/(1-t))

S = 30*24*60*60*t + 1620000000

dates = [datetime.fromtimestamp(_)

            for _ in S]

fig, ax = plt.subplots()

ax.scatter(dates,y,marker=11)

plt.grid()

ax.xaxis.set_major_formatter(

    mdates.DateFormatter('%Y-%m-%d %H:%S'))

fig.autofmt_xdate()

Figure 12-7

MATLAB

Figure 12-8

Python

12.1.4 Double Y Axes

Competing unit systems, for example, metric vs. US customary, can be shown simultaneously by labeling the left and right vertical axes with the different units. This example shows the modulus of elasticity, commonly known as Young’s modulus , for several metals1 in gigapascals (GPa) on the left and megapounds per square inch (Mpsi) on the right. This is somewhat easier in MATLAB with yyaxis left and yyaxis right commands which define the vertical axis on which subsequent commands operate. In matplotlib, we create a new vertical axis object by making a copy of the first one, then modify the copy with properties of the second unit system.

MATLAB:

Python:

% file: code/plots/double_y_axis.m

yL = [68   108 112 116 193 200];

yR = [9.86 15.7 16.2 16.8 28 29];

yLmax = max(yL) * 1.1;

materials = {'aluminum',...

  'zinc', 'bronze', 'titanium',...

  'iron', 'steel'};

bar(yL)

yyaxis left

ylabel('GPa')

ylim([0 yLmax])

yyaxis right

ylabel('Mpsi')

ylim([0 yLmax/6.8965])

xticklabels(materials)

grid()

title("Young's Modulus")

# file: code/plots/double_y_axis.py

import numpy as np

import matplotlib.pyplot as plt

fig, ax = plt.subplots()

yL = [68,   108, 112, 116, 193, 200]

yR = [9.86, 15.7, 16.2, 16.8, 28, 29]

yLmax = np.max(yL) * 1.1

materials = ['aluminum', 'zinc',

  'bronze', 'titanium',

  'iron', 'steel']

P = ax.bar(materials, yL)

ax.set_ylabel('GPa')

ax2 = ax.twinx()

ax2.set_ylabel('Mpsi')

ax.set_ylim(ymax=yLmax)

ax2.set_ylim(ymax=yLmax/6.8965)

ax.grid(True)

ax.set_axisbelow(True)

plt.title("Young's Modulus")

plt.show()

Figure 12-9

MATLAB

Figure 12-10

Python

12.1.5 Histograms

The following examples display histograms of normal and Rayleigh distributions. The MATLAB functions to produce these, normrnd() and raylrnd(), are in the Statistics and Machine Learning Toolbox. I don’t have access to that, so instead in MATLAB I’ll call the equivalent functions from NumPy. By using the same source of random numbers in MATLAB and Python, I can also initialize the same seed to produce identical distributions in both languages.

MATLAB:

Python:

% file: code/plots/hist_demo.m

Im = @py.importlib.import_module;

rnd = Im('numpy.random');

N = int64(10000);

rnd.seed(int64(123));

w1 = rnd.rayleigh(int64(3), N);

w1 = py2mat(w1);

w2 = rnd.normal(int64(8),...

                int64(3), N);

w2 = py2mat(w2) ;

bins = linspace(-5, 20, 80);

h1 = histogram(w1,bins);

set(h1,'FaceAlpha',0.5,...

       'EdgeAlpha',0.0);

hold on

h2 = histogram(w2,bins);

set(h2,'FaceAlpha',0.5,...

       'EdgeAlpha',0.0);

legend('Rayleigh','Normal');

grid on

# file: code/plots/hist_demo.py

import numpy as np

import matplotlib.pyplot as plt

N = 10000

np.random.seed(123)

w1 = np.random.rayleigh(3, N)

w2 = np.random.normal(8, 3, N)

bins = np.linspace(-5, 20, 80)

plt.hist(w1, bins, alpha=0.5,

         label="Rayleigh")

plt.hist(w2, bins, alpha=0.5,

         label="Normal")

plt.legend(loc='upper right')

plt.ylim([0, 700])

plt.grid(True)

plt.show()

Figure 12-11

MATLAB

Figure 12-12

Python

12.1.6 Stack Plots

Stack (or sand) plots can be created with area() in MATLAB and stackplot() in matplotlib. (MATLAB has a stackedplot() function, but its purpose is to create multiple vertically aligned plots that share one x axis.)

MATLAB:

Python:

% file: code/plots/stack_demo.m

t = linspace(0, 20, 100);

w = [0.11, 0.25, 0.17, 0.32];

L = {};

y = zeros(length(t),length(w));

for i = 1:length(w)

    F = w(i);

    y(:,i) = cos(F*t).ˆ2 + F;

    L{i} = sprintf(...

        '\omega = %.2f', F);

end

area(t, y)

legend(L);

xlabel('omega')

grid on

# file: code/plots/stack_demo.py

import numpy as np

import matplotlib.pyplot as plt

t = np.linspace(0, 20, 100)

w = [0.11, 0.25, 0.17, 0.32]

L = [f'$omega$ = {_:.2f}'

     for _ in w]

y = np.zeros((len(w),len(t)))

for i,F in enumerate(w):

    y[i,:] = np.cos(F*t)**2 + F

fig, ax = plt.subplots()

ax.stackplot(t, y, labels=L)

ax.legend(loc='upper right')

ax.set_xlabel('$omega$')

plt.grid()

ax.set_axisbelow(True)

plt.show()

Figure 12-13

MATLAB

Figure 12-14

Python

12.2 Area Plots

12.2.1 imshow()

The simplest two-dimensional plot is a direct rendering of a two-dimensional numeric array using a colormap to color individual array elements. Both MATLAB and matplotlib can display 2D arrays as images using a function named imshow() . MATLAB’s imshow(), however, displays the array so that each array element maps to one screen pixel; a 5 × 5 array appears microscopically small on most screens. The optional argument pair 'InitialMagnification','fit' is needed to scale small arrays to reasonably sized images. MATLAB’s imagesc() will scale the array to a comfortable size, but it doesn’t offer different interpolation methods as imshow().

This first example shows imagesc() in MATLAB and imshow() in matplotlib for a 16 × 16 array. (See Section 11.​1.​13 for a review of np.newaxis.) matplotlib conveniently sets a 1:1 aspect ratio:

MATLAB:

Python:

% file: code/plots/imshow_demo.m

x = 0:0.2:pi;

S = sin(x);

z = S .* S';

imagesc(z)

colorbar

# file: code/plots/imshow1_demo.py

import numpy as np

import matplotlib.pyplot as plt

x = np.arange(0,np.pi,0.2)

S = np.sin(x)

z = S[:,np.newaxis] * S[np.newaxis,:]

hz = plt.imshow(z)

plt.colorbar(hz)

Figure 12-15

MATLAB

Figure 12-16

Python

The next example uses imshow() with bilinear interpolation in both MATLAB and matplotlib. The MATLAB image would appear in grayscale without the explicit colorbar argument.

MATLAB:

Python:

% file: code/plots/imshow_demo.m

x = 0:0.2:pi;

S = sin(x);

z = S .* S';

imshow(z, 'Interpolation', ...

  'bilinear','Colormap',    ...

  parula,                   ...

  'InitialMagnification','fit')

colorbar

# file: code/plots/imshow2_demo.py

import numpy as np

import matplotlib.pyplot as plt

x = np.arange(0,np.pi,0.2)

S = np.sin(x)

z = S[:,np.newaxis] * S[np.newaxis,:]

hz = plt.imshow(z,

        interpolation="bilinear")

plt.colorbar(hz)

More specialized plots in Python require the user to obtain handles to separate figure and axis objects, however. Note the different prefixes plt., ax., and fig. before function names in the following contour example:
Figure 12-17

MATLAB

Figure 12-18

Python

MATLAB:

Python:

N = 500;

rN = 1:N ;

[J, I] = meshgrid(rN, rN);

x = -10 + 20*J/N;

y = -10 + 20*I/N;

z = cos(2*x/3).*sin(y/3)./ ...

       (100 + x.ˆ2+y.ˆ2);

contourf(z)

daspect([1 1 1])

colorbar()

title('Filled Contour')

saveas(1,"m_contourf.png")

import numpy as np

import matplotlib.pyplot as plt

N = 500; rN = range(N)

J, I = np.meshgrid(rN, rN)

x = -10 + 20*J/N

y = -10 + 20*I/N

fig, ax = plt.subplots()

ax.set_aspect('equal')

z = np.cos(2*x/3)*np.sin(y/3)/(

        100 + x**2+y**2)

cf = ax.contourf(z)

fig.colorbar(cf)

ax.set_title('Filled Contour')

plt.savefig('contourf.png')

Figure 12-19

MATLAB

Figure 12-20

Python

12.3 Animations

Matplotlib can animate plots similarly to MATLAB. Unless the animations are simple though, it can be a challenge to avoid using global variables. We’ll begin with a simple program that draws an ellipse whose width changes with every frame. All the action is encapsulated in one function, and no global variables are required.

Key elements of the animation are A_values, the third argument to FuncAnimation() on line 23, and the update() function on line 18. A_values is a vector of coefficients that will be used to scale the width of the ellipse; its values range from 0.9 down to 0.1 and then back up to 0.9. Each time update() is called, the next coefficient from A_values is taken, and the ellipse’s image is redrawn.

Python:
 1   # file: code/plots/circle_ellipse.py
 2   import numpy as np
 3   import matplotlib.pyplot as plt
 4   from matplotlib.animation import FuncAnimation
 5   def circle_to_ellipse():
 6       fig, ax = plt.subplots()
 7       T = np.linspace(0, 2*np.pi, 200)
 8       x = np.cos(T)
 9       y = np.sin(T)
10       curve, = plt.plot(x,y,'b')
11       A_values = np.hstack([np.linspace(0.1, 0.9, 100),
12                             np.linspace(0.9, 0.1, 100)])
13       def init():
14           ax.set_xlim(-1, 1)
15           ax.set_ylim(-1, 1)
16           ax.set_aspect('equal')
17           return curve,
18       def update(A):
19           x = A*np.cos(T)
20           y = np.sin(T)
21           curve, = plt.plot(x,y,'b')
22           return curve,
23       ani = FuncAnimation(fig, update, A_values,
24                           interval=20, # milliseconds
25                           init_func=init, blit=True)
26       plt.show()
27
28   circle_to_ellipse()

The second animation example shows a ball bouncing in a parabolic bowl. It is considerably more complex and uses global variables to store the ball’s evolving trajectory.

Python:
# file: code/plots/bouncing_ball.py
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from copy import copy
A  = 0.08 # bowl shape: y = A*xˆ2
dt = 1.0/500 # time step, seconds
T  = np.arange(0, 20, dt)
n_iter = len(T)
damping = 0.9997
restitution = 0.995
ball_radius = 0.3
history_x, history_y = [], []
point, trace = None, None
def timestep(pos,vel):
    new_pos = copy(pos)
    new_vel = copy(vel)
    new_vel[1] += -9.8*dt
    new_vel *= damping
    new_pos[0] += new_vel[0]*dt
    new_pos[1] += new_vel[1]*dt
    bowl_y = A*new_pos[0]**2
    if new_pos[1] < bowl_y:
        # hit the bowl, bounce back
        normal = np.array([-2*A*new_pos[0],1])
        normal /= np.linalg.norm(normal)
        new_vel += -2*new_vel.dot(normal)*normal
        new_pos[1] = bowl_y
    return new_pos, new_vel
def run_simulation():
    nX = 21
    pos = np.zeros((n_iter,2))
    pos[0,:] = np.array([-3., 3.]) # initial position, m
    vel = np.array([0.5, 0])       # initial velocity, m/s
    bowl = np.zeros((nX,2))
    bowl[:,0] = np.linspace(-3.5, 3.5, num=nX)
    X = bowl[:,0]
    bowl[:,1] = A*X**2
    # centerline is the curve offset from the parabolic
    # bowl by the radius of the ball; it is where the
    # ball's center appears to bounce
    den = 1/np.sqrt(1 + (2*A*X)**2)
    centerline = np.zeros((nX,2))
    centerline[:,0] = X + 2*A*ball_radius*X*den
    centerline[:,1] = A*X**2 - ball_radius*den
    for i in range(1,n_iter):
        pos[i], vel = timestep(pos[i-1],vel)
    return bowl, centerline, pos, vel
def init(bowl, centerline):
    global point, trace
    fig, ax = plt.subplots()
    plt.plot(bowl[:,0],bowl[:,1],':')
    plt.plot(centerline[:,0],centerline[:,1])
    plt.ylim([-1,4])
    ax.set_aspect('equal')
    point, = ax.plot([], [], 'o', markersize=25)
    trace, = ax.plot([], [], ',-', lw=1)
    return fig
def animate(i):
    global history_x, history_y
    if i == 0:
        history_x.clear()
        history_y.clear()
    history_x.append(pos[i,0])
    history_y.append(pos[i,1])
    point.set_data(pos[i,0], pos[i,1])
    trace.set_data(history_x, history_y)
    return point, trace
bowl, centerline, pos, vel = run_simulation()
fig = init(bowl, centerline)
ani = animation.FuncAnimation(
    fig, animate, n_iter, interval=dt*1000, blit=True)
plt.show()

12.4 Plotting on Maps with Cartopy

Python’s Cartopy module, developed by the UK’s Meteorological Office, provides a vast collection of cartographic transformation and projection functions that work with matplotlib. It is as feature-rich as the Mapping Toolbox for MATLAB. Several Cartopy submodules make use of web-based map tile services, so a connection to the Internet is desirable for making imagery-enhanced plots.

As with all plotting packages, the easiest way to start using Cartopy is to view its gallery2 and modify working examples.

We’ll begin our exploration of plotting on maps with point and line data in Cartopy and with MATLAB’s family of geo* functions. More complex geographical overlay plots in MATLAB need the Mapping Toolbox though. Alternatively, we can call Cartopy from MATLAB; this will be demonstrated with recipes to overlay sea surface temperature on a globe (Section 12.6) and draw a map of South America with countries shaded in proportion to their wheat production (Section 12.7).

12.4.1 Points

MATLAB’s geo* functions work with tables, covered in the next chapter, as well as regular matrix variables. This example shows two dots are overlaid on a high-resolution Paris city map, using a table in MATLAB and scalar variables in Python. The red dot is over the Notre Dame cathedral and the blue dot over the Pantheon.

Tip

Use Google Earth3 to find latitude and longitude bounds for your maps (calls to ax.set_extent()).

MATLAB:

Python:

% code/plots/paris_pt.m

T = table([48.853;48.846], ...

          [2.3497;2.3464],...

      'VariableNames', ...

      {'Lat','Lon'}, ...

      'RowNames', {'Notre Dame',

                   'Pantheon'});

C = [1 0 0; 0 0 1];

geoscatter(T.Lat, T.Lon,124,...

           C, 'Filled')

geolimits([48.8425 48.8625], ...

          [2.3283 2.3722])

geobasemap streets

title("Notre Dame (red) and " + ...

      "the Pantheon (blue)")

#!/usr/bin/env python3

# code/plots/cartopy_pt.py

import matplotlib.pyplot as plt

import numpy as np

import cartopy.crs as ccrs

from cartopy.io.img_tiles import OSM

imagery = OSM()

PC = ccrs.PlateCarree()

fig = plt.figure()

ax = fig.add_subplot(1, 1, 1,

        projection=imagery.crs)

ax.set_extent([2.3283, 2.3722,

               48.8425, 48.8625],

               crs=PC)

ax.add_image(imagery, 14)

ax.plot(2.3497, 48.853, transform=PC,

        marker="o", color="red",

        markersize=6)

ax.plot(2.3464, 48.846, transform=PC,

        marker="o", color="blue",

        markersize=6)

ax.set_title('Notre Dame (red) and '

             'the Pantheon (blue)')

plt.savefig('Paris.png')

plt.show()

Figure 12-21

MATLAB

Figure 12-22

Python

A commonly seen error relates to downloading map tiles from the Internet:
<urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed:
  unable to get local issuer certificate (_ssl.c:1125)>

Numerous explanations and workarounds can be found on the Internet, but for the specific case of getting map tiles for Cartopy, I’ve found the most reliable solution to be modifying Cartopy’s source code directly to use the requests module instead of urllib. Details appear in Section 12.4.4.

12.4.2 Lines

In this example, we plot the path of a whale known as “Blue Whale 158390” between September 21, 2018, and November 18, 2018. The location data4 must have a significant error margin because one of the data points, 48.64328° N, 66.47227° W, is in a field in New Brunswick, Canada, about 60 km inland. This corresponds to roughly 0.5° of latitude or longitude at the equator. In Python, we’ll use this information to overlay in blue a region 0.5° wide indicating the error bound on the whale’s path. The MATLAB code, while elegant in its extreme simplicity, has no mechanism to control the width of lines drawn with geoplot() so its figure lacks this feature.

This example also shows how additional detail—latitude and longitude grid lines, country borders, rivers—is added to Cartopy maps.

The numeric values for latitude and longitude returned by whale_data() are in data/plots/whale_path.txt.

MATLAB:

Python:

% code/plots/whale.m

P = importdata('whale_path.txt');

geoplot(P(:,2), P(:,1),'r')

geolimits([30 55], [-80 -50])

geobasemap colorterrain

title("Blue Whale 158390, " + ...

      "Fall 2018")

#!/usr/bin/env python3

# code/plots/cartopy_whale.py

import numpy as np

import shapely.geometry as sgeom

import cartopy.crs as ccrs

import cartopy.feature as cfeature

import matplotlib.pyplot as plt

fig = plt.figure()

PC = ccrs.PlateCarree()

ax = fig.add_subplot(1, 1, 1, projection=PC)

ax.coastlines(resolution='50m')

ax.set_extent([-80, -50, 30, 55], crs=PC)

ax.add_feature(cfeature.LAND)

ax.add_feature(cfeature.OCEAN)

ax.add_feature(cfeature.COASTLINE)

ax.add_feature(cfeature.BORDERS, linestyle='-')

ax.add_feature(cfeature.LAKES.with_scale('50m'),

                alpha=0.5)

ax.add_feature(cfeature.RIVERS.with_scale('50m'))

plt.title('Blue Whale 158390, Fall 2018')

path = np.loadtxt('whale_path.txt')

track = sgeom.LineString(path)

error = track.buffer(.5) # degrees

ax.gridlines(draw_labels=True)

ax.add_geometries([error], ccrs.PlateCarree(),

        facecolor='#1F18F4', alpha=.5)

ax.add_geometries([track], ccrs.PlateCarree(),

        facecolor="none", edgecolor="r")

plt.savefig('whale.png', bbox_inches="tight",

        pad_inches=0, transparent=True)

plt.show()

Figure 12-23

MATLAB

Figure 12-24

Python

If you need the fine control over geographic lines in your MATLAB plots that geoplot() can’t give you, see Recipe 12.5, which shows how to reproduce the Cartopy plot on the right from MATLAB.

12.4.3 Area

The majority of data overlaid on maps spans areas rather than points or lines. There’s an endless list of these: earth science data such as land and sea surface temperatures, land surface type, vegetation index, and rainfall; commercial data such as agricultural production and gross domestic production; political data such as voting results, party affiliation, and conflict zones.

Native MATLAB options for overlaying area data on maps are the Mapping Toolbox and the freely available Climate Data Toolbox.5 In Python, we’ll continue with Cartopy.

12.4.3.1 Sea Surface Temperature

Our first example shows global sea surface temperature using a pair of projections. The plot is enhanced by using a custom background image of the earth from NASA’s Earth Observations6 collection—the vegetation index in this case. To do this, save the vegetation index image file, earth_veg_index.jpeg , and accompanying JSON metadata to a directory, then set the environment variable CARTOPY_USER_BACKGROUNDS to this directory.7

The sea surface temperature (SST) data comes from NOAA’s collection of daily AVHRR satellite data.8 Each day’s data is in a NetCDF file (Section 7.​10) containing SST on a 0.5° latitude and longitude grid. The first row and column correspond to latitude –89.875° and longitude 0.125°; the equator is spanned by rows 359 and 360 (using zero-based indexing), while the international date line is spanned by columns 719 and 720.

Python:
#!/usr/bin/env python3
# code/plots/cartopy_sst.py
import os
import numpy as np
import matplotlib.pyplot as plt
from netCDF4 import Dataset
import cartopy.crs as ccrs
from mpl_toolkits.axes_grid1 import make_axes_locatable
os.environ["CARTOPY_USER_BACKGROUNDS"] = "cartopy/background"
File = 'oisst-avhrr-v02r01.20190101.nc'
rg = Dataset(File, 'r')
lat = rg["lat"][:]
lon = rg["lon"][:]
sst_ds = rg["sst"][0][0]
sst = sst_ds.data
mask = sst_ds.mask
sst[mask] = np.NaN
fig = plt.figure()
Ortho = ccrs.Orthographic(-10, 15)
PC = ccrs.PlateCarree()
ax = fig.add_subplot(1, 1, 1, projection=Ortho)
#         - or -
# ax = fig.add_subplot(1, 1, 1, projection=PC)
ax.coastlines()
ax.set_global()
ax.gridlines()
h = ax.contourf(lon, lat, sst, levels=20,
    vmin=-2, vmax=35, transform=PC, cmap='jet')
divider = make_axes_locatable(ax)
ax_cb = divider.new_horizontal(size="5%", pad=0.1,
    axes_class=plt.Axes)
fig.add_axes(ax_cb)
plt.colorbar(h, cax=ax_cb)
ax.background_img(name='VI', resolution='low')
ax.set_title('SST 2020-01-02 [Celcius]')
plt.savefig('sst_globe.png', bbox_inches='tight',
    pad_inches=0, transparent=True)
plt.show()

Many projections are available. An orthographic projection over –10° longitude and 15° latitude can be produced by replacing the PlateCarree projection with

Python:
Ortho = ccrs.Orthographic(-10, 15)
ax = fig.add_subplot(1, 1, 1, projection=Ortho)

Create this plot in MATLAB with the recipe in Section 12.6.

12.4.3.2 South American Wheat Production, 2014

Our second example shows data bounded by country borders. Our dataset will be South American wheat production from 2014.9 The goal is to shade each country in proportion to its production values.

While Cartopy comes with a database of shore lines, it does not include country or state borders. Ideally, we would like to say something like plot(region(country='Canada'), 'grey') or plot(region(zipcode=90210), 'orange'), but such a capability does not exist. (Spoiler: GeoPandas, covered in Section 13.​12, can do this.)

Instead, we will create our own database of country borders by downloading shape files and associated metadata from the Natural Earth project. The “low resolution” file10 contains Cartopy-readable files that contain country borders, country names in a dozen languages, name of the containing continent, economic status, and population and GDP estimates, among other pieces of information. However, the burden is on us to acquire the data, read it, and store it in a convenient form.

The following Python code expects the file ne_110m_admin_0_countries.zip to have been expanded into a subdirectory called nat_earth. We’ll use Cartopy’s shape reader to load both the country shapes and metadata and from these construct a dictionary, shp, storing the English country names in keys and border geometry as the associated values. For fun, we’ll populate a second dictionary, rec, storing each country’s metadata.

Python:
In : from cartopy.io.shapereader import Reader
In : shp = {} # country shape indexed by name
In : rec = {} # country metadata indexed by name
In : Countries = Reader('nat_earth/ne_110m_admin_0_countries')
In : for geom, meta in zip(Countries.geometries(), Countries.records()):
...:     name = meta.attributes['NAME_EN']
...:     shp[name] = geom
...:     rec[name] = meta

Exploring these dictionaries reveals interesting information:

Python:
In : sorted( shp.keys() )
Out: shp.keys()
['Afghanistan',
 'Albania',
 'Algeria',
 'Angola',
 'Antarctica',
 'Argentina',
 ...
 'Vietnam',
 'Western Sahara',
 'Yemen',
 'Zambia',
 'Zimbabwe',
 'eSwatini']
In : rec['Vietnam']
Out[9]: <Record: <shapely.geometry.polygon.Polygon object at 0x7fe5f7055220>,
{'featurecla': 'Admin-0 country', 'scalerank': 1, 'LABELRANK': 2,
'SOVEREIGNT': 'Vietnam', 'POSTAL': 'VN', 'MAPCOLOR7': 5, 'MAPCOLOR8': 6,
 'POP_EST': 96160163, 'POP_RANK': 16, 'GDP_MD_EST': 594900.0,
 'POP_YEAR': 2017, 'LASTCENSUS': 2009, 'GDP_YEAR': 2016,
 ...
[ins] In [12]: rec['Vietnam'].attributes['POP_EST']
Out[12]: 96160163

More importantly, the matplotlib + Cartopy combination understands polygonal regions created from shape files and thus allows countries to be shaded with desired colors. There is one subtlety with polygons made from geopolitical borders: some borders are individual polygons, while others (e.g., the islands of Indonesia) have multiple disjointed polygons. The matplotlib add_geometries() function , used to add regions defined by polygonal borders, expects a list of polygons as the first argument. Therefore, we must first check to see if a country’s shape is a single polygon or a list of polygons; if it is a single, the calling argument to add_geometries() must be extended to a one-item list. Line 42 in the following listing shows how to handle these cases appropriately:

Python:
 1   #!/usr/local/anaconda3/2020.07/bin/python
 2   # code/plots/cartopy_wheat.py
 3   import numpy as np
 4   import cartopy.crs as ccrs
 5   import cartopy.feature as cfeature
 6   from cartopy.io.shapereader import Reader
 7   from shapely.geometry.polygon import Polygon
 8   import matplotlib.pyplot as plt
 9   from matplotlib.colors import from_levels_and_colors
10   from mpl_toolkits.axes_grid1 import make_axes_locatable
11
12   wheat_tons = {
13       'Argentina' : 13_930_078 ,
14       'Brazil' : 6_261_895 ,
15       'Chile' : 1_358_128 ,
16       'Uruguay' : 1_076_000 ,
17       'Paraguay' : 840_000 ,
18       'Bolivia' : 263_076 ,
19       'Peru' : 214_533 ,
20       'Colombia' : 9_718 ,
21       'Ecuador' : 6_584 ,
22       'Venezuela' : 161 , }
23
24   Countries = Reader('nat_earth/ne_110m_admin_0_countries')
25   shp = {} # country shape indexed by name
26   for geom, meta in zip(Countries.geometries(),
27                         Countries.records()):
28       name = meta.attributes['NAME_EN']
29       shp[name] = geom
30
31   PC = ccrs.PlateCarree()
32   fig, ax = plt.subplots(1, 1, subplot_kw=dict(projection=PC))
33   ax.set_extent([-85, -33, # lon
34                  -55, 14]) # lat
35
36   ton_intervals = np.array([0, 10_000, 100_000, 1_000_000, 10_000_000, 15_000_000,])
37   fraction_interval = np.linspace(0.12, 1, num=len(ton_intervals))
38   colors = plt.cm.BuPu(fraction_interval[:-1])
39
40   for ctry in wheat_tons:
41      color = colors[np.searchsorted(ton_intervals, wheat_tons[ctry]) - 1]
42      shape = [shp[ctry]] if isinstance(shp[ctry], Polygon) else shp[ctry]
43      ax.add_geometries(shape, crs=PC, facecolor=color)
44
45   ax.add_feature(cfeature.BORDERS)
46   ax.add_feature(cfeature.COASTLINE)
47   ax.set_title('Wheat Production, 2014 [10$ˆ6$ tons]')
48
49   # colorbar
50   divider = make_axes_locatable(ax)
51   ax_cb = divider.new_horizontal(size="5%", pad=0.1, axes_class=plt.Axes)
52   fig.add_axes(ax_cb)
53   cmap, norm = from_levels_and_colors(ton_intervals/1.0e+6, colors)
54   sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
55   plt.colorbar(sm, cax=ax_cb)
56
57   plt.show()
58   #plt.savefig('SA_wheat_2014.png', dpi=200,
59   # bbox_inches='tight', pad_inches=0, transparent=True)

Create this plot in MATLAB with the recipe in Section 12.7.

12.4.4 MATLAB and Cartopy

There is a substantial challenge to using Cartopy from MATLAB though. Cartopy version 0.18.0 (current as of July 2021) exhibits an unusual interaction with MATLAB when it attempts to download imagery tiles from map services. One of two things can happen: either this error appears
<urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable
to get local issuer certificate (_ssl.c:1124)>

or the resulting plots simply lack background images.

There is a workaround, but it involves modifying the Cartopy’s file cartopy/io/img_tiles.py to use the requests module to download tiles instead of urllib.request from the Python standard library. The first step is to find the directory or folder containing img_tiles.py. That can be done within ipython by importing cartopy.io and then examining its __path__ attribute. On my Linux computer, this directory is

ipython:
In : import cartopy.io
In : cartopy.io.__path__
Out: ['/usr/local/anaconda3/2020.07/envs/matpy/lib/python3.8/site-packages/cartopy/io']
Note

If you installed Cartopy in multiple virtual environments, you’ll need to edit img tiles.py in each one you plan to start MATLAB from.

Next, edit img_tiles.py so that it includes an import to requests (line 41 on the right) and replaces calls to urlopen(), six.BytesIO(), and fh.close() (lines 190–193 on the left) with the highlighted lines on the right. (The actual line numbers will vary depending on the version of Cartopy you’ve installed.)

img_tiles.py original:

img_tiles.py fixed:

 39 import numpy as np

 40  import six

 41

 42  import cartopy

 43  import cartopy.crs as ccrs

188  url = self._image_url(tile)

189  try:

190    request = Request(url, headers={

         "User-Agent": self.user_agent})

191    fh = urlopen(request)

192    im_data = six.BytesIO(fh.read())

193    fh.close()

194    img = Image.open(im_data)

39  import numpy as np

40  import six

41  import requests

42

43  import cartopy

44  import cartopy.crs as ccrs

189  url = self._image_url(tile)

190  try:

191  request = requests.get(url,

                     stream=True)

192  request.raw.decode content = True

193  img = Image.open(request.raw)

The source listing for a utility that automates this patch, patch_cartopy.py, appears in Appendix E.

12.4.5 Avoid matplotlib’s Qt Backend in MATLAB!

MATLAB uses the Qt library to render graphics. While matplotlib can use a half dozen different graphics libraries, it defaults to Qt. This is a problem! The Qt libraries that come with an Anaconda distribution are unlikely to match those shipped with MATLAB. As a result, if you use matplotlib’s plt.show() command in MATLAB, you’ll cause MATLAB to crash.

There’s an easy workaround though: simply configure matplotlib to use a different backend. First, we’ll query the Python installation to see what backend it currently uses:

Python:
In : import matplotlib
In : matplotlib.get_backend()
Out: 'Qt5Agg'

“Qt5Agg” will definitely be a problem in MATLAB. A good alternative is the TkAgg backend on Linux and macOS and WXAgg on Windows. Switching to the TkAgg backend, for example, is done like this in Python:

Python:
import matplotlib
matplotlib.use('TkAgg')

and like this in MATLAB:

MATLAB 2020b:
Im  = @py.importlib.import_module;
mpl = Im('matplotlib');
mpl.use('TkAgg')

12.5 Recipe 12-1: Drawing Lines on Maps with Cartopy

The MATLAB equivalent of the whale’s track shown in Section 12.4.2 looks like this:

MATLAB 2020b:
% file: code/matlab_py/cartopy_whale.m
Im = @py.importlib.import_module;
np     = Im('numpy');
sgeom  = Im('shapely.geometry');
ccrs   = Im('cartopy.crs');
cfeat  = Im('cartopy.feature');
plt    = Im('matplotlib.pyplot');
mpl    = Im('matplotlib');
if ispc
    mpl.use('WXAgg')
else
    mpl.use('TkAgg')
end
PC = ccrs.PlateCarree();
fig = plt.figure();
i1 = int64(1);
ax = fig.add_subplot(i1, i1, i1, pyargs('projection',PC));
ax.coastlines(pyargs('resolution','50m'));
ax.set_extent([-80 -50 30 55], pyargs('crs',PC));
ax.add_feature(cfeat.LAND);
ax.add_feature(cfeat.OCEAN);
ax.add_feature(cfeat.COASTLINE);
ax.add_feature(cfeat.BORDERS, pyargs('linestyle','-'));
ax.add_feature(cfeat.LAKES.with_scale('50m'), pyargs('alpha', 0.5));
ax.add_feature(cfeat.RIVERS.with_scale('50m'));
plt.title('Blue Whale 158390, Fall 2018');
path = load('whale_path.txt');
track = sgeom.LineString(np.array(path));
error = track.buffer(.5); % degrees
ax.gridlines(pyargs('draw_labels','True'));
ax.add_geometries({error}, PC, pyargs('facecolor','#1F18F4','alpha',.5));
ax.add_geometries({track}, PC, pyargs('facecolor','none','edgecolor','r'));
plt.savefig('whale.png', pyargs('bbox_inches','tight','pad_inches',0, ...
     'transparent','True'));
plt.show()

12.6 Recipe 12-2: Overlay Contours on Globe with Cartopy

The sea surface temperature overlay made in Section 12.4.3.1 can be made in MATLAB with Cartopy.

MATLAB 2020b:
% code/matlab_py/cartopy_sst.m
Im = @py.importlib.import_module;
OS    = Im('os');
np    = Im('numpy');
plt   = Im('matplotlib.pyplot');
ccrs  = Im('cartopy.crs');
axes  = Im('mpl_toolkits.axes_grid1');
mpl   = Im('matplotlib');
if ispc
    netcdf.inqLibVers; % avoid DLL conflict
    mpl.use('WXAgg')
else
    mpl.use('TkAgg')
end
nc   = Im('netCDF4');
i1 = int64(1);
OS.environ{'CARTOPY_USER_BACKGROUNDS'} = ...
     '../plots/cartopy/background';
File = '../plots/oisst-avhrr-v02r01.20201001_preliminary.nc';
lat = ncread(File, 'lat');
lon = ncread(File, 'lon');
sst = ncread(File, 'sst')';
mask = isnan(sst);
fig = plt.figure();
Ortho = ccrs.Orthographic(-10, 15);
PC    = ccrs.PlateCarree();
ax = fig.add_subplot(i1, i1, i1,pyargs('projection',Ortho));
%            - or -
%ax = fig.add_subplot(i1, i1, i1, pyargs('projection',PC));
ax.coastlines();
ax.set_global();
ax.gridlines();
h = ax.contourf(lon, lat, sst,...
    pyargs('levels',int64(20), 'vmin',-2,'vmax',35,...
           'transform',PC,'cmap','jet'));
divider = axes.make_axes_locatable(ax);
ax_cb = divider.new_horizontal(...
    pyargs('size','5%','pad',0.1,'axes_class',plt.Axes));
fig.add_axes(ax_cb);
plt.colorbar(h, pyargs('cax',ax_cb));
ax.background_img(pyargs('name',"VI",'resolution',"low"));
ax.set_title('SST 2020-01-02 [Celcius]');
plt.savefig('sst_globe.png', pyargs('bbox_inches','tight',...
            'pad_inches',0,'transparent','True'));
plt.show()

12.7 Recipe 12-3: Shade Map Regions by Value with Cartopy

The map of South American wheat production made in Section 12.4.3.2 can be made in MATLAB with Cartopy.

MATLAB 2020b:
Im = @py.importlib.import_module;
np     = Im('numpy');
ccrs   = Im('cartopy.crs');
cfeat  = Im('cartopy.feature');
shape  = Im('cartopy.io.shapereader');
poly   = Im('shapely.geometry.polygon');
plt    = Im('matplotlib.pyplot');
col    = Im('matplotlib.colors'); % from_levels_and_colors
axes   = Im('mpl_toolkits.axes_grid1'); % make_axes_locatable
mpl    = Im('matplotlib');
if ispc
    mpl.use('WXAgg')
else
    mpl.use('TkAgg')
end
i1 = int64(1);
wheat_tons.Argentina = 13930078;
wheat_tons.Brazil    = 6261895;
wheat_tons.Chile     = 1358128;
wheat_tons.Uruguay   = 1076000;
wheat_tons.Paraguay  = 840000;
wheat_tons.Bolivia   = 263076;
wheat_tons.Peru      = 214533;
wheat_tons.Colombia  = 9718;
wheat_tons.Ecuador   = 6584;
wheat_tons.Venezuela = 161;
Countries = shape.Reader('nat_earth/ne_110m_admin_0_countries.shp');
shp = py.dict(); % country shape indexed by name
for geom_meta = py.list(py.zip(...
       Countries.geometries(), Countries.records()))
    name = geom_meta{1}{2}.attributes.get('NAME_EN'); % {1}{2} = meta
    shp.update(pyargs(name, geom_meta{1}{1})); % {1}{1} = geom
end
PC = ccrs.PlateCarree();
fig_ax = plt.subplots(i1, i1, pyargs('subplot_kw',...
                   py.dict(pyargs('projection',PC))));
fig = fig_ax{1};
ax  = fig_ax{2};
ax.set_extent([-85, -33, ... % lon
               -55, 14]) % lat
ton_intervals = [0, 10000, 100000, 1000000, 10000000, 15000000];
fraction_interval = linspace(0.12, 1, length(ton_intervals));
colors = plt.cm.BuPu(fraction_interval(1:end-1));
col_ind = np.arange(4, pyargs('dtype',np.int64));
country_names = fieldnames(wheat_tons);
for i = 1:numel(country_names)
   ctry = country_names{i};
   w_tons = wheat_tons.(ctry);
   color_row = np.searchsorted(ton_intervals, w_tons) - 1;
   fprintf('%20s %10d tons, color=%d ', ctry, w_tons, int32(color_row))
   color = colors.take(col_ind + int64(4)*color_row);
   if py.isinstance(shp.get(ctry), poly.Polygon)
      shape = py.list({shp.get(ctry)});
   else
      shape = shp.get(ctry);
   end
   ax.add_geometries(shape, pyargs('crs',PC,'facecolor',color));
end
ax.add_feature(cfeat.BORDERS);
ax.add_feature(cfeat.COASTLINE);
ax.set_title('Wheat Production, 2014 [10$ˆ6$ tons]');
% colorbar
divider = axes.make_axes_locatable(ax);
ax_cb = divider.new_horizontal(pyargs('size',"5%",'pad',0.1,...
                                      'axes_class',plt.Axes));
fig.add_axes(ax_cb);
cmap_norm = col.from_levels_and_colors(ton_intervals/1.0e+6, colors);
cmap = cmap_norm{1};
norm = cmap_norm{2};
sm = plt.cm.ScalarMappable(pyargs('cmap',cmap,'norm',norm));
plt.colorbar(sm, pyargs('cax',ax_cb));
plt.savefig('SA_wheat_2014.png', pyargs('bbox_inches','tight',...
            'pad_inches',0,'transparent','True'));
plt.show()

12.8 Plotting on Maps with GeoPandas

The next chapter covers MATLAB tables and Pandas dataframes. GeoPandas, an extension to Pandas, combines Cartopy, Pandas, and Shapely (among other Python modules) and makes it possible to plot geographical data quite easily. Section 13.​12 shows how to make a map of Los Angeles County home prices by zip code. It takes considerably less effort than calling Cartopy functions directly.

12.9 Making Plots in Batch Mode

Plot generation is mostly an interactive experience where a user views plots on a display as they are drawn. A fully automated workflow on the other hand eliminates displays entirely; plots can be written directly to PNG or other image formats for direct inclusion in websites or documents. Both MATLAB and Python support “headless” plot generation albeit with different methods.

MATLAB’s batch plot method relies on command-line arguments to the matlab command. This line runs the hist_demo.m script of Section 12.1.5:

MATLAB:
> matlab -nodesktop -nodisplay -nosplash -r "hist_demo;saveas(1,'hist.png');exit"

matplotlib’s batch plot method requires the use of the “Agg” backend as shown on lines 4 and 5:

Python:
 1   #!/usr/bin/env python3
 2   # file: code/plots/hist_demo_Agg.py
 3   import numpy as np
 4   import matplotlib as mpl
 5   mpl.use('Agg')
 6   import matplotlib.pyplot as plt
 7   N = 10000
 8   np.random.seed(123)
 9   w1 = np.random.rayleigh(3, N)
10   w2 = np.random.normal(8, 3, N)
11   bins = np.linspace(-5, 20, 80)
12   plt.hist(w1, bins, alpha=0.5, label='Rayleigh')
13   plt.hist(w2, bins, alpha=0.5, label='Normal')
14   plt.legend(loc='upper right')
15   plt.ylim([0, 700])
16   plt.grid(True)
17   plt.savefig('hist.png', bbox_inches='tight', pad_inches=0, transparent=True)
Tip

From an ipython session, you can get a list of all available backends with

In :   import matplotlib

In :   matplotlib.rcsetup.interactive_bk

12.10 Interactive Plot Editing

Fine-tuning matplotlib plots for publication can become a maddening exercise of font, color, glyph, and coordinate selection. MATLAB enjoys an advantage here in that its plot editor allows one to modify plot elements interactively.

Matplotlib’s Qt5Agg backend allows some interactive plot capability which can be accessed through the indicated icons in the display tool:

The Qt5Agg backend, however, is not included in the matpy conda environment because it conflicts with MATLAB’s own Qt5 libraries. You’ll need to leave that conda environment (conda deactivate) and return to the “base” environment to make such edits.

pylustrator11 is a more advanced tool for interactive editing of matplotlib plots. Its capabilities are best appreciated by viewing the YouTube videos at its documentation site, https://pylustrator.readthedocs.io/.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.195.204