Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/matplotlib/legend.py: 19%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

481 statements  

1""" 

2The legend module defines the Legend class, which is responsible for 

3drawing legends associated with Axes and/or figures. 

4 

5.. important:: 

6 

7 It is unlikely that you would ever create a Legend instance manually. 

8 Most users would normally create a legend via the `~.Axes.legend` 

9 function. For more details on legends there is also a :ref:`legend guide 

10 <legend_guide>`. 

11 

12The `Legend` class is a container of legend handles and legend texts. 

13 

14The legend handler map specifies how to create legend handles from artists 

15(lines, patches, etc.) in the Axes or figures. Default legend handlers are 

16defined in the :mod:`~matplotlib.legend_handler` module. While not all artist 

17types are covered by the default legend handlers, custom legend handlers can be 

18defined to support arbitrary objects. 

19 

20See the :ref`<legend_guide>` for more 

21information. 

22""" 

23 

24import itertools 

25import logging 

26import numbers 

27import time 

28 

29import numpy as np 

30 

31import matplotlib as mpl 

32from matplotlib import _api, _docstring, cbook, colors, offsetbox 

33from matplotlib.artist import Artist, allow_rasterization 

34from matplotlib.cbook import silent_list 

35from matplotlib.font_manager import FontProperties 

36from matplotlib.lines import Line2D 

37from matplotlib.patches import (Patch, Rectangle, Shadow, FancyBboxPatch, 

38 StepPatch) 

39from matplotlib.collections import ( 

40 Collection, CircleCollection, LineCollection, PathCollection, 

41 PolyCollection, RegularPolyCollection) 

42from matplotlib.text import Text 

43from matplotlib.transforms import Bbox, BboxBase, TransformedBbox 

44from matplotlib.transforms import BboxTransformTo, BboxTransformFrom 

45from matplotlib.offsetbox import ( 

46 AnchoredOffsetbox, DraggableOffsetBox, 

47 HPacker, VPacker, 

48 DrawingArea, TextArea, 

49) 

50from matplotlib.container import ErrorbarContainer, BarContainer, StemContainer 

51from . import legend_handler 

52 

53 

54class DraggableLegend(DraggableOffsetBox): 

55 def __init__(self, legend, use_blit=False, update="loc"): 

56 """ 

57 Wrapper around a `.Legend` to support mouse dragging. 

58 

59 Parameters 

60 ---------- 

61 legend : `.Legend` 

62 The `.Legend` instance to wrap. 

63 use_blit : bool, optional 

64 Use blitting for faster image composition. For details see 

65 :ref:`func-animation`. 

66 update : {'loc', 'bbox'}, optional 

67 If "loc", update the *loc* parameter of the legend upon finalizing. 

68 If "bbox", update the *bbox_to_anchor* parameter. 

69 """ 

70 self.legend = legend 

71 

72 _api.check_in_list(["loc", "bbox"], update=update) 

73 self._update = update 

74 

75 super().__init__(legend, legend._legend_box, use_blit=use_blit) 

76 

77 def finalize_offset(self): 

78 if self._update == "loc": 

79 self._update_loc(self.get_loc_in_canvas()) 

80 elif self._update == "bbox": 

81 self._update_bbox_to_anchor(self.get_loc_in_canvas()) 

82 

83 def _update_loc(self, loc_in_canvas): 

84 bbox = self.legend.get_bbox_to_anchor() 

85 # if bbox has zero width or height, the transformation is 

86 # ill-defined. Fall back to the default bbox_to_anchor. 

87 if bbox.width == 0 or bbox.height == 0: 

88 self.legend.set_bbox_to_anchor(None) 

89 bbox = self.legend.get_bbox_to_anchor() 

90 _bbox_transform = BboxTransformFrom(bbox) 

91 self.legend._loc = tuple(_bbox_transform.transform(loc_in_canvas)) 

92 

93 def _update_bbox_to_anchor(self, loc_in_canvas): 

94 loc_in_bbox = self.legend.axes.transAxes.transform(loc_in_canvas) 

95 self.legend.set_bbox_to_anchor(loc_in_bbox) 

96 

97 

98_legend_kw_doc_base = """ 

99bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats 

100 Box that is used to position the legend in conjunction with *loc*. 

101 Defaults to `axes.bbox` (if called as a method to `.Axes.legend`) or 

102 `figure.bbox` (if `.Figure.legend`). This argument allows arbitrary 

103 placement of the legend. 

104 

105 Bbox coordinates are interpreted in the coordinate system given by 

106 *bbox_transform*, with the default transform 

107 Axes or Figure coordinates, depending on which ``legend`` is called. 

108 

109 If a 4-tuple or `.BboxBase` is given, then it specifies the bbox 

110 ``(x, y, width, height)`` that the legend is placed in. 

111 To put the legend in the best location in the bottom right 

112 quadrant of the Axes (or figure):: 

113 

114 loc='best', bbox_to_anchor=(0.5, 0., 0.5, 0.5) 

115 

116 A 2-tuple ``(x, y)`` places the corner of the legend specified by *loc* at 

117 x, y. For example, to put the legend's upper right-hand corner in the 

118 center of the Axes (or figure) the following keywords can be used:: 

119 

120 loc='upper right', bbox_to_anchor=(0.5, 0.5) 

121 

122ncols : int, default: 1 

123 The number of columns that the legend has. 

124 

125 For backward compatibility, the spelling *ncol* is also supported 

126 but it is discouraged. If both are given, *ncols* takes precedence. 

127 

128prop : None or `~matplotlib.font_manager.FontProperties` or dict 

129 The font properties of the legend. If None (default), the current 

130 :data:`matplotlib.rcParams` will be used. 

131 

132fontsize : int or {'xx-small', 'x-small', 'small', 'medium', 'large', \ 

133'x-large', 'xx-large'} 

134 The font size of the legend. If the value is numeric the size will be the 

135 absolute font size in points. String values are relative to the current 

136 default font size. This argument is only used if *prop* is not specified. 

137 

138labelcolor : str or list, default: :rc:`legend.labelcolor` 

139 The color of the text in the legend. Either a valid color string 

140 (for example, 'red'), or a list of color strings. The labelcolor can 

141 also be made to match the color of the line or marker using 'linecolor', 

142 'markerfacecolor' (or 'mfc'), or 'markeredgecolor' (or 'mec'). 

143 

144 Labelcolor can be set globally using :rc:`legend.labelcolor`. If None, 

145 use :rc:`text.color`. 

146 

147numpoints : int, default: :rc:`legend.numpoints` 

148 The number of marker points in the legend when creating a legend 

149 entry for a `.Line2D` (line). 

150 

151scatterpoints : int, default: :rc:`legend.scatterpoints` 

152 The number of marker points in the legend when creating 

153 a legend entry for a `.PathCollection` (scatter plot). 

154 

155scatteryoffsets : iterable of floats, default: ``[0.375, 0.5, 0.3125]`` 

156 The vertical offset (relative to the font size) for the markers 

157 created for a scatter plot legend entry. 0.0 is at the base the 

158 legend text, and 1.0 is at the top. To draw all markers at the 

159 same height, set to ``[0.5]``. 

160 

161markerscale : float, default: :rc:`legend.markerscale` 

162 The relative size of legend markers compared to the originally drawn ones. 

163 

164markerfirst : bool, default: True 

165 If *True*, legend marker is placed to the left of the legend label. 

166 If *False*, legend marker is placed to the right of the legend label. 

167 

168reverse : bool, default: False 

169 If *True*, the legend labels are displayed in reverse order from the input. 

170 If *False*, the legend labels are displayed in the same order as the input. 

171 

172 .. versionadded:: 3.7 

173 

174frameon : bool, default: :rc:`legend.frameon` 

175 Whether the legend should be drawn on a patch (frame). 

176 

177fancybox : bool, default: :rc:`legend.fancybox` 

178 Whether round edges should be enabled around the `.FancyBboxPatch` which 

179 makes up the legend's background. 

180 

181shadow : None, bool or dict, default: :rc:`legend.shadow` 

182 Whether to draw a shadow behind the legend. 

183 The shadow can be configured using `.Patch` keywords. 

184 Customization via :rc:`legend.shadow` is currently not supported. 

185 

186framealpha : float, default: :rc:`legend.framealpha` 

187 The alpha transparency of the legend's background. 

188 If *shadow* is activated and *framealpha* is ``None``, the default value is 

189 ignored. 

190 

191facecolor : "inherit" or color, default: :rc:`legend.facecolor` 

192 The legend's background color. 

193 If ``"inherit"``, use :rc:`axes.facecolor`. 

194 

195edgecolor : "inherit" or color, default: :rc:`legend.edgecolor` 

196 The legend's background patch edge color. 

197 If ``"inherit"``, use :rc:`axes.edgecolor`. 

198 

199mode : {"expand", None} 

200 If *mode* is set to ``"expand"`` the legend will be horizontally 

201 expanded to fill the Axes area (or *bbox_to_anchor* if defines 

202 the legend's size). 

203 

204bbox_transform : None or `~matplotlib.transforms.Transform` 

205 The transform for the bounding box (*bbox_to_anchor*). For a value 

206 of ``None`` (default) the Axes' 

207 :data:`~matplotlib.axes.Axes.transAxes` transform will be used. 

208 

209title : str or None 

210 The legend's title. Default is no title (``None``). 

211 

212title_fontproperties : None or `~matplotlib.font_manager.FontProperties` or dict 

213 The font properties of the legend's title. If None (default), the 

214 *title_fontsize* argument will be used if present; if *title_fontsize* is 

215 also None, the current :rc:`legend.title_fontsize` will be used. 

216 

217title_fontsize : int or {'xx-small', 'x-small', 'small', 'medium', 'large', \ 

218'x-large', 'xx-large'}, default: :rc:`legend.title_fontsize` 

219 The font size of the legend's title. 

220 Note: This cannot be combined with *title_fontproperties*. If you want 

221 to set the fontsize alongside other font properties, use the *size* 

222 parameter in *title_fontproperties*. 

223 

224alignment : {'center', 'left', 'right'}, default: 'center' 

225 The alignment of the legend title and the box of entries. The entries 

226 are aligned as a single block, so that markers always lined up. 

227 

228borderpad : float, default: :rc:`legend.borderpad` 

229 The fractional whitespace inside the legend border, in font-size units. 

230 

231labelspacing : float, default: :rc:`legend.labelspacing` 

232 The vertical space between the legend entries, in font-size units. 

233 

234handlelength : float, default: :rc:`legend.handlelength` 

235 The length of the legend handles, in font-size units. 

236 

237handleheight : float, default: :rc:`legend.handleheight` 

238 The height of the legend handles, in font-size units. 

239 

240handletextpad : float, default: :rc:`legend.handletextpad` 

241 The pad between the legend handle and text, in font-size units. 

242 

243borderaxespad : float, default: :rc:`legend.borderaxespad` 

244 The pad between the Axes and legend border, in font-size units. 

245 

246columnspacing : float, default: :rc:`legend.columnspacing` 

247 The spacing between columns, in font-size units. 

248 

249handler_map : dict or None 

250 The custom dictionary mapping instances or types to a legend 

251 handler. This *handler_map* updates the default handler map 

252 found at `matplotlib.legend.Legend.get_legend_handler_map`. 

253 

254draggable : bool, default: False 

255 Whether the legend can be dragged with the mouse. 

256""" 

257 

258_loc_doc_base = """ 

259loc : str or pair of floats, default: {default} 

260 The location of the legend. 

261 

262 The strings ``'upper left'``, ``'upper right'``, ``'lower left'``, 

263 ``'lower right'`` place the legend at the corresponding corner of the 

264 {parent}. 

265 

266 The strings ``'upper center'``, ``'lower center'``, ``'center left'``, 

267 ``'center right'`` place the legend at the center of the corresponding edge 

268 of the {parent}. 

269 

270 The string ``'center'`` places the legend at the center of the {parent}. 

271{best} 

272 The location can also be a 2-tuple giving the coordinates of the lower-left 

273 corner of the legend in {parent} coordinates (in which case *bbox_to_anchor* 

274 will be ignored). 

275 

276 For back-compatibility, ``'center right'`` (but no other location) can also 

277 be spelled ``'right'``, and each "string" location can also be given as a 

278 numeric value: 

279 

280 ================== ============= 

281 Location String Location Code 

282 ================== ============= 

283 'best' (Axes only) 0 

284 'upper right' 1 

285 'upper left' 2 

286 'lower left' 3 

287 'lower right' 4 

288 'right' 5 

289 'center left' 6 

290 'center right' 7 

291 'lower center' 8 

292 'upper center' 9 

293 'center' 10 

294 ================== ============= 

295 {outside}""" 

296 

297_loc_doc_best = """ 

298 The string ``'best'`` places the legend at the location, among the nine 

299 locations defined so far, with the minimum overlap with other drawn 

300 artists. This option can be quite slow for plots with large amounts of 

301 data; your plotting speed may benefit from providing a specific location. 

302""" 

303 

304_legend_kw_axes_st = ( 

305 _loc_doc_base.format(parent='axes', default=':rc:`legend.loc`', 

306 best=_loc_doc_best, outside='') + 

307 _legend_kw_doc_base) 

308_docstring.interpd.update(_legend_kw_axes=_legend_kw_axes_st) 

309 

310_outside_doc = """ 

311 If a figure is using the constrained layout manager, the string codes 

312 of the *loc* keyword argument can get better layout behaviour using the 

313 prefix 'outside'. There is ambiguity at the corners, so 'outside 

314 upper right' will make space for the legend above the rest of the 

315 axes in the layout, and 'outside right upper' will make space on the 

316 right side of the layout. In addition to the values of *loc* 

317 listed above, we have 'outside right upper', 'outside right lower', 

318 'outside left upper', and 'outside left lower'. See 

319 :ref:`legend_guide` for more details. 

320""" 

321 

322_legend_kw_figure_st = ( 

323 _loc_doc_base.format(parent='figure', default="'upper right'", 

324 best='', outside=_outside_doc) + 

325 _legend_kw_doc_base) 

326_docstring.interpd.update(_legend_kw_figure=_legend_kw_figure_st) 

327 

328_legend_kw_both_st = ( 

329 _loc_doc_base.format(parent='axes/figure', 

330 default=":rc:`legend.loc` for Axes, 'upper right' for Figure", 

331 best=_loc_doc_best, outside=_outside_doc) + 

332 _legend_kw_doc_base) 

333_docstring.interpd.update(_legend_kw_doc=_legend_kw_both_st) 

334 

335_legend_kw_set_loc_st = ( 

336 _loc_doc_base.format(parent='axes/figure', 

337 default=":rc:`legend.loc` for Axes, 'upper right' for Figure", 

338 best=_loc_doc_best, outside=_outside_doc)) 

339_docstring.interpd.update(_legend_kw_set_loc_doc=_legend_kw_set_loc_st) 

340 

341 

342class Legend(Artist): 

343 """ 

344 Place a legend on the figure/axes. 

345 """ 

346 

347 # 'best' is only implemented for Axes legends 

348 codes = {'best': 0, **AnchoredOffsetbox.codes} 

349 zorder = 5 

350 

351 def __str__(self): 

352 return "Legend" 

353 

354 @_docstring.dedent_interpd 

355 def __init__( 

356 self, parent, handles, labels, 

357 *, 

358 loc=None, 

359 numpoints=None, # number of points in the legend line 

360 markerscale=None, # relative size of legend markers vs. original 

361 markerfirst=True, # left/right ordering of legend marker and label 

362 reverse=False, # reverse ordering of legend marker and label 

363 scatterpoints=None, # number of scatter points 

364 scatteryoffsets=None, 

365 prop=None, # properties for the legend texts 

366 fontsize=None, # keyword to set font size directly 

367 labelcolor=None, # keyword to set the text color 

368 

369 # spacing & pad defined as a fraction of the font-size 

370 borderpad=None, # whitespace inside the legend border 

371 labelspacing=None, # vertical space between the legend entries 

372 handlelength=None, # length of the legend handles 

373 handleheight=None, # height of the legend handles 

374 handletextpad=None, # pad between the legend handle and text 

375 borderaxespad=None, # pad between the Axes and legend border 

376 columnspacing=None, # spacing between columns 

377 

378 ncols=1, # number of columns 

379 mode=None, # horizontal distribution of columns: None or "expand" 

380 

381 fancybox=None, # True: fancy box, False: rounded box, None: rcParam 

382 shadow=None, 

383 title=None, # legend title 

384 title_fontsize=None, # legend title font size 

385 framealpha=None, # set frame alpha 

386 edgecolor=None, # frame patch edgecolor 

387 facecolor=None, # frame patch facecolor 

388 

389 bbox_to_anchor=None, # bbox to which the legend will be anchored 

390 bbox_transform=None, # transform for the bbox 

391 frameon=None, # draw frame 

392 handler_map=None, 

393 title_fontproperties=None, # properties for the legend title 

394 alignment="center", # control the alignment within the legend box 

395 ncol=1, # synonym for ncols (backward compatibility) 

396 draggable=False # whether the legend can be dragged with the mouse 

397 ): 

398 """ 

399 Parameters 

400 ---------- 

401 parent : `~matplotlib.axes.Axes` or `.Figure` 

402 The artist that contains the legend. 

403 

404 handles : list of (`.Artist` or tuple of `.Artist`) 

405 A list of Artists (lines, patches) to be added to the legend. 

406 

407 labels : list of str 

408 A list of labels to show next to the artists. The length of handles 

409 and labels should be the same. If they are not, they are truncated 

410 to the length of the shorter list. 

411 

412 Other Parameters 

413 ---------------- 

414 %(_legend_kw_doc)s 

415 

416 Attributes 

417 ---------- 

418 legend_handles 

419 List of `.Artist` objects added as legend entries. 

420 

421 .. versionadded:: 3.7 

422 """ 

423 # local import only to avoid circularity 

424 from matplotlib.axes import Axes 

425 from matplotlib.figure import FigureBase 

426 

427 super().__init__() 

428 

429 if prop is None: 

430 self.prop = FontProperties(size=mpl._val_or_rc(fontsize, "legend.fontsize")) 

431 else: 

432 self.prop = FontProperties._from_any(prop) 

433 if isinstance(prop, dict) and "size" not in prop: 

434 self.prop.set_size(mpl.rcParams["legend.fontsize"]) 

435 

436 self._fontsize = self.prop.get_size_in_points() 

437 

438 self.texts = [] 

439 self.legend_handles = [] 

440 self._legend_title_box = None 

441 

442 #: A dictionary with the extra handler mappings for this Legend 

443 #: instance. 

444 self._custom_handler_map = handler_map 

445 

446 self.numpoints = mpl._val_or_rc(numpoints, 'legend.numpoints') 

447 self.markerscale = mpl._val_or_rc(markerscale, 'legend.markerscale') 

448 self.scatterpoints = mpl._val_or_rc(scatterpoints, 'legend.scatterpoints') 

449 self.borderpad = mpl._val_or_rc(borderpad, 'legend.borderpad') 

450 self.labelspacing = mpl._val_or_rc(labelspacing, 'legend.labelspacing') 

451 self.handlelength = mpl._val_or_rc(handlelength, 'legend.handlelength') 

452 self.handleheight = mpl._val_or_rc(handleheight, 'legend.handleheight') 

453 self.handletextpad = mpl._val_or_rc(handletextpad, 'legend.handletextpad') 

454 self.borderaxespad = mpl._val_or_rc(borderaxespad, 'legend.borderaxespad') 

455 self.columnspacing = mpl._val_or_rc(columnspacing, 'legend.columnspacing') 

456 self.shadow = mpl._val_or_rc(shadow, 'legend.shadow') 

457 # trim handles and labels if illegal label... 

458 _lab, _hand = [], [] 

459 for label, handle in zip(labels, handles): 

460 if isinstance(label, str) and label.startswith('_'): 

461 _api.warn_deprecated("3.8", message=( 

462 "An artist whose label starts with an underscore was passed to " 

463 "legend(); such artists will no longer be ignored in the future. " 

464 "To suppress this warning, explicitly filter out such artists, " 

465 "e.g. with `[art for art in artists if not " 

466 "art.get_label().startswith('_')]`.")) 

467 else: 

468 _lab.append(label) 

469 _hand.append(handle) 

470 labels, handles = _lab, _hand 

471 

472 if reverse: 

473 labels.reverse() 

474 handles.reverse() 

475 

476 if len(handles) < 2: 

477 ncols = 1 

478 self._ncols = ncols if ncols != 1 else ncol 

479 

480 if self.numpoints <= 0: 

481 raise ValueError("numpoints must be > 0; it was %d" % numpoints) 

482 

483 # introduce y-offset for handles of the scatter plot 

484 if scatteryoffsets is None: 

485 self._scatteryoffsets = np.array([3. / 8., 4. / 8., 2.5 / 8.]) 

486 else: 

487 self._scatteryoffsets = np.asarray(scatteryoffsets) 

488 reps = self.scatterpoints // len(self._scatteryoffsets) + 1 

489 self._scatteryoffsets = np.tile(self._scatteryoffsets, 

490 reps)[:self.scatterpoints] 

491 

492 # _legend_box is a VPacker instance that contains all 

493 # legend items and will be initialized from _init_legend_box() 

494 # method. 

495 self._legend_box = None 

496 

497 if isinstance(parent, Axes): 

498 self.isaxes = True 

499 self.axes = parent 

500 self.set_figure(parent.figure) 

501 elif isinstance(parent, FigureBase): 

502 self.isaxes = False 

503 self.set_figure(parent) 

504 else: 

505 raise TypeError( 

506 "Legend needs either Axes or FigureBase as parent" 

507 ) 

508 self.parent = parent 

509 

510 self._mode = mode 

511 self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform) 

512 

513 # Figure out if self.shadow is valid 

514 # If shadow was None, rcParams loads False 

515 # So it shouldn't be None here 

516 

517 self._shadow_props = {'ox': 2, 'oy': -2} # default location offsets 

518 if isinstance(self.shadow, dict): 

519 self._shadow_props.update(self.shadow) 

520 self.shadow = True 

521 elif self.shadow in (0, 1, True, False): 

522 self.shadow = bool(self.shadow) 

523 else: 

524 raise ValueError( 

525 'Legend shadow must be a dict or bool, not ' 

526 f'{self.shadow!r} of type {type(self.shadow)}.' 

527 ) 

528 

529 # We use FancyBboxPatch to draw a legend frame. The location 

530 # and size of the box will be updated during the drawing time. 

531 

532 facecolor = mpl._val_or_rc(facecolor, "legend.facecolor") 

533 if facecolor == 'inherit': 

534 facecolor = mpl.rcParams["axes.facecolor"] 

535 

536 edgecolor = mpl._val_or_rc(edgecolor, "legend.edgecolor") 

537 if edgecolor == 'inherit': 

538 edgecolor = mpl.rcParams["axes.edgecolor"] 

539 

540 fancybox = mpl._val_or_rc(fancybox, "legend.fancybox") 

541 

542 self.legendPatch = FancyBboxPatch( 

543 xy=(0, 0), width=1, height=1, 

544 facecolor=facecolor, edgecolor=edgecolor, 

545 # If shadow is used, default to alpha=1 (#8943). 

546 alpha=(framealpha if framealpha is not None 

547 else 1 if shadow 

548 else mpl.rcParams["legend.framealpha"]), 

549 # The width and height of the legendPatch will be set (in draw()) 

550 # to the length that includes the padding. Thus we set pad=0 here. 

551 boxstyle=("round,pad=0,rounding_size=0.2" if fancybox 

552 else "square,pad=0"), 

553 mutation_scale=self._fontsize, 

554 snap=True, 

555 visible=mpl._val_or_rc(frameon, "legend.frameon") 

556 ) 

557 self._set_artist_props(self.legendPatch) 

558 

559 _api.check_in_list(["center", "left", "right"], alignment=alignment) 

560 self._alignment = alignment 

561 

562 # init with null renderer 

563 self._init_legend_box(handles, labels, markerfirst) 

564 

565 # Set legend location 

566 self.set_loc(loc) 

567 

568 # figure out title font properties: 

569 if title_fontsize is not None and title_fontproperties is not None: 

570 raise ValueError( 

571 "title_fontsize and title_fontproperties can't be specified " 

572 "at the same time. Only use one of them. ") 

573 title_prop_fp = FontProperties._from_any(title_fontproperties) 

574 if isinstance(title_fontproperties, dict): 

575 if "size" not in title_fontproperties: 

576 title_fontsize = mpl.rcParams["legend.title_fontsize"] 

577 title_prop_fp.set_size(title_fontsize) 

578 elif title_fontsize is not None: 

579 title_prop_fp.set_size(title_fontsize) 

580 elif not isinstance(title_fontproperties, FontProperties): 

581 title_fontsize = mpl.rcParams["legend.title_fontsize"] 

582 title_prop_fp.set_size(title_fontsize) 

583 

584 self.set_title(title, prop=title_prop_fp) 

585 

586 self._draggable = None 

587 self.set_draggable(state=draggable) 

588 

589 # set the text color 

590 

591 color_getters = { # getter function depends on line or patch 

592 'linecolor': ['get_color', 'get_facecolor'], 

593 'markerfacecolor': ['get_markerfacecolor', 'get_facecolor'], 

594 'mfc': ['get_markerfacecolor', 'get_facecolor'], 

595 'markeredgecolor': ['get_markeredgecolor', 'get_edgecolor'], 

596 'mec': ['get_markeredgecolor', 'get_edgecolor'], 

597 } 

598 labelcolor = mpl._val_or_rc(labelcolor, 'legend.labelcolor') 

599 if labelcolor is None: 

600 labelcolor = mpl.rcParams['text.color'] 

601 if isinstance(labelcolor, str) and labelcolor in color_getters: 

602 getter_names = color_getters[labelcolor] 

603 for handle, text in zip(self.legend_handles, self.texts): 

604 try: 

605 if handle.get_array() is not None: 

606 continue 

607 except AttributeError: 

608 pass 

609 for getter_name in getter_names: 

610 try: 

611 color = getattr(handle, getter_name)() 

612 if isinstance(color, np.ndarray): 

613 if ( 

614 color.shape[0] == 1 

615 or np.isclose(color, color[0]).all() 

616 ): 

617 text.set_color(color[0]) 

618 else: 

619 pass 

620 else: 

621 text.set_color(color) 

622 break 

623 except AttributeError: 

624 pass 

625 elif cbook._str_equal(labelcolor, 'none'): 

626 for text in self.texts: 

627 text.set_color(labelcolor) 

628 elif np.iterable(labelcolor): 

629 for text, color in zip(self.texts, 

630 itertools.cycle( 

631 colors.to_rgba_array(labelcolor))): 

632 text.set_color(color) 

633 else: 

634 raise ValueError(f"Invalid labelcolor: {labelcolor!r}") 

635 

636 def _set_artist_props(self, a): 

637 """ 

638 Set the boilerplate props for artists added to Axes. 

639 """ 

640 a.set_figure(self.figure) 

641 if self.isaxes: 

642 a.axes = self.axes 

643 

644 a.set_transform(self.get_transform()) 

645 

646 @_docstring.dedent_interpd 

647 def set_loc(self, loc=None): 

648 """ 

649 Set the location of the legend. 

650 

651 .. versionadded:: 3.8 

652 

653 Parameters 

654 ---------- 

655 %(_legend_kw_set_loc_doc)s 

656 """ 

657 loc0 = loc 

658 self._loc_used_default = loc is None 

659 if loc is None: 

660 loc = mpl.rcParams["legend.loc"] 

661 if not self.isaxes and loc in [0, 'best']: 

662 loc = 'upper right' 

663 

664 type_err_message = ("loc must be string, coordinate tuple, or" 

665 f" an integer 0-10, not {loc!r}") 

666 

667 # handle outside legends: 

668 self._outside_loc = None 

669 if isinstance(loc, str): 

670 if loc.split()[0] == 'outside': 

671 # strip outside: 

672 loc = loc.split('outside ')[1] 

673 # strip "center" at the beginning 

674 self._outside_loc = loc.replace('center ', '') 

675 # strip first 

676 self._outside_loc = self._outside_loc.split()[0] 

677 locs = loc.split() 

678 if len(locs) > 1 and locs[0] in ('right', 'left'): 

679 # locs doesn't accept "left upper", etc, so swap 

680 if locs[0] != 'center': 

681 locs = locs[::-1] 

682 loc = locs[0] + ' ' + locs[1] 

683 # check that loc is in acceptable strings 

684 loc = _api.check_getitem(self.codes, loc=loc) 

685 elif np.iterable(loc): 

686 # coerce iterable into tuple 

687 loc = tuple(loc) 

688 # validate the tuple represents Real coordinates 

689 if len(loc) != 2 or not all(isinstance(e, numbers.Real) for e in loc): 

690 raise ValueError(type_err_message) 

691 elif isinstance(loc, int): 

692 # validate the integer represents a string numeric value 

693 if loc < 0 or loc > 10: 

694 raise ValueError(type_err_message) 

695 else: 

696 # all other cases are invalid values of loc 

697 raise ValueError(type_err_message) 

698 

699 if self.isaxes and self._outside_loc: 

700 raise ValueError( 

701 f"'outside' option for loc='{loc0}' keyword argument only " 

702 "works for figure legends") 

703 

704 if not self.isaxes and loc == 0: 

705 raise ValueError( 

706 "Automatic legend placement (loc='best') not implemented for " 

707 "figure legend") 

708 

709 tmp = self._loc_used_default 

710 self._set_loc(loc) 

711 self._loc_used_default = tmp # ignore changes done by _set_loc 

712 

713 def _set_loc(self, loc): 

714 # find_offset function will be provided to _legend_box and 

715 # _legend_box will draw itself at the location of the return 

716 # value of the find_offset. 

717 self._loc_used_default = False 

718 self._loc_real = loc 

719 self.stale = True 

720 self._legend_box.set_offset(self._findoffset) 

721 

722 def set_ncols(self, ncols): 

723 """Set the number of columns.""" 

724 self._ncols = ncols 

725 

726 def _get_loc(self): 

727 return self._loc_real 

728 

729 _loc = property(_get_loc, _set_loc) 

730 

731 def _findoffset(self, width, height, xdescent, ydescent, renderer): 

732 """Helper function to locate the legend.""" 

733 

734 if self._loc == 0: # "best". 

735 x, y = self._find_best_position(width, height, renderer) 

736 elif self._loc in Legend.codes.values(): # Fixed location. 

737 bbox = Bbox.from_bounds(0, 0, width, height) 

738 x, y = self._get_anchored_bbox(self._loc, bbox, 

739 self.get_bbox_to_anchor(), 

740 renderer) 

741 else: # Axes or figure coordinates. 

742 fx, fy = self._loc 

743 bbox = self.get_bbox_to_anchor() 

744 x, y = bbox.x0 + bbox.width * fx, bbox.y0 + bbox.height * fy 

745 

746 return x + xdescent, y + ydescent 

747 

748 @allow_rasterization 

749 def draw(self, renderer): 

750 # docstring inherited 

751 if not self.get_visible(): 

752 return 

753 

754 renderer.open_group('legend', gid=self.get_gid()) 

755 

756 fontsize = renderer.points_to_pixels(self._fontsize) 

757 

758 # if mode == fill, set the width of the legend_box to the 

759 # width of the parent (minus pads) 

760 if self._mode in ["expand"]: 

761 pad = 2 * (self.borderaxespad + self.borderpad) * fontsize 

762 self._legend_box.set_width(self.get_bbox_to_anchor().width - pad) 

763 

764 # update the location and size of the legend. This needs to 

765 # be done in any case to clip the figure right. 

766 bbox = self._legend_box.get_window_extent(renderer) 

767 self.legendPatch.set_bounds(bbox.bounds) 

768 self.legendPatch.set_mutation_scale(fontsize) 

769 

770 # self.shadow is validated in __init__ 

771 # So by here it is a bool and self._shadow_props contains any configs 

772 

773 if self.shadow: 

774 Shadow(self.legendPatch, **self._shadow_props).draw(renderer) 

775 

776 self.legendPatch.draw(renderer) 

777 self._legend_box.draw(renderer) 

778 

779 renderer.close_group('legend') 

780 self.stale = False 

781 

782 # _default_handler_map defines the default mapping between plot 

783 # elements and the legend handlers. 

784 

785 _default_handler_map = { 

786 StemContainer: legend_handler.HandlerStem(), 

787 ErrorbarContainer: legend_handler.HandlerErrorbar(), 

788 Line2D: legend_handler.HandlerLine2D(), 

789 Patch: legend_handler.HandlerPatch(), 

790 StepPatch: legend_handler.HandlerStepPatch(), 

791 LineCollection: legend_handler.HandlerLineCollection(), 

792 RegularPolyCollection: legend_handler.HandlerRegularPolyCollection(), 

793 CircleCollection: legend_handler.HandlerCircleCollection(), 

794 BarContainer: legend_handler.HandlerPatch( 

795 update_func=legend_handler.update_from_first_child), 

796 tuple: legend_handler.HandlerTuple(), 

797 PathCollection: legend_handler.HandlerPathCollection(), 

798 PolyCollection: legend_handler.HandlerPolyCollection() 

799 } 

800 

801 # (get|set|update)_default_handler_maps are public interfaces to 

802 # modify the default handler map. 

803 

804 @classmethod 

805 def get_default_handler_map(cls): 

806 """Return the global default handler map, shared by all legends.""" 

807 return cls._default_handler_map 

808 

809 @classmethod 

810 def set_default_handler_map(cls, handler_map): 

811 """Set the global default handler map, shared by all legends.""" 

812 cls._default_handler_map = handler_map 

813 

814 @classmethod 

815 def update_default_handler_map(cls, handler_map): 

816 """Update the global default handler map, shared by all legends.""" 

817 cls._default_handler_map.update(handler_map) 

818 

819 def get_legend_handler_map(self): 

820 """Return this legend instance's handler map.""" 

821 default_handler_map = self.get_default_handler_map() 

822 return ({**default_handler_map, **self._custom_handler_map} 

823 if self._custom_handler_map else default_handler_map) 

824 

825 @staticmethod 

826 def get_legend_handler(legend_handler_map, orig_handle): 

827 """ 

828 Return a legend handler from *legend_handler_map* that 

829 corresponds to *orig_handler*. 

830 

831 *legend_handler_map* should be a dictionary object (that is 

832 returned by the get_legend_handler_map method). 

833 

834 It first checks if the *orig_handle* itself is a key in the 

835 *legend_handler_map* and return the associated value. 

836 Otherwise, it checks for each of the classes in its 

837 method-resolution-order. If no matching key is found, it 

838 returns ``None``. 

839 """ 

840 try: 

841 return legend_handler_map[orig_handle] 

842 except (TypeError, KeyError): # TypeError if unhashable. 

843 pass 

844 for handle_type in type(orig_handle).mro(): 

845 try: 

846 return legend_handler_map[handle_type] 

847 except KeyError: 

848 pass 

849 return None 

850 

851 def _init_legend_box(self, handles, labels, markerfirst=True): 

852 """ 

853 Initialize the legend_box. The legend_box is an instance of 

854 the OffsetBox, which is packed with legend handles and 

855 texts. Once packed, their location is calculated during the 

856 drawing time. 

857 """ 

858 

859 fontsize = self._fontsize 

860 

861 # legend_box is a HPacker, horizontally packed with columns. 

862 # Each column is a VPacker, vertically packed with legend items. 

863 # Each legend item is a HPacker packed with: 

864 # - handlebox: a DrawingArea which contains the legend handle. 

865 # - labelbox: a TextArea which contains the legend text. 

866 

867 text_list = [] # the list of text instances 

868 handle_list = [] # the list of handle instances 

869 handles_and_labels = [] 

870 

871 # The approximate height and descent of text. These values are 

872 # only used for plotting the legend handle. 

873 descent = 0.35 * fontsize * (self.handleheight - 0.7) # heuristic. 

874 height = fontsize * self.handleheight - descent 

875 # each handle needs to be drawn inside a box of (x, y, w, h) = 

876 # (0, -descent, width, height). And their coordinates should 

877 # be given in the display coordinates. 

878 

879 # The transformation of each handle will be automatically set 

880 # to self.get_transform(). If the artist does not use its 

881 # default transform (e.g., Collections), you need to 

882 # manually set their transform to the self.get_transform(). 

883 legend_handler_map = self.get_legend_handler_map() 

884 

885 for orig_handle, label in zip(handles, labels): 

886 handler = self.get_legend_handler(legend_handler_map, orig_handle) 

887 if handler is None: 

888 _api.warn_external( 

889 "Legend does not support handles for " 

890 f"{type(orig_handle).__name__} " 

891 "instances.\nA proxy artist may be used " 

892 "instead.\nSee: https://matplotlib.org/" 

893 "stable/users/explain/axes/legend_guide.html" 

894 "#controlling-the-legend-entries") 

895 # No handle for this artist, so we just defer to None. 

896 handle_list.append(None) 

897 else: 

898 textbox = TextArea(label, multilinebaseline=True, 

899 textprops=dict( 

900 verticalalignment='baseline', 

901 horizontalalignment='left', 

902 fontproperties=self.prop)) 

903 handlebox = DrawingArea(width=self.handlelength * fontsize, 

904 height=height, 

905 xdescent=0., ydescent=descent) 

906 

907 text_list.append(textbox._text) 

908 # Create the artist for the legend which represents the 

909 # original artist/handle. 

910 handle_list.append(handler.legend_artist(self, orig_handle, 

911 fontsize, handlebox)) 

912 handles_and_labels.append((handlebox, textbox)) 

913 

914 columnbox = [] 

915 # array_split splits n handles_and_labels into ncols columns, with the 

916 # first n%ncols columns having an extra entry. filter(len, ...) 

917 # handles the case where n < ncols: the last ncols-n columns are empty 

918 # and get filtered out. 

919 for handles_and_labels_column in filter( 

920 len, np.array_split(handles_and_labels, self._ncols)): 

921 # pack handlebox and labelbox into itembox 

922 itemboxes = [HPacker(pad=0, 

923 sep=self.handletextpad * fontsize, 

924 children=[h, t] if markerfirst else [t, h], 

925 align="baseline") 

926 for h, t in handles_and_labels_column] 

927 # pack columnbox 

928 alignment = "baseline" if markerfirst else "right" 

929 columnbox.append(VPacker(pad=0, 

930 sep=self.labelspacing * fontsize, 

931 align=alignment, 

932 children=itemboxes)) 

933 

934 mode = "expand" if self._mode == "expand" else "fixed" 

935 sep = self.columnspacing * fontsize 

936 self._legend_handle_box = HPacker(pad=0, 

937 sep=sep, align="baseline", 

938 mode=mode, 

939 children=columnbox) 

940 self._legend_title_box = TextArea("") 

941 self._legend_box = VPacker(pad=self.borderpad * fontsize, 

942 sep=self.labelspacing * fontsize, 

943 align=self._alignment, 

944 children=[self._legend_title_box, 

945 self._legend_handle_box]) 

946 self._legend_box.set_figure(self.figure) 

947 self._legend_box.axes = self.axes 

948 self.texts = text_list 

949 self.legend_handles = handle_list 

950 

951 def _auto_legend_data(self): 

952 """ 

953 Return display coordinates for hit testing for "best" positioning. 

954 

955 Returns 

956 ------- 

957 bboxes 

958 List of bounding boxes of all patches. 

959 lines 

960 List of `.Path` corresponding to each line. 

961 offsets 

962 List of (x, y) offsets of all collection. 

963 """ 

964 assert self.isaxes # always holds, as this is only called internally 

965 bboxes = [] 

966 lines = [] 

967 offsets = [] 

968 for artist in self.parent._children: 

969 if isinstance(artist, Line2D): 

970 lines.append( 

971 artist.get_transform().transform_path(artist.get_path())) 

972 elif isinstance(artist, Rectangle): 

973 bboxes.append( 

974 artist.get_bbox().transformed(artist.get_data_transform())) 

975 elif isinstance(artist, Patch): 

976 lines.append( 

977 artist.get_transform().transform_path(artist.get_path())) 

978 elif isinstance(artist, PolyCollection): 

979 lines.extend(artist.get_transform().transform_path(path) 

980 for path in artist.get_paths()) 

981 elif isinstance(artist, Collection): 

982 transform, transOffset, hoffsets, _ = artist._prepare_points() 

983 if len(hoffsets): 

984 offsets.extend(transOffset.transform(hoffsets)) 

985 elif isinstance(artist, Text): 

986 bboxes.append(artist.get_window_extent()) 

987 

988 return bboxes, lines, offsets 

989 

990 def get_children(self): 

991 # docstring inherited 

992 return [self._legend_box, self.get_frame()] 

993 

994 def get_frame(self): 

995 """Return the `~.patches.Rectangle` used to frame the legend.""" 

996 return self.legendPatch 

997 

998 def get_lines(self): 

999 r"""Return the list of `~.lines.Line2D`\s in the legend.""" 

1000 return [h for h in self.legend_handles if isinstance(h, Line2D)] 

1001 

1002 def get_patches(self): 

1003 r"""Return the list of `~.patches.Patch`\s in the legend.""" 

1004 return silent_list('Patch', 

1005 [h for h in self.legend_handles 

1006 if isinstance(h, Patch)]) 

1007 

1008 def get_texts(self): 

1009 r"""Return the list of `~.text.Text`\s in the legend.""" 

1010 return silent_list('Text', self.texts) 

1011 

1012 def set_alignment(self, alignment): 

1013 """ 

1014 Set the alignment of the legend title and the box of entries. 

1015 

1016 The entries are aligned as a single block, so that markers always 

1017 lined up. 

1018 

1019 Parameters 

1020 ---------- 

1021 alignment : {'center', 'left', 'right'}. 

1022 

1023 """ 

1024 _api.check_in_list(["center", "left", "right"], alignment=alignment) 

1025 self._alignment = alignment 

1026 self._legend_box.align = alignment 

1027 

1028 def get_alignment(self): 

1029 """Get the alignment value of the legend box""" 

1030 return self._legend_box.align 

1031 

1032 def set_title(self, title, prop=None): 

1033 """ 

1034 Set legend title and title style. 

1035 

1036 Parameters 

1037 ---------- 

1038 title : str 

1039 The legend title. 

1040 

1041 prop : `.font_manager.FontProperties` or `str` or `pathlib.Path` 

1042 The font properties of the legend title. 

1043 If a `str`, it is interpreted as a fontconfig pattern parsed by 

1044 `.FontProperties`. If a `pathlib.Path`, it is interpreted as the 

1045 absolute path to a font file. 

1046 

1047 """ 

1048 self._legend_title_box._text.set_text(title) 

1049 if title: 

1050 self._legend_title_box._text.set_visible(True) 

1051 self._legend_title_box.set_visible(True) 

1052 else: 

1053 self._legend_title_box._text.set_visible(False) 

1054 self._legend_title_box.set_visible(False) 

1055 

1056 if prop is not None: 

1057 self._legend_title_box._text.set_fontproperties(prop) 

1058 

1059 self.stale = True 

1060 

1061 def get_title(self): 

1062 """Return the `.Text` instance for the legend title.""" 

1063 return self._legend_title_box._text 

1064 

1065 def get_window_extent(self, renderer=None): 

1066 # docstring inherited 

1067 if renderer is None: 

1068 renderer = self.figure._get_renderer() 

1069 return self._legend_box.get_window_extent(renderer=renderer) 

1070 

1071 def get_tightbbox(self, renderer=None): 

1072 # docstring inherited 

1073 return self._legend_box.get_window_extent(renderer) 

1074 

1075 def get_frame_on(self): 

1076 """Get whether the legend box patch is drawn.""" 

1077 return self.legendPatch.get_visible() 

1078 

1079 def set_frame_on(self, b): 

1080 """ 

1081 Set whether the legend box patch is drawn. 

1082 

1083 Parameters 

1084 ---------- 

1085 b : bool 

1086 """ 

1087 self.legendPatch.set_visible(b) 

1088 self.stale = True 

1089 

1090 draw_frame = set_frame_on # Backcompat alias. 

1091 

1092 def get_bbox_to_anchor(self): 

1093 """Return the bbox that the legend will be anchored to.""" 

1094 if self._bbox_to_anchor is None: 

1095 return self.parent.bbox 

1096 else: 

1097 return self._bbox_to_anchor 

1098 

1099 def set_bbox_to_anchor(self, bbox, transform=None): 

1100 """ 

1101 Set the bbox that the legend will be anchored to. 

1102 

1103 Parameters 

1104 ---------- 

1105 bbox : `~matplotlib.transforms.BboxBase` or tuple 

1106 The bounding box can be specified in the following ways: 

1107 

1108 - A `.BboxBase` instance 

1109 - A tuple of ``(left, bottom, width, height)`` in the given 

1110 transform (normalized axes coordinate if None) 

1111 - A tuple of ``(left, bottom)`` where the width and height will be 

1112 assumed to be zero. 

1113 - *None*, to remove the bbox anchoring, and use the parent bbox. 

1114 

1115 transform : `~matplotlib.transforms.Transform`, optional 

1116 A transform to apply to the bounding box. If not specified, this 

1117 will use a transform to the bounding box of the parent. 

1118 """ 

1119 if bbox is None: 

1120 self._bbox_to_anchor = None 

1121 return 

1122 elif isinstance(bbox, BboxBase): 

1123 self._bbox_to_anchor = bbox 

1124 else: 

1125 try: 

1126 l = len(bbox) 

1127 except TypeError as err: 

1128 raise ValueError(f"Invalid bbox: {bbox}") from err 

1129 

1130 if l == 2: 

1131 bbox = [bbox[0], bbox[1], 0, 0] 

1132 

1133 self._bbox_to_anchor = Bbox.from_bounds(*bbox) 

1134 

1135 if transform is None: 

1136 transform = BboxTransformTo(self.parent.bbox) 

1137 

1138 self._bbox_to_anchor = TransformedBbox(self._bbox_to_anchor, 

1139 transform) 

1140 self.stale = True 

1141 

1142 def _get_anchored_bbox(self, loc, bbox, parentbbox, renderer): 

1143 """ 

1144 Place the *bbox* inside the *parentbbox* according to a given 

1145 location code. Return the (x, y) coordinate of the bbox. 

1146 

1147 Parameters 

1148 ---------- 

1149 loc : int 

1150 A location code in range(1, 11). This corresponds to the possible 

1151 values for ``self._loc``, excluding "best". 

1152 bbox : `~matplotlib.transforms.Bbox` 

1153 bbox to be placed, in display coordinates. 

1154 parentbbox : `~matplotlib.transforms.Bbox` 

1155 A parent box which will contain the bbox, in display coordinates. 

1156 """ 

1157 return offsetbox._get_anchored_bbox( 

1158 loc, bbox, parentbbox, 

1159 self.borderaxespad * renderer.points_to_pixels(self._fontsize)) 

1160 

1161 def _find_best_position(self, width, height, renderer): 

1162 """Determine the best location to place the legend.""" 

1163 assert self.isaxes # always holds, as this is only called internally 

1164 

1165 start_time = time.perf_counter() 

1166 

1167 bboxes, lines, offsets = self._auto_legend_data() 

1168 

1169 bbox = Bbox.from_bounds(0, 0, width, height) 

1170 

1171 candidates = [] 

1172 for idx in range(1, len(self.codes)): 

1173 l, b = self._get_anchored_bbox(idx, bbox, 

1174 self.get_bbox_to_anchor(), 

1175 renderer) 

1176 legendBox = Bbox.from_bounds(l, b, width, height) 

1177 # XXX TODO: If markers are present, it would be good to take them 

1178 # into account when checking vertex overlaps in the next line. 

1179 badness = (sum(legendBox.count_contains(line.vertices) 

1180 for line in lines) 

1181 + legendBox.count_contains(offsets) 

1182 + legendBox.count_overlaps(bboxes) 

1183 + sum(line.intersects_bbox(legendBox, filled=False) 

1184 for line in lines)) 

1185 # Include the index to favor lower codes in case of a tie. 

1186 candidates.append((badness, idx, (l, b))) 

1187 if badness == 0: 

1188 break 

1189 

1190 _, _, (l, b) = min(candidates) 

1191 

1192 if self._loc_used_default and time.perf_counter() - start_time > 1: 

1193 _api.warn_external( 

1194 'Creating legend with loc="best" can be slow with large ' 

1195 'amounts of data.') 

1196 

1197 return l, b 

1198 

1199 @_api.rename_parameter("3.8", "event", "mouseevent") 

1200 def contains(self, mouseevent): 

1201 return self.legendPatch.contains(mouseevent) 

1202 

1203 def set_draggable(self, state, use_blit=False, update='loc'): 

1204 """ 

1205 Enable or disable mouse dragging support of the legend. 

1206 

1207 Parameters 

1208 ---------- 

1209 state : bool 

1210 Whether mouse dragging is enabled. 

1211 use_blit : bool, optional 

1212 Use blitting for faster image composition. For details see 

1213 :ref:`func-animation`. 

1214 update : {'loc', 'bbox'}, optional 

1215 The legend parameter to be changed when dragged: 

1216 

1217 - 'loc': update the *loc* parameter of the legend 

1218 - 'bbox': update the *bbox_to_anchor* parameter of the legend 

1219 

1220 Returns 

1221 ------- 

1222 `.DraggableLegend` or *None* 

1223 If *state* is ``True`` this returns the `.DraggableLegend` helper 

1224 instance. Otherwise this returns *None*. 

1225 """ 

1226 if state: 

1227 if self._draggable is None: 

1228 self._draggable = DraggableLegend(self, 

1229 use_blit, 

1230 update=update) 

1231 else: 

1232 if self._draggable is not None: 

1233 self._draggable.disconnect() 

1234 self._draggable = None 

1235 return self._draggable 

1236 

1237 def get_draggable(self): 

1238 """Return ``True`` if the legend is draggable, ``False`` otherwise.""" 

1239 return self._draggable is not None 

1240 

1241 

1242# Helper functions to parse legend arguments for both `figure.legend` and 

1243# `axes.legend`: 

1244def _get_legend_handles(axs, legend_handler_map=None): 

1245 """Yield artists that can be used as handles in a legend.""" 

1246 handles_original = [] 

1247 for ax in axs: 

1248 handles_original += [ 

1249 *(a for a in ax._children 

1250 if isinstance(a, (Line2D, Patch, Collection, Text))), 

1251 *ax.containers] 

1252 # support parasite Axes: 

1253 if hasattr(ax, 'parasites'): 

1254 for axx in ax.parasites: 

1255 handles_original += [ 

1256 *(a for a in axx._children 

1257 if isinstance(a, (Line2D, Patch, Collection, Text))), 

1258 *axx.containers] 

1259 

1260 handler_map = {**Legend.get_default_handler_map(), 

1261 **(legend_handler_map or {})} 

1262 has_handler = Legend.get_legend_handler 

1263 for handle in handles_original: 

1264 label = handle.get_label() 

1265 if label != '_nolegend_' and has_handler(handler_map, handle): 

1266 yield handle 

1267 elif (label and not label.startswith('_') and 

1268 not has_handler(handler_map, handle)): 

1269 _api.warn_external( 

1270 "Legend does not support handles for " 

1271 f"{type(handle).__name__} " 

1272 "instances.\nSee: https://matplotlib.org/stable/" 

1273 "tutorials/intermediate/legend_guide.html" 

1274 "#implementing-a-custom-legend-handler") 

1275 continue 

1276 

1277 

1278def _get_legend_handles_labels(axs, legend_handler_map=None): 

1279 """Return handles and labels for legend.""" 

1280 handles = [] 

1281 labels = [] 

1282 for handle in _get_legend_handles(axs, legend_handler_map): 

1283 label = handle.get_label() 

1284 if label and not label.startswith('_'): 

1285 handles.append(handle) 

1286 labels.append(label) 

1287 return handles, labels 

1288 

1289 

1290def _parse_legend_args(axs, *args, handles=None, labels=None, **kwargs): 

1291 """ 

1292 Get the handles and labels from the calls to either ``figure.legend`` 

1293 or ``axes.legend``. 

1294 

1295 The parser is a bit involved because we support:: 

1296 

1297 legend() 

1298 legend(labels) 

1299 legend(handles, labels) 

1300 legend(labels=labels) 

1301 legend(handles=handles) 

1302 legend(handles=handles, labels=labels) 

1303 

1304 The behavior for a mixture of positional and keyword handles and labels 

1305 is undefined and issues a warning; it will be an error in the future. 

1306 

1307 Parameters 

1308 ---------- 

1309 axs : list of `.Axes` 

1310 If handles are not given explicitly, the artists in these Axes are 

1311 used as handles. 

1312 *args : tuple 

1313 Positional parameters passed to ``legend()``. 

1314 handles 

1315 The value of the keyword argument ``legend(handles=...)``, or *None* 

1316 if that keyword argument was not used. 

1317 labels 

1318 The value of the keyword argument ``legend(labels=...)``, or *None* 

1319 if that keyword argument was not used. 

1320 **kwargs 

1321 All other keyword arguments passed to ``legend()``. 

1322 

1323 Returns 

1324 ------- 

1325 handles : list of (`.Artist` or tuple of `.Artist`) 

1326 The legend handles. 

1327 labels : list of str 

1328 The legend labels. 

1329 kwargs : dict 

1330 *kwargs* with keywords handles and labels removed. 

1331 

1332 """ 

1333 log = logging.getLogger(__name__) 

1334 

1335 handlers = kwargs.get('handler_map') 

1336 

1337 if (handles is not None or labels is not None) and args: 

1338 _api.warn_deprecated("3.9", message=( 

1339 "You have mixed positional and keyword arguments, some input may " 

1340 "be discarded. This is deprecated since %(since)s and will " 

1341 "become an error %(removal)s.")) 

1342 

1343 if (hasattr(handles, "__len__") and 

1344 hasattr(labels, "__len__") and 

1345 len(handles) != len(labels)): 

1346 _api.warn_external(f"Mismatched number of handles and labels: " 

1347 f"len(handles) = {len(handles)} " 

1348 f"len(labels) = {len(labels)}") 

1349 # if got both handles and labels as kwargs, make same length 

1350 if handles and labels: 

1351 handles, labels = zip(*zip(handles, labels)) 

1352 

1353 elif handles is not None and labels is None: 

1354 labels = [handle.get_label() for handle in handles] 

1355 

1356 elif labels is not None and handles is None: 

1357 # Get as many handles as there are labels. 

1358 handles = [handle for handle, label 

1359 in zip(_get_legend_handles(axs, handlers), labels)] 

1360 

1361 elif len(args) == 0: # 0 args: automatically detect labels and handles. 

1362 handles, labels = _get_legend_handles_labels(axs, handlers) 

1363 if not handles: 

1364 _api.warn_external( 

1365 "No artists with labels found to put in legend. Note that " 

1366 "artists whose label start with an underscore are ignored " 

1367 "when legend() is called with no argument.") 

1368 

1369 elif len(args) == 1: # 1 arg: user defined labels, automatic handle detection. 

1370 labels, = args 

1371 if any(isinstance(l, Artist) for l in labels): 

1372 raise TypeError("A single argument passed to legend() must be a " 

1373 "list of labels, but found an Artist in there.") 

1374 

1375 # Get as many handles as there are labels. 

1376 handles = [handle for handle, label 

1377 in zip(_get_legend_handles(axs, handlers), labels)] 

1378 

1379 elif len(args) == 2: # 2 args: user defined handles and labels. 

1380 handles, labels = args[:2] 

1381 

1382 else: 

1383 raise _api.nargs_error('legend', '0-2', len(args)) 

1384 

1385 return handles, labels, kwargs