8000 Merge branch 'legend_refactor' · matplotlib/matplotlib@d4b22ff · GitHub
[go: up one dir, main page]

Skip to content

Commit d4b22ff

Browse files
committed
Merge branch 'legend_refactor'
Conflicts: lib/matplotlib/axes.py
2 parents 578f695 + 1b17994 commit d4b22ff

File tree

9 files changed

+1003
-213
lines changed

9 files changed

+1003
-213
lines changed

doc/users/legend_guide.rst

Lines changed: 156 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,39 +28,43 @@ For example, *ax.legend()* is equivalent to::
2828
The :meth:`~matplotlib.axes.Axes.get_legend_handles_labels` method
2929
returns a tuple of two lists, i.e., list of artists and list of labels
3030
(python string). However, it does not return all of its child
31-
artists. It returns all artists in *ax.lines* and *ax.patches* and
32-
some artists in *ax.collection* which are instance of
31+
artists. It returns artists that are currently supported by matplotlib.
32+
33+
For matplotlib v1.0 and before, the supported artists are as follows.
34+
35+
* :class:`~matplotlib.lines.Line2D`
36+
* :class:`~matplotlib.patches.Patch`
37+
* :class:`~matplotlib.collections.LineCollection`
38+
* :class:`~matplotlib.collections.RegularPolyCollection`
39+
40+
And, :meth:`~matplotlib.axes.Axes.get_legend_handles_labels` returns
41+
all artists in *ax.lines*, *ax.patches* and
42+
artists in *ax.collection* which are instance of
3343
:class:`~matplotlib.collections.LineCollection` or
3444
:class:`~matplotlib.collections.RegularPolyCollection`. The label
3545
attributes (returned by get_label() method) of collected artists are
3646
used as text labels. If label attribute is empty string or starts with
37-
"_", that artist will be ignored.
47+
"_", those artists will be ignored.
3848

3949

40-
* Note that not all kind of artists are supported by the legend. The
41-
following is the list of artists that are currently supported.
50+
Therefore, plots drawn by some *pyplot* commands are not supported by
51+
legend. For example, :func:`~matplotlib.pyplot.fill_between` creates
52+
:class:`~matplotlib.collections.PolyCollection` that is not
53+
supported. Also support is limted for some commands that creat
54+
multiple artists. For example, :func:`~matplotlib.pyplot.errorbar`
55+
creates multiples :class:`~matplotlib.lines.Line2D` instances.
4256

43-
* :class:`~matplotlib.lines.Line2D`
44-
* :class:`~matplotlib.patches.Patch`
45-
* :class:`~matplotlib.collections.LineCollection`
46-
* :class:`~matplotlib.collections.RegularPolyCollection`
57+
Unfortunately, there is no easy workaround when you need legend for an
58+
artist not supported by matplotlib (You may use one of the supported
59+
artist as a proxy. See below)
4760

48-
Unfortunately, there is no easy workaround when you need legend for
49-
an artist not in the above list (You may use one of the supported
50-
artist as a proxy. See below), or customize it beyond what is
51-
supported by :class:`matplotlib.legend.Legend`.
61+
In newer version of matplotlib (v1.1 and later), the matplotlib
62+
internals are revised to support
5263

53-
* Remember that some *pyplot* commands return artist not supported by
54-
legend, e.g., :func:`~matplotlib.pyplot.fill_between` returns
55-
:class:`~matplotlib.collections.PolyCollection` that is not
56-
supported. Or some return multiple artists. For example,
57-
:func:`~matplotlib.pyplot.plot` returns list of
58-
:class:`~matplotlib.lines.Line2D` instances, and
59-
:func:`~matplotlib.pyplot.errorbar` returns a length 3 tuple of
60-
:class:`~matplotlib.lines.Line2D` instances.
64+
* complex plots that creates multiple artists (e.g., bar, errorbar, etc)
65+
* custom legend handles
6166

62-
* The legend does not care about the axes that given artists belongs,
63-
i.e., the artists may belong to other axes or even none.
67+
See below for details of new functionality.
6468

6569

6670
Adjusting the Order of Legend items
@@ -180,3 +184,132 @@ legend.
180184

181185
.. plot:: users/plotting/examples/simple_legend02.py
182186
:include-source:
187+
188+
189+
Legend of Complex Plots
190+
=======================
191+
192+
In matplotlib v1.1 (FIXME when released) and later, the legend is
193+
improved to support more plot commands and ease the customization.
194+
195+
Artist Container
196+
----------------
197+
198+
The Artist Container is simple class (derived from tuple) that
199+
contains multiple artists. This is introduced primarily to support
200+
legends for complex plot commands that create multiple artists.
201+
202+
Axes instances now have a "containers" attribute (which is a list, and
203+
this is only intended to be used for generating a legend). The items
204+
in this attribute are also returned by
205+
:meth:`~matplotlib.axes.Axes.get_legend_handles_labels`.
206+
207+
For example, "bar" command creates a series of Rectangle
208+
patches. Previously, it returned a list of these patches. With the
209+
current change, it creates a container object of these rectangle
210+
patches (and these patches are added to Axes.patches attribute as
211+
before) and return it instead. As the container class is derived from
212+
a tuple, it should be backward-compatible. Furthermore, the container
213+
object is added to the Axes.containers attributes so that legend
214+
command can properly create a legend for the bar. Thus, you may do ::
215+
216+
b1 = bar([0, 1, 2], [0.2, 0.3, 0.1], width=0.4,
217+
label="Bar 1", align="center")
218+
legend()
219+
220+
or ::
221+
222+
b1 = bar([0, 1, 2], [0.2, 0.3, 0.1], width=0.4, align="center")
223+
legend([b1], ["Bar 1"])
224+
225+
226+
At this time of writing, however, "bar" and "errorbar" are only
227+
supported (hopefully the list will increase). Here is an example.
228+
229+
.. plot:: mpl_examples/pylab_examples/legend_demo4.py
230+
231+
Legend Handler
232+
--------------
233+
234+
One of the change is that drawing of legend handles is delegated to
235+
legend handlers. For example, :class:`~matplotlib.lines.Line2D`
236+
instances are handled by
237+
:class:`~matplotlib.legend_handler.HandlerLine2D`. The mapping
238+
between the artists and their corresponding handlers are defined in a
239+
handler_map of the legend. The handler_map is a dictionary of
240+
key-handler pair, where key can be an artist instance or its
241+
class. And the handler is a Handler instance.
242+
243+
Let's consider the following sample code, ::
244+
245+
legend([p_1, p_2,..., p_i, ...], ["Test 1", "Test 2", ..., "Test i",...])
246+
247+
For each *p_i*, matplotlib
248+
249+
1. check if *p_i* itself is in the handler_map
250+
2. if not, iterate over type(p_i).mro() until a matching key is found in the handler_map
251+
252+
253+
Unless specified, the defaul handler_map is used. Below is a partial
254+
list of key-handler pairs included in the default handler map.
255+
256+
* Line2D : legend_handler.HandlerLine2D()
257+
* Patch : legend_handler.HandlerPatch()
258+
* LineCollection : legend_handler.HandlerLineCollection()
259+
* ...
260+
261+
262+
The legend command takes an optional argument of "handler_map". When
263+
provided, the default handler map will be updated (using dict.update
264+
method) with the provided one. ::
265+
266+
p1, = plot(x, "ro", label="test1")
267+
p2, = plot(y, "b+", ms=10, label="test2")
268+
269+
my_handler = HandlerLine2D(numpoints=1)
270+
271+
legend(handler_map={Line2D:my_handler})
272+
273+
The above example will use *my_handler* for any Line2D
274+
instances (p1 and p2). ::
275+
276+
legend(handler_map={p1:HandlerLine2D(numpoints=1)})
277+
278+
In the above example, only *p1* will be handled by *my_handler*, while
279+
others will be handled by default handlers.
280+
281+
The curent default handler_map has handlers for errobar and bar
282+
plots. Also, it includes an entry for ¡°tuple¡± which is mapped to
283+
*HandlerTuple*. It simply overplots all the handles for items in the
284+
given tuple. For example,
285+
286+
287+
.. plot::
288+
:include-source:
289+
290+
z = np.random.randn(10)
291+
292+
p1a, = plt.plot(z, "ro", ms=10, mfc="r", mew=2, mec="r") # red filled circle
293+
p1b, = plt.plot(z[:5], "w+", ms=10, mec="w", mew=2) # white cross
294+
295+
plt.legend([p1a, (p1a, p1b)], ["Attr A", "Attr A+B"])
296+
297+
298+
299+
Implement a Custom Handler
300+
--------------------------
301+
302+
Handler can be any callable object with following signature. ::
303+
304+
def __call__(self, legend, orig_handle,
305+
fontsize,
306+
handlebox):
307+
308+
Where *legend* is the legend itself, *orig_handle* is the original
309+
plot (*p_i* in the above example), *fontsize* is the fontsize in
310+
pixles, and *handlebox* is a OffsetBox instance. Within the call, you
311+
create relevant artists (using relevant properties from the *legend*
312+
and/or *orig_handle*) and add them into the handlebox. The artists
313+
needs to be scaled according to the fontsize (note that the size is in
314+
pixel, i.e., this is dpi-scaled value). See legend_handler.py for more
315+
details.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import matplotlib.pyplot as plt
2+
3+
ax = plt.subplot(111)
4+
5+
b1 = ax.bar([0, 1, 2], [0.2, 0.3, 0.1], width=0.4,
6+
label="Bar 1", align="center")
7+
8+
b2 = ax.bar([0.5, 1.5, 2.5], [0.3, 0.2, 0.2], color="red", width=0.4,
9+
label="Bar 2", align="center")
10+
11+
err1 = ax.errorbar([0, 1, 2], [2, 3, 1], xerr=0.4, fmt="s",
12+
label="test 1")
13+
err2 = ax.errorbar([0, 1, 2], [3, 2, 4], yerr=0.3, fmt="o",
14+
label="test 2")
15+
err3 = ax.errorbar([0, 1, 2], [1, 1, 3], xerr=0.4, yerr=0.3, fmt="^",
16+
label="test 3")
17+
18+
# legend
19+
leg1 = plt.legend(loc=1)
20+
21+
plt.show()
22+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import matplotlib.pyplot as plt
2+
import numpy as np
3+
4+
ax = plt.subplot(111, aspect=1)
5+
x, y = np.random.randn(2, 20)
6+
#[1.1, 2, 2.8], [1.1, 2, 1.8]
7+
l1, = ax.plot(x,y, "k+", mew=3, ms=12)
8+
l2, = ax.plot(x,y, "w+", mew=1, ms=10)
9+
10+
import matplotlib.patches as mpatches
11+
c = mpatches.Circle((0, 0), 1, fc="g", ec="r", lw=3)
12+
ax.add_patch(c)
13+
14+
15+
16+
from matplotlib.legend_handler import HandlerPatch
17+
18+
def make_legend_ellipse(legend, orig_handle,
19+
xdescent, ydescent,
20+
width, height, fontsize):
21+
p = mpatches.Ellipse(xy=(0.5*width-0.5*xdescent, 0.5*height-0.5*ydescent),
22+
width = width+xdescent, height=(height+ydescent))
23+
24+
return p
25+
26+
plt.legend([c, (l1, l2)], ["Label 1", "Label 2"],
27+
handler_map={mpatches.Circle:HandlerPatch(patch_func=ma 425B ke_legend_ellipse),
28+
})
29+
30+
plt.show()

0 commit comments

Comments
 (0)
0