The matplotlib object-oriented API

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:

  • Convert the pyplot procedural-style definition of values to functions that return values
  • Create an object that wraps the experimental data
  • Create an object that wraps the configuration data that is needed by our plots
  • Create an object that manages the matplotlib instances that we need for our plot
  • Create some experiments and have their plots saved in separate files

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.

Equations

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:

Equations

Tip

A note for the curious reader—if you are following along in the IPython Notebook for this chapter, you will see a link in the section where this equation is presented. This link will take you to another notebook accompanying this chapter, which guides you through the preceding derivation.

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)

Helper classes

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:

  • A class that is used to organize experimental data
  • A class that is used to configure data

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)

The Plotter class

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:

  • Create a figure manager
  • Provide access to the managed figure instance
  • Create and configure the axes
  • Plot the data
  • Save the plot to a file

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:

  • We named the axes-generating method intuitively. It is not obvious, however, that the Figure.add_subplot method returns an Axes instance.
  • The 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).
  • We used the 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).

Running the jobs

To bring all of these together, we need some code to perform the following tasks:

  • Create a configuration instance that the Plotter class can use
  • Create a list of the Experiment instances, complete with the data that has to be plotted
  • Iterate through each of these instances, saving the plots to a file

Here'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:

Running the jobs
In [42]: Image("expmt_2.png")

The following plot is the result of the preceding command:

Running the jobs

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.

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

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