Mathematical Plotting with Matplotlib
Plotting is a fundamental tool in all of mathematics. A good plot can reveal hidden details, suggest future directions, verify results, or reinforce an argument. It is no surprise, then, that the scientific Python stack features a powerful and flexible plotting library called Matplotlib.
In this chapter, we will plot functions and data in a variety of styles and create figures that are fully labeled and annotated. We will create three-dimensional plots, customize the appearance of figures, create figures that contain multiple plots using subplots, and save figures directly to files for applications that are not running in an interactive environment.
Plotting is one of the most important aspects covered in this book. Plotting data, functions, or solutions can often help you gain an understanding of a problem that can really help to reason about your methods. We will see plotting again in every chapter of this book.
In this chapter, we will cover the following recipes:
- Basic plotting with Matplotlib
- Adding subplots
- Plotting with error bars
- Saving Matplotlib figures
- Surface and contour plots
- Customizing three-dimensional plots
- Plotting vector fields with quiver plots
Technical requirements
The main plotting package for Python is Matplotlib, which can be installed using your favorite package manager, such as pip
:
python3.10 -m pip install matplotlib
This will install the most recent version of Matplotlib, which, at the time of writing this book, is version 3.5.2.
Matplotlib contains numerous sub-packages, but the main user interface (UI) is the matplotlib.pyplot
package, which, by convention, is imported under the plt
alias. This is achieved using the following import
statement:
import matplotlib.pyplot as plt
Many of the recipes in this chapter also require NumPy, which, as usual, is imported under the np
alias.
The code for this chapter can be found in the Chapter 02
folder of the GitHub repository at https://github.com/PacktPublishing/Applying-Math-with-Python-2nd-Edition/tree/main/Chapter%2002.
Basic plotting with Matplotlib
Plotting is an important part of understanding behavior. So much can be learned by simply plotting a function or data that would otherwise be hidden. In this recipe, we will walk through how to plot simple functions or data using Matplotlib, set the plotting style, and add labels to a plot.
Matplotlib is a very powerful plotting library, which means it can be rather intimidating to perform simple tasks with it. For users who are used to working with MATLAB and other mathematical software packages, there is a state-based interface called pyplot
. There is also an object-oriented interface (OOI), which might be more appropriate for more complex plots. In either case, the pyplot
interface is a convenient way to create basic objects.
Getting ready
Most commonly, the data that you wish to plot will be stored in two separate NumPy arrays, which we will label x
and y
for clarity (although this naming does not matter in practice). We will demonstrate plotting the graph of a function, so we will generate an array of x
values and use the function to generate the corresponding y
values. We’re going to plot three different functions over the range on the same axes:
def f(x): return x*(x - 2)*np.exp(3 – x) def g(x): return x**2 def h(x): return 1 - x
Let’s plot these three functions in Python using Matplotlib.
How to do it...
Before we can plot the function, we must generate x
and y
data to be plotted. If you are plotting existing data, you can skip these commands. We need to create a set of x
values that cover the desired range, and then use the function to create y
values:
- The
linspace
routine from NumPy is ideal for creating arrays of numbers for plotting. By default, it will create 50 equally spaced points between the specified arguments. The number of points can be customized by providing an additional argument, but 50 is sufficient for most cases:x = np.linspace(-0.5, 3.0) # 50 values between -0.5 and 3.0
- Once we have created
x
values, we can generatey
values:y1 = f(x) # evaluate f on the x points
y2 = g(x) # evaluate g on the x points
y3 = h(x) # evaluate h on the x points
- To plot the data, we first need to create a new figure and attach axes objects, which can be achieved by calling the
plt.subplots
routine without any arguments:fig, ax = plt.subplots()
Now, we use the plot
method on the ax
object to plot the first function. The first two arguments are and
coordinates to be plotted, and the third (optional) argument specifies that the line color should be black:
ax.plot(x, y1, "k") # black solid line style
To help distinguish the plots for the other functions, we plot those with a dashed line and a dot-dash line:
ax.plot(x, y2, "k--") # black dashed line style ax.plot(x, y3, "k.-") # black dot-dashed line style
Every plot should have a title and axis labels. In this case, there isn’t anything interesting to label the axes with, so we just label them "x"
and "y"
:
ax.set_title("Plot of the functions f, g, and h") ax.set_xlabel("x") ax.set_ylabel("y")
Let’s also add a legend to help you distinguish between the different function plots without having to look elsewhere to see which line is which:
ax.legend(["f", "g", "h"])
Finally, let’s annotate the plot to mark the intersection between the functions and
with text:
ax.text(0.4, 2.0, "Intersection")
This will plot the y
values against the x
values on a new figure. If you are working within IPython or with a Jupyter notebook, then the plot should automatically appear at this point; otherwise, you might need to call the plt.show
function to make the plot appear:
plt.show()
If you use plt.show
, the figure should appear in a new window. We won’t add this command to any further recipes in this chapter, but you should be aware that you will need to use it if you are not working in an environment where plots will be rendered automatically, such as an IPython console or a Jupyter Notebook. The resulting plot should look something like the plot in Figure 2.1:

Figure 2.1 – Three functions on a single set of axes, each with a different style, with labels, legend, and an annotation
Note
If you are using a Jupyter notebook and the subplots
command, you must include the call to subplots
within the same cell as the plotting commands or the figure will not be produced.
How it works…
Here, we’re using the OOI because it allows us to keep track of exactly which figure and axes object we’re plotting on. This isn’t so important here where we have only a single figure
and axes
, but one can easily envisage situations where you might have two or more figures and axes concurrently. Another reason to follow this pattern is to be consistent when you add multiple subplots—see the Adding subplots recipe.
You can produce the same plot as in the recipe via the state-based interface by using the following sequence of commands:
plt.plot(x, y1, "k", x, y2, "k--", x, y3, "k.-") plt.title("Plot of the functions f, g, and h") plt.xlabel("x") plt.ylabel("y") plt.legend(["f", "g", "h"]) plt.text(0.4, 2.0, "Intersection")
If there are currently no Figure
or Axes
objects, the plt.plot
routine creates a new Figure
object, adds a new Axes
object to the figure, and populates this Axes
object with the plotted data. A list of handles to the plotted lines is returned. Each of these handles is a Lines2D
object. In this case, this list will contain a single Lines2D
object. We could use this Lines2D
object to further customize the appearance of the line later.
Notice that in the preceding code, we combined all the calls to the plot
routine together. This is also possible if you use the OOI; the state-based interface is passing the arguments to the axes method on the set of axes that it either retrieves or creates.
The object layer of Matplotlib interacts with a lower-level backend, which does the heavy lifting of producing the graphical plot. The plt.show
function issues an instruction to the backend to render the current figure. There are a number of backends that can be used with Matplotlib, which can be customized by setting the MPLBACKEND
environment variable, modifying the matplotlibrc
file, or by calling matplotlib.use
from within Python with the name of an alternative backend. By default, Matplotlib picks a backend that is appropriate for the platform (Windows, macOS, Linux) and purpose (interactive or non-interactive), based on which backends are available. For example, on the author’s system, the QtAgg
backend is the default. This is an interactive backend based on the Anti-Grain Geometry (AGG) library. Alternatively, one might want to use the QtCairo
backend, which uses the Cairo library for rendering.
Note
The plt.show
function does more than simply call the show
method on a figure. It also hooks into an event loop to correctly display the figure. The plt.show
routine should be used to display a figure, rather than the show
method on a Figure
object.
The format string used to quickly specify the line style has three optional parts, each consisting of one or more characters. The first part controls the marker style, which is the symbol that is printed at each data point; the second controls the style of the line that connects the data points; the third controls the color of the plot. In this recipe, we only specified the line style. However, one could specify both line style and marker style or just marker style. If you only provide the marker style, no connecting lines are drawn between the points. This is useful for plotting discrete data where no interpolation between points is necessary.
Four line-style parameters are available: a solid line (-
), a dashed line (--
), a dash-dot line (-.
), or a dotted line (:
). Only a limited number of colors can be specified in the format string; they are red, green, blue, cyan, yellow, magenta, black, and white. The character used in the format string is the first letter of each color (with the exception of black), so the corresponding characters are r
, g
, b
, c
, y
, m
, k
, and w
, respectively.
In the recipe, we saw three examples of these format strings: the single k
format string only changed the color of the line and kept the other settings at default (small point markers and unbroken blue line); the k--
and k.-
format strings both changed the color and the line style. For an example of changing the point style, see the There’s more... section and Figure 2.2:

Figure 2.2 - Plot of three sets of data, each plotted using a different marker style
The set_title
, set_xlabel
, and set_ylabel
methods simply add the text argument to the corresponding position of the Axes
object. The legend
method, as called in the preceding code, adds the labels to the datasets in the order that they were added to the plot—in this case, y1
, y2
, and then y3
.
There are a number of keyword arguments that can be supplied to the set_title
, set_xlabel
, and set_ylabel
routines to control the style of the text. For example, the fontsize
keyword can be used to specify the size of the label font in the usual pt
point measure.
The annotate
method on the Axes
object adds arbitrary text to a specific position on the plot. This routine takes two arguments—the text to display as a string and the coordinates of the point at which the annotation should be placed. This routine also accepts keyword arguments that can be used to customize the style of the annotation.
There’s more…
The plt.plot
routine accepts a variable number of positional inputs. In the preceding code, we supplied two positional arguments that were interpreted as x
values and y
values (in that order). If we had instead provided only a single array, the plot
routine would have plotted the values against their position in the array; that is, the x
values are taken to be 0
, 1
, 2
, and so on.
The plot
method also accepts a number of keyword arguments that can also be used to control the style of a plot. Keyword arguments take precedence over format string parameters if both are present, and they apply to all sets of data plotted by the call. The keyword to control the marker style is marker
, the keyword for the line style is linestyle
, and the keyword for color is color
. The color
keyword argument accepts a number of different formats to specify a color, which includes RGB values as a (r, g, b)
tuple, where each character is a float between 0
and 1
or is a hex string. The width of the line plotted can be controlled using the linewidth
keyword, which should be provided with a float
value. Many other keyword arguments can be passed to plot
; a list is given in the Matplotlib documentation. Many of these keyword arguments have a shorter version, such as c
for color
and lw
for linewidth
.
In the this recipe, we plotted a large number of coordinates generated by evaluating functions on a selection of values. In other applications, one might have data sampled from the real world (as opposed to generated). In these situations, it might be better to leave out the connecting lines and simply plot the markers at the points. Here is an example of how this might be done:
y1 = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) y2 = np.array([1.2, 1.6, 3.1, 4.2, 4.8]) y3 = np.array([3.2, 1.1, 2.0, 4.9, 2.5]) fig, ax = plt.subplots() ax.plot(y1, 'o', y2, 'x', y3, '*', color="k")
The result of these commands is shown in Figure 2.2. Matplotlib has a specialized method for producing scatter plots such as this, called scatter
.
Other aspects of the plot can be customized by using methods on the Axes
object. The axes ticks can be modified using the set_xticks
and set_yticks
methods on the Axes
object, and the grid appearance can be configured using the grid
method. There are also convenient methods in the pyplot
interface that apply these modifications to the current axes (if they exist).
For example, we modify the axis limits, set the ticks at every multiple of 0.5
in both the and
direction, and add a grid to the plot by using the following commands:
ax.axis([-0.5, 5.5, 0, 5.5]) # set axes ax.set_xticks([0.5*i for i in range(9)]) # set xticks ax.set_yticks([0.5*i for i in range(11)]) # set yticks ax.grid() # add a grid
Notice how we set the limits slightly larger than the extent of the plot. This is to avoid markers being placed on the boundary of the plot window.
Matplotlib has many other plotting routines besides the plot
routine described here. For example, there are plotting methods that use a different scale for the axes, including the logarithmic or
axes separately (
semilogx
or semilogy
, respectively) or together (loglog
). These are explained in the Matplotlib documentation. The scatter
plotting routine may be useful if you wish to plot discrete data on axes without connecting the points with a line. This allows more control over the style of the marker. For example, you can scale the marker according to some additional information.
We can use a different font by using the fontfamily
keyword, the value of which can be the name of a font or serif
, sans-serif
, or monospace
, which will choose the appropriate built-in font. A complete list of modifiers can be found in the Matplotlib documentation for the matplotlib.text.Text
class.
Text arguments can also be rendered using TeX for additional formatting by supplying usetex=True
to the routine. We’ll demonstrate the use of TeX formatting of labels in Figure 2.3 in the following recipe. This is especially useful if the title or axis label contains a mathematical formula. Unfortunately, the usetex
keyword argument cannot be used if TeX is not installed on the system—it will cause an error in this case. However, it is still possible to use the TeX syntax for formatting mathematical text within labels, but this will be typeset by Matplotlib, rather than by TeX.
Adding subplots
Occasionally, it is useful to place multiple related plots within the same figure side by side but not on the same axes. Subplots allow us to produce a grid of individual plots within a single figure. In this recipe, we will see how to create two plots side by side on a single figure using subplots.
Getting ready
You will need the data to be plotted on each subplot. As an example, we will plot the first five iterates of Newton’s method applied to the function with an initial value of
on the first subplot, and for the second, we will plot the error of the iterate. We first define a generator function to get the iterates:
def generate_newton_iters(x0, number): iterates = [x0] errors = [abs(x0 - 1.)] for _ in range(number): x0 = x0 - (x0*x0 - 1.)/(2*x0) iterates.append(x0) errors.append(abs(x0 - 1.)) return iterates, errors
This routine generates two lists. The first list contains iterates of Newton’s method applied to the function, and the second contains the error in the approximation:
iterates, errors = generate_newton_iters(2.0, 5)
How to do it...
The following steps show how to create a figure that contains multiple subplots:
- We use the
subplots
routine to create a new figure and references to all of theAxes
objects in each subplot, arranged in a grid with one row and two columns. We also set thetight_layout
keyword argument toTrue
to fix the layout of the resulting plots. This isn’t strictly necessary, but it is in this case as it produces a better result than the default:fig, (ax1, ax2) = plt.subplots(1, 2,
tight_layout=True)
#1 row, 2 columns
- Once
Figure
andAxes
objects are created, we can populate the figure by calling the relevant plotting method on eachAxes
object. For the first plot (displayed on the left), we use theplot
method on theax1
object, which has the same signature as the standardplt.plot
routine. We can then call theset_title
,set_xlabel
, andset_ylabel
methods onax1
to set the title and thex
andy
labels. We also use TeX formatting for the axes labels by providing theusetex
keyword argument; you can ignore this if you don’t have TeX installed on your system:ax1.plot(iterates, "kx")
ax1.set_title("Iterates")
ax1.set_xlabel("$i$", usetex=True)
ax1.set_ylabel("$x_i$", usetex=True)
- Now, we can plot the error values on the second plot (displayed on the right) using the
ax2
object. We use an alternative plotting method that uses a logarithmic scale on theaxis, called
semilogy
. The signature for this method is the same as the standardplot
method. Again, we set the axes labels and the title. Again, the use ofusetex
can be left out if you don’t have TeX installed:ax2.semilogy(errors, "kx") # plot y on logarithmic scale
ax2.set_title("Error")
ax2.set_xlabel("$i$", usetex=True)
ax2.set_ylabel("Error")
The result of this sequence of commands is shown here:

Figure 2.3 - Multiple subplots on the same Matplotlib figure
The left-hand side plots the first five iterates of Newton’s method, and the right-hand side is the approximation error plotted on a logarithmic scale.
How it works...
A Figure
object in Matplotlib is simply a container for plot elements, such as Axes
, of a certain size. A Figure
object will usually only hold a single Axes
object, which occupies the entire figure area, but it can contain any number of Axes
objects in the same area. The subplots
routine does several things. It first creates a new figure and then creates a grid with the specified shape in the figure area. Then, a new Axes
object is added to each position of the grid. The new Figure
object and one or more Axes
objects are then returned to the user. If a single subplot is requested (one row and one column, with no arguments) then a plain Axes
object is returned. If a single row or column is requested (with more than one column or row, respectively), then a list of Axes
objects is returned. If more than one row and column are requested, a list of lists, with rows represented by inner lists filled with Axes
objects, will be returned. We can then use the plotting methods on each of the Axes
objects to populate the figure with the desired plots.
In this recipe, we used the standard plot
method for the left-hand side plot, as we have seen in previous recipes. However, for the right-hand side plot, we used a plot where the axis had been changed to a logarithmic scale. This means that each unit on the
axis represents a change of a power of 10 rather than a change of one unit so that
0
represents ,
1
represents 10, 2
represents 100, and so on. The axes labels are automatically changed to reflect this change in scale. This type of scaling is useful when the values change by an order of magnitude, such as the error in an approximation, as we use more and more iterations. We can also plot with a logarithmic scale for only by using the
semilogx
method, or both axes on a logarithmic scale by using the loglog
method.
There’s more...
There are several ways to create subplots in Matplotlib. If you have already created a Figure
object, then subplots can be added using the add_subplot
method of the Figure
object. Alternatively, you can use the subplot
routine from matplotlib.pyplot
to add subplots to the current figure. If one does not yet exist, it will be created when this routine is called. The subplot
routine is a convenience wrapper of the add_subplot
method on the Figure
object.
In the preceding example, we created two plots with differently scaled axes. This demonstrates one of the many possible uses of subplots. Another common use is for plotting data in a matrix where columns have a common
x
label and rows have a common y
label, which is especially common in multivariate statistics when investigating the correlation between various sets of data. The plt.subplots
routine for creating subplots accepts the sharex
and sharey
keyword parameters, which allows the axes to be shared among all subplots or among a row or column. This setting affects the scale and ticks of the axes.
See also
Matplotlib supports more advanced layouts by providing the gridspec_kw
keyword arguments to the subplots
routine. See the documentation for matplotlib.gridspec
for more information.
Plotting with error bars
It is quite common that the values that we gather from the real world carry some uncertainty; no measurement of a real-world quantity is perfectly accurate. For example, if we measure a distance with a tape measure, there is a certain amount of accuracy that we can assume in our results, but beyond this accuracy, we cannot be sure that our measurement is valid. For such a situation, we can probably be confident of our accuracy up to about 1 millimeter or a little less than 1/16 inch. (This is, of course, assuming that we are measuring perfectly.) These values are the smallest subdivisions on typical tape measures. Let’s assume that we have collected such a set of 10 measurements (in centimeters) and we wish to plot these values along with the accuracy that we are confident about. (The range of values that lie above or below the measurement by the accuracy amount is called the error.) This is what we address in this recipe.
Getting ready
As usual, we have the Matplotlib pyplot
interface imported under the alias plt
. We first need to generate our hypothetical data and the assumed accuracy in NumPy arrays:
measurement_id = np.arange(1, 11) measurements = np.array([2.3, 1.9, 4.4, 1.5, 3.0, 3.3, 2.9, 2.6, 4.1, 3.6]) # cm err = np.array([0.1]*10) # 1mm
Let’s see how to use plotting routines in Matplotlib to plot these measurements with error bars to indicate the uncertainty in each measurement.
How to do it…
The following steps show how to plot measurements along with accuracy information on a figure.
First, we need to generate a new figure
and axis
object as usual:
fig, ax = plt.subplots()
Next, we use the errorbar
method on the axis object to plot the data along with the error bars. The accuracy information (the error) is passed as the yerr
argument:
ax.errorbar(measurement_id, measurements, yerr=err, fmt="kx", capsize=2.0)
As usual, we should always add meaningful labels to the axes and a title to the plot:
ax.set_title("Plot of measurements and their estimated error") ax.set_xlabel("Measurement ID") ax.set_ylabel("Measurement(cm)")
Since Matplotlib will not produce xlabel
ticks at every value by default, we set the x-tick values to the measurement IDs so that they are all displayed on the plot:
ax.set_xticks(measurement_id)
The resulting plot is shown in Figure 2.4. The recorded value is shown at the x
markers, and the error bar extends above and below that value by an accuracy of 0.1 cm (1 mm):

Figure 2.4 - Plot of a set of 10 sample measurements (in centimeters) with their measurement error shown
We can see here that each of the markers has a vertical bar that indicates the range in which we expect the true measurement (-value) to lie.
How it works…
The errorbar
method works in a similar way to other plotting methods. The first two arguments are the and
coordinates of the points to be plotted. (Note that both must be provided, which is not the case for other plotting methods.) The
yerr
argument indicates the size of the error bars to be added to the plot and should all be positive values. The form of the value(s) passed to this argument determines the nature of the error bars. In the recipe, we provided a flat NumPy array with 10 entries—one for each measurement—which leads to error bars above and below each point with the same size (the corresponding value from the argument). Alternatively, we could have specified a 2-by-10 array, where the first row contains the lower error and the second row contains the upper error. (Since all our errors are the same, we could also have provided a single float containing the common error for all measurements.)
In addition to the data arguments, there are the usual format arguments, including the fmt
format string. (We used this here as a keyword argument because we named the yerr
argument that precedes it.) In addition to the formatting of lines and points found in other plotting methods, there are special arguments for customizing the look of error bars. In the recipe, we used the capsize
argument to add “caps” to either end of the error bars so that we could easily identify the ends of those bars; the default style is a simple line.
There’s more...
In the recipe, we only plotted errors in the axis because the
values were simply ID values. If both sets of values have uncertainty, you can also specify the
error values using the
xerr
argument. This argument functions in the same way as the yerr
argument used previously.
If you are plotting a very large number of points that follow some kind of trend, you might wish to plot error bars more selectively. For this, you can use the errorevery
keyword argument to instruct Matplotlib to add error bars at every n
th data point rather than at all of them. This can be either a positive integer—indicating the “stride” to use to select points that will have errors—or a tuple containing an offset from the first value and a stride. For example, errorevery=(2, 5)
would place error bars every five data points, starting from the second entry.
You can also add error bars to bar charts in the same way (except here, the xerr
and yerr
arguments are keywords only). We could have plotted the data from the recipe as a bar chart using the following commands:
ax.bar(measurement_id, measurements, yerr=err, capsize=2.0, alpha=0.4)
If this line is used instead of the call to errorbar
in the recipe, then we would get a bar chart, as shown in Figure 2.5:

Figure 2.5 - Bar chart of measurements with error bars
As before, the measurement bar is capped with an indicator of the range in which we expect the true measurement to lie.
Saving Matplotlib figures
When you work in an interactive environment, such as an IPython console or a Jupyter notebook, displaying a figure at runtime is perfectly normal. However, there are plenty of situations where it would be more appropriate to store a figure directly to a file, rather than rendering it on screen. In this recipe, we will see how to save a figure directly to a file, rather than displaying it on screen.
Getting ready
You will need the data to be plotted and the path or file object in which you wish to store the output. We store the result in savingfigs.png
in the current directory. In this example, we will plot the following data:
x = np.arange(1, 5, 0.1) y = x*x
Let’s see how to plot this curve using Matplotlib and save the resulting plot to a file (without needing to interact with the plot GUI).
How to do it...
The following steps show how to save a Matplotlib plot directly to a file:
- The first step is to create a figure, as usual, and add any labels, titles, and annotations that are necessary. The figure will be written to the file in its current state, so any changes to the figure should be made before saving:
fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_title("Graph of $y = x^2$", usetex=True)
ax.set_xlabel("$x$", usetex=True)
ax.set_ylabel("$y$", usetex=True)
- Then, we use the
savefig
method onfig
to save this figure to a file. The only required argument is the path to output to or a file-like object that the figure can be written to. We can adjust various settings for the output format, such as the resolution, by providing the appropriate keyword arguments. We’ll set the Dots per Inch (DPI) of the output figure to300
, which is a reasonable resolution for most applications:fig.savefig("savingfigs.png", dpi=300)
Matplotlib will infer that we wish to save the image in the Portable Network Graphics (PNG) format from the extension of the file given. Alternatively, a format can be explicitly provided as a keyword argument (by using the format
keyword), or it will fall back to the default from the configuration file.
How it works...
The savefig
method chooses the appropriate backend for the output format and then renders the current figure in that format. The resulting image data is written to the specified path or file-like object. If you have manually created a Figure
instance, the same effect can be achieved by calling the savefig
method on that instance.
There’s more...
The savefig
routine takes a number of additional optional keyword arguments to customize the output image. For example, the resolution of the image can be specified using the dpi
keyword. The plots in this chapter have been produced by saving the Matplotlib figures to the file.
The output formats available include PNG, Scalable Vector Graphics (SVG), PostScript (PS), Encapsulated PostScript (EPS), and Portable Document Format (PDF). You can also save to JPEG format if the Pillow package is installed, but Matplotlib does not support this natively since version 3.1. There are additional customization keyword arguments for JPEG images, such as quality
and optimize
. A dictionary of image metadata can be passed to the metadata
keyword, which will be written as image metadata when saving.
See also
The examples gallery on the Matplotlib website includes examples of embedding Matplotlib figures into a GUI application using several common Python GUI frameworks.
Surface and contour plots
Matplotlib can also plot three-dimensional data in a variety of ways. Two common choices for displaying data such as this are using surface plots or contour plots (think of contour lines on a map). In this recipe, we will see a method for plotting surfaces from three-dimensional data and how to plot contours of three-dimensional data.
Getting ready
To plot three-dimensional data, it needs to be arranged into two-dimensional arrays for the ,
, and
components, where both the
and
components must be of the same shape as the
component. For the sake of this demonstration, we will plot the surface corresponding to the following function:

For 3D data, we can’t just use the routines from the pyplot
interface. We need to import some extra functionality from another part of Matplotlib. We’ll see how to do this next.
How to do it...
We want to plot the function on the
and
range. The first task is to create a suitable grid of
pairs on which to evaluate this function:
- We first use
np.linspace
to generate a reasonable number of points in these ranges:X = np.linspace(-5, 5)
Y = np.linspace(-5, 5)
- Now, we need to create a grid on which to create our
values. For this, we use the
np.meshgrid
routine:grid_x, grid_y = np.meshgrid(X, Y)
- Now, we can create
values to plot, which hold the value of the function at each of the grid points:
z = np.exp(-((grid_x-2.)**2 + (
grid_y-3.)**2)/4) - np.exp(-(
(grid_x+3.)**2 + (grid_y+2.)**2)/3)
- To plot three-dimensional surfaces, we need to load a Matplotlib toolbox,
mplot3d
, which comes with the Matplotlib package. This won’t be used explicitly in the code, but behind the scenes, it makes the three-dimensional plotting utilities available to Matplotlib:from mpl_toolkits import mplot3d
- Next, we create a new figure and a set of three-dimensional axes for the figure:
fig = plt.figure()
# declare 3d plot
ax = fig.add_subplot(projection="3d")
- Now, we can call the
plot_surface
method on these axes to plot the data (we set the colormap to gray for better visibility in print; see the next recipe for a more detailed discussion):ax.plot_surface(grid_x, grid_y, z, cmap="gray")
- It is extra important to add axis labels to three-dimensional plots because it might not be clear which axis is which on the displayed plot. We also set the title at this point:
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("z")
ax.set_title("Graph of the function f(x, y)")
You can use the plt.show
routine to display the figure in a new window (if you are using Python interactively and not in a Jupyter notebook or on an IPython console) or plt.savefig
to save the figure to a file. The result of the preceding sequence is shown here:

Figure 2.6 - A three-dimensional surface plot produced with Matplotlib
- Contour plots do not require the
mplot3d
toolkit, and there is acontour
routine in thepyplot
interface that produces contour plots. However, unlike the usual (two-dimensional) plotting routines, thecontour
routine requires the same arguments as theplot_surface
method. We use the following sequence to produce a plot:fig = plt.figure() # Force a new figure
plt.contour(grid_x, grid_y, z, cmap="gray")
plt.title("Contours of f(x, y)")
plt.xlabel("x")
plt.ylabel("y")
The result is shown in the following plot:

Figure 2.7 - Contour plot produced using Matplotlib with the default settings
The peak and basin of the function are shown clearly here by the rings of concentric circles. In the top right, the shading is lighter, indicating that the function is increasing, and in the bottom left, the shade is darker, indicating that the function is decreasing. The curve that separates the regions in which the function is increasing and decreasing is shown between them.
How it works...
The mplot3d
toolkit provides an Axes3D
object, which is a three-dimensional version of the Axes
object in the core Matplotlib package. This is made available to the axes
method on a Figure
object when the projection="3d"
keyword argument is given. A surface plot is obtained by drawing quadrilaterals in the three-dimensional projection between nearby points in the same way that a two-dimensional curve is approximated by straight lines joining adjacent points.
The plot_surface
method needs the values to be provided as a two-dimensional array that encodes the
values on a grid of
pairs. We created a range of
and
values that we are interested in, but if we simply evaluate our function on the pairs of corresponding values from these arrays, we will get the
values along a line and not over a grid. Instead, we use the
meshgrid
routine, which takes the two X
and Y
arrays and creates from them a grid consisting of all the possible combinations of values in X
and Y
. The output is a pair of two-dimensional arrays on which we can evaluate our function. We can then provide all three of these two-dimensional arrays to the plot_surface
method.
There’s more...
The routines described in the preceding section, contour
and plot_surface
, only work with highly structured data where the ,
, and
components are arranged into grids. Unfortunately, real-life data is rarely so structured. In this case, you need to perform some kind of interpolation between known points to approximate the value on a uniform grid, which can then be plotted. A common method for performing this interpolation is by triangulating the collection of
pairs and then using the values of the function on the vertices of each triangle to estimate the value on the grid points. Fortunately, Matplotlib has a method that does all of these steps and then plots the result, which is the
plot_trisurf
routine. We briefly explain how this can be used here:
- To illustrate the use of
plot_trisurf
, we will plot a surface and contours from the following data:x = np.array([ 0.19, -0.82, 0.8 , 0.95, 0.46, 0.71,
-0.86, -0.55, 0.75,-0.98, 0.55, -0.17, -0.89,
-0.4 , 0.48, -0.09, 1., -0.03, -0.87, -0.43])
y = np.array([-0.25, -0.71, -0.88, 0.55, -0.88, 0.23,
0.18,-0.06, 0.95, 0.04, -0.59, -0.21, 0.14, 0.94,
0.51, 0.47, 0.79, 0.33, -0.85, 0.19])
z = np.array([-0.04, 0.44, -0.53, 0.4, -0.31,
0.13,-0.12, 0.03, 0.53, -0.03, -0.25, 0.03,
-0.1 ,-0.29, 0.19, -0.03, 0.58, -0.01, 0.55,
-0.06])
- This time, we will plot both the surface and contour (approximations) on the same figure as two separate subplots. For this, we supply the
projection="3d"
keyword argument to the subplot that will contain the surface. We use theplot_trisurf
method on the three-dimensional axes to plot the approximated surface, and thetricontour
method on the two-dimensional axes to plot the approximated contours:fig = plt.figure(tight_layout=True) # force new figure
ax1 = fig.add_subplot(1, 2, 1, projection="3d") # 3d axes
ax1.plot_trisurf(x, y, z)
ax1.set_xlabel("x")
ax1.set_ylabel("y")
ax1.set_zlabel("z")
ax1.set_title("Approximate surface")
- We can now plot the contours for the triangulated surface using the following command:
ax2 = fig.add_subplot(1, 2, 2) # 2d axes
ax2.tricontour(x, y, z)
ax2.set_xlabel("x")
ax2.set_ylabel("y")
ax2.set_title("Approximate contours")
We include the tight_layout=True
keyword argument with the figure to save a call to the plt.tight_layout
routine later. The result is shown here:

Figure 2.8 - Approximate surface and contour plots generated from unstructured data using triangulation
In addition to surface plotting routines, the Axes3D
object has a plot
(or plot3D
) routine for simple three-dimensional plotting, which works exactly as the usual plot
routine but on three-dimensional axes. This method can also be used to plot two-dimensional data on one of the axes.
See also
Matplotlib is the go-to plotting library for Python, but other options do exist. We’ll see the Bokeh library in Chapter 6. There are other libraries, such as Plotly (https://plotly.com/python/), that simplify the process of creating certain types of plots and adding more features, such as interactive plots.
Customizing three-dimensional plots
Contour plots can hide some detail of the surface that they represent since they only show where the “height” is similar and not what the value is, even in relation to the surrounding values. On a map, this is remedied by printing the height onto certain contours. Surface plots are more revealing, but the problem of projecting three-dimensional objects into 2D to be displayed on a screen can itself obscure some details. To address these issues, we can customize the appearance of a three-dimensional plot (or contour plot) to enhance the plot and make sure the detail that we wish to highlight is clear. The easiest way to do this is by changing the colormap of the plot, as we saw in the previous recipe. (By default, Matplotlib will produce surface plots with a single color, which makes details difficult to see in printed media.) In this recipe, we look at some other ways we can customize 3D surface plots, including changing the initial angle of the display and changing the normalization applied for the colormap.
Getting ready
In this recipe, we will further customize the function we plotted in the previous recipe:

We generate points at which this should be plotted, as in the previous recipe:
t = np.linspace(-5, 5) x, y = np.meshgrid(t, t) z = np.exp(-((x-2.)**2 + (y-3.)**2)/4) - np.exp( -((x+3.)**2 + (y+2.)**2)/3)
Let’s see how to customize a three-dimensional plot of these values.
How to do it...
The following steps show how to customize the appearance of a 3D plot:
As usual, our first task is to create a new figure and axes on which we will plot. Since we’re going to customize the properties of the Axes3D
object, we’ll just create a new figure first:
fig = plt.figure()
Now, we need to add a new Axes3D
object to this figure and change the initial viewing angle by setting the azim
and elev
keyword arguments along with the projection=
"3d"
keyword argument that we have seen before:
ax = fig.add_subplot(projection="3d", azim=-80, elev=22)
With this done, we can now plot the surface. We’re going to change the bounds of the normalization so that the maximum value and minimum value are not at the extreme ends of our colormap. We do this by changing the vmin
and vmax
arguments:
ax.plot_surface(x, y, z, cmap="gray", vmin=-1.2, vmax=1.2)
Finally, we can set up the axes labels and the title as usual:
ax.set_title("Customized 3D surface plot") ax.set_xlabel("x") ax.set_ylabel("y") ax.set_zlabel("z")
The resulting plot is shown in Figure 2.9:

Figure 2.9 - Customized 3D surface plot with modified normalization and an initial viewing angle
Comparing Figure 2.6 with Figure 2.9, we can see that the latter generally contains darker shades compared to the former, and the viewing angle offers a better view of the basin where the function is minimized. The darker shade is due to the normalization applied to the values for the colormap, which we altered using the vmin
and vmax
keyword arguments.
How it works...
Color mapping works by assigning an RGB value according to a scale—the colormap. First, the values are normalized so that they lie between 0
and 1
, which is typically done by a linear transformation that takes the minimum value to 0
and the maximum value to 1
. The appropriate color is then applied to each face of the surface plot (or line, in another kind of plot).
In the recipe, we used the vmin
and vmax
keyword arguments to artificially change the value that is mapped to 0
and 1
, respectively, for the purposes of fitting the colormap. In effect, we changed the ends of the color range applied to the plot.
Matplotlib comes with a number of built-in colormaps that can be applied by simply passing the name to the cmap
keyword argument. A list of these colormaps is given in the documentation (https://matplotlib.org/tutorials/colors/colormaps.html) and also comes with a reversed variant, which is obtained by adding the _r
suffix to the name of the chosen colormap.
The viewing angle for a 3D plot is described by two angles: the Azimuthal angle, measured within the reference plane (here, the -
-plane), and the elevation angle, measured as the angle from the reference plane. The default viewing angle for
Axes3D
is -60 Azimuthal and 30 elevation. In the recipe, we used the azim
keyword argument of plot_surface
to change the initial Azimuthal angle to -80 degrees (almost from the direction of the negative axis) and the
elev
argument to change the initial elevation to 22 degrees.
There’s more...
The normalization step in applying a colormap is performed by an object derived from the Normalize
class. Matplotlib provides a number of standard normalization routines, including LogNorm
and PowerNorm
. Of course, you can also create your own subclass of Normalize
to perform the normalization. An alternative Normalize
subclass can be added using the norm
keyword of plot_surface
or other plotting functions.
For more advanced uses, Matplotlib provides an interface for creating custom shading using light sources. This is done by importing the LightSource
class from the matplotlib.colors
package, and then using an instance of this class to shade the surface elements according to the value. This is done using the
shade
method on the LightSource
object:
from matplotlib.colors import LightSource light_source = LightSource(0, 45) # angles of lightsource cmap = plt.get_cmap("binary_r") vals = light_source.shade(z, cmap) surf = ax.plot_surface(x, y, z, facecolors=vals)
Complete examples are shown in the Matplotlib gallery should you wish to learn more about how this.
In addition to the viewing angle, we can also change the type of projection used to represent 3D data as a 2D image. The default is a perspective projection, but we can also use an orthogonal projection by setting the proj_type
keyword argument to "ortho"
.
Plotting vector fields with quiver plots
A vector field is a function that assigns to each point in a region a vector—it is a vector-valued function defined on a space. These are especially common in the study of (systems of) differential equations, where a vector field typically appears as the right-hand side of the equation. (See the Solving systems of differential equations recipe from Chapter 3 for more details.) For this reason, it is often useful to visualize a vector field and understand how the function will evolve over space. For now, we’re simply going to produce a plot of a vector field using a quiver plot, which takes a set of and
coordinates and a set of
and
vectors, and produces a plot on which each point has an arrow in the direction
and whose length is the length of this vector. (Hopefully, this will become more clear when we actually create the said plot.)
Getting ready
As usual, we import the Matplotlib pyplot
interface under the alias plt
. Before we start, we need to define a function that takes a point and produces a vector; we’ll use this later to generate and
data that will be passed to the plotting function.
For this example, we’re going to plot the following vector field:

For this example, we’ll plot the vector field over the region where and
.
How to do it…
The following steps show how to visualize the aforementioned vector field over the specified region.
First, we need to define a Python function that evaluates our vector field at points:
def f(x, y): v = x**2 +y**2 return np.exp(-2*v)*(x+y), np.exp( -2*v)*(x-y)
Next, we need to create our grid of points covering the region. For this, we first create a temporary linspace
routine with values between -1
and 1
. Then, we use meshgrid
to generate a grid of points:
t = np.linspace(-1., 1.) x, y = np.meshgrid(t, t)
Next, we use our function to generate dx
and dy
values that describe the vectors at each grid point:
dx, dy = f(x, y)
Now, we can create a new figure and axis and use the quiver
method to generate a plot:
fig, ax = plt.subplots() ax.quiver(x, y, dx, dy)
The resulting plot is shown in Figure 2.10:

Figure 2.10 - Visualization of a vector field using a quiver plot
In Figure 2.10, we can see the value represented as an arrow based at each
coordinate. The size of the arrow is determined by the magnitude of the vector field. At the origin, the vector field has
, so the arrows nearby are very small.
How it works…
Our example from the recipe is a mathematical construction rather than something that might arise from real data. For this particular case, the arrows describe how some quantity might evolve if it flows according to the vector field we specified.
Each point in the grid is the base of an arrow. The direction of the arrow is given by the corresponding value, and the length of the arrow is normalized by length (so, a vector
with smaller components produces a shorter arrow). This can be customized by changing the
scale
keyword argument. Many other aspects of the plot can be customized too.
There’s more…
If you want to plot a set of trajectories that follow a vector field, you can use the streamplot
method. This will plot trajectories starting at various points to indicate the general flow in different parts of the domain. Each streamline has an arrow to indicate the direction of flow. For example, Figure 2.11 shows the result of using the streamplot
method with the vector field in the recipe:

Figure 2.11 – Plot of the trajectories described by the vector field from the recipe
In a different scenario, you might have data about wind speed (or similar quantities) at a number of coordinates—on a map, say—and you want to plot these quantities in the standard style for weather charts. Then, we can use the barbs
plotting method. The arguments are similar to the quiver
method.
Further reading
The Matplotlib package is extensive, and we can scarcely do it justice in such a short space. The documentation contains far more detail than is provided here. Moreover, there is a large gallery of examples (https://matplotlib.org/gallery/index.html#) covering many more of the capabilities of the package than in this book.
Other packages build on top of Matplotlib to offer high-level plotting methods for specific applications. For example, the Seaborn libraries provide routines for visualizing data (https://seaborn.pydata.org/).