Menu

[r4817]: / trunk / matplotlib / lib / matplotlib / legend.py  Maximize  Restore  History

Download this file

519 lines (416 with data), 18.5 kB

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
"""
Place a legend on the axes at location loc. Labels are a
sequence of strings and loc can be a string or an integer
specifying the legend location
The location codes are
'best' : 0, (only implemented for axis legends)
'upper right' : 1,
'upper left' : 2,
'lower left' : 3,
'lower right' : 4,
'right' : 5,
'center left' : 6,
'center right' : 7,
'lower center' : 8,
'upper center' : 9,
'center' : 10,
Return value is a sequence of text, line instances that make
up the legend
"""
from __future__ import division
import warnings
import numpy as npy
from matplotlib import rcParams
from artist import Artist
from cbook import is_string_like, iterable, silent_list
from font_manager import FontProperties
from lines import Line2D
from mlab import segments_intersect
from patches import Patch, Rectangle, Shadow, bbox_artist
from collections import LineCollection, RegularPolyCollection
from text import Text
from transforms import Affine2D, Bbox, BboxTransformTo
class Legend(Artist):
"""
Place a legend on the axes at location loc. Labels are a
sequence of strings and loc can be a string or an integer
specifying the legend location
The location codes are
'best' : 0, (only implemented for axis legends)
'upper right' : 1,
'upper left' : 2,
'lower left' : 3,
'lower right' : 4,
'right' : 5,
'center left' : 6,
'center right' : 7,
'lower center' : 8,
'upper center' : 9,
'center' : 10,
Return value is a sequence of text, line instances that make
up the legend
"""
codes = {'best' : 0, # only implemented for axis legends
'upper right' : 1,
'upper left' : 2,
'lower left' : 3,
'lower right' : 4,
'right' : 5,
'center left' : 6,
'center right' : 7,
'lower center' : 8,
'upper center' : 9,
'center' : 10,
}
zorder = 5
def __str__(self):
return "Legend"
def __init__(self, parent, handles, labels,
loc = None,
numpoints = None, # the number of points in the legend line
prop = None,
pad = None, # the fractional whitespace inside the legend border
markerscale = None, # the relative size of legend markers vs. original
# the following dimensions are in axes coords
labelsep = None, # the vertical space between the legend entries
handlelen = None, # the length of the legend lines
handletextsep = None, # the space between the legend line and legend text
axespad = None, # the border between the axes and legend edge
shadow = None
):
"""
parent # the artist that contains the legend
handles # a list of artists (lines, patches) to add to the legend
labels # a list of strings to label the legend
loc # a location code
numpoints = 4 # the number of points in the legend line
prop = FontProperties(size='smaller') # the font property
pad = 0.2 # the fractional whitespace inside the legend border
markerscale = 0.6 # the relative size of legend markers vs. original
shadow # if True, draw a shadow behind legend
The following dimensions are in axes coords
labelsep = 0.005 # the vertical space between the legend entries
handlelen = 0.05 # the length of the legend lines
handletextsep = 0.02 # the space between the legend line and legend text
axespad = 0.02 # the border between the axes and legend edge
"""
from axes import Axes # local import only to avoid circularity
from figure import Figure # local import only to avoid circularity
Artist.__init__(self)
proplist=[numpoints, pad, markerscale, labelsep, handlelen, handletextsep, axespad, shadow]
propnames=['numpoints', 'pad', 'markerscale', 'labelsep', 'handlelen', 'handletextsep', 'axespad', 'shadow']
for name, value in zip(propnames,proplist):
if value is None:
value=rcParams["legend."+name]
setattr(self,name,value)
if prop is None:
self.prop=FontProperties(size=rcParams["legend.fontsize"])
else:
self.prop=prop
self.fontsize = self.prop.get_size_in_points()
if isinstance(parent,Axes):
self.isaxes = True
self.set_figure(parent.figure)
elif isinstance(parent,Figure):
self.isaxes = False
self.set_figure(parent)
else:
raise TypeError("Legend needs either Axes or Figure as parent")
self.parent = parent
self._offsetTransform = Affine2D()
self._parentTransform = BboxTransformTo(parent.bbox)
Artist.set_transform(self, self._offsetTransform + self._parentTransform)
if loc is None:
loc = rcParams["legend.loc"]
if not self.isaxes and loc in [0,'best']:
loc = 'upper right'
if is_string_like(loc):
if not self.codes.has_key(loc):
if self.isaxes:
warnings.warn('Unrecognized location "%s". Falling back on "best"; '
'valid locations are\n\t%s\n'
% (loc, '\n\t'.join(self.codes.keys())))
loc = 0
else:
warnings.warn('Unrecognized location "%s". Falling back on "upper right"; '
'valid locations are\n\t%s\n'
% (loc, '\n\t'.join(self.codes.keys())))
loc = 1
else:
loc = self.codes[loc]
if not self.isaxes and loc == 0:
warnings.warn('Automatic legend placement (loc="best") not implemented for figure legend. '
'Falling back on "upper right".')
loc = 1
self._loc = loc
self.legendPatch = Rectangle(
xy=(0.0, 0.0), width=0.5, height=0.5,
facecolor='w', edgecolor='k',
)
self._set_artist_props(self.legendPatch)
# make a trial box in the middle of the axes. relocate it
# based on it's bbox
left, top = 0.5, 0.5
if self.numpoints == 1:
self._xdata = npy.array([left + self.handlelen*0.5])
else:
self._xdata = npy.linspace(left, left + self.handlelen, self.numpoints)
textleft = left+ self.handlelen+self.handletextsep
self.texts = self._get_texts(labels, textleft, top)
self.legendHandles = self._get_handles(handles, self.texts)
self._drawFrame = True
def _set_artist_props(self, a):
a.set_figure(self.figure)
a.set_transform(self.get_transform())
def _approx_text_height(self):
return self.fontsize/72.0*self.figure.dpi/self.parent.bbox.height
def draw(self, renderer):
if not self.get_visible(): return
renderer.open_group('legend')
self._update_positions(renderer)
if self._drawFrame:
if self.shadow:
shadow = Shadow(self.legendPatch, -0.005, -0.005)
shadow.draw(renderer)
self.legendPatch.draw(renderer)
if not len(self.legendHandles) and not len(self.texts): return
for h in self.legendHandles:
if h is not None:
h.draw(renderer)
if 0: bbox_artist(h, renderer)
for t in self.texts:
if 0: bbox_artist(t, renderer)
t.draw(renderer)
renderer.close_group('legend')
#draw_bbox(self.save, renderer, 'g')
#draw_bbox(self.ibox, renderer, 'r', self.get_transform())
def _get_handle_text_bbox(self, renderer):
'Get a bbox for the text and lines in axes coords'
bboxesText = [t.get_window_extent(renderer) for t in self.texts]
bboxesHandles = [h.get_window_extent(renderer) for h in self.legendHandles if h is not None]
bboxesAll = bboxesText
bboxesAll.extend(bboxesHandles)
bbox = Bbox.union(bboxesAll)
self.save = bbox
ibox = bbox.inverse_transformed(self.get_transform())
self.ibox = ibox
return ibox
def _get_handles(self, handles, texts):
HEIGHT = self._approx_text_height()
ret = [] # the returned legend lines
for handle, label in zip(handles, texts):
x, y = label.get_position()
x -= self.handlelen + self.handletextsep
if isinstance(handle, Line2D):
ydata = (y-HEIGHT/2)*npy.ones(self._xdata.shape, float)
legline = Line2D(self._xdata, ydata)
legline.update_from(handle)
self._set_artist_props(legline) # after update
legline.set_clip_box(None)
legline.set_clip_path(self.legendPatch)
legline.set_markersize(self.markerscale*legline.get_markersize())
ret.append(legline)
elif isinstance(handle, Patch):
p = Rectangle(xy=(min(self._xdata), y-3/4*HEIGHT),
width = self.handlelen, height=HEIGHT/2,
)
p.update_from(handle)
self._set_artist_props(p)
p.set_clip_box(None)
p.set_clip_path(self.legendPatch)
ret.append(p)
elif isinstance(handle, LineCollection):
ydata = (y-HEIGHT/2)*npy.ones(self._xdata.shape, float)
legline = Line2D(self._xdata, ydata)
self._set_artist_props(legline)
legline.set_clip_box(None)
legline.set_clip_path(self.legendPatch)
lw = handle.get_linewidth()[0]
dashes = handle.get_dashes()
color = handle.get_colors()[0]
legline.set_color(color)
legline.set_linewidth(lw)
legline.set_dashes(dashes)
ret.append(legline)
elif isinstance(handle, RegularPolyCollection):
p = Rectangle(xy=(min(self._xdata), y-3/4*HEIGHT),
width = self.handlelen, height=HEIGHT/2,
)
p.set_facecolor(handle._facecolors[0])
if handle._edgecolors != 'None':
p.set_edgecolor(handle._edgecolors[0])
self._set_artist_props(p)
p.set_clip_box(None)
p.set_clip_path(self.legendPatch)
ret.append(p)
else:
ret.append(None)
return ret
def _auto_legend_data(self):
""" Returns list of vertices and extents covered by the plot.
Returns a two long list.
First element is a list of (x, y) vertices (in
axes-coordinates) covered by all the lines and line
collections, in the legend's handles.
Second element is a list of bounding boxes for all the patches in
the legend's handles.
"""
assert self.isaxes # should always hold because function is only called internally
ax = self.parent
vertices = []
bboxes = []
lines = []
inverse_transform = ax.transAxes.inverted()
for handle in ax.lines:
assert isinstance(handle, Line2D)
path = handle.get_path()
trans = handle.get_transform()
tpath = trans.transform_path(path)
apath = inverse_transform.transform_path(tpath)
lines.append(apath)
for handle in ax.patches:
assert isinstance(handle, Patch)
if isinstance(handle, Rectangle):
transform = handle.get_data_transform() + inverse_transform
bboxes.append(handle.get_bbox().transformed(transform))
else:
transform = handle.get_transform() + inverse_transform
bboxes.append(handle.get_path().get_extents(transform))
return [vertices, bboxes, lines]
def draw_frame(self, b):
'b is a boolean. Set draw frame to b'
self._drawFrame = b
def get_frame(self):
'return the Rectangle instance used to frame the legend'
return self.legendPatch
def get_lines(self):
'return a list of lines.Line2D instances in the legend'
return [h for h in self.legendHandles if isinstance(h, Line2D)]
def get_patches(self):
'return a list of patch instances in the legend'
return silent_list('Patch', [h for h in self.legendHandles if isinstance(h, Patch)])
def get_texts(self):
'return a list of text.Text instance in the legend'
return silent_list('Text', self.texts)
def _get_texts(self, labels, left, upper):
# height in axes coords
HEIGHT = self._approx_text_height()
pos = upper
x = left
ret = [] # the returned list of text instances
for l in labels:
text = Text(
x=x, y=pos,
text=l,
fontproperties=self.prop,
verticalalignment='top',
horizontalalignment='left'
)
self._set_artist_props(text)
ret.append(text)
pos -= HEIGHT
return ret
def get_window_extent(self):
return self.legendPatch.get_window_extent()
def _offset(self, ox, oy):
'Move all the artists by ox,oy (axes coords)'
self._offsetTransform.clear().translate(ox, oy)
def _find_best_position(self, width, height, consider=None):
"""Determine the best location to place the legend.
`consider` is a list of (x, y) pairs to consider as a potential
lower-left corner of the legend. All are axes coords.
"""
assert self.isaxes # should always hold because function is only called internally
verts, bboxes, lines = self._auto_legend_data()
consider = [self._loc_to_axes_coords(x, width, height) for x in range(1, len(self.codes))]
tx, ty = self.legendPatch.get_x(), self.legendPatch.get_y()
candidates = []
for l, b in consider:
legendBox = Bbox.from_bounds(l, b, width, height)
badness = 0
badness = legendBox.count_contains(verts)
badness += legendBox.count_overlaps(bboxes)
for line in lines:
if line.intersects_bbox(legendBox):
badness += 1
ox, oy = l-tx, b-ty
if badness == 0:
return ox, oy
candidates.append((badness, (ox, oy)))
# rather than use min() or list.sort(), do this so that we are assured
# that in the case of two equal badnesses, the one first considered is
# returned.
minCandidate = candidates[0]
for candidate in candidates:
if candidate[0] < minCandidate[0]:
minCandidate = candidate
ox, oy = minCandidate[1]
return ox, oy
def _loc_to_axes_coords(self, loc, width, height):
"""Convert a location code to axes coordinates.
- loc: a location code in range(1, 11).
This corresponds to the possible values for self._loc, excluding "best".
- width, height: the final size of the legend, axes units.
"""
assert loc in range(1,11) # called only internally
BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11)
if loc in (UL, LL, CL): # left
x = self.axespad
elif loc in (UR, LR, CR, R): # right
x = 1.0 - (width + self.axespad)
elif loc in (LC, UC, C): # center x
x = (0.5 - width/2.0)
if loc in (UR, UL, UC): # upper
y = 1.0 - (height + self.axespad)
elif loc in (LL, LR, LC): # lower
y = self.axespad
elif loc in (CL, CR, C, R): # center y
y = (0.5 - height/2.0)
return x,y
def _update_positions(self, renderer):
# called from renderer to allow more precise estimates of
# widths and heights with get_window_extent
if not len(self.legendHandles) and not len(self.texts): return
def get_tbounds(text): #get text bounds in axes coords
bbox = text.get_window_extent(renderer)
bboxa = bbox.inverse_transformed(self.get_transform())
return bboxa.bounds
hpos = []
for t, tabove in zip(self.texts[1:], self.texts[:-1]):
x,y = t.get_position()
l,b,w,h = get_tbounds(tabove)
b -= self.labelsep
h += 2*self.labelsep
hpos.append( (b,h) )
t.set_position( (x, b-0.1*h) )
# now do the same for last line
l,b,w,h = get_tbounds(self.texts[-1])
b -= self.labelsep
h += 2*self.labelsep
hpos.append( (b,h) )
for handle, tup in zip(self.legendHandles, hpos):
y,h = tup
if isinstance(handle, Line2D):
ydata = y*npy.ones(self._xdata.shape, float)
handle.set_ydata(ydata+h/2)
elif isinstance(handle, Rectangle):
handle.set_y(y+1/4*h)
handle.set_height(h/2)
# Set the data for the legend patch
bbox = self._get_handle_text_bbox(renderer)
bbox = bbox.expanded(1 + self.pad, 1 + self.pad)
l, b, w, h = bbox.bounds
self.legendPatch.set_bounds(l, b, w, h)
ox, oy = 0, 0 # center
if iterable(self._loc) and len(self._loc)==2:
xo = self.legendPatch.get_x()
yo = self.legendPatch.get_y()
x, y = self._loc
ox, oy = x-xo, y-yo
elif self._loc == 0: # "best"
ox, oy = self._find_best_position(w, h)
else:
x, y = self._loc_to_axes_coords(self._loc, w, h)
ox, oy = x-l, y-b
self._offset(ox, oy)
#artist.kwdocd['Legend'] = kwdoc(Legend)