8000 DOC: attempt to explain the main different APIs · matplotlib/matplotlib@d61831c · GitHub
[go: up one dir, main page]

Skip to content

Commit d61831c

Browse files
committed
DOC: attempt to explain the main different APIs
1 parent 2ccc67f commit d61831c

File tree

3 files changed

+314
-0
lines changed

3 files changed

+314
-0
lines changed

doc/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ def _check_dependencies():
162162
'python': ('https://docs.python.org/3/', None),
163163
'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None),
164164
'tornado': ('https://www.tornadoweb.org/en/stable/', None),
165+
'xarray': ('https://xarray.pydata.org/en/stable/', None),
165166
}
166167

167168

doc/users/explain/api_interfaces.rst

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
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+

doc/users/explain/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Explanations
88
.. toctree::
99
:maxdepth: 2
1010

11+
api_interfaces.rst
1112
backends.rst
1213
interactive.rst
1314
fonts.rst

0 commit comments

Comments
 (0)
0