Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/matplotlib/collections.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

933 statements  

1""" 

2Classes for the efficient drawing of large collections of objects that 

3share most properties, e.g., a large number of line segments or 

4polygons. 

5 

6The classes are not meant to be as flexible as their single element 

7counterparts (e.g., you may not be able to select all line styles) but 

8they are meant to be fast for common use cases (e.g., a large set of solid 

9line segments). 

10""" 

11 

12import itertools 

13import math 

14from numbers import Number, Real 

15import warnings 

16 

17import numpy as np 

18 

19import matplotlib as mpl 

20from . import (_api, _path, artist, cbook, cm, colors as mcolors, _docstring, 

21 hatch as mhatch, lines as mlines, path as mpath, transforms) 

22from ._enums import JoinStyle, CapStyle 

23 

24 

25# "color" is excluded; it is a compound setter, and its docstring differs 

26# in LineCollection. 

27@_api.define_aliases({ 

28 "antialiased": ["antialiaseds", "aa"], 

29 "edgecolor": ["edgecolors", "ec"], 

30 "facecolor": ["facecolors", "fc"], 

31 "linestyle": ["linestyles", "dashes", "ls"], 

32 "linewidth": ["linewidths", "lw"], 

33 "offset_transform": ["transOffset"], 

34}) 

35class Collection(artist.Artist, cm.ScalarMappable): 

36 r""" 

37 Base class for Collections. Must be subclassed to be usable. 

38 

39 A Collection represents a sequence of `.Patch`\es that can be drawn 

40 more efficiently together than individually. For example, when a single 

41 path is being drawn repeatedly at different offsets, the renderer can 

42 typically execute a ``draw_marker()`` call much more efficiently than a 

43 series of repeated calls to ``draw_path()`` with the offsets put in 

44 one-by-one. 

45 

46 Most properties of a collection can be configured per-element. Therefore, 

47 Collections have "plural" versions of many of the properties of a `.Patch` 

48 (e.g. `.Collection.get_paths` instead of `.Patch.get_path`). Exceptions are 

49 the *zorder*, *hatch*, *pickradius*, *capstyle* and *joinstyle* properties, 

50 which can only be set globally for the whole collection. 

51 

52 Besides these exceptions, all properties can be specified as single values 

53 (applying to all elements) or sequences of values. The property of the 

54 ``i``\th element of the collection is:: 

55 

56 prop[i % len(prop)] 

57 

58 Each Collection can optionally be used as its own `.ScalarMappable` by 

59 passing the *norm* and *cmap* parameters to its constructor. If the 

60 Collection's `.ScalarMappable` matrix ``_A`` has been set (via a call 

61 to `.Collection.set_array`), then at draw time this internal scalar 

62 mappable will be used to set the ``facecolors`` and ``edgecolors``, 

63 ignoring those that were manually passed in. 

64 """ 

65 #: Either a list of 3x3 arrays or an Nx3x3 array (representing N 

66 #: transforms), suitable for the `all_transforms` argument to 

67 #: `~matplotlib.backend_bases.RendererBase.draw_path_collection`; 

68 #: each 3x3 array is used to initialize an 

69 #: `~matplotlib.transforms.Affine2D` object. 

70 #: Each kind of collection defines this based on its arguments. 

71 _transforms = np.empty((0, 3, 3)) 

72 

73 # Whether to draw an edge by default. Set on a 

74 # subclass-by-subclass basis. 

75 _edge_default = False 

76 

77 @_docstring.interpd 

78 def __init__(self, *, 

79 edgecolors=None, 

80 facecolors=None, 

81 linewidths=None, 

82 linestyles='solid', 

83 capstyle=None, 

84 joinstyle=None, 

85 antialiaseds=None, 

86 offsets=None, 

87 offset_transform=None, 

88 norm=None, # optional for ScalarMappable 

89 cmap=None, # ditto 

90 pickradius=5.0, 

91 hatch=None, 

92 urls=None, 

93 zorder=1, 

94 **kwargs 

95 ): 

96 """ 

97 Parameters 

98 ---------- 

99 edgecolors : :mpltype:`color` or list of colors, default: :rc:`patch.edgecolor` 

100 Edge color for each patch making up the collection. The special 

101 value 'face' can be passed to make the edgecolor match the 

102 facecolor. 

103 facecolors : :mpltype:`color` or list of colors, default: :rc:`patch.facecolor` 

104 Face color for each patch making up the collection. 

105 linewidths : float or list of floats, default: :rc:`patch.linewidth` 

106 Line width for each patch making up the collection. 

107 linestyles : str or tuple or list thereof, default: 'solid' 

108 Valid strings are ['solid', 'dashed', 'dashdot', 'dotted', '-', 

109 '--', '-.', ':']. Dash tuples should be of the form:: 

110 

111 (offset, onoffseq), 

112 

113 where *onoffseq* is an even length tuple of on and off ink lengths 

114 in points. For examples, see 

115 :doc:`/gallery/lines_bars_and_markers/linestyles`. 

116 capstyle : `.CapStyle`-like, default: :rc:`patch.capstyle` 

117 Style to use for capping lines for all paths in the collection. 

118 Allowed values are %(CapStyle)s. 

119 joinstyle : `.JoinStyle`-like, default: :rc:`patch.joinstyle` 

120 Style to use for joining lines for all paths in the collection. 

121 Allowed values are %(JoinStyle)s. 

122 antialiaseds : bool or list of bool, default: :rc:`patch.antialiased` 

123 Whether each patch in the collection should be drawn with 

124 antialiasing. 

125 offsets : (float, float) or list thereof, default: (0, 0) 

126 A vector by which to translate each patch after rendering (default 

127 is no translation). The translation is performed in screen (pixel) 

128 coordinates (i.e. after the Artist's transform is applied). 

129 offset_transform : `~.Transform`, default: `.IdentityTransform` 

130 A single transform which will be applied to each *offsets* vector 

131 before it is used. 

132 cmap, norm 

133 Data normalization and colormapping parameters. See 

134 `.ScalarMappable` for a detailed description. 

135 hatch : str, optional 

136 Hatching pattern to use in filled paths, if any. Valid strings are 

137 ['/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*']. See 

138 :doc:`/gallery/shapes_and_collections/hatch_style_reference` for 

139 the meaning of each hatch type. 

140 pickradius : float, default: 5.0 

141 If ``pickradius <= 0``, then `.Collection.contains` will return 

142 ``True`` whenever the test point is inside of one of the polygons 

143 formed by the control points of a Path in the Collection. On the 

144 other hand, if it is greater than 0, then we instead check if the 

145 test point is contained in a stroke of width ``2*pickradius`` 

146 following any of the Paths in the Collection. 

147 urls : list of str, default: None 

148 A URL for each patch to link to once drawn. Currently only works 

149 for the SVG backend. See :doc:`/gallery/misc/hyperlinks_sgskip` for 

150 examples. 

151 zorder : float, default: 1 

152 The drawing order, shared by all Patches in the Collection. See 

153 :doc:`/gallery/misc/zorder_demo` for all defaults and examples. 

154 **kwargs 

155 Remaining keyword arguments will be used to set properties as 

156 ``Collection.set_{key}(val)`` for each key-value pair in *kwargs*. 

157 """ 

158 artist.Artist.__init__(self) 

159 cm.ScalarMappable.__init__(self, norm, cmap) 

160 # list of un-scaled dash patterns 

161 # this is needed scaling the dash pattern by linewidth 

162 self._us_linestyles = [(0, None)] 

163 # list of dash patterns 

164 self._linestyles = [(0, None)] 

165 # list of unbroadcast/scaled linewidths 

166 self._us_lw = [0] 

167 self._linewidths = [0] 

168 

169 self._gapcolor = None # Currently only used by LineCollection. 

170 

171 # Flags set by _set_mappable_flags: are colors from mapping an array? 

172 self._face_is_mapped = None 

173 self._edge_is_mapped = None 

174 self._mapped_colors = None # calculated in update_scalarmappable 

175 self._hatch_color = mcolors.to_rgba(mpl.rcParams['hatch.color']) 

176 self.set_facecolor(facecolors) 

177 self.set_edgecolor(edgecolors) 

178 self.set_linewidth(linewidths) 

179 self.set_linestyle(linestyles) 

180 self.set_antialiased(antialiaseds) 

181 self.set_pickradius(pickradius) 

182 self.set_urls(urls) 

183 self.set_hatch(hatch) 

184 self.set_zorder(zorder) 

185 

186 if capstyle: 

187 self.set_capstyle(capstyle) 

188 else: 

189 self._capstyle = None 

190 

191 if joinstyle: 

192 self.set_joinstyle(joinstyle) 

193 else: 

194 self._joinstyle = None 

195 

196 if offsets is not None: 

197 offsets = np.asanyarray(offsets, float) 

198 # Broadcast (2,) -> (1, 2) but nothing else. 

199 if offsets.shape == (2,): 

200 offsets = offsets[None, :] 

201 

202 self._offsets = offsets 

203 self._offset_transform = offset_transform 

204 

205 self._path_effects = None 

206 self._internal_update(kwargs) 

207 self._paths = None 

208 

209 def get_paths(self): 

210 return self._paths 

211 

212 def set_paths(self, paths): 

213 self._paths = paths 

214 self.stale = True 

215 

216 def get_transforms(self): 

217 return self._transforms 

218 

219 def get_offset_transform(self): 

220 """Return the `.Transform` instance used by this artist offset.""" 

221 if self._offset_transform is None: 

222 self._offset_transform = transforms.IdentityTransform() 

223 elif (not isinstance(self._offset_transform, transforms.Transform) 

224 and hasattr(self._offset_transform, '_as_mpl_transform')): 

225 self._offset_transform = \ 

226 self._offset_transform._as_mpl_transform(self.axes) 

227 return self._offset_transform 

228 

229 def set_offset_transform(self, offset_transform): 

230 """ 

231 Set the artist offset transform. 

232 

233 Parameters 

234 ---------- 

235 offset_transform : `.Transform` 

236 """ 

237 self._offset_transform = offset_transform 

238 

239 def get_datalim(self, transData): 

240 # Calculate the data limits and return them as a `.Bbox`. 

241 # 

242 # This operation depends on the transforms for the data in the 

243 # collection and whether the collection has offsets: 

244 # 

245 # 1. offsets = None, transform child of transData: use the paths for 

246 # the automatic limits (i.e. for LineCollection in streamline). 

247 # 2. offsets != None: offset_transform is child of transData: 

248 # 

249 # a. transform is child of transData: use the path + offset for 

250 # limits (i.e for bar). 

251 # b. transform is not a child of transData: just use the offsets 

252 # for the limits (i.e. for scatter) 

253 # 

254 # 3. otherwise return a null Bbox. 

255 

256 transform = self.get_transform() 

257 offset_trf = self.get_offset_transform() 

258 if not (isinstance(offset_trf, transforms.IdentityTransform) 

259 or offset_trf.contains_branch(transData)): 

260 # if the offsets are in some coords other than data, 

261 # then don't use them for autoscaling. 

262 return transforms.Bbox.null() 

263 

264 paths = self.get_paths() 

265 if not len(paths): 

266 # No paths to transform 

267 return transforms.Bbox.null() 

268 

269 if not transform.is_affine: 

270 paths = [transform.transform_path_non_affine(p) for p in paths] 

271 # Don't convert transform to transform.get_affine() here because 

272 # we may have transform.contains_branch(transData) but not 

273 # transforms.get_affine().contains_branch(transData). But later, 

274 # be careful to only apply the affine part that remains. 

275 

276 offsets = self.get_offsets() 

277 

278 if any(transform.contains_branch_seperately(transData)): 

279 # collections that are just in data units (like quiver) 

280 # can properly have the axes limits set by their shape + 

281 # offset. LineCollections that have no offsets can 

282 # also use this algorithm (like streamplot). 

283 if isinstance(offsets, np.ma.MaskedArray): 

284 offsets = offsets.filled(np.nan) 

285 # get_path_collection_extents handles nan but not masked arrays 

286 return mpath.get_path_collection_extents( 

287 transform.get_affine() - transData, paths, 

288 self.get_transforms(), 

289 offset_trf.transform_non_affine(offsets), 

290 offset_trf.get_affine().frozen()) 

291 

292 # NOTE: None is the default case where no offsets were passed in 

293 if self._offsets is not None: 

294 # this is for collections that have their paths (shapes) 

295 # in physical, axes-relative, or figure-relative units 

296 # (i.e. like scatter). We can't uniquely set limits based on 

297 # those shapes, so we just set the limits based on their 

298 # location. 

299 offsets = (offset_trf - transData).transform(offsets) 

300 # note A-B means A B^{-1} 

301 offsets = np.ma.masked_invalid(offsets) 

302 if not offsets.mask.all(): 

303 bbox = transforms.Bbox.null() 

304 bbox.update_from_data_xy(offsets) 

305 return bbox 

306 return transforms.Bbox.null() 

307 

308 def get_window_extent(self, renderer=None): 

309 # TODO: check to ensure that this does not fail for 

310 # cases other than scatter plot legend 

311 return self.get_datalim(transforms.IdentityTransform()) 

312 

313 def _prepare_points(self): 

314 # Helper for drawing and hit testing. 

315 

316 transform = self.get_transform() 

317 offset_trf = self.get_offset_transform() 

318 offsets = self.get_offsets() 

319 paths = self.get_paths() 

320 

321 if self.have_units(): 

322 paths = [] 

323 for path in self.get_paths(): 

324 vertices = path.vertices 

325 xs, ys = vertices[:, 0], vertices[:, 1] 

326 xs = self.convert_xunits(xs) 

327 ys = self.convert_yunits(ys) 

328 paths.append(mpath.Path(np.column_stack([xs, ys]), path.codes)) 

329 xs = self.convert_xunits(offsets[:, 0]) 

330 ys = self.convert_yunits(offsets[:, 1]) 

331 offsets = np.ma.column_stack([xs, ys]) 

332 

333 if not transform.is_affine: 

334 paths = [transform.transform_path_non_affine(path) 

335 for path in paths] 

336 transform = transform.get_affine() 

337 if not offset_trf.is_affine: 

338 offsets = offset_trf.transform_non_affine(offsets) 

339 # This might have changed an ndarray into a masked array. 

340 offset_trf = offset_trf.get_affine() 

341 

342 if isinstance(offsets, np.ma.MaskedArray): 

343 offsets = offsets.filled(np.nan) 

344 # Changing from a masked array to nan-filled ndarray 

345 # is probably most efficient at this point. 

346 

347 return transform, offset_trf, offsets, paths 

348 

349 @artist.allow_rasterization 

350 def draw(self, renderer): 

351 if not self.get_visible(): 

352 return 

353 renderer.open_group(self.__class__.__name__, self.get_gid()) 

354 

355 self.update_scalarmappable() 

356 

357 transform, offset_trf, offsets, paths = self._prepare_points() 

358 

359 gc = renderer.new_gc() 

360 self._set_gc_clip(gc) 

361 gc.set_snap(self.get_snap()) 

362 

363 if self._hatch: 

364 gc.set_hatch(self._hatch) 

365 gc.set_hatch_color(self._hatch_color) 

366 

367 if self.get_sketch_params() is not None: 

368 gc.set_sketch_params(*self.get_sketch_params()) 

369 

370 if self.get_path_effects(): 

371 from matplotlib.patheffects import PathEffectRenderer 

372 renderer = PathEffectRenderer(self.get_path_effects(), renderer) 

373 

374 # If the collection is made up of a single shape/color/stroke, 

375 # it can be rendered once and blitted multiple times, using 

376 # `draw_markers` rather than `draw_path_collection`. This is 

377 # *much* faster for Agg, and results in smaller file sizes in 

378 # PDF/SVG/PS. 

379 

380 trans = self.get_transforms() 

381 facecolors = self.get_facecolor() 

382 edgecolors = self.get_edgecolor() 

383 do_single_path_optimization = False 

384 if (len(paths) == 1 and len(trans) <= 1 and 

385 len(facecolors) == 1 and len(edgecolors) == 1 and 

386 len(self._linewidths) == 1 and 

387 all(ls[1] is None for ls in self._linestyles) and 

388 len(self._antialiaseds) == 1 and len(self._urls) == 1 and 

389 self.get_hatch() is None): 

390 if len(trans): 

391 combined_transform = transforms.Affine2D(trans[0]) + transform 

392 else: 

393 combined_transform = transform 

394 extents = paths[0].get_extents(combined_transform) 

395 if (extents.width < self.figure.bbox.width 

396 and extents.height < self.figure.bbox.height): 

397 do_single_path_optimization = True 

398 

399 if self._joinstyle: 

400 gc.set_joinstyle(self._joinstyle) 

401 

402 if self._capstyle: 

403 gc.set_capstyle(self._capstyle) 

404 

405 if do_single_path_optimization: 

406 gc.set_foreground(tuple(edgecolors[0])) 

407 gc.set_linewidth(self._linewidths[0]) 

408 gc.set_dashes(*self._linestyles[0]) 

409 gc.set_antialiased(self._antialiaseds[0]) 

410 gc.set_url(self._urls[0]) 

411 renderer.draw_markers( 

412 gc, paths[0], combined_transform.frozen(), 

413 mpath.Path(offsets), offset_trf, tuple(facecolors[0])) 

414 else: 

415 if self._gapcolor is not None: 

416 # First draw paths within the gaps. 

417 ipaths, ilinestyles = self._get_inverse_paths_linestyles() 

418 renderer.draw_path_collection( 

419 gc, transform.frozen(), ipaths, 

420 self.get_transforms(), offsets, offset_trf, 

421 [mcolors.to_rgba("none")], self._gapcolor, 

422 self._linewidths, ilinestyles, 

423 self._antialiaseds, self._urls, 

424 "screen") 

425 

426 renderer.draw_path_collection( 

427 gc, transform.frozen(), paths, 

428 self.get_transforms(), offsets, offset_trf, 

429 self.get_facecolor(), self.get_edgecolor(), 

430 self._linewidths, self._linestyles, 

431 self._antialiaseds, self._urls, 

432 "screen") # offset_position, kept for backcompat. 

433 

434 gc.restore() 

435 renderer.close_group(self.__class__.__name__) 

436 self.stale = False 

437 

438 def set_pickradius(self, pickradius): 

439 """ 

440 Set the pick radius used for containment tests. 

441 

442 Parameters 

443 ---------- 

444 pickradius : float 

445 Pick radius, in points. 

446 """ 

447 if not isinstance(pickradius, Real): 

448 raise ValueError( 

449 f"pickradius must be a real-valued number, not {pickradius!r}") 

450 self._pickradius = pickradius 

451 

452 def get_pickradius(self): 

453 return self._pickradius 

454 

455 def contains(self, mouseevent): 

456 """ 

457 Test whether the mouse event occurred in the collection. 

458 

459 Returns ``bool, dict(ind=itemlist)``, where every item in itemlist 

460 contains the event. 

461 """ 

462 if self._different_canvas(mouseevent) or not self.get_visible(): 

463 return False, {} 

464 pickradius = ( 

465 float(self._picker) 

466 if isinstance(self._picker, Number) and 

467 self._picker is not True # the bool, not just nonzero or 1 

468 else self._pickradius) 

469 if self.axes: 

470 self.axes._unstale_viewLim() 

471 transform, offset_trf, offsets, paths = self._prepare_points() 

472 # Tests if the point is contained on one of the polygons formed 

473 # by the control points of each of the paths. A point is considered 

474 # "on" a path if it would lie within a stroke of width 2*pickradius 

475 # following the path. If pickradius <= 0, then we instead simply check 

476 # if the point is *inside* of the path instead. 

477 ind = _path.point_in_path_collection( 

478 mouseevent.x, mouseevent.y, pickradius, 

479 transform.frozen(), paths, self.get_transforms(), 

480 offsets, offset_trf, pickradius <= 0) 

481 return len(ind) > 0, dict(ind=ind) 

482 

483 def set_urls(self, urls): 

484 """ 

485 Parameters 

486 ---------- 

487 urls : list of str or None 

488 

489 Notes 

490 ----- 

491 URLs are currently only implemented by the SVG backend. They are 

492 ignored by all other backends. 

493 """ 

494 self._urls = urls if urls is not None else [None] 

495 self.stale = True 

496 

497 def get_urls(self): 

498 """ 

499 Return a list of URLs, one for each element of the collection. 

500 

501 The list contains *None* for elements without a URL. See 

502 :doc:`/gallery/misc/hyperlinks_sgskip` for an example. 

503 """ 

504 return self._urls 

505 

506 def set_hatch(self, hatch): 

507 r""" 

508 Set the hatching pattern 

509 

510 *hatch* can be one of:: 

511 

512 / - diagonal hatching 

513 \ - back diagonal 

514 | - vertical 

515 - - horizontal 

516 + - crossed 

517 x - crossed diagonal 

518 o - small circle 

519 O - large circle 

520 . - dots 

521 * - stars 

522 

523 Letters can be combined, in which case all the specified 

524 hatchings are done. If same letter repeats, it increases the 

525 density of hatching of that pattern. 

526 

527 Unlike other properties such as linewidth and colors, hatching 

528 can only be specified for the collection as a whole, not separately 

529 for each member. 

530 

531 Parameters 

532 ---------- 

533 hatch : {'/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'} 

534 """ 

535 # Use validate_hatch(list) after deprecation. 

536 mhatch._validate_hatch_pattern(hatch) 

537 self._hatch = hatch 

538 self.stale = True 

539 

540 def get_hatch(self): 

541 """Return the current hatching pattern.""" 

542 return self._hatch 

543 

544 def set_offsets(self, offsets): 

545 """ 

546 Set the offsets for the collection. 

547 

548 Parameters 

549 ---------- 

550 offsets : (N, 2) or (2,) array-like 

551 """ 

552 offsets = np.asanyarray(offsets) 

553 if offsets.shape == (2,): # Broadcast (2,) -> (1, 2) but nothing else. 

554 offsets = offsets[None, :] 

555 cstack = (np.ma.column_stack if isinstance(offsets, np.ma.MaskedArray) 

556 else np.column_stack) 

557 self._offsets = cstack( 

558 (np.asanyarray(self.convert_xunits(offsets[:, 0]), float), 

559 np.asanyarray(self.convert_yunits(offsets[:, 1]), float))) 

560 self.stale = True 

561 

562 def get_offsets(self): 

563 """Return the offsets for the collection.""" 

564 # Default to zeros in the no-offset (None) case 

565 return np.zeros((1, 2)) if self._offsets is None else self._offsets 

566 

567 def _get_default_linewidth(self): 

568 # This may be overridden in a subclass. 

569 return mpl.rcParams['patch.linewidth'] # validated as float 

570 

571 def set_linewidth(self, lw): 

572 """ 

573 Set the linewidth(s) for the collection. *lw* can be a scalar 

574 or a sequence; if it is a sequence the patches will cycle 

575 through the sequence 

576 

577 Parameters 

578 ---------- 

579 lw : float or list of floats 

580 """ 

581 if lw is None: 

582 lw = self._get_default_linewidth() 

583 # get the un-scaled/broadcast lw 

584 self._us_lw = np.atleast_1d(lw) 

585 

586 # scale all of the dash patterns. 

587 self._linewidths, self._linestyles = self._bcast_lwls( 

588 self._us_lw, self._us_linestyles) 

589 self.stale = True 

590 

591 def set_linestyle(self, ls): 

592 """ 

593 Set the linestyle(s) for the collection. 

594 

595 =========================== ================= 

596 linestyle description 

597 =========================== ================= 

598 ``'-'`` or ``'solid'`` solid line 

599 ``'--'`` or ``'dashed'`` dashed line 

600 ``'-.'`` or ``'dashdot'`` dash-dotted line 

601 ``':'`` or ``'dotted'`` dotted line 

602 =========================== ================= 

603 

604 Alternatively a dash tuple of the following form can be provided:: 

605 

606 (offset, onoffseq), 

607 

608 where ``onoffseq`` is an even length tuple of on and off ink in points. 

609 

610 Parameters 

611 ---------- 

612 ls : str or tuple or list thereof 

613 Valid values for individual linestyles include {'-', '--', '-.', 

614 ':', '', (offset, on-off-seq)}. See `.Line2D.set_linestyle` for a 

615 complete description. 

616 """ 

617 try: 

618 dashes = [mlines._get_dash_pattern(ls)] 

619 except ValueError: 

620 try: 

621 dashes = [mlines._get_dash_pattern(x) for x in ls] 

622 except ValueError as err: 

623 emsg = f'Do not know how to convert {ls!r} to dashes' 

624 raise ValueError(emsg) from err 

625 

626 # get the list of raw 'unscaled' dash patterns 

627 self._us_linestyles = dashes 

628 

629 # broadcast and scale the lw and dash patterns 

630 self._linewidths, self._linestyles = self._bcast_lwls( 

631 self._us_lw, self._us_linestyles) 

632 

633 @_docstring.interpd 

634 def set_capstyle(self, cs): 

635 """ 

636 Set the `.CapStyle` for the collection (for all its elements). 

637 

638 Parameters 

639 ---------- 

640 cs : `.CapStyle` or %(CapStyle)s 

641 """ 

642 self._capstyle = CapStyle(cs) 

643 

644 @_docstring.interpd 

645 def get_capstyle(self): 

646 """ 

647 Return the cap style for the collection (for all its elements). 

648 

649 Returns 

650 ------- 

651 %(CapStyle)s or None 

652 """ 

653 return self._capstyle.name if self._capstyle else None 

654 

655 @_docstring.interpd 

656 def set_joinstyle(self, js): 

657 """ 

658 Set the `.JoinStyle` for the collection (for all its elements). 

659 

660 Parameters 

661 ---------- 

662 js : `.JoinStyle` or %(JoinStyle)s 

663 """ 

664 self._joinstyle = JoinStyle(js) 

665 

666 @_docstring.interpd 

667 def get_joinstyle(self): 

668 """ 

669 Return the join style for the collection (for all its elements). 

670 

671 Returns 

672 ------- 

673 %(JoinStyle)s or None 

674 """ 

675 return self._joinstyle.name if self._joinstyle else None 

676 

677 @staticmethod 

678 def _bcast_lwls(linewidths, dashes): 

679 """ 

680 Internal helper function to broadcast + scale ls/lw 

681 

682 In the collection drawing code, the linewidth and linestyle are cycled 

683 through as circular buffers (via ``v[i % len(v)]``). Thus, if we are 

684 going to scale the dash pattern at set time (not draw time) we need to 

685 do the broadcasting now and expand both lists to be the same length. 

686 

687 Parameters 

688 ---------- 

689 linewidths : list 

690 line widths of collection 

691 dashes : list 

692 dash specification (offset, (dash pattern tuple)) 

693 

694 Returns 

695 ------- 

696 linewidths, dashes : list 

697 Will be the same length, dashes are scaled by paired linewidth 

698 """ 

699 if mpl.rcParams['_internal.classic_mode']: 

700 return linewidths, dashes 

701 # make sure they are the same length so we can zip them 

702 if len(dashes) != len(linewidths): 

703 l_dashes = len(dashes) 

704 l_lw = len(linewidths) 

705 gcd = math.gcd(l_dashes, l_lw) 

706 dashes = list(dashes) * (l_lw // gcd) 

707 linewidths = list(linewidths) * (l_dashes // gcd) 

708 

709 # scale the dash patterns 

710 dashes = [mlines._scale_dashes(o, d, lw) 

711 for (o, d), lw in zip(dashes, linewidths)] 

712 

713 return linewidths, dashes 

714 

715 def get_antialiased(self): 

716 """ 

717 Get the antialiasing state for rendering. 

718 

719 Returns 

720 ------- 

721 array of bools 

722 """ 

723 return self._antialiaseds 

724 

725 def set_antialiased(self, aa): 

726 """ 

727 Set the antialiasing state for rendering. 

728 

729 Parameters 

730 ---------- 

731 aa : bool or list of bools 

732 """ 

733 if aa is None: 

734 aa = self._get_default_antialiased() 

735 self._antialiaseds = np.atleast_1d(np.asarray(aa, bool)) 

736 self.stale = True 

737 

738 def _get_default_antialiased(self): 

739 # This may be overridden in a subclass. 

740 return mpl.rcParams['patch.antialiased'] 

741 

742 def set_color(self, c): 

743 """ 

744 Set both the edgecolor and the facecolor. 

745 

746 Parameters 

747 ---------- 

748 c : :mpltype:`color` or list of RGBA tuples 

749 

750 See Also 

751 -------- 

752 Collection.set_facecolor, Collection.set_edgecolor 

753 For setting the edge or face color individually. 

754 """ 

755 self.set_facecolor(c) 

756 self.set_edgecolor(c) 

757 

758 def _get_default_facecolor(self): 

759 # This may be overridden in a subclass. 

760 return mpl.rcParams['patch.facecolor'] 

761 

762 def _set_facecolor(self, c): 

763 if c is None: 

764 c = self._get_default_facecolor() 

765 

766 self._facecolors = mcolors.to_rgba_array(c, self._alpha) 

767 self.stale = True 

768 

769 def set_facecolor(self, c): 

770 """ 

771 Set the facecolor(s) of the collection. *c* can be a color (all patches 

772 have same color), or a sequence of colors; if it is a sequence the 

773 patches will cycle through the sequence. 

774 

775 If *c* is 'none', the patch will not be filled. 

776 

777 Parameters 

778 ---------- 

779 c : :mpltype:`color` or list of :mpltype:`color` 

780 """ 

781 if isinstance(c, str) and c.lower() in ("none", "face"): 

782 c = c.lower() 

783 self._original_facecolor = c 

784 self._set_facecolor(c) 

785 

786 def get_facecolor(self): 

787 return self._facecolors 

788 

789 def get_edgecolor(self): 

790 if cbook._str_equal(self._edgecolors, 'face'): 

791 return self.get_facecolor() 

792 else: 

793 return self._edgecolors 

794 

795 def _get_default_edgecolor(self): 

796 # This may be overridden in a subclass. 

797 return mpl.rcParams['patch.edgecolor'] 

798 

799 def _set_edgecolor(self, c): 

800 set_hatch_color = True 

801 if c is None: 

802 if (mpl.rcParams['patch.force_edgecolor'] 

803 or self._edge_default 

804 or cbook._str_equal(self._original_facecolor, 'none')): 

805 c = self._get_default_edgecolor() 

806 else: 

807 c = 'none' 

808 set_hatch_color = False 

809 if cbook._str_lower_equal(c, 'face'): 

810 self._edgecolors = 'face' 

811 self.stale = True 

812 return 

813 self._edgecolors = mcolors.to_rgba_array(c, self._alpha) 

814 if set_hatch_color and len(self._edgecolors): 

815 self._hatch_color = tuple(self._edgecolors[0]) 

816 self.stale = True 

817 

818 def set_edgecolor(self, c): 

819 """ 

820 Set the edgecolor(s) of the collection. 

821 

822 Parameters 

823 ---------- 

824 c : :mpltype:`color` or list of :mpltype:`color` or 'face' 

825 The collection edgecolor(s). If a sequence, the patches cycle 

826 through it. If 'face', match the facecolor. 

827 """ 

828 # We pass through a default value for use in LineCollection. 

829 # This allows us to maintain None as the default indicator in 

830 # _original_edgecolor. 

831 if isinstance(c, str) and c.lower() in ("none", "face"): 

832 c = c.lower() 

833 self._original_edgecolor = c 

834 self._set_edgecolor(c) 

835 

836 def set_alpha(self, alpha): 

837 """ 

838 Set the transparency of the collection. 

839 

840 Parameters 

841 ---------- 

842 alpha : float or array of float or None 

843 If not None, *alpha* values must be between 0 and 1, inclusive. 

844 If an array is provided, its length must match the number of 

845 elements in the collection. Masked values and nans are not 

846 supported. 

847 """ 

848 artist.Artist._set_alpha_for_array(self, alpha) 

849 self._set_facecolor(self._original_facecolor) 

850 self._set_edgecolor(self._original_edgecolor) 

851 

852 set_alpha.__doc__ = artist.Artist._set_alpha_for_array.__doc__ 

853 

854 def get_linewidth(self): 

855 return self._linewidths 

856 

857 def get_linestyle(self): 

858 return self._linestyles 

859 

860 def _set_mappable_flags(self): 

861 """ 

862 Determine whether edges and/or faces are color-mapped. 

863 

864 This is a helper for update_scalarmappable. 

865 It sets Boolean flags '_edge_is_mapped' and '_face_is_mapped'. 

866 

867 Returns 

868 ------- 

869 mapping_change : bool 

870 True if either flag is True, or if a flag has changed. 

871 """ 

872 # The flags are initialized to None to ensure this returns True 

873 # the first time it is called. 

874 edge0 = self._edge_is_mapped 

875 face0 = self._face_is_mapped 

876 # After returning, the flags must be Booleans, not None. 

877 self._edge_is_mapped = False 

878 self._face_is_mapped = False 

879 if self._A is not None: 

880 if not cbook._str_equal(self._original_facecolor, 'none'): 

881 self._face_is_mapped = True 

882 if cbook._str_equal(self._original_edgecolor, 'face'): 

883 self._edge_is_mapped = True 

884 else: 

885 if self._original_edgecolor is None: 

886 self._edge_is_mapped = True 

887 

888 mapped = self._face_is_mapped or self._edge_is_mapped 

889 changed = (edge0 is None or face0 is None 

890 or self._edge_is_mapped != edge0 

891 or self._face_is_mapped != face0) 

892 return mapped or changed 

893 

894 def update_scalarmappable(self): 

895 """ 

896 Update colors from the scalar mappable array, if any. 

897 

898 Assign colors to edges and faces based on the array and/or 

899 colors that were directly set, as appropriate. 

900 """ 

901 if not self._set_mappable_flags(): 

902 return 

903 # Allow possibility to call 'self.set_array(None)'. 

904 if self._A is not None: 

905 # QuadMesh can map 2d arrays (but pcolormesh supplies 1d array) 

906 if self._A.ndim > 1 and not isinstance(self, _MeshData): 

907 raise ValueError('Collections can only map rank 1 arrays') 

908 if np.iterable(self._alpha): 

909 if self._alpha.size != self._A.size: 

910 raise ValueError( 

911 f'Data array shape, {self._A.shape} ' 

912 'is incompatible with alpha array shape, ' 

913 f'{self._alpha.shape}. ' 

914 'This can occur with the deprecated ' 

915 'behavior of the "flat" shading option, ' 

916 'in which a row and/or column of the data ' 

917 'array is dropped.') 

918 # pcolormesh, scatter, maybe others flatten their _A 

919 self._alpha = self._alpha.reshape(self._A.shape) 

920 self._mapped_colors = self.to_rgba(self._A, self._alpha) 

921 

922 if self._face_is_mapped: 

923 self._facecolors = self._mapped_colors 

924 else: 

925 self._set_facecolor(self._original_facecolor) 

926 if self._edge_is_mapped: 

927 self._edgecolors = self._mapped_colors 

928 else: 

929 self._set_edgecolor(self._original_edgecolor) 

930 self.stale = True 

931 

932 def get_fill(self): 

933 """Return whether face is colored.""" 

934 return not cbook._str_lower_equal(self._original_facecolor, "none") 

935 

936 def update_from(self, other): 

937 """Copy properties from other to self.""" 

938 

939 artist.Artist.update_from(self, other) 

940 self._antialiaseds = other._antialiaseds 

941 self._mapped_colors = other._mapped_colors 

942 self._edge_is_mapped = other._edge_is_mapped 

943 self._original_edgecolor = other._original_edgecolor 

944 self._edgecolors = other._edgecolors 

945 self._face_is_mapped = other._face_is_mapped 

946 self._original_facecolor = other._original_facecolor 

947 self._facecolors = other._facecolors 

948 self._linewidths = other._linewidths 

949 self._linestyles = other._linestyles 

950 self._us_linestyles = other._us_linestyles 

951 self._pickradius = other._pickradius 

952 self._hatch = other._hatch 

953 

954 # update_from for scalarmappable 

955 self._A = other._A 

956 self.norm = other.norm 

957 self.cmap = other.cmap 

958 self.stale = True 

959 

960 

961class _CollectionWithSizes(Collection): 

962 """ 

963 Base class for collections that have an array of sizes. 

964 """ 

965 _factor = 1.0 

966 

967 def get_sizes(self): 

968 """ 

969 Return the sizes ('areas') of the elements in the collection. 

970 

971 Returns 

972 ------- 

973 array 

974 The 'area' of each element. 

975 """ 

976 return self._sizes 

977 

978 def set_sizes(self, sizes, dpi=72.0): 

979 """ 

980 Set the sizes of each member of the collection. 

981 

982 Parameters 

983 ---------- 

984 sizes : `numpy.ndarray` or None 

985 The size to set for each element of the collection. The 

986 value is the 'area' of the element. 

987 dpi : float, default: 72 

988 The dpi of the canvas. 

989 """ 

990 if sizes is None: 

991 self._sizes = np.array([]) 

992 self._transforms = np.empty((0, 3, 3)) 

993 else: 

994 self._sizes = np.asarray(sizes) 

995 self._transforms = np.zeros((len(self._sizes), 3, 3)) 

996 scale = np.sqrt(self._sizes) * dpi / 72.0 * self._factor 

997 self._transforms[:, 0, 0] = scale 

998 self._transforms[:, 1, 1] = scale 

999 self._transforms[:, 2, 2] = 1.0 

1000 self.stale = True 

1001 

1002 @artist.allow_rasterization 

1003 def draw(self, renderer): 

1004 self.set_sizes(self._sizes, self.figure.dpi) 

1005 super().draw(renderer) 

1006 

1007 

1008class PathCollection(_CollectionWithSizes): 

1009 r""" 

1010 A collection of `~.path.Path`\s, as created by e.g. `~.Axes.scatter`. 

1011 """ 

1012 

1013 def __init__(self, paths, sizes=None, **kwargs): 

1014 """ 

1015 Parameters 

1016 ---------- 

1017 paths : list of `.path.Path` 

1018 The paths that will make up the `.Collection`. 

1019 sizes : array-like 

1020 The factor by which to scale each drawn `~.path.Path`. One unit 

1021 squared in the Path's data space is scaled to be ``sizes**2`` 

1022 points when rendered. 

1023 **kwargs 

1024 Forwarded to `.Collection`. 

1025 """ 

1026 

1027 super().__init__(**kwargs) 

1028 self.set_paths(paths) 

1029 self.set_sizes(sizes) 

1030 self.stale = True 

1031 

1032 def get_paths(self): 

1033 return self._paths 

1034 

1035 def legend_elements(self, prop="colors", num="auto", 

1036 fmt=None, func=lambda x: x, **kwargs): 

1037 """ 

1038 Create legend handles and labels for a PathCollection. 

1039 

1040 Each legend handle is a `.Line2D` representing the Path that was drawn, 

1041 and each label is a string that represents the Path. 

1042 

1043 This is useful for obtaining a legend for a `~.Axes.scatter` plot; 

1044 e.g.:: 

1045 

1046 scatter = plt.scatter([1, 2, 3], [4, 5, 6], c=[7, 2, 3], num=None) 

1047 plt.legend(*scatter.legend_elements()) 

1048 

1049 creates three legend elements, one for each color with the numerical 

1050 values passed to *c* as the labels. 

1051 

1052 Also see the :ref:`automatedlegendcreation` example. 

1053 

1054 Parameters 

1055 ---------- 

1056 prop : {"colors", "sizes"}, default: "colors" 

1057 If "colors", the legend handles will show the different colors of 

1058 the collection. If "sizes", the legend will show the different 

1059 sizes. To set both, use *kwargs* to directly edit the `.Line2D` 

1060 properties. 

1061 num : int, None, "auto" (default), array-like, or `~.ticker.Locator` 

1062 Target number of elements to create. 

1063 If None, use all unique elements of the mappable array. If an 

1064 integer, target to use *num* elements in the normed range. 

1065 If *"auto"*, try to determine which option better suits the nature 

1066 of the data. 

1067 The number of created elements may slightly deviate from *num* due 

1068 to a `~.ticker.Locator` being used to find useful locations. 

1069 If a list or array, use exactly those elements for the legend. 

1070 Finally, a `~.ticker.Locator` can be provided. 

1071 fmt : str, `~matplotlib.ticker.Formatter`, or None (default) 

1072 The format or formatter to use for the labels. If a string must be 

1073 a valid input for a `.StrMethodFormatter`. If None (the default), 

1074 use a `.ScalarFormatter`. 

1075 func : function, default: ``lambda x: x`` 

1076 Function to calculate the labels. Often the size (or color) 

1077 argument to `~.Axes.scatter` will have been pre-processed by the 

1078 user using a function ``s = f(x)`` to make the markers visible; 

1079 e.g. ``size = np.log10(x)``. Providing the inverse of this 

1080 function here allows that pre-processing to be inverted, so that 

1081 the legend labels have the correct values; e.g. ``func = lambda 

1082 x: 10**x``. 

1083 **kwargs 

1084 Allowed keyword arguments are *color* and *size*. E.g. it may be 

1085 useful to set the color of the markers if *prop="sizes"* is used; 

1086 similarly to set the size of the markers if *prop="colors"* is 

1087 used. Any further parameters are passed onto the `.Line2D` 

1088 instance. This may be useful to e.g. specify a different 

1089 *markeredgecolor* or *alpha* for the legend handles. 

1090 

1091 Returns 

1092 ------- 

1093 handles : list of `.Line2D` 

1094 Visual representation of each element of the legend. 

1095 labels : list of str 

1096 The string labels for elements of the legend. 

1097 """ 

1098 handles = [] 

1099 labels = [] 

1100 hasarray = self.get_array() is not None 

1101 if fmt is None: 

1102 fmt = mpl.ticker.ScalarFormatter(useOffset=False, useMathText=True) 

1103 elif isinstance(fmt, str): 

1104 fmt = mpl.ticker.StrMethodFormatter(fmt) 

1105 fmt.create_dummy_axis() 

1106 

1107 if prop == "colors": 

1108 if not hasarray: 

1109 warnings.warn("Collection without array used. Make sure to " 

1110 "specify the values to be colormapped via the " 

1111 "`c` argument.") 

1112 return handles, labels 

1113 u = np.unique(self.get_array()) 

1114 size = kwargs.pop("size", mpl.rcParams["lines.markersize"]) 

1115 elif prop == "sizes": 

1116 u = np.unique(self.get_sizes()) 

1117 color = kwargs.pop("color", "k") 

1118 else: 

1119 raise ValueError("Valid values for `prop` are 'colors' or " 

1120 f"'sizes'. You supplied '{prop}' instead.") 

1121 

1122 fu = func(u) 

1123 fmt.axis.set_view_interval(fu.min(), fu.max()) 

1124 fmt.axis.set_data_interval(fu.min(), fu.max()) 

1125 if num == "auto": 

1126 num = 9 

1127 if len(u) <= num: 

1128 num = None 

1129 if num is None: 

1130 values = u 

1131 label_values = func(values) 

1132 else: 

1133 if prop == "colors": 

1134 arr = self.get_array() 

1135 elif prop == "sizes": 

1136 arr = self.get_sizes() 

1137 if isinstance(num, mpl.ticker.Locator): 

1138 loc = num 

1139 elif np.iterable(num): 

1140 loc = mpl.ticker.FixedLocator(num) 

1141 else: 

1142 num = int(num) 

1143 loc = mpl.ticker.MaxNLocator(nbins=num, min_n_ticks=num-1, 

1144 steps=[1, 2, 2.5, 3, 5, 6, 8, 10]) 

1145 label_values = loc.tick_values(func(arr).min(), func(arr).max()) 

1146 cond = ((label_values >= func(arr).min()) & 

1147 (label_values <= func(arr).max())) 

1148 label_values = label_values[cond] 

1149 yarr = np.linspace(arr.min(), arr.max(), 256) 

1150 xarr = func(yarr) 

1151 ix = np.argsort(xarr) 

1152 values = np.interp(label_values, xarr[ix], yarr[ix]) 

1153 

1154 kw = {"markeredgewidth": self.get_linewidths()[0], 

1155 "alpha": self.get_alpha(), 

1156 **kwargs} 

1157 

1158 for val, lab in zip(values, label_values): 

1159 if prop == "colors": 

1160 color = self.cmap(self.norm(val)) 

1161 elif prop == "sizes": 

1162 size = np.sqrt(val) 

1163 if np.isclose(size, 0.0): 

1164 continue 

1165 h = mlines.Line2D([0], [0], ls="", color=color, ms=size, 

1166 marker=self.get_paths()[0], **kw) 

1167 handles.append(h) 

1168 if hasattr(fmt, "set_locs"): 

1169 fmt.set_locs(label_values) 

1170 l = fmt(lab) 

1171 labels.append(l) 

1172 

1173 return handles, labels 

1174 

1175 

1176class PolyCollection(_CollectionWithSizes): 

1177 

1178 def __init__(self, verts, sizes=None, *, closed=True, **kwargs): 

1179 """ 

1180 Parameters 

1181 ---------- 

1182 verts : list of array-like 

1183 The sequence of polygons [*verts0*, *verts1*, ...] where each 

1184 element *verts_i* defines the vertices of polygon *i* as a 2D 

1185 array-like of shape (M, 2). 

1186 sizes : array-like, default: None 

1187 Squared scaling factors for the polygons. The coordinates of each 

1188 polygon *verts_i* are multiplied by the square-root of the 

1189 corresponding entry in *sizes* (i.e., *sizes* specify the scaling 

1190 of areas). The scaling is applied before the Artist master 

1191 transform. 

1192 closed : bool, default: True 

1193 Whether the polygon should be closed by adding a CLOSEPOLY 

1194 connection at the end. 

1195 **kwargs 

1196 Forwarded to `.Collection`. 

1197 """ 

1198 super().__init__(**kwargs) 

1199 self.set_sizes(sizes) 

1200 self.set_verts(verts, closed) 

1201 self.stale = True 

1202 

1203 def set_verts(self, verts, closed=True): 

1204 """ 

1205 Set the vertices of the polygons. 

1206 

1207 Parameters 

1208 ---------- 

1209 verts : list of array-like 

1210 The sequence of polygons [*verts0*, *verts1*, ...] where each 

1211 element *verts_i* defines the vertices of polygon *i* as a 2D 

1212 array-like of shape (M, 2). 

1213 closed : bool, default: True 

1214 Whether the polygon should be closed by adding a CLOSEPOLY 

1215 connection at the end. 

1216 """ 

1217 self.stale = True 

1218 if isinstance(verts, np.ma.MaskedArray): 

1219 verts = verts.astype(float).filled(np.nan) 

1220 

1221 # No need to do anything fancy if the path isn't closed. 

1222 if not closed: 

1223 self._paths = [mpath.Path(xy) for xy in verts] 

1224 return 

1225 

1226 # Fast path for arrays 

1227 if isinstance(verts, np.ndarray) and len(verts.shape) == 3: 

1228 verts_pad = np.concatenate((verts, verts[:, :1]), axis=1) 

1229 # Creating the codes once is much faster than having Path do it 

1230 # separately each time by passing closed=True. 

1231 codes = np.empty(verts_pad.shape[1], dtype=mpath.Path.code_type) 

1232 codes[:] = mpath.Path.LINETO 

1233 codes[0] = mpath.Path.MOVETO 

1234 codes[-1] = mpath.Path.CLOSEPOLY 

1235 self._paths = [mpath.Path(xy, codes) for xy in verts_pad] 

1236 return 

1237 

1238 self._paths = [] 

1239 for xy in verts: 

1240 if len(xy): 

1241 self._paths.append(mpath.Path._create_closed(xy)) 

1242 else: 

1243 self._paths.append(mpath.Path(xy)) 

1244 

1245 set_paths = set_verts 

1246 

1247 def set_verts_and_codes(self, verts, codes): 

1248 """Initialize vertices with path codes.""" 

1249 if len(verts) != len(codes): 

1250 raise ValueError("'codes' must be a 1D list or array " 

1251 "with the same length of 'verts'") 

1252 self._paths = [mpath.Path(xy, cds) if len(xy) else mpath.Path(xy) 

1253 for xy, cds in zip(verts, codes)] 

1254 self.stale = True 

1255 

1256 

1257class RegularPolyCollection(_CollectionWithSizes): 

1258 """A collection of n-sided regular polygons.""" 

1259 

1260 _path_generator = mpath.Path.unit_regular_polygon 

1261 _factor = np.pi ** (-1/2) 

1262 

1263 def __init__(self, 

1264 numsides, 

1265 *, 

1266 rotation=0, 

1267 sizes=(1,), 

1268 **kwargs): 

1269 """ 

1270 Parameters 

1271 ---------- 

1272 numsides : int 

1273 The number of sides of the polygon. 

1274 rotation : float 

1275 The rotation of the polygon in radians. 

1276 sizes : tuple of float 

1277 The area of the circle circumscribing the polygon in points^2. 

1278 **kwargs 

1279 Forwarded to `.Collection`. 

1280 

1281 Examples 

1282 -------- 

1283 See :doc:`/gallery/event_handling/lasso_demo` for a complete example:: 

1284 

1285 offsets = np.random.rand(20, 2) 

1286 facecolors = [cm.jet(x) for x in np.random.rand(20)] 

1287 

1288 collection = RegularPolyCollection( 

1289 numsides=5, # a pentagon 

1290 rotation=0, sizes=(50,), 

1291 facecolors=facecolors, 

1292 edgecolors=("black",), 

1293 linewidths=(1,), 

1294 offsets=offsets, 

1295 offset_transform=ax.transData, 

1296 ) 

1297 """ 

1298 super().__init__(**kwargs) 

1299 self.set_sizes(sizes) 

1300 self._numsides = numsides 

1301 self._paths = [self._path_generator(numsides)] 

1302 self._rotation = rotation 

1303 self.set_transform(transforms.IdentityTransform()) 

1304 

1305 def get_numsides(self): 

1306 return self._numsides 

1307 

1308 def get_rotation(self): 

1309 return self._rotation 

1310 

1311 @artist.allow_rasterization 

1312 def draw(self, renderer): 

1313 self.set_sizes(self._sizes, self.figure.dpi) 

1314 self._transforms = [ 

1315 transforms.Affine2D(x).rotate(-self._rotation).get_matrix() 

1316 for x in self._transforms 

1317 ] 

1318 # Explicitly not super().draw, because set_sizes must be called before 

1319 # updating self._transforms. 

1320 Collection.draw(self, renderer) 

1321 

1322 

1323class StarPolygonCollection(RegularPolyCollection): 

1324 """Draw a collection of regular stars with *numsides* points.""" 

1325 _path_generator = mpath.Path.unit_regular_star 

1326 

1327 

1328class AsteriskPolygonCollection(RegularPolyCollection): 

1329 """Draw a collection of regular asterisks with *numsides* points.""" 

1330 _path_generator = mpath.Path.unit_regular_asterisk 

1331 

1332 

1333class LineCollection(Collection): 

1334 r""" 

1335 Represents a sequence of `.Line2D`\s that should be drawn together. 

1336 

1337 This class extends `.Collection` to represent a sequence of 

1338 `.Line2D`\s instead of just a sequence of `.Patch`\s. 

1339 Just as in `.Collection`, each property of a *LineCollection* may be either 

1340 a single value or a list of values. This list is then used cyclically for 

1341 each element of the LineCollection, so the property of the ``i``\th element 

1342 of the collection is:: 

1343 

1344 prop[i % len(prop)] 

1345 

1346 The properties of each member of a *LineCollection* default to their values 

1347 in :rc:`lines.*` instead of :rc:`patch.*`, and the property *colors* is 

1348 added in place of *edgecolors*. 

1349 """ 

1350 

1351 _edge_default = True 

1352 

1353 def __init__(self, segments, # Can be None. 

1354 *, 

1355 zorder=2, # Collection.zorder is 1 

1356 **kwargs 

1357 ): 

1358 """ 

1359 Parameters 

1360 ---------- 

1361 segments : list of array-like 

1362 A sequence (*line0*, *line1*, *line2*) of lines, where each line is a list 

1363 of points:: 

1364 

1365 lineN = [(x0, y0), (x1, y1), ... (xm, ym)] 

1366 

1367 or the equivalent Mx2 numpy array with two columns. Each line 

1368 can have a different number of segments. 

1369 linewidths : float or list of float, default: :rc:`lines.linewidth` 

1370 The width of each line in points. 

1371 colors : :mpltype:`color` or list of color, default: :rc:`lines.color` 

1372 A sequence of RGBA tuples (e.g., arbitrary color strings, etc, not 

1373 allowed). 

1374 antialiaseds : bool or list of bool, default: :rc:`lines.antialiased` 

1375 Whether to use antialiasing for each line. 

1376 zorder : float, default: 2 

1377 zorder of the lines once drawn. 

1378 

1379 facecolors : :mpltype:`color` or list of :mpltype:`color`, default: 'none' 

1380 When setting *facecolors*, each line is interpreted as a boundary 

1381 for an area, implicitly closing the path from the last point to the 

1382 first point. The enclosed area is filled with *facecolor*. 

1383 In order to manually specify what should count as the "interior" of 

1384 each line, please use `.PathCollection` instead, where the 

1385 "interior" can be specified by appropriate usage of 

1386 `~.path.Path.CLOSEPOLY`. 

1387 

1388 **kwargs 

1389 Forwarded to `.Collection`. 

1390 """ 

1391 # Unfortunately, mplot3d needs this explicit setting of 'facecolors'. 

1392 kwargs.setdefault('facecolors', 'none') 

1393 super().__init__( 

1394 zorder=zorder, 

1395 **kwargs) 

1396 self.set_segments(segments) 

1397 

1398 def set_segments(self, segments): 

1399 if segments is None: 

1400 return 

1401 

1402 self._paths = [mpath.Path(seg) if isinstance(seg, np.ma.MaskedArray) 

1403 else mpath.Path(np.asarray(seg, float)) 

1404 for seg in segments] 

1405 self.stale = True 

1406 

1407 set_verts = set_segments # for compatibility with PolyCollection 

1408 set_paths = set_segments 

1409 

1410 def get_segments(self): 

1411 """ 

1412 Returns 

1413 ------- 

1414 list 

1415 List of segments in the LineCollection. Each list item contains an 

1416 array of vertices. 

1417 """ 

1418 segments = [] 

1419 

1420 for path in self._paths: 

1421 vertices = [ 

1422 vertex 

1423 for vertex, _ 

1424 # Never simplify here, we want to get the data-space values 

1425 # back and there in no way to know the "right" simplification 

1426 # threshold so never try. 

1427 in path.iter_segments(simplify=False) 

1428 ] 

1429 vertices = np.asarray(vertices) 

1430 segments.append(vertices) 

1431 

1432 return segments 

1433 

1434 def _get_default_linewidth(self): 

1435 return mpl.rcParams['lines.linewidth'] 

1436 

1437 def _get_default_antialiased(self): 

1438 return mpl.rcParams['lines.antialiased'] 

1439 

1440 def _get_default_edgecolor(self): 

1441 return mpl.rcParams['lines.color'] 

1442 

1443 def _get_default_facecolor(self): 

1444 return 'none' 

1445 

1446 def set_alpha(self, alpha): 

1447 # docstring inherited 

1448 super().set_alpha(alpha) 

1449 if self._gapcolor is not None: 

1450 self.set_gapcolor(self._original_gapcolor) 

1451 

1452 def set_color(self, c): 

1453 """ 

1454 Set the edgecolor(s) of the LineCollection. 

1455 

1456 Parameters 

1457 ---------- 

1458 c : :mpltype:`color` or list of :mpltype:`color` 

1459 Single color (all lines have same color), or a 

1460 sequence of RGBA tuples; if it is a sequence the lines will 

1461 cycle through the sequence. 

1462 """ 

1463 self.set_edgecolor(c) 

1464 

1465 set_colors = set_color 

1466 

1467 def get_color(self): 

1468 return self._edgecolors 

1469 

1470 get_colors = get_color # for compatibility with old versions 

1471 

1472 def set_gapcolor(self, gapcolor): 

1473 """ 

1474 Set a color to fill the gaps in the dashed line style. 

1475 

1476 .. note:: 

1477 

1478 Striped lines are created by drawing two interleaved dashed lines. 

1479 There can be overlaps between those two, which may result in 

1480 artifacts when using transparency. 

1481 

1482 This functionality is experimental and may change. 

1483 

1484 Parameters 

1485 ---------- 

1486 gapcolor : :mpltype:`color` or list of :mpltype:`color` or None 

1487 The color with which to fill the gaps. If None, the gaps are 

1488 unfilled. 

1489 """ 

1490 self._original_gapcolor = gapcolor 

1491 self._set_gapcolor(gapcolor) 

1492 

1493 def _set_gapcolor(self, gapcolor): 

1494 if gapcolor is not None: 

1495 gapcolor = mcolors.to_rgba_array(gapcolor, self._alpha) 

1496 self._gapcolor = gapcolor 

1497 self.stale = True 

1498 

1499 def get_gapcolor(self): 

1500 return self._gapcolor 

1501 

1502 def _get_inverse_paths_linestyles(self): 

1503 """ 

1504 Returns the path and pattern for the gaps in the non-solid lines. 

1505 

1506 This path and pattern is the inverse of the path and pattern used to 

1507 construct the non-solid lines. For solid lines, we set the inverse path 

1508 to nans to prevent drawing an inverse line. 

1509 """ 

1510 path_patterns = [ 

1511 (mpath.Path(np.full((1, 2), np.nan)), ls) 

1512 if ls == (0, None) else 

1513 (path, mlines._get_inverse_dash_pattern(*ls)) 

1514 for (path, ls) in 

1515 zip(self._paths, itertools.cycle(self._linestyles))] 

1516 

1517 return zip(*path_patterns) 

1518 

1519 

1520class EventCollection(LineCollection): 

1521 """ 

1522 A collection of locations along a single axis at which an "event" occurred. 

1523 

1524 The events are given by a 1-dimensional array. They do not have an 

1525 amplitude and are displayed as parallel lines. 

1526 """ 

1527 

1528 _edge_default = True 

1529 

1530 def __init__(self, 

1531 positions, # Cannot be None. 

1532 orientation='horizontal', 

1533 *, 

1534 lineoffset=0, 

1535 linelength=1, 

1536 linewidth=None, 

1537 color=None, 

1538 linestyle='solid', 

1539 antialiased=None, 

1540 **kwargs 

1541 ): 

1542 """ 

1543 Parameters 

1544 ---------- 

1545 positions : 1D array-like 

1546 Each value is an event. 

1547 orientation : {'horizontal', 'vertical'}, default: 'horizontal' 

1548 The sequence of events is plotted along this direction. 

1549 The marker lines of the single events are along the orthogonal 

1550 direction. 

1551 lineoffset : float, default: 0 

1552 The offset of the center of the markers from the origin, in the 

1553 direction orthogonal to *orientation*. 

1554 linelength : float, default: 1 

1555 The total height of the marker (i.e. the marker stretches from 

1556 ``lineoffset - linelength/2`` to ``lineoffset + linelength/2``). 

1557 linewidth : float or list thereof, default: :rc:`lines.linewidth` 

1558 The line width of the event lines, in points. 

1559 color : :mpltype:`color` or list of :mpltype:`color`, default: :rc:`lines.color` 

1560 The color of the event lines. 

1561 linestyle : str or tuple or list thereof, default: 'solid' 

1562 Valid strings are ['solid', 'dashed', 'dashdot', 'dotted', 

1563 '-', '--', '-.', ':']. Dash tuples should be of the form:: 

1564 

1565 (offset, onoffseq), 

1566 

1567 where *onoffseq* is an even length tuple of on and off ink 

1568 in points. 

1569 antialiased : bool or list thereof, default: :rc:`lines.antialiased` 

1570 Whether to use antialiasing for drawing the lines. 

1571 **kwargs 

1572 Forwarded to `.LineCollection`. 

1573 

1574 Examples 

1575 -------- 

1576 .. plot:: gallery/lines_bars_and_markers/eventcollection_demo.py 

1577 """ 

1578 super().__init__([], 

1579 linewidths=linewidth, linestyles=linestyle, 

1580 colors=color, antialiaseds=antialiased, 

1581 **kwargs) 

1582 self._is_horizontal = True # Initial value, may be switched below. 

1583 self._linelength = linelength 

1584 self._lineoffset = lineoffset 

1585 self.set_orientation(orientation) 

1586 self.set_positions(positions) 

1587 

1588 def get_positions(self): 

1589 """ 

1590 Return an array containing the floating-point values of the positions. 

1591 """ 

1592 pos = 0 if self.is_horizontal() else 1 

1593 return [segment[0, pos] for segment in self.get_segments()] 

1594 

1595 def set_positions(self, positions): 

1596 """Set the positions of the events.""" 

1597 if positions is None: 

1598 positions = [] 

1599 if np.ndim(positions) != 1: 

1600 raise ValueError('positions must be one-dimensional') 

1601 lineoffset = self.get_lineoffset() 

1602 linelength = self.get_linelength() 

1603 pos_idx = 0 if self.is_horizontal() else 1 

1604 segments = np.empty((len(positions), 2, 2)) 

1605 segments[:, :, pos_idx] = np.sort(positions)[:, None] 

1606 segments[:, 0, 1 - pos_idx] = lineoffset + linelength / 2 

1607 segments[:, 1, 1 - pos_idx] = lineoffset - linelength / 2 

1608 self.set_segments(segments) 

1609 

1610 def add_positions(self, position): 

1611 """Add one or more events at the specified positions.""" 

1612 if position is None or (hasattr(position, 'len') and 

1613 len(position) == 0): 

1614 return 

1615 positions = self.get_positions() 

1616 positions = np.hstack([positions, np.asanyarray(position)]) 

1617 self.set_positions(positions) 

1618 extend_positions = append_positions = add_positions 

1619 

1620 def is_horizontal(self): 

1621 """True if the eventcollection is horizontal, False if vertical.""" 

1622 return self._is_horizontal 

1623 

1624 def get_orientation(self): 

1625 """ 

1626 Return the orientation of the event line ('horizontal' or 'vertical'). 

1627 """ 

1628 return 'horizontal' if self.is_horizontal() else 'vertical' 

1629 

1630 def switch_orientation(self): 

1631 """ 

1632 Switch the orientation of the event line, either from vertical to 

1633 horizontal or vice versus. 

1634 """ 

1635 segments = self.get_segments() 

1636 for i, segment in enumerate(segments): 

1637 segments[i] = np.fliplr(segment) 

1638 self.set_segments(segments) 

1639 self._is_horizontal = not self.is_horizontal() 

1640 self.stale = True 

1641 

1642 def set_orientation(self, orientation): 

1643 """ 

1644 Set the orientation of the event line. 

1645 

1646 Parameters 

1647 ---------- 

1648 orientation : {'horizontal', 'vertical'} 

1649 """ 

1650 is_horizontal = _api.check_getitem( 

1651 {"horizontal": True, "vertical": False}, 

1652 orientation=orientation) 

1653 if is_horizontal == self.is_horizontal(): 

1654 return 

1655 self.switch_orientation() 

1656 

1657 def get_linelength(self): 

1658 """Return the length of the lines used to mark each event.""" 

1659 return self._linelength 

1660 

1661 def set_linelength(self, linelength): 

1662 """Set the length of the lines used to mark each event.""" 

1663 if linelength == self.get_linelength(): 

1664 return 

1665 lineoffset = self.get_lineoffset() 

1666 segments = self.get_segments() 

1667 pos = 1 if self.is_horizontal() else 0 

1668 for segment in segments: 

1669 segment[0, pos] = lineoffset + linelength / 2. 

1670 segment[1, pos] = lineoffset - linelength / 2. 

1671 self.set_segments(segments) 

1672 self._linelength = linelength 

1673 

1674 def get_lineoffset(self): 

1675 """Return the offset of the lines used to mark each event.""" 

1676 return self._lineoffset 

1677 

1678 def set_lineoffset(self, lineoffset): 

1679 """Set the offset of the lines used to mark each event.""" 

1680 if lineoffset == self.get_lineoffset(): 

1681 return 

1682 linelength = self.get_linelength() 

1683 segments = self.get_segments() 

1684 pos = 1 if self.is_horizontal() else 0 

1685 for segment in segments: 

1686 segment[0, pos] = lineoffset + linelength / 2. 

1687 segment[1, pos] = lineoffset - linelength / 2. 

1688 self.set_segments(segments) 

1689 self._lineoffset = lineoffset 

1690 

1691 def get_linewidth(self): 

1692 """Get the width of the lines used to mark each event.""" 

1693 return super().get_linewidth()[0] 

1694 

1695 def get_linewidths(self): 

1696 return super().get_linewidth() 

1697 

1698 def get_color(self): 

1699 """Return the color of the lines used to mark each event.""" 

1700 return self.get_colors()[0] 

1701 

1702 

1703class CircleCollection(_CollectionWithSizes): 

1704 """A collection of circles, drawn using splines.""" 

1705 

1706 _factor = np.pi ** (-1/2) 

1707 

1708 def __init__(self, sizes, **kwargs): 

1709 """ 

1710 Parameters 

1711 ---------- 

1712 sizes : float or array-like 

1713 The area of each circle in points^2. 

1714 **kwargs 

1715 Forwarded to `.Collection`. 

1716 """ 

1717 super().__init__(**kwargs) 

1718 self.set_sizes(sizes) 

1719 self.set_transform(transforms.IdentityTransform()) 

1720 self._paths = [mpath.Path.unit_circle()] 

1721 

1722 

1723class EllipseCollection(Collection): 

1724 """A collection of ellipses, drawn using splines.""" 

1725 

1726 def __init__(self, widths, heights, angles, *, units='points', **kwargs): 

1727 """ 

1728 Parameters 

1729 ---------- 

1730 widths : array-like 

1731 The lengths of the first axes (e.g., major axis lengths). 

1732 heights : array-like 

1733 The lengths of second axes. 

1734 angles : array-like 

1735 The angles of the first axes, degrees CCW from the x-axis. 

1736 units : {'points', 'inches', 'dots', 'width', 'height', 'x', 'y', 'xy'} 

1737 The units in which majors and minors are given; 'width' and 

1738 'height' refer to the dimensions of the axes, while 'x' and 'y' 

1739 refer to the *offsets* data units. 'xy' differs from all others in 

1740 that the angle as plotted varies with the aspect ratio, and equals 

1741 the specified angle only when the aspect ratio is unity. Hence 

1742 it behaves the same as the `~.patches.Ellipse` with 

1743 ``axes.transData`` as its transform. 

1744 **kwargs 

1745 Forwarded to `Collection`. 

1746 """ 

1747 super().__init__(**kwargs) 

1748 self.set_widths(widths) 

1749 self.set_heights(heights) 

1750 self.set_angles(angles) 

1751 self._units = units 

1752 self.set_transform(transforms.IdentityTransform()) 

1753 self._transforms = np.empty((0, 3, 3)) 

1754 self._paths = [mpath.Path.unit_circle()] 

1755 

1756 def _set_transforms(self): 

1757 """Calculate transforms immediately before drawing.""" 

1758 

1759 ax = self.axes 

1760 fig = self.figure 

1761 

1762 if self._units == 'xy': 

1763 sc = 1 

1764 elif self._units == 'x': 

1765 sc = ax.bbox.width / ax.viewLim.width 

1766 elif self._units == 'y': 

1767 sc = ax.bbox.height / ax.viewLim.height 

1768 elif self._units == 'inches': 

1769 sc = fig.dpi 

1770 elif self._units == 'points': 

1771 sc = fig.dpi / 72.0 

1772 elif self._units == 'width': 

1773 sc = ax.bbox.width 

1774 elif self._units == 'height': 

1775 sc = ax.bbox.height 

1776 elif self._units == 'dots': 

1777 sc = 1.0 

1778 else: 

1779 raise ValueError(f'Unrecognized units: {self._units!r}') 

1780 

1781 self._transforms = np.zeros((len(self._widths), 3, 3)) 

1782 widths = self._widths * sc 

1783 heights = self._heights * sc 

1784 sin_angle = np.sin(self._angles) 

1785 cos_angle = np.cos(self._angles) 

1786 self._transforms[:, 0, 0] = widths * cos_angle 

1787 self._transforms[:, 0, 1] = heights * -sin_angle 

1788 self._transforms[:, 1, 0] = widths * sin_angle 

1789 self._transforms[:, 1, 1] = heights * cos_angle 

1790 self._transforms[:, 2, 2] = 1.0 

1791 

1792 _affine = transforms.Affine2D 

1793 if self._units == 'xy': 

1794 m = ax.transData.get_affine().get_matrix().copy() 

1795 m[:2, 2:] = 0 

1796 self.set_transform(_affine(m)) 

1797 

1798 def set_widths(self, widths): 

1799 """Set the lengths of the first axes (e.g., major axis).""" 

1800 self._widths = 0.5 * np.asarray(widths).ravel() 

1801 self.stale = True 

1802 

1803 def set_heights(self, heights): 

1804 """Set the lengths of second axes (e.g., minor axes).""" 

1805 self._heights = 0.5 * np.asarray(heights).ravel() 

1806 self.stale = True 

1807 

1808 def set_angles(self, angles): 

1809 """Set the angles of the first axes, degrees CCW from the x-axis.""" 

1810 self._angles = np.deg2rad(angles).ravel() 

1811 self.stale = True 

1812 

1813 def get_widths(self): 

1814 """Get the lengths of the first axes (e.g., major axis).""" 

1815 return self._widths * 2 

1816 

1817 def get_heights(self): 

1818 """Set the lengths of second axes (e.g., minor axes).""" 

1819 return self._heights * 2 

1820 

1821 def get_angles(self): 

1822 """Get the angles of the first axes, degrees CCW from the x-axis.""" 

1823 return np.rad2deg(self._angles) 

1824 

1825 @artist.allow_rasterization 

1826 def draw(self, renderer): 

1827 self._set_transforms() 

1828 super().draw(renderer) 

1829 

1830 

1831class PatchCollection(Collection): 

1832 """ 

1833 A generic collection of patches. 

1834 

1835 PatchCollection draws faster than a large number of equivalent individual 

1836 Patches. It also makes it easier to assign a colormap to a heterogeneous 

1837 collection of patches. 

1838 """ 

1839 

1840 def __init__(self, patches, *, match_original=False, **kwargs): 

1841 """ 

1842 Parameters 

1843 ---------- 

1844 patches : list of `.Patch` 

1845 A sequence of Patch objects. This list may include 

1846 a heterogeneous assortment of different patch types. 

1847 

1848 match_original : bool, default: False 

1849 If True, use the colors and linewidths of the original 

1850 patches. If False, new colors may be assigned by 

1851 providing the standard collection arguments, facecolor, 

1852 edgecolor, linewidths, norm or cmap. 

1853 

1854 **kwargs 

1855 All other parameters are forwarded to `.Collection`. 

1856 

1857 If any of *edgecolors*, *facecolors*, *linewidths*, *antialiaseds* 

1858 are None, they default to their `.rcParams` patch setting, in 

1859 sequence form. 

1860 

1861 Notes 

1862 ----- 

1863 The use of `~matplotlib.cm.ScalarMappable` functionality is optional. 

1864 If the `~matplotlib.cm.ScalarMappable` matrix ``_A`` has been set (via 

1865 a call to `~.ScalarMappable.set_array`), at draw time a call to scalar 

1866 mappable will be made to set the face colors. 

1867 """ 

1868 

1869 if match_original: 

1870 def determine_facecolor(patch): 

1871 if patch.get_fill(): 

1872 return patch.get_facecolor() 

1873 return [0, 0, 0, 0] 

1874 

1875 kwargs['facecolors'] = [determine_facecolor(p) for p in patches] 

1876 kwargs['edgecolors'] = [p.get_edgecolor() for p in patches] 

1877 kwargs['linewidths'] = [p.get_linewidth() for p in patches] 

1878 kwargs['linestyles'] = [p.get_linestyle() for p in patches] 

1879 kwargs['antialiaseds'] = [p.get_antialiased() for p in patches] 

1880 

1881 super().__init__(**kwargs) 

1882 

1883 self.set_paths(patches) 

1884 

1885 def set_paths(self, patches): 

1886 paths = [p.get_transform().transform_path(p.get_path()) 

1887 for p in patches] 

1888 self._paths = paths 

1889 

1890 

1891class TriMesh(Collection): 

1892 """ 

1893 Class for the efficient drawing of a triangular mesh using Gouraud shading. 

1894 

1895 A triangular mesh is a `~matplotlib.tri.Triangulation` object. 

1896 """ 

1897 def __init__(self, triangulation, **kwargs): 

1898 super().__init__(**kwargs) 

1899 self._triangulation = triangulation 

1900 self._shading = 'gouraud' 

1901 

1902 self._bbox = transforms.Bbox.unit() 

1903 

1904 # Unfortunately this requires a copy, unless Triangulation 

1905 # was rewritten. 

1906 xy = np.hstack((triangulation.x.reshape(-1, 1), 

1907 triangulation.y.reshape(-1, 1))) 

1908 self._bbox.update_from_data_xy(xy) 

1909 

1910 def get_paths(self): 

1911 if self._paths is None: 

1912 self.set_paths() 

1913 return self._paths 

1914 

1915 def set_paths(self): 

1916 self._paths = self.convert_mesh_to_paths(self._triangulation) 

1917 

1918 @staticmethod 

1919 def convert_mesh_to_paths(tri): 

1920 """ 

1921 Convert a given mesh into a sequence of `.Path` objects. 

1922 

1923 This function is primarily of use to implementers of backends that do 

1924 not directly support meshes. 

1925 """ 

1926 triangles = tri.get_masked_triangles() 

1927 verts = np.stack((tri.x[triangles], tri.y[triangles]), axis=-1) 

1928 return [mpath.Path(x) for x in verts] 

1929 

1930 @artist.allow_rasterization 

1931 def draw(self, renderer): 

1932 if not self.get_visible(): 

1933 return 

1934 renderer.open_group(self.__class__.__name__, gid=self.get_gid()) 

1935 transform = self.get_transform() 

1936 

1937 # Get a list of triangles and the color at each vertex. 

1938 tri = self._triangulation 

1939 triangles = tri.get_masked_triangles() 

1940 

1941 verts = np.stack((tri.x[triangles], tri.y[triangles]), axis=-1) 

1942 

1943 self.update_scalarmappable() 

1944 colors = self._facecolors[triangles] 

1945 

1946 gc = renderer.new_gc() 

1947 self._set_gc_clip(gc) 

1948 gc.set_linewidth(self.get_linewidth()[0]) 

1949 renderer.draw_gouraud_triangles(gc, verts, colors, transform.frozen()) 

1950 gc.restore() 

1951 renderer.close_group(self.__class__.__name__) 

1952 

1953 

1954class _MeshData: 

1955 r""" 

1956 Class for managing the two dimensional coordinates of Quadrilateral meshes 

1957 and the associated data with them. This class is a mixin and is intended to 

1958 be used with another collection that will implement the draw separately. 

1959 

1960 A quadrilateral mesh is a grid of M by N adjacent quadrilaterals that are 

1961 defined via a (M+1, N+1) grid of vertices. The quadrilateral (m, n) is 

1962 defined by the vertices :: 

1963 

1964 (m+1, n) ----------- (m+1, n+1) 

1965 / / 

1966 / / 

1967 / / 

1968 (m, n) -------- (m, n+1) 

1969 

1970 The mesh need not be regular and the polygons need not be convex. 

1971 

1972 Parameters 

1973 ---------- 

1974 coordinates : (M+1, N+1, 2) array-like 

1975 The vertices. ``coordinates[m, n]`` specifies the (x, y) coordinates 

1976 of vertex (m, n). 

1977 

1978 shading : {'flat', 'gouraud'}, default: 'flat' 

1979 """ 

1980 def __init__(self, coordinates, *, shading='flat'): 

1981 _api.check_shape((None, None, 2), coordinates=coordinates) 

1982 self._coordinates = coordinates 

1983 self._shading = shading 

1984 

1985 def set_array(self, A): 

1986 """ 

1987 Set the data values. 

1988 

1989 Parameters 

1990 ---------- 

1991 A : array-like 

1992 The mesh data. Supported array shapes are: 

1993 

1994 - (M, N) or (M*N,): a mesh with scalar data. The values are mapped 

1995 to colors using normalization and a colormap. See parameters 

1996 *norm*, *cmap*, *vmin*, *vmax*. 

1997 - (M, N, 3): an image with RGB values (0-1 float or 0-255 int). 

1998 - (M, N, 4): an image with RGBA values (0-1 float or 0-255 int), 

1999 i.e. including transparency. 

2000 

2001 If the values are provided as a 2D grid, the shape must match the 

2002 coordinates grid. If the values are 1D, they are reshaped to 2D. 

2003 M, N follow from the coordinates grid, where the coordinates grid 

2004 shape is (M, N) for 'gouraud' *shading* and (M+1, N+1) for 'flat' 

2005 shading. 

2006 """ 

2007 height, width = self._coordinates.shape[0:-1] 

2008 if self._shading == 'flat': 

2009 h, w = height - 1, width - 1 

2010 else: 

2011 h, w = height, width 

2012 ok_shapes = [(h, w, 3), (h, w, 4), (h, w), (h * w,)] 

2013 if A is not None: 

2014 shape = np.shape(A) 

2015 if shape not in ok_shapes: 

2016 raise ValueError( 

2017 f"For X ({width}) and Y ({height}) with {self._shading} " 

2018 f"shading, A should have shape " 

2019 f"{' or '.join(map(str, ok_shapes))}, not {A.shape}") 

2020 return super().set_array(A) 

2021 

2022 def get_coordinates(self): 

2023 """ 

2024 Return the vertices of the mesh as an (M+1, N+1, 2) array. 

2025 

2026 M, N are the number of quadrilaterals in the rows / columns of the 

2027 mesh, corresponding to (M+1, N+1) vertices. 

2028 The last dimension specifies the components (x, y). 

2029 """ 

2030 return self._coordinates 

2031 

2032 def get_edgecolor(self): 

2033 # docstring inherited 

2034 # Note that we want to return an array of shape (N*M, 4) 

2035 # a flattened RGBA collection 

2036 return super().get_edgecolor().reshape(-1, 4) 

2037 

2038 def get_facecolor(self): 

2039 # docstring inherited 

2040 # Note that we want to return an array of shape (N*M, 4) 

2041 # a flattened RGBA collection 

2042 return super().get_facecolor().reshape(-1, 4) 

2043 

2044 @staticmethod 

2045 def _convert_mesh_to_paths(coordinates): 

2046 """ 

2047 Convert a given mesh into a sequence of `.Path` objects. 

2048 

2049 This function is primarily of use to implementers of backends that do 

2050 not directly support quadmeshes. 

2051 """ 

2052 if isinstance(coordinates, np.ma.MaskedArray): 

2053 c = coordinates.data 

2054 else: 

2055 c = coordinates 

2056 points = np.concatenate([ 

2057 c[:-1, :-1], 

2058 c[:-1, 1:], 

2059 c[1:, 1:], 

2060 c[1:, :-1], 

2061 c[:-1, :-1] 

2062 ], axis=2).reshape((-1, 5, 2)) 

2063 return [mpath.Path(x) for x in points] 

2064 

2065 def _convert_mesh_to_triangles(self, coordinates): 

2066 """ 

2067 Convert a given mesh into a sequence of triangles, each point 

2068 with its own color. The result can be used to construct a call to 

2069 `~.RendererBase.draw_gouraud_triangles`. 

2070 """ 

2071 if isinstance(coordinates, np.ma.MaskedArray): 

2072 p = coordinates.data 

2073 else: 

2074 p = coordinates 

2075 

2076 p_a = p[:-1, :-1] 

2077 p_b = p[:-1, 1:] 

2078 p_c = p[1:, 1:] 

2079 p_d = p[1:, :-1] 

2080 p_center = (p_a + p_b + p_c + p_d) / 4.0 

2081 triangles = np.concatenate([ 

2082 p_a, p_b, p_center, 

2083 p_b, p_c, p_center, 

2084 p_c, p_d, p_center, 

2085 p_d, p_a, p_center, 

2086 ], axis=2).reshape((-1, 3, 2)) 

2087 

2088 c = self.get_facecolor().reshape((*coordinates.shape[:2], 4)) 

2089 z = self.get_array() 

2090 mask = z.mask if np.ma.is_masked(z) else None 

2091 if mask is not None: 

2092 c[mask, 3] = np.nan 

2093 c_a = c[:-1, :-1] 

2094 c_b = c[:-1, 1:] 

2095 c_c = c[1:, 1:] 

2096 c_d = c[1:, :-1] 

2097 c_center = (c_a + c_b + c_c + c_d) / 4.0 

2098 colors = np.concatenate([ 

2099 c_a, c_b, c_center, 

2100 c_b, c_c, c_center, 

2101 c_c, c_d, c_center, 

2102 c_d, c_a, c_center, 

2103 ], axis=2).reshape((-1, 3, 4)) 

2104 tmask = np.isnan(colors[..., 2, 3]) 

2105 return triangles[~tmask], colors[~tmask] 

2106 

2107 

2108class QuadMesh(_MeshData, Collection): 

2109 r""" 

2110 Class for the efficient drawing of a quadrilateral mesh. 

2111 

2112 A quadrilateral mesh is a grid of M by N adjacent quadrilaterals that are 

2113 defined via a (M+1, N+1) grid of vertices. The quadrilateral (m, n) is 

2114 defined by the vertices :: 

2115 

2116 (m+1, n) ----------- (m+1, n+1) 

2117 / / 

2118 / / 

2119 / / 

2120 (m, n) -------- (m, n+1) 

2121 

2122 The mesh need not be regular and the polygons need not be convex. 

2123 

2124 Parameters 

2125 ---------- 

2126 coordinates : (M+1, N+1, 2) array-like 

2127 The vertices. ``coordinates[m, n]`` specifies the (x, y) coordinates 

2128 of vertex (m, n). 

2129 

2130 antialiased : bool, default: True 

2131 

2132 shading : {'flat', 'gouraud'}, default: 'flat' 

2133 

2134 Notes 

2135 ----- 

2136 Unlike other `.Collection`\s, the default *pickradius* of `.QuadMesh` is 0, 

2137 i.e. `~.Artist.contains` checks whether the test point is within any of the 

2138 mesh quadrilaterals. 

2139 

2140 """ 

2141 

2142 def __init__(self, coordinates, *, antialiased=True, shading='flat', 

2143 **kwargs): 

2144 kwargs.setdefault("pickradius", 0) 

2145 super().__init__(coordinates=coordinates, shading=shading) 

2146 Collection.__init__(self, **kwargs) 

2147 

2148 self._antialiased = antialiased 

2149 self._bbox = transforms.Bbox.unit() 

2150 self._bbox.update_from_data_xy(self._coordinates.reshape(-1, 2)) 

2151 self.set_mouseover(False) 

2152 

2153 def get_paths(self): 

2154 if self._paths is None: 

2155 self.set_paths() 

2156 return self._paths 

2157 

2158 def set_paths(self): 

2159 self._paths = self._convert_mesh_to_paths(self._coordinates) 

2160 self.stale = True 

2161 

2162 def get_datalim(self, transData): 

2163 return (self.get_transform() - transData).transform_bbox(self._bbox) 

2164 

2165 @artist.allow_rasterization 

2166 def draw(self, renderer): 

2167 if not self.get_visible(): 

2168 return 

2169 renderer.open_group(self.__class__.__name__, self.get_gid()) 

2170 transform = self.get_transform() 

2171 offset_trf = self.get_offset_transform() 

2172 offsets = self.get_offsets() 

2173 

2174 if self.have_units(): 

2175 xs = self.convert_xunits(offsets[:, 0]) 

2176 ys = self.convert_yunits(offsets[:, 1]) 

2177 offsets = np.column_stack([xs, ys]) 

2178 

2179 self.update_scalarmappable() 

2180 

2181 if not transform.is_affine: 

2182 coordinates = self._coordinates.reshape((-1, 2)) 

2183 coordinates = transform.transform(coordinates) 

2184 coordinates = coordinates.reshape(self._coordinates.shape) 

2185 transform = transforms.IdentityTransform() 

2186 else: 

2187 coordinates = self._coordinates 

2188 

2189 if not offset_trf.is_affine: 

2190 offsets = offset_trf.transform_non_affine(offsets) 

2191 offset_trf = offset_trf.get_affine() 

2192 

2193 gc = renderer.new_gc() 

2194 gc.set_snap(self.get_snap()) 

2195 self._set_gc_clip(gc) 

2196 gc.set_linewidth(self.get_linewidth()[0]) 

2197 

2198 if self._shading == 'gouraud': 

2199 triangles, colors = self._convert_mesh_to_triangles(coordinates) 

2200 renderer.draw_gouraud_triangles( 

2201 gc, triangles, colors, transform.frozen()) 

2202 else: 

2203 renderer.draw_quad_mesh( 

2204 gc, transform.frozen(), 

2205 coordinates.shape[1] - 1, coordinates.shape[0] - 1, 

2206 coordinates, offsets, offset_trf, 

2207 # Backends expect flattened rgba arrays (n*m, 4) for fc and ec 

2208 self.get_facecolor().reshape((-1, 4)), 

2209 self._antialiased, self.get_edgecolors().reshape((-1, 4))) 

2210 gc.restore() 

2211 renderer.close_group(self.__class__.__name__) 

2212 self.stale = False 

2213 

2214 def get_cursor_data(self, event): 

2215 contained, info = self.contains(event) 

2216 if contained and self.get_array() is not None: 

2217 return self.get_array().ravel()[info["ind"]] 

2218 return None 

2219 

2220 

2221class PolyQuadMesh(_MeshData, PolyCollection): 

2222 """ 

2223 Class for drawing a quadrilateral mesh as individual Polygons. 

2224 

2225 A quadrilateral mesh is a grid of M by N adjacent quadrilaterals that are 

2226 defined via a (M+1, N+1) grid of vertices. The quadrilateral (m, n) is 

2227 defined by the vertices :: 

2228 

2229 (m+1, n) ----------- (m+1, n+1) 

2230 / / 

2231 / / 

2232 / / 

2233 (m, n) -------- (m, n+1) 

2234 

2235 The mesh need not be regular and the polygons need not be convex. 

2236 

2237 Parameters 

2238 ---------- 

2239 coordinates : (M+1, N+1, 2) array-like 

2240 The vertices. ``coordinates[m, n]`` specifies the (x, y) coordinates 

2241 of vertex (m, n). 

2242 

2243 Notes 

2244 ----- 

2245 Unlike `.QuadMesh`, this class will draw each cell as an individual Polygon. 

2246 This is significantly slower, but allows for more flexibility when wanting 

2247 to add additional properties to the cells, such as hatching. 

2248 

2249 Another difference from `.QuadMesh` is that if any of the vertices or data 

2250 of a cell are masked, that Polygon will **not** be drawn and it won't be in 

2251 the list of paths returned. 

2252 """ 

2253 

2254 def __init__(self, coordinates, **kwargs): 

2255 # We need to keep track of whether we are using deprecated compression 

2256 # Update it after the initializers 

2257 self._deprecated_compression = False 

2258 super().__init__(coordinates=coordinates) 

2259 PolyCollection.__init__(self, verts=[], **kwargs) 

2260 # Store this during the compression deprecation period 

2261 self._original_mask = ~self._get_unmasked_polys() 

2262 self._deprecated_compression = np.any(self._original_mask) 

2263 # Setting the verts updates the paths of the PolyCollection 

2264 # This is called after the initializers to make sure the kwargs 

2265 # have all been processed and available for the masking calculations 

2266 self._set_unmasked_verts() 

2267 

2268 def _get_unmasked_polys(self): 

2269 """Get the unmasked regions using the coordinates and array""" 

2270 # mask(X) | mask(Y) 

2271 mask = np.any(np.ma.getmaskarray(self._coordinates), axis=-1) 

2272 

2273 # We want the shape of the polygon, which is the corner of each X/Y array 

2274 mask = (mask[0:-1, 0:-1] | mask[1:, 1:] | mask[0:-1, 1:] | mask[1:, 0:-1]) 

2275 

2276 if (getattr(self, "_deprecated_compression", False) and 

2277 np.any(self._original_mask)): 

2278 return ~(mask | self._original_mask) 

2279 # Take account of the array data too, temporarily avoiding 

2280 # the compression warning and resetting the variable after the call 

2281 with cbook._setattr_cm(self, _deprecated_compression=False): 

2282 arr = self.get_array() 

2283 if arr is not None: 

2284 arr = np.ma.getmaskarray(arr) 

2285 if arr.ndim == 3: 

2286 # RGB(A) case 

2287 mask |= np.any(arr, axis=-1) 

2288 elif arr.ndim == 2: 

2289 mask |= arr 

2290 else: 

2291 mask |= arr.reshape(self._coordinates[:-1, :-1, :].shape[:2]) 

2292 return ~mask 

2293 

2294 def _set_unmasked_verts(self): 

2295 X = self._coordinates[..., 0] 

2296 Y = self._coordinates[..., 1] 

2297 

2298 unmask = self._get_unmasked_polys() 

2299 X1 = np.ma.filled(X[:-1, :-1])[unmask] 

2300 Y1 = np.ma.filled(Y[:-1, :-1])[unmask] 

2301 X2 = np.ma.filled(X[1:, :-1])[unmask] 

2302 Y2 = np.ma.filled(Y[1:, :-1])[unmask] 

2303 X3 = np.ma.filled(X[1:, 1:])[unmask] 

2304 Y3 = np.ma.filled(Y[1:, 1:])[unmask] 

2305 X4 = np.ma.filled(X[:-1, 1:])[unmask] 

2306 Y4 = np.ma.filled(Y[:-1, 1:])[unmask] 

2307 npoly = len(X1) 

2308 

2309 xy = np.ma.stack([X1, Y1, X2, Y2, X3, Y3, X4, Y4, X1, Y1], axis=-1) 

2310 verts = xy.reshape((npoly, 5, 2)) 

2311 self.set_verts(verts) 

2312 

2313 def get_edgecolor(self): 

2314 # docstring inherited 

2315 # We only want to return the facecolors of the polygons 

2316 # that were drawn. 

2317 ec = super().get_edgecolor() 

2318 unmasked_polys = self._get_unmasked_polys().ravel() 

2319 if len(ec) != len(unmasked_polys): 

2320 # Mapping is off 

2321 return ec 

2322 return ec[unmasked_polys, :] 

2323 

2324 def get_facecolor(self): 

2325 # docstring inherited 

2326 # We only want to return the facecolors of the polygons 

2327 # that were drawn. 

2328 fc = super().get_facecolor() 

2329 unmasked_polys = self._get_unmasked_polys().ravel() 

2330 if len(fc) != len(unmasked_polys): 

2331 # Mapping is off 

2332 return fc 

2333 return fc[unmasked_polys, :] 

2334 

2335 def set_array(self, A): 

2336 # docstring inherited 

2337 prev_unmask = self._get_unmasked_polys() 

2338 # MPL <3.8 compressed the mask, so we need to handle flattened 1d input 

2339 # until the deprecation expires, also only warning when there are masked 

2340 # elements and thus compression occurring. 

2341 if self._deprecated_compression and np.ndim(A) == 1: 

2342 _api.warn_deprecated("3.8", message="Setting a PolyQuadMesh array using " 

2343 "the compressed values is deprecated. " 

2344 "Pass the full 2D shape of the original array " 

2345 f"{prev_unmask.shape} including the masked elements.") 

2346 Afull = np.empty(self._original_mask.shape) 

2347 Afull[~self._original_mask] = A 

2348 # We also want to update the mask with any potential 

2349 # new masked elements that came in. But, we don't want 

2350 # to update any of the compression from the original 

2351 mask = self._original_mask.copy() 

2352 mask[~self._original_mask] |= np.ma.getmask(A) 

2353 A = np.ma.array(Afull, mask=mask) 

2354 return super().set_array(A) 

2355 self._deprecated_compression = False 

2356 super().set_array(A) 

2357 # If the mask has changed at all we need to update 

2358 # the set of Polys that we are drawing 

2359 if not np.array_equal(prev_unmask, self._get_unmasked_polys()): 

2360 self._set_unmasked_verts() 

2361 

2362 def get_array(self): 

2363 # docstring inherited 

2364 # Can remove this entire function once the deprecation period ends 

2365 A = super().get_array() 

2366 if A is None: 

2367 return 

2368 if self._deprecated_compression and np.any(np.ma.getmask(A)): 

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

2370 "Getting the array from a PolyQuadMesh will return the full " 

2371 "array in the future (uncompressed). To get this behavior now " 

2372 "set the PolyQuadMesh with a 2D array .set_array(data2d).")) 

2373 # Setting an array of a polycollection required 

2374 # compressing the array 

2375 return np.ma.compressed(A) 

2376 return A