Before we begin, a caveat about the terminology is in order. We use the phrase matplotlib object-oriented API to refer to a direct access to matplotlib's artist and backend layers. The pyplot scripting layer is also object-oriented in that it is composed of functions and instances that are object-oriented by design. The pyplot interface is built upon the OOP methodologies utilized by matplotlib. The distinction that we attempt to make in this section is that we will be creating these objects directly instead of using a scripting layer that does the same for us.
As with the previous section, we acknowledge that the object-oriented API for matplotlib has been covered in great detail in other materials and we will therefore simply provide an overview of the interface via an example, much like with the pyplot API overview.
Of the many circumstances that require fine-grained control over the elements of a plot or the customized use of the backend, a very clear use case to directly access the object-oriented matplotlib API is a noninteractive, programmatic generation of plots.
Let's assume that you have just received an enormous amount of data from some experiments that measured magnetic fields under varying conditions, and you need to generate plots for all the interesting datasets, which happen to number in the hundreds. Unless you are a very unlucky grad student (who doesn't have this book), this is a task for which you will have to create specialized code. This is not something that you would want to do manually, creating one image file at a time. You might even run your new code on a cluster, splitting the plotting tasks up across many machines, allowing your results to be viewed much more quickly.
Let's get started by refactoring one of the pylab examples from the notebook—magnetic fields that are generated due to the current that flows through two wires. We will need to accomplish the following:
The first three points represent the work that has to be done in support of our use of matplotlib; the last two points are the focus of the following section. Let's get started.
This is another magnetic field example, but to keep things interesting, we are going to use a different equation. We will use the equation that describes the magnetic field that is generated due to the current passing through two wires in opposite directions (again, in the z axis). Here's the equation that represents this, and which we will solve:
Let's convert this to a function. The following code should look very familiar after the last section—one function is responsible for the building of the vectors that will feed the quiver
plot function, and the other for doing the hard work of computing the vectors of the magnetic field:
In [35]: def get_grid_values(xrange: tuple, yrange:tuple) -> tuple: return np.meshgrid(np.linspace(*xrange), np.linspace(*yrange)) def get_field_components(distance: float, currents: tuple, magconst: float, xrange: tuple, yrange:tuple) -> tuple: (x, y) = get_grid_values(xrange, yrange) x1 = x - distance x2 = x + distance s12 = x1 ** 2 + y ** 2 s22 = x2 ** 2 + y ** 2 (I1, I2) = currents const = magconst / (s12 * s22) Bx = const * -y * ((I1 * s22) + (I2 * s12)) By = const * ((I1 * s22 * x1) + (I2 * s12 * x2)) return (Bx, By)
As mentioned at the beginning of the preceding section, we are going to create some classes that will make our code cleaner. The code will be easier to read six months from now, and it will be easier to troubleshoot it should something go wrong. There are two areas that need to be addressed:
The Experiment
class just needs to accept experimental data in its constructor and then provide access to this data via the attributes. As such, only a single method is needed—its constructor:
In [36]: class Experiment: def __init__(self, d: float, Is: tuple, xrange, yrange, m: float=2.0e-7): self.distance = d self.magconst = m (self.current1, self.current2) = Is self.xrange = xrange (self.xmin, self.xmax, _) = xrange self.yrange = yrange (self.ymin, self.ymax, _) = yrange (self.x, self.y) = get_grid_values(xrange, yrange) self.ranges = [self.xmin, self.xmax, self.ymin, self.ymax] (self.Bx, self.By) = get_field_components( self.distance, Is, self.magconst, self.xrange, self.yrange) self.B = self.Bx + self.By
Next, let's create a configuration class. This will hold everything the artist and backend layers need to create the plots:
In [37]: from matplotlib.colors import LinearSegmentedColormap class ExperimentPlotConfig: def __init__(self, size: tuple, title_size: int=14, label_size: int=10, bgcolor: str="#aaaaaa", num_colors: int=8, colorbar_adjust: float=1.0, aspect_ratio=1.0): self.size = size self.title_size = title_size self.label_size = label_size self.bgcolor = bgcolor self.num_colors = num_colors self.colorbar_adjust = colorbar_adjust self.aspect_ratio = aspect_ratio def fg_cmap(self, palette_name="husl"): colors = sns.color_palette( pallete_name, self.num_colors) colors.reverse() return LinearSegmentedColormap.from_list( pallete_name, colors) def bg_cmap(self): return sns.dark_palette(self.bgcolor, as_cmap=True)
We've arrived at the point where we need to create the most significant bit of functionality in our task. This code will serve the same purpose as pyplot in matplotlib. In our particular case, it will batch jobs via matplotlib's object-oriented interface.
From your previous reading (as well as this book's chapter on the matplotlib architecture), you'll remember that the Plotter
class needs to do the following tasks:
In our case, we will have two plots—one representing the magnitude of the vector field at any given point (this will be a background image), on top of which will be the second plot, which is a quiver
plot of the vectors from the grid of coordinates.
Here's the Plotter
class, which demonstrates the object-oriented API of matplotlib:
In [38]: class Plotter: def __init__(self, index, plot_config, experiment): self.cfg = plot_config self.data = experiment self.figure_manager = backend_agg.new_figure_manager( index, figsize=self.cfg.size) self.figure = self.figure_manager.canvas.figure def get_axes(self): gs = GridSpec(1, 1) return self.figure.add_subplot(gs[0, 0]) def update_axes(self, axes): tmpl = ('Magnetic Field for Two Wires ' '$I_1$={} A, $I_2$={} A, at d={} m') title = tmpl.format(self.data.current1, self.data.current2, self.data.distance) axes.set_title( title, fontsize=self.cfg.title_size) axes.set_xlabel( '$x$ m', fontsize=self.cfg.label_size) axes.set_ylabel( '$y$ m', fontsize=self.cfg.label_size) axes.axis( self.data.ranges, aspect=self.cfg.aspect_ratio) return axes def make_background(self, axes): return axes.imshow( self.data.B, extent=self.data.ranges, cmap=self.cfg.bg_cmap()) def make_quiver(self, axes): return axes.quiver( self.data.x, self.data.y, self.data.Bx, self.data.By, self.data.B, cmap=self.cfg.fore_cmap()) def make_colorbar(self, figure, quiver): return self.figure.colorbar( quiver, shrink=self.cfg.colorbar_adjust) def save(self, filename, **kwargs): axes = self.update_axes(self.get_axes()) back = self.make_background(axes) quiver = self.make_quiver(axes) colorbar = self.make_colorbar(self.figure, quiver) self.figure.savefig(filename, **kwargs) print("Saved {}.".format(filename))
Take a look at the creation of the figure manager in the constructor method of the Plotter
class—we directly interfaced with the backend layer. Likewise, when we obtain the figure reference from the canvas, this is the domain of the backend layer.
Most of the remaining code interfaces with the artist layer of the matplotlib architecture. There are some points worth making in some of this code:
Figure.add_subplot
method returns an Axes
instance.Axes.imshow
method may not be immediately obvious either. It is named show
and the docstring says that it displays, but what it really does is create an AxesImage
instance from the given data and add the image to the Axes
instance (returning the AxesImage
instance).shrink
keyword in the Figure.colorbar
call for an aesthetic purpose. It balanced the relative size of the colorbar with the plot.The last bit that touches the backend layer is done indirectly through the artist layer—via the call to savefig
. Under the hood, what really happens here is that the backend layer's particular canvas instance (in our case, FigureCanvasAgg
) calls its print_figure
method (which, in turn, calls a method appropriate for the given output format).
To bring all of these together, we need some code to perform the following tasks:
Plotter
class can useExperiment
instances, complete with the data that has to be plottedHere's the code for this:
In [39]: plot_config = ExperimentPlotConfig( size=(12,10), title_size=20, label_size=16, bgcolor="#666666", colorbar_adjust=0.96) experiments = [ Experiment(d=0.04, Is=(1,1), xrange=(-0.1, 0.1, 20), yrange=(-0.1, 0.1, 20)), Experiment(d=2.0, Is=(10,20), xrange=(-1.2, 1.2, 70), yrange=(-1.2, 1.2, 70)), Experiment(d=4.0, Is=(45,15), xrange=(-5.3, 5.3, 60), yrange=(-5.3, 5.3, 60)), Experiment(d=2.0, Is=(1,2), xrange=(-8.0, 8.0, 50), yrange=(-8.0, 8.0, 50))] for (index, experiment) in enumerate(experiments): filename = "expmt_{}.png".format(index) Plotter(index, plot_config, experiment).save(filename)
When you press the Shift + Enter keys for the cell in the IPython Notebook, you will see whether the output for each file that it saves is printed or not. You can also verify the same with the following code:
In [40]: ls -1 expmt*.png expmt_0.png expmt_1.png expmt_2.png expmt_3.png
If you would like to view the files in the notebook, you can import the image display class from IPython in the following way:
In [41]: Image("expmt_1.png")
The following plot is the result of the preceding command:
In [42]: Image("expmt_2.png")
The following plot is the result of the preceding command:
This brings the review of the object-oriented matplotlib API to a close. However, before we finish the chapter, we will take a look at how the other libraries use matplotlib.