|
| 1 | +.. _api_interfaces: |
| 2 | + |
| 3 | +======================================== |
| 4 | +Matplotlib Application Interfaces (APIs) |
| 5 | +======================================== |
| 6 | + |
| 7 | +Python is a flexible language allowing different design choices when |
| 8 | +creating an application programming interface (API). Matplotlib |
| 9 | +has seen two of these over the years (or more, depending on how you |
| 10 | +count). In addition, external libraries have their own interfaces to |
| 11 | +Matplotlib. This means that code snippets across the internet are written |
| 12 | +using different interfaces, often leading to confusion. The three major |
| 13 | +interfaces are: |
| 14 | + |
| 15 | +- an explicit interface that uses methods on a Figure or Axes object to |
| 16 | + create other Artists, and build a visualization step by step in an |
| 17 | + `imperative <https://en.wikipedia.org/wiki/Imperative_programming>`_ |
| 18 | + manner. |
| 19 | +- an implicit interface that keeps track of the last Figure and Axes |
| 20 | + created, and adds Artists to the object it thinks the user wants. This |
| 21 | + interface is also imperative. |
| 22 | +- a number of downstream libraries offer a |
| 23 | + `declarative <https://en.wikipedia.org/wiki/Declarative_programming>`_ |
| 24 | + interface, usually where a data object has a ``plot`` method implemented |
| 25 | + that will plot the data. |
| 26 | + |
| 27 | +The explicit "Axes" interface |
| 28 | +----------------------------- |
| 29 | + |
| 30 | +The "Axes" interface is how Matplotlib is implemented, and many customizations |
| 31 | +and fine-tuning end up needing to be done at this level. So even if you |
| 32 | +prefer the higher-level interfaces, it is very useful to understand that this |
| 33 | +interface exists, and how to access it. |
| 34 | + |
| 35 | +In general, using it is as easy as instantiating an instance of a |
| 36 | +`~.matplotlib.figure.Figure` class (``fig`` below), using an |
| 37 | +`~.Figure.add_axes` method (or similar) on that object to create an |
| 38 | +`~.matplotlib.axes.Axes` object (``ax`` below), and then calling drawing methods |
| 39 | +on the Axes (``plot`` in this example): |
| 40 | + |
| 41 | +.. plot:: |
| 42 | + :include-source: |
| 43 | + :align: center |
| 44 | + |
| 45 | + import matplotlib.pyplot as plt |
| 46 | + |
| 47 | + fig = plt.figure() |
| 48 | + ax = fig.add_axes([0.1, 0.1, 0.8, 0.8]) |
| 49 | + l = ax.plot([1, 2, 3, 4], [0, 0.5, 1, 0.2]) |
| 50 | + |
| 51 | +Note that ``plot`` itself also returns an object that can be subsequently |
| 52 | +manipulated. |
| 53 | + |
| 54 | +We call this an "explicit" interface because each object is explicitly |
| 55 | +referenced, and used to make the next object. Keeping handles to the |
| 56 | +objects is very flexible, and allows us to customize the objects after |
| 57 | +they are created, but before they are displayed. |
| 58 | + |
| 59 | +This is also an example of an "imperative" interface because the user tells |
| 60 | +Matplotlib what to do each step of the way. Of course Matplotlib has defaults |
| 61 | +for many things (line widths, fonts, colors, etc.), but this interface tells |
| 62 | +Matplotlib how to compose the visualization. Of course this paradigm is not |
| 63 | +pure, in that ``fig.add_axes()`` encapsulates creating many of the objects on |
| 64 | +an Axes (spines, labels, ticks) and ``ax.plot()`` encapsulates creating a |
| 65 | +line plot. But it is much more "imperative" than something like |
| 66 | +``data.plot()`` as we will see below. |
| 67 | + |
| 68 | + |
| 69 | +The implicit "pyplot" interface |
| 70 | +------------------------------- |
| 71 | + |
| 72 | +The "pyplot" interface arose from Matlab, where one would just call |
| 73 | +``plot([1, 2, 3, 4], [0, 0.5, 1, 0.2])`` and a figure with axes would be |
| 74 | +created for you. The `~.matplotlib.pyplot` module shadows most of the |
| 75 | +`~.matplotlib.axes.Axes` plotting methods to give the equivalent of |
| 76 | +the above: |
| 77 | + |
| 78 | +.. plot:: |
| 79 | + :include-source: |
| 80 | + :align: center |
| 81 | + |
| 82 | + import matplotlib.pyplot as plt |
| 83 | + |
| 84 | + plt.plot([1, 2, 3, 4], [0, 0.5, 1, 0.2]) |
| 85 | + |
| 86 | +This can be convenient, particularly when doing interactive work or simple |
| 87 | +scripts - it saves at least a line of typing, and saves the user from having |
| 88 | +to think about figure and axes creation. However, for more complicated scripts |
| 89 | +it can be at more of a disadvantage - imagine you want to loop through a number |
| 90 | +of variables and plot them on separate Axes, it is often more straight-forward |
| 91 | +to create the Axes beforehand. It can also become ambiguous when you have |
| 92 | +multiple Figures or Axes which one is being acted on. In general the |
| 93 | +Matplotlib project recommends using the explicit interface, at least for code |
| 94 | +you want to share or preserve. |
| 95 | + |
| 96 | +Whichever interface you choose, it is helpful to decode what is happening |
| 97 | +in the pyplot interface. Matplotib retains a list of Figures, and each Figure |
| 98 | +retains a list of Axes on the figure. The most recent figure can be retrieved |
| 99 | +with `~.pyplot.gcf` and the most recent Axes with `~.pyplot.gca`. So for a more |
| 100 | +complicated example: |
| 101 | + |
| 102 | +.. plot:: |
| 103 | + :include-source: |
| 104 | + :align: center |
| 105 | + |
| 106 | + import matplotlib.pyplot as plt |
| 107 | + |
| 108 | + plt.subplot(1, 2, 1) |
| 109 | + plt.plot([1, 2, 3], [0, 0.5, 0.2]) |
| 110 | + |
| 111 | + plt.subplot(1, 2, 2) |
| 112 | + plt.plot([3, 2, 1], [0, 0.5, 0.2]) |
| 113 | + |
| 114 | +is equivalent to |
| 115 | + |
| 116 | +.. plot:: |
| 117 | + :include-source: |
| 118 | + :align: center |
| 119 | + |
| 120 | + import matplotlib.pyplot as plt |
| 121 | + |
| 122 | + plt.subplot(1, 2, 1) |
| 123 | + ax = plt.gca() |
| 124 | + ax.plot([1, 2, 3], [0, 0.5, 0.2]) |
| 125 | + |
| 126 | + plt.subplot(1, 2, 2) |
| 127 | + ax = plt.gca() |
| 128 | + ax.plot([3, 2, 1], [0, 0.5, 0.2]) |
| 129 | + |
| 130 | +which in the explict interface would be: |
| 131 | + |
| 132 | +.. plot:: |
| 133 | + :include-source: |
| 134 | + :align: center |
| 135 | + |
| 136 | + import matplotlib.pyplot as plt |
| 137 | + |
| 138 | + fig, axs = plt.subplots(1, 2) |
| 139 | + axs[0].plot([1, 2, 3], [0, 0.5, 0.2]) |
| 140 | + axs[1].plot([3, 2, 1], [0, 0.5, 0.2]) |
| 141 | + |
| 142 | +The "pyplot" interface creates and uses all the same objects, that the "Axes" |
| 143 | +interface does, but it implicitly keeps track of the current Figure |
| 144 | +(``plt.gcf()``) and Axes (``plt.gca()``) for the user, and uses those Figures |
| 145 | +and Axes for any "pyplot" plotting methods until another Axes or Figure is |
| 146 | +selected. |
| 147 | + |
| 148 | +Why be explicit? |
| 149 | +~~~~~~~~~~~~~~~~ |
| 150 | + |
| 151 | +What happens if you have to backtrack, and operate on an old axes? One that |
| 152 | +is not referenced by ``plt.gca()``. Of course you can do this, the simplest |
| 153 | +way is to call ``subplot`` again with the same arguments. However, that quickly |
| 154 | +becomes inelegant. You can also peer into the Figure object and get its list |
| 155 | +of Axes, however, that can be misleading (colorbars are axes too!). The best |
| 156 | +solution is probably to save a handle to every axes you create, but if you |
| 157 | +do that, why not simply create the axes at the start? |
| 158 | + |
| 159 | +The first approach is to call ``plt.subplot`` again: |
| 160 | + |
| 161 | +.. plot:: |
| 162 | + :include-source: |
| 163 | + :align: center |
| 164 | + |
| 165 | + import matplotlib.pyplot as plt |
| 166 | + |
| 167 | + plt.subplot(1, 2, 1) |
| 168 | + plt.plot([1, 2, 3], [0, 0.5, 0.2]) |
| 169 | + |
| 170 | + plt.subplot(1, 2, 2) |
| 171 | + plt.plot([3, 2, 1], [0, 0.5, 0.2]) |
| 172 | + |
| 173 | + plt.suptitle('Implicit Interface: re-call subplot') |
| 174 | + |
| 175 | + for i in range(1, 3): |
| 176 | + plt.subplot(1, 2, i) |
| 177 | + plt.xlabel('Boo') |
| 178 | + |
| 179 | +The second is to save a handle: |
| 180 | + |
| 181 | +.. plot:: |
| 182 | + :include-source: |
| 183 | + :align: center |
| 184 | + |
| 185 | + import matplotlib.pyplot as plt |
| 186 | + |
| 187 | + axs = [] |
| 188 | + ax = plt.subplot(1, 2, 1) |
| 189 | + axs += [ax] |
| 190 | + plt.plot([1, 2, 3], [0, 0.5, 0.2]) |
| 191 | + |
| 192 | + ax = plt.subplot(1, 2, 2) |
| 193 | + axs += [ax] |
| 194 | + plt.plot([3, 2, 1], [0, 0.5, 0.2]) |
| 195 | + |
| 196 | + plt.suptitle('Implicit Interface: save handles') |
| 197 | + |
| 198 | + for i in range(2): |
| 199 | + plt.sca(axs[i]) |
| 200 | + plt.xlabel('Boo') |
| 201 | + |
| 202 | +But the recommended way would be to just be explicit from the outset: |
| 203 | + |
| 204 | +.. plot:: |
| 205 | + :include-source: |
| 206 | + :align: center |
| 207 | + |
| 208 | + import matplotlib.pyplot as plt |
| 209 | + |
| 210 | + fig, axs = plt.subplots(1, 2) |
| 211 | + axs[0].plot([1, 2, 3], [0, 0.5, 0.2]) |
| 212 | + axs[1].plot([3, 2, 1], [0, 0.5, 0.2]) |
| 213 | + fig.suptitle('Explicit Interface') |
| 214 | + for i in range(2): |
| 215 | + axs[i].set_xlabel('Boo') |
| 216 | + |
| 217 | + |
| 218 | +The declarative "Data" interface |
| 219 | +-------------------------------- |
| 220 | + |
| 221 | +Matplotlib does not implement this interface natively. However, it is seen |
| 222 | +in `pandas`, `xarray`, and |
| 223 | +other third-party libraries. These interfaces are "declarative" in that |
| 224 | +the data object is told to plot itself, and then it uses Matplotlib behind |
| 225 | +the scenes to create the visualization. |
| 226 | + |
| 227 | +For illustrative purposes, a downstream library may implement a simple |
| 228 | +data container that has ``x`` and ``y`` data stored together, and then |
| 229 | +impliments a ``plot`` method as so: |
| 230 | + |
| 231 | +.. plot:: |
| 232 | + :include-source: |
| 233 | + :align: center |
| 234 | + |
| 235 | + import matplotlib.pyplot as plt |
| 236 | + |
| 237 | + # supplied by downstream library: |
| 238 | + class DataContainer(): |
| 239 | + |
| 240 | + def __init__(self, x, y): |
| 241 | + """ |
| 242 | + Proper docstring here! |
| 243 | + """ |
| 244 | + self._x = x |
| 245 | + self._y = y |
| 246 | + |
| 247 | + def plot(self, ax=None, **kwargs): |
| 248 | + if ax is None: |
| 249 | + ax = plt.gca() |
| 250 | + ax.plot(self._x, self._y, **kwargs) |
| 251 | + ax.set_title('Plotted from DataClass!') |
| 252 | + return ax |
| 253 | +
|
| 254 | + |
| 255 | + # what the user usually calls: |
| 256 | + data = DataContainer([0, 1, 2, 3], [0, 0.2, 0.5, 0.3]) |
| 257 | + data.plot() |
| 258 | + |
| 259 | +So the library can hide all the nitty-gritty from the user, and can make a |
| 260 | +visualization appropriate to the data type, often with good labels, choices of |
| 261 | +colormaps, and other nice features. |
| 262 | + |
| 263 | +In the above, however, we may not have liked the title the library provided. |
| 264 | +Thankfully, they pass us back the axes object from the ``plot()`` method, and |
| 265 | +understanding the explicit Axes interface, we could manually manipulate it |
| 266 | +``ax.set_title('My preferred title')``. |
| 267 | + |
| 268 | +Many libraries also allow ``plot`` to take an optional *ax* argument. |
| 269 | +This allows us to place the visualization in an Axes of our explicit |
| 270 | +choice. |
| 271 | + |
| 272 | +Summary |
| 273 | +------- |
| 274 | + |
| 275 | +Overall, it is useful to understand the explicit "Axes" interface since it is |
| 276 | +the most flexible and underlies the other interfaces. A user can usually |
| 277 | +figure out how to drop down to the explicit interface and operate on the |
| 278 | +underlying objects. While the explicit interface can be a bit more verbose |
| 279 | +to setup, complicated plots will often end up simpler than trying to use |
| 280 | +the implicit "pyplot" interface. |
| 281 | + |
| 282 | +.. note:: |
| 283 | + |
| 284 | + It is sometimes confusing to people that we import ``pyplot`` for both |
| 285 | + interfaces. Currently, the ``pyplot`` module implements the "pyplot" |
| 286 | + interface, but it also provides top-level Figure and Axes creation |
| 287 | + methods, and ultimately spins up the graphical user interface, if one |
| 288 | + is being used. So ``pyplot`` is still needed regardless of the |
| 289 | + interface chosen. |
| 290 | + |
| 291 | +Similarly, the declarative interfaces provided by partner libraries use the |
| 292 | +objects accessible by the Axes interface, and often accept these as arguments |
| 293 | +or pass them back from methods. It is usually essential to use the explicit |
| 294 | +"Axes" interface to perform any customization of the initial visualization. |
| 295 | + |
| 296 | +Appendix: "Axes" interface with data structures |
| 297 | +----------------------------------------------- |
| 298 | + |
| 299 | +Most `~.axes.Axes` methods allow yet another API addressing by passing a |
| 300 | +*data* object to the method and specifying the arguments as strings: |
| 301 | + |
| 302 | +.. plot:: |
| 303 | + :include-source: |
| 304 | + :align: center |
| 305 | + |
| 306 | + import matplotlib.pyplot as plt |
| 307 | + |
| 308 | + data = {'x1': [0, 1, 2, 3], 'y1': [0, 0.2, 0.4, 0.1]} |
| 309 | + fig, ax = plt.subplots() |
| 310 | + ax.plot(x='x1', y='y1', data=data) |
| 311 | + |
| 312 | + |
0 commit comments