Coverage for /usr/lib/python3/dist-packages/matplotlib/contour.py: 10%

707 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-06-14 15:25 +0200

1""" 

2Classes to support contour plotting and labelling for the Axes class. 

3""" 

4 

5import functools 

6from numbers import Integral 

7 

8import numpy as np 

9from numpy import ma 

10 

11import matplotlib as mpl 

12from matplotlib import _api, _docstring 

13from matplotlib.backend_bases import MouseButton 

14import matplotlib.path as mpath 

15import matplotlib.ticker as ticker 

16import matplotlib.cm as cm 

17import matplotlib.colors as mcolors 

18import matplotlib.collections as mcoll 

19import matplotlib.font_manager as font_manager 

20import matplotlib.text as text 

21import matplotlib.cbook as cbook 

22import matplotlib.patches as mpatches 

23import matplotlib.transforms as mtransforms 

24 

25 

26# We can't use a single line collection for contour because a line 

27# collection can have only a single line style, and we want to be able to have 

28# dashed negative contours, for example, and solid positive contours. 

29# We could use a single polygon collection for filled contours, but it 

30# seems better to keep line and filled contours similar, with one collection 

31# per level. 

32 

33 

34class ClabelText(text.Text): 

35 """ 

36 Unlike the ordinary text, the get_rotation returns an updated 

37 angle in the pixel coordinate assuming that the input rotation is 

38 an angle in data coordinate (or whatever transform set). 

39 """ 

40 

41 def get_rotation(self): 

42 new_angle, = self.get_transform().transform_angles( 

43 [super().get_rotation()], [self.get_position()]) 

44 return new_angle 

45 

46 

47def _contour_labeler_event_handler(cs, inline, inline_spacing, event): 

48 canvas = cs.axes.figure.canvas 

49 is_button = event.name == "button_press_event" 

50 is_key = event.name == "key_press_event" 

51 # Quit (even if not in infinite mode; this is consistent with 

52 # MATLAB and sometimes quite useful, but will require the user to 

53 # test how many points were actually returned before using data). 

54 if (is_button and event.button == MouseButton.MIDDLE 

55 or is_key and event.key in ["escape", "enter"]): 

56 canvas.stop_event_loop() 

57 # Pop last click. 

58 elif (is_button and event.button == MouseButton.RIGHT 

59 or is_key and event.key in ["backspace", "delete"]): 

60 # Unfortunately, if one is doing inline labels, then there is currently 

61 # no way to fix the broken contour - once humpty-dumpty is broken, he 

62 # can't be put back together. In inline mode, this does nothing. 

63 if not inline: 

64 cs.pop_label() 

65 canvas.draw() 

66 # Add new click. 

67 elif (is_button and event.button == MouseButton.LEFT 

68 # On macOS/gtk, some keys return None. 

69 or is_key and event.key is not None): 

70 if event.inaxes == cs.axes: 

71 cs.add_label_near(event.x, event.y, transform=False, 

72 inline=inline, inline_spacing=inline_spacing) 

73 canvas.draw() 

74 

75 

76class ContourLabeler: 

77 """Mixin to provide labelling capability to `.ContourSet`.""" 

78 

79 def clabel(self, levels=None, *, 

80 fontsize=None, inline=True, inline_spacing=5, fmt=None, 

81 colors=None, use_clabeltext=False, manual=False, 

82 rightside_up=True, zorder=None): 

83 """ 

84 Label a contour plot. 

85 

86 Adds labels to line contours in this `.ContourSet` (which inherits from 

87 this mixin class). 

88 

89 Parameters 

90 ---------- 

91 levels : array-like, optional 

92 A list of level values, that should be labeled. The list must be 

93 a subset of ``cs.levels``. If not given, all levels are labeled. 

94 

95 fontsize : str or float, default: :rc:`font.size` 

96 Size in points or relative size e.g., 'smaller', 'x-large'. 

97 See `.Text.set_size` for accepted string values. 

98 

99 colors : color or colors or None, default: None 

100 The label colors: 

101 

102 - If *None*, the color of each label matches the color of 

103 the corresponding contour. 

104 

105 - If one string color, e.g., *colors* = 'r' or *colors* = 

106 'red', all labels will be plotted in this color. 

107 

108 - If a tuple of colors (string, float, rgb, etc), different labels 

109 will be plotted in different colors in the order specified. 

110 

111 inline : bool, default: True 

112 If ``True`` the underlying contour is removed where the label is 

113 placed. 

114 

115 inline_spacing : float, default: 5 

116 Space in pixels to leave on each side of label when placing inline. 

117 

118 This spacing will be exact for labels at locations where the 

119 contour is straight, less so for labels on curved contours. 

120 

121 fmt : `.Formatter` or str or callable or dict, optional 

122 How the levels are formatted: 

123 

124 - If a `.Formatter`, it is used to format all levels at once, using 

125 its `.Formatter.format_ticks` method. 

126 - If a str, it is interpreted as a %-style format string. 

127 - If a callable, it is called with one level at a time and should 

128 return the corresponding label. 

129 - If a dict, it should directly map levels to labels. 

130 

131 The default is to use a standard `.ScalarFormatter`. 

132 

133 manual : bool or iterable, default: False 

134 If ``True``, contour labels will be placed manually using 

135 mouse clicks. Click the first button near a contour to 

136 add a label, click the second button (or potentially both 

137 mouse buttons at once) to finish adding labels. The third 

138 button can be used to remove the last label added, but 

139 only if labels are not inline. Alternatively, the keyboard 

140 can be used to select label locations (enter to end label 

141 placement, delete or backspace act like the third mouse button, 

142 and any other key will select a label location). 

143 

144 *manual* can also be an iterable object of (x, y) tuples. 

145 Contour labels will be created as if mouse is clicked at each 

146 (x, y) position. 

147 

148 rightside_up : bool, default: True 

149 If ``True``, label rotations will always be plus 

150 or minus 90 degrees from level. 

151 

152 use_clabeltext : bool, default: False 

153 If ``True``, `.ClabelText` class (instead of `.Text`) is used to 

154 create labels. `ClabelText` recalculates rotation angles 

155 of texts during the drawing time, therefore this can be used if 

156 aspect of the axes changes. 

157 

158 zorder : float or None, default: ``(2 + contour.get_zorder())`` 

159 zorder of the contour labels. 

160 

161 Returns 

162 ------- 

163 labels 

164 A list of `.Text` instances for the labels. 

165 """ 

166 

167 # clabel basically takes the input arguments and uses them to 

168 # add a list of "label specific" attributes to the ContourSet 

169 # object. These attributes are all of the form label* and names 

170 # should be fairly self explanatory. 

171 # 

172 # Once these attributes are set, clabel passes control to the 

173 # labels method (case of automatic label placement) or 

174 # `BlockingContourLabeler` (case of manual label placement). 

175 

176 if fmt is None: 

177 fmt = ticker.ScalarFormatter(useOffset=False) 

178 fmt.create_dummy_axis() 

179 self.labelFmt = fmt 

180 self._use_clabeltext = use_clabeltext 

181 # Detect if manual selection is desired and remove from argument list. 

182 self.labelManual = manual 

183 self.rightside_up = rightside_up 

184 if zorder is None: 

185 self._clabel_zorder = 2+self._contour_zorder 

186 else: 

187 self._clabel_zorder = zorder 

188 

189 if levels is None: 

190 levels = self.levels 

191 indices = list(range(len(self.cvalues))) 

192 else: 

193 levlabs = list(levels) 

194 indices, levels = [], [] 

195 for i, lev in enumerate(self.levels): 

196 if lev in levlabs: 

197 indices.append(i) 

198 levels.append(lev) 

199 if len(levels) < len(levlabs): 

200 raise ValueError(f"Specified levels {levlabs} don't match " 

201 f"available levels {self.levels}") 

202 self.labelLevelList = levels 

203 self.labelIndiceList = indices 

204 

205 self.labelFontProps = font_manager.FontProperties() 

206 self.labelFontProps.set_size(fontsize) 

207 font_size_pts = self.labelFontProps.get_size_in_points() 

208 self.labelFontSizeList = [font_size_pts] * len(levels) 

209 

210 if colors is None: 

211 self.labelMappable = self 

212 self.labelCValueList = np.take(self.cvalues, self.labelIndiceList) 

213 else: 

214 cmap = mcolors.ListedColormap(colors, N=len(self.labelLevelList)) 

215 self.labelCValueList = list(range(len(self.labelLevelList))) 

216 self.labelMappable = cm.ScalarMappable(cmap=cmap, 

217 norm=mcolors.NoNorm()) 

218 

219 self.labelXYs = [] 

220 

221 if np.iterable(self.labelManual): 

222 for x, y in self.labelManual: 

223 self.add_label_near(x, y, inline, inline_spacing) 

224 elif self.labelManual: 

225 print('Select label locations manually using first mouse button.') 

226 print('End manual selection with second mouse button.') 

227 if not inline: 

228 print('Remove last label by clicking third mouse button.') 

229 mpl._blocking_input.blocking_input_loop( 

230 self.axes.figure, ["button_press_event", "key_press_event"], 

231 timeout=-1, handler=functools.partial( 

232 _contour_labeler_event_handler, 

233 self, inline, inline_spacing)) 

234 else: 

235 self.labels(inline, inline_spacing) 

236 

237 self.labelTextsList = cbook.silent_list('text.Text', self.labelTexts) 

238 return self.labelTextsList 

239 

240 def print_label(self, linecontour, labelwidth): 

241 """Return whether a contour is long enough to hold a label.""" 

242 return (len(linecontour) > 10 * labelwidth 

243 or (np.ptp(linecontour, axis=0) > 1.2 * labelwidth).any()) 

244 

245 def too_close(self, x, y, lw): 

246 """Return whether a label is already near this location.""" 

247 thresh = (1.2 * lw) ** 2 

248 return any((x - loc[0]) ** 2 + (y - loc[1]) ** 2 < thresh 

249 for loc in self.labelXYs) 

250 

251 def _get_nth_label_width(self, nth): 

252 """Return the width of the *nth* label, in pixels.""" 

253 fig = self.axes.figure 

254 renderer = fig._get_renderer() 

255 return ( 

256 text.Text(0, 0, 

257 self.get_text(self.labelLevelList[nth], self.labelFmt), 

258 figure=fig, 

259 size=self.labelFontSizeList[nth], 

260 fontproperties=self.labelFontProps) 

261 .get_window_extent(renderer).width) 

262 

263 @_api.deprecated("3.5") 

264 def get_label_width(self, lev, fmt, fsize): 

265 """Return the width of the label in points.""" 

266 if not isinstance(lev, str): 

267 lev = self.get_text(lev, fmt) 

268 fig = self.axes.figure 

269 renderer = fig._get_renderer() 

270 width = (text.Text(0, 0, lev, figure=fig, 

271 size=fsize, fontproperties=self.labelFontProps) 

272 .get_window_extent(renderer).width) 

273 width *= 72 / fig.dpi 

274 return width 

275 

276 def set_label_props(self, label, text, color): 

277 """Set the label properties - color, fontsize, text.""" 

278 label.set_text(text) 

279 label.set_color(color) 

280 label.set_fontproperties(self.labelFontProps) 

281 label.set_clip_box(self.axes.bbox) 

282 

283 def get_text(self, lev, fmt): 

284 """Get the text of the label.""" 

285 if isinstance(lev, str): 

286 return lev 

287 elif isinstance(fmt, dict): 

288 return fmt.get(lev, '%1.3f') 

289 elif callable(getattr(fmt, "format_ticks", None)): 

290 return fmt.format_ticks([*self.labelLevelList, lev])[-1] 

291 elif callable(fmt): 

292 return fmt(lev) 

293 else: 

294 return fmt % lev 

295 

296 def locate_label(self, linecontour, labelwidth): 

297 """ 

298 Find good place to draw a label (relatively flat part of the contour). 

299 """ 

300 ctr_size = len(linecontour) 

301 n_blocks = int(np.ceil(ctr_size / labelwidth)) if labelwidth > 1 else 1 

302 block_size = ctr_size if n_blocks == 1 else int(labelwidth) 

303 # Split contour into blocks of length ``block_size``, filling the last 

304 # block by cycling the contour start (per `np.resize` semantics). (Due 

305 # to cycling, the index returned is taken modulo ctr_size.) 

306 xx = np.resize(linecontour[:, 0], (n_blocks, block_size)) 

307 yy = np.resize(linecontour[:, 1], (n_blocks, block_size)) 

308 yfirst = yy[:, :1] 

309 ylast = yy[:, -1:] 

310 xfirst = xx[:, :1] 

311 xlast = xx[:, -1:] 

312 s = (yfirst - yy) * (xlast - xfirst) - (xfirst - xx) * (ylast - yfirst) 

313 l = np.hypot(xlast - xfirst, ylast - yfirst) 

314 # Ignore warning that divide by zero throws, as this is a valid option 

315 with np.errstate(divide='ignore', invalid='ignore'): 

316 distances = (abs(s) / l).sum(axis=-1) 

317 # Labels are drawn in the middle of the block (``hbsize``) where the 

318 # contour is the closest (per ``distances``) to a straight line, but 

319 # not `too_close()` to a preexisting label. 

320 hbsize = block_size // 2 

321 adist = np.argsort(distances) 

322 # If all candidates are `too_close()`, go back to the straightest part 

323 # (``adist[0]``). 

324 for idx in np.append(adist, adist[0]): 

325 x, y = xx[idx, hbsize], yy[idx, hbsize] 

326 if not self.too_close(x, y, labelwidth): 

327 break 

328 return x, y, (idx * block_size + hbsize) % ctr_size 

329 

330 def calc_label_rot_and_inline(self, slc, ind, lw, lc=None, spacing=5): 

331 """ 

332 Calculate the appropriate label rotation given the linecontour 

333 coordinates in screen units, the index of the label location and the 

334 label width. 

335 

336 If *lc* is not None or empty, also break contours and compute 

337 inlining. 

338 

339 *spacing* is the empty space to leave around the label, in pixels. 

340 

341 Both tasks are done together to avoid calculating path lengths 

342 multiple times, which is relatively costly. 

343 

344 The method used here involves computing the path length along the 

345 contour in pixel coordinates and then looking approximately (label 

346 width / 2) away from central point to determine rotation and then to 

347 break contour if desired. 

348 """ 

349 

350 if lc is None: 

351 lc = [] 

352 # Half the label width 

353 hlw = lw / 2.0 

354 

355 # Check if closed and, if so, rotate contour so label is at edge 

356 closed = _is_closed_polygon(slc) 

357 if closed: 

358 slc = np.concatenate([slc[ind:-1], slc[:ind + 1]]) 

359 if len(lc): # Rotate lc also if not empty 

360 lc = np.concatenate([lc[ind:-1], lc[:ind + 1]]) 

361 ind = 0 

362 

363 # Calculate path lengths 

364 pl = np.zeros(slc.shape[0], dtype=float) 

365 dx = np.diff(slc, axis=0) 

366 pl[1:] = np.cumsum(np.hypot(dx[:, 0], dx[:, 1])) 

367 pl = pl - pl[ind] 

368 

369 # Use linear interpolation to get points around label 

370 xi = np.array([-hlw, hlw]) 

371 if closed: # Look at end also for closed contours 

372 dp = np.array([pl[-1], 0]) 

373 else: 

374 dp = np.zeros_like(xi) 

375 

376 # Get angle of vector between the two ends of the label - must be 

377 # calculated in pixel space for text rotation to work correctly. 

378 (dx,), (dy,) = (np.diff(np.interp(dp + xi, pl, slc_col)) 

379 for slc_col in slc.T) 

380 rotation = np.rad2deg(np.arctan2(dy, dx)) 

381 

382 if self.rightside_up: 

383 # Fix angle so text is never upside-down 

384 rotation = (rotation + 90) % 180 - 90 

385 

386 # Break contour if desired 

387 nlc = [] 

388 if len(lc): 

389 # Expand range by spacing 

390 xi = dp + xi + np.array([-spacing, spacing]) 

391 

392 # Get (integer) indices near points of interest; use -1 as marker 

393 # for out of bounds. 

394 I = np.interp(xi, pl, np.arange(len(pl)), left=-1, right=-1) 

395 I = [np.floor(I[0]).astype(int), np.ceil(I[1]).astype(int)] 

396 if I[0] != -1: 

397 xy1 = [np.interp(xi[0], pl, lc_col) for lc_col in lc.T] 

398 if I[1] != -1: 

399 xy2 = [np.interp(xi[1], pl, lc_col) for lc_col in lc.T] 

400 

401 # Actually break contours 

402 if closed: 

403 # This will remove contour if shorter than label 

404 if all(i != -1 for i in I): 

405 nlc.append(np.row_stack([xy2, lc[I[1]:I[0]+1], xy1])) 

406 else: 

407 # These will remove pieces of contour if they have length zero 

408 if I[0] != -1: 

409 nlc.append(np.row_stack([lc[:I[0]+1], xy1])) 

410 if I[1] != -1: 

411 nlc.append(np.row_stack([xy2, lc[I[1]:]])) 

412 

413 # The current implementation removes contours completely 

414 # covered by labels. Uncomment line below to keep 

415 # original contour if this is the preferred behavior. 

416 # if not len(nlc): nlc = [ lc ] 

417 

418 return rotation, nlc 

419 

420 def _get_label_text(self, x, y, rotation): 

421 dx, dy = self.axes.transData.inverted().transform((x, y)) 

422 t = text.Text(dx, dy, rotation=rotation, 

423 horizontalalignment='center', 

424 verticalalignment='center', zorder=self._clabel_zorder) 

425 return t 

426 

427 def _get_label_clabeltext(self, x, y, rotation): 

428 # x, y, rotation is given in pixel coordinate. Convert them to 

429 # the data coordinate and create a label using ClabelText 

430 # class. This way, the rotation of the clabel is along the 

431 # contour line always. 

432 transDataInv = self.axes.transData.inverted() 

433 dx, dy = transDataInv.transform((x, y)) 

434 drotation = transDataInv.transform_angles(np.array([rotation]), 

435 np.array([[x, y]])) 

436 t = ClabelText(dx, dy, rotation=drotation[0], 

437 horizontalalignment='center', 

438 verticalalignment='center', zorder=self._clabel_zorder) 

439 

440 return t 

441 

442 def _add_label(self, t, x, y, lev, cvalue): 

443 color = self.labelMappable.to_rgba(cvalue, alpha=self.alpha) 

444 

445 _text = self.get_text(lev, self.labelFmt) 

446 self.set_label_props(t, _text, color) 

447 self.labelTexts.append(t) 

448 self.labelCValues.append(cvalue) 

449 self.labelXYs.append((x, y)) 

450 

451 # Add label to plot here - useful for manual mode label selection 

452 self.axes.add_artist(t) 

453 

454 def add_label(self, x, y, rotation, lev, cvalue): 

455 """ 

456 Add contour label using :class:`~matplotlib.text.Text` class. 

457 """ 

458 t = self._get_label_text(x, y, rotation) 

459 self._add_label(t, x, y, lev, cvalue) 

460 

461 def add_label_clabeltext(self, x, y, rotation, lev, cvalue): 

462 """ 

463 Add contour label using :class:`ClabelText` class. 

464 """ 

465 # x, y, rotation is given in pixel coordinate. Convert them to 

466 # the data coordinate and create a label using ClabelText 

467 # class. This way, the rotation of the clabel is along the 

468 # contour line always. 

469 t = self._get_label_clabeltext(x, y, rotation) 

470 self._add_label(t, x, y, lev, cvalue) 

471 

472 def add_label_near(self, x, y, inline=True, inline_spacing=5, 

473 transform=None): 

474 """ 

475 Add a label near the point ``(x, y)``. 

476 

477 Parameters 

478 ---------- 

479 x, y : float 

480 The approximate location of the label. 

481 inline : bool, default: True 

482 If *True* remove the segment of the contour beneath the label. 

483 inline_spacing : int, default: 5 

484 Space in pixels to leave on each side of label when placing 

485 inline. This spacing will be exact for labels at locations where 

486 the contour is straight, less so for labels on curved contours. 

487 transform : `.Transform` or `False`, default: ``self.axes.transData`` 

488 A transform applied to ``(x, y)`` before labeling. The default 

489 causes ``(x, y)`` to be interpreted as data coordinates. `False` 

490 is a synonym for `.IdentityTransform`; i.e. ``(x, y)`` should be 

491 interpreted as display coordinates. 

492 """ 

493 

494 if transform is None: 

495 transform = self.axes.transData 

496 if transform: 

497 x, y = transform.transform((x, y)) 

498 

499 # find the nearest contour _in screen units_ 

500 conmin, segmin, imin, xmin, ymin = self.find_nearest_contour( 

501 x, y, self.labelIndiceList)[:5] 

502 

503 # calc_label_rot_and_inline() requires that (xmin, ymin) 

504 # be a vertex in the path. So, if it isn't, add a vertex here 

505 paths = self.collections[conmin].get_paths() # paths of correct coll. 

506 lc = paths[segmin].vertices # vertices of correct segment 

507 # Where should the new vertex be added in data-units? 

508 xcmin = self.axes.transData.inverted().transform([xmin, ymin]) 

509 if not np.allclose(xcmin, lc[imin]): 

510 # No vertex is close enough, so add a new point in the vertices and 

511 # replace the path by the new one. 

512 lc = np.insert(lc, imin, xcmin, axis=0) 

513 paths[segmin] = mpath.Path(lc) 

514 

515 # Get index of nearest level in subset of levels used for labeling 

516 lmin = self.labelIndiceList.index(conmin) 

517 

518 # Get label width for rotating labels and breaking contours 

519 lw = self._get_nth_label_width(lmin) 

520 

521 # Figure out label rotation. 

522 rotation, nlc = self.calc_label_rot_and_inline( 

523 self.axes.transData.transform(lc), # to pixel space. 

524 imin, lw, lc if inline else None, inline_spacing) 

525 

526 self.add_label(xmin, ymin, rotation, self.labelLevelList[lmin], 

527 self.labelCValueList[lmin]) 

528 

529 if inline: 

530 # Remove old, not looping over paths so we can do this up front 

531 paths.pop(segmin) 

532 

533 # Add paths if not empty or single point 

534 paths.extend([mpath.Path(n) for n in nlc if len(n) > 1]) 

535 

536 def pop_label(self, index=-1): 

537 """Defaults to removing last label, but any index can be supplied""" 

538 self.labelCValues.pop(index) 

539 t = self.labelTexts.pop(index) 

540 t.remove() 

541 

542 def labels(self, inline, inline_spacing): 

543 

544 if self._use_clabeltext: 

545 add_label = self.add_label_clabeltext 

546 else: 

547 add_label = self.add_label 

548 

549 for idx, (icon, lev, cvalue) in enumerate(zip( 

550 self.labelIndiceList, 

551 self.labelLevelList, 

552 self.labelCValueList, 

553 )): 

554 

555 con = self.collections[icon] 

556 trans = con.get_transform() 

557 lw = self._get_nth_label_width(idx) 

558 additions = [] 

559 paths = con.get_paths() 

560 for segNum, linepath in enumerate(paths): 

561 lc = linepath.vertices # Line contour 

562 slc = trans.transform(lc) # Line contour in screen coords 

563 

564 # Check if long enough for a label 

565 if self.print_label(slc, lw): 

566 x, y, ind = self.locate_label(slc, lw) 

567 

568 rotation, new = self.calc_label_rot_and_inline( 

569 slc, ind, lw, lc if inline else None, inline_spacing) 

570 

571 # Actually add the label 

572 add_label(x, y, rotation, lev, cvalue) 

573 

574 # If inline, add new contours 

575 if inline: 

576 for n in new: 

577 # Add path if not empty or single point 

578 if len(n) > 1: 

579 additions.append(mpath.Path(n)) 

580 else: # If not adding label, keep old path 

581 additions.append(linepath) 

582 

583 # After looping over all segments on a contour, replace old paths 

584 # by new ones if inlining. 

585 if inline: 

586 paths[:] = additions 

587 

588 

589def _is_closed_polygon(X): 

590 """ 

591 Return whether first and last object in a sequence are the same. These are 

592 presumably coordinates on a polygonal curve, in which case this function 

593 tests if that curve is closed. 

594 """ 

595 return np.allclose(X[0], X[-1], rtol=1e-10, atol=1e-13) 

596 

597 

598def _find_closest_point_on_path(xys, p): 

599 """ 

600 Parameters 

601 ---------- 

602 xys : (N, 2) array-like 

603 Coordinates of vertices. 

604 p : (float, float) 

605 Coordinates of point. 

606 

607 Returns 

608 ------- 

609 d2min : float 

610 Minimum square distance of *p* to *xys*. 

611 proj : (float, float) 

612 Projection of *p* onto *xys*. 

613 imin : (int, int) 

614 Consecutive indices of vertices of segment in *xys* where *proj* is. 

615 Segments are considered as including their end-points; i.e. if the 

616 closest point on the path is a node in *xys* with index *i*, this 

617 returns ``(i-1, i)``. For the special case where *xys* is a single 

618 point, this returns ``(0, 0)``. 

619 """ 

620 if len(xys) == 1: 

621 return (((p - xys[0]) ** 2).sum(), xys[0], (0, 0)) 

622 dxys = xys[1:] - xys[:-1] # Individual segment vectors. 

623 norms = (dxys ** 2).sum(axis=1) 

624 norms[norms == 0] = 1 # For zero-length segment, replace 0/0 by 0/1. 

625 rel_projs = np.clip( # Project onto each segment in relative 0-1 coords. 

626 ((p - xys[:-1]) * dxys).sum(axis=1) / norms, 

627 0, 1)[:, None] 

628 projs = xys[:-1] + rel_projs * dxys # Projs. onto each segment, in (x, y). 

629 d2s = ((projs - p) ** 2).sum(axis=1) # Squared distances. 

630 imin = np.argmin(d2s) 

631 return (d2s[imin], projs[imin], (imin, imin+1)) 

632 

633 

634_docstring.interpd.update(contour_set_attributes=r""" 

635Attributes 

636---------- 

637ax : `~matplotlib.axes.Axes` 

638 The Axes object in which the contours are drawn. 

639 

640collections : `.silent_list` of `.PathCollection`\s 

641 The `.Artist`\s representing the contour. This is a list of 

642 `.PathCollection`\s for both line and filled contours. 

643 

644levels : array 

645 The values of the contour levels. 

646 

647layers : array 

648 Same as levels for line contours; half-way between 

649 levels for filled contours. See ``ContourSet._process_colors``. 

650""") 

651 

652 

653@_docstring.dedent_interpd 

654class ContourSet(cm.ScalarMappable, ContourLabeler): 

655 """ 

656 Store a set of contour lines or filled regions. 

657 

658 User-callable method: `~.Axes.clabel` 

659 

660 Parameters 

661 ---------- 

662 ax : `~.axes.Axes` 

663 

664 levels : [level0, level1, ..., leveln] 

665 A list of floating point numbers indicating the contour levels. 

666 

667 allsegs : [level0segs, level1segs, ...] 

668 List of all the polygon segments for all the *levels*. 

669 For contour lines ``len(allsegs) == len(levels)``, and for 

670 filled contour regions ``len(allsegs) = len(levels)-1``. The lists 

671 should look like :: 

672 

673 level0segs = [polygon0, polygon1, ...] 

674 polygon0 = [[x0, y0], [x1, y1], ...] 

675 

676 allkinds : ``None`` or [level0kinds, level1kinds, ...] 

677 Optional list of all the polygon vertex kinds (code types), as 

678 described and used in Path. This is used to allow multiply- 

679 connected paths such as holes within filled polygons. 

680 If not ``None``, ``len(allkinds) == len(allsegs)``. The lists 

681 should look like :: 

682 

683 level0kinds = [polygon0kinds, ...] 

684 polygon0kinds = [vertexcode0, vertexcode1, ...] 

685 

686 If *allkinds* is not ``None``, usually all polygons for a 

687 particular contour level are grouped together so that 

688 ``level0segs = [polygon0]`` and ``level0kinds = [polygon0kinds]``. 

689 

690 **kwargs 

691 Keyword arguments are as described in the docstring of 

692 `~.Axes.contour`. 

693 

694 %(contour_set_attributes)s 

695 """ 

696 

697 def __init__(self, ax, *args, 

698 levels=None, filled=False, linewidths=None, linestyles=None, 

699 hatches=(None,), alpha=None, origin=None, extent=None, 

700 cmap=None, colors=None, norm=None, vmin=None, vmax=None, 

701 extend='neither', antialiased=None, nchunk=0, locator=None, 

702 transform=None, negative_linestyles=None, 

703 **kwargs): 

704 """ 

705 Draw contour lines or filled regions, depending on 

706 whether keyword arg *filled* is ``False`` (default) or ``True``. 

707 

708 Call signature:: 

709 

710 ContourSet(ax, levels, allsegs, [allkinds], **kwargs) 

711 

712 Parameters 

713 ---------- 

714 ax : `~.axes.Axes` 

715 The `~.axes.Axes` object to draw on. 

716 

717 levels : [level0, level1, ..., leveln] 

718 A list of floating point numbers indicating the contour 

719 levels. 

720 

721 allsegs : [level0segs, level1segs, ...] 

722 List of all the polygon segments for all the *levels*. 

723 For contour lines ``len(allsegs) == len(levels)``, and for 

724 filled contour regions ``len(allsegs) = len(levels)-1``. The lists 

725 should look like :: 

726 

727 level0segs = [polygon0, polygon1, ...] 

728 polygon0 = [[x0, y0], [x1, y1], ...] 

729 

730 allkinds : [level0kinds, level1kinds, ...], optional 

731 Optional list of all the polygon vertex kinds (code types), as 

732 described and used in Path. This is used to allow multiply- 

733 connected paths such as holes within filled polygons. 

734 If not ``None``, ``len(allkinds) == len(allsegs)``. The lists 

735 should look like :: 

736 

737 level0kinds = [polygon0kinds, ...] 

738 polygon0kinds = [vertexcode0, vertexcode1, ...] 

739 

740 If *allkinds* is not ``None``, usually all polygons for a 

741 particular contour level are grouped together so that 

742 ``level0segs = [polygon0]`` and ``level0kinds = [polygon0kinds]``. 

743 

744 **kwargs 

745 Keyword arguments are as described in the docstring of 

746 `~.Axes.contour`. 

747 """ 

748 self.axes = ax 

749 self.levels = levels 

750 self.filled = filled 

751 self.linewidths = linewidths 

752 self.linestyles = linestyles 

753 self.hatches = hatches 

754 self.alpha = alpha 

755 self.origin = origin 

756 self.extent = extent 

757 self.colors = colors 

758 self.extend = extend 

759 self.antialiased = antialiased 

760 if self.antialiased is None and self.filled: 

761 # Eliminate artifacts; we are not stroking the boundaries. 

762 self.antialiased = False 

763 # The default for line contours will be taken from the 

764 # LineCollection default, which uses :rc:`lines.antialiased`. 

765 

766 self.nchunk = nchunk 

767 self.locator = locator 

768 if (isinstance(norm, mcolors.LogNorm) 

769 or isinstance(self.locator, ticker.LogLocator)): 

770 self.logscale = True 

771 if norm is None: 

772 norm = mcolors.LogNorm() 

773 else: 

774 self.logscale = False 

775 

776 _api.check_in_list([None, 'lower', 'upper', 'image'], origin=origin) 

777 if self.extent is not None and len(self.extent) != 4: 

778 raise ValueError( 

779 "If given, 'extent' must be None or (x0, x1, y0, y1)") 

780 if self.colors is not None and cmap is not None: 

781 raise ValueError('Either colors or cmap must be None') 

782 if self.origin == 'image': 

783 self.origin = mpl.rcParams['image.origin'] 

784 

785 self._transform = transform 

786 

787 self.negative_linestyles = negative_linestyles 

788 # If negative_linestyles was not defined as a keyword argument, define 

789 # negative_linestyles with rcParams 

790 if self.negative_linestyles is None: 

791 self.negative_linestyles = \ 

792 mpl.rcParams['contour.negative_linestyle'] 

793 

794 kwargs = self._process_args(*args, **kwargs) 

795 self._process_levels() 

796 

797 self._extend_min = self.extend in ['min', 'both'] 

798 self._extend_max = self.extend in ['max', 'both'] 

799 if self.colors is not None: 

800 ncolors = len(self.levels) 

801 if self.filled: 

802 ncolors -= 1 

803 i0 = 0 

804 

805 # Handle the case where colors are given for the extended 

806 # parts of the contour. 

807 

808 use_set_under_over = False 

809 # if we are extending the lower end, and we've been given enough 

810 # colors then skip the first color in the resulting cmap. For the 

811 # extend_max case we don't need to worry about passing more colors 

812 # than ncolors as ListedColormap will clip. 

813 total_levels = (ncolors + 

814 int(self._extend_min) + 

815 int(self._extend_max)) 

816 if (len(self.colors) == total_levels and 

817 (self._extend_min or self._extend_max)): 

818 use_set_under_over = True 

819 if self._extend_min: 

820 i0 = 1 

821 

822 cmap = mcolors.ListedColormap(self.colors[i0:None], N=ncolors) 

823 

824 if use_set_under_over: 

825 if self._extend_min: 

826 cmap.set_under(self.colors[0]) 

827 if self._extend_max: 

828 cmap.set_over(self.colors[-1]) 

829 

830 self.collections = cbook.silent_list(None) 

831 

832 # label lists must be initialized here 

833 self.labelTexts = [] 

834 self.labelCValues = [] 

835 

836 kw = {'cmap': cmap} 

837 if norm is not None: 

838 kw['norm'] = norm 

839 # sets self.cmap, norm if needed; 

840 cm.ScalarMappable.__init__(self, **kw) 

841 if vmin is not None: 

842 self.norm.vmin = vmin 

843 if vmax is not None: 

844 self.norm.vmax = vmax 

845 self._process_colors() 

846 

847 if getattr(self, 'allsegs', None) is None: 

848 self.allsegs, self.allkinds = self._get_allsegs_and_allkinds() 

849 elif self.allkinds is None: 

850 # allsegs specified in constructor may or may not have allkinds as 

851 # well. Must ensure allkinds can be zipped below. 

852 self.allkinds = [None] * len(self.allsegs) 

853 

854 if self.filled: 

855 if self.linewidths is not None: 

856 _api.warn_external('linewidths is ignored by contourf') 

857 # Lower and upper contour levels. 

858 lowers, uppers = self._get_lowers_and_uppers() 

859 # Default zorder taken from Collection 

860 self._contour_zorder = kwargs.pop('zorder', 1) 

861 

862 self.collections[:] = [ 

863 mcoll.PathCollection( 

864 self._make_paths(segs, kinds), 

865 antialiaseds=(self.antialiased,), 

866 edgecolors='none', 

867 alpha=self.alpha, 

868 transform=self.get_transform(), 

869 zorder=self._contour_zorder) 

870 for level, level_upper, segs, kinds 

871 in zip(lowers, uppers, self.allsegs, self.allkinds)] 

872 else: 

873 self.tlinewidths = tlinewidths = self._process_linewidths() 

874 tlinestyles = self._process_linestyles() 

875 aa = self.antialiased 

876 if aa is not None: 

877 aa = (self.antialiased,) 

878 # Default zorder taken from LineCollection, which is higher than 

879 # for filled contours so that lines are displayed on top. 

880 self._contour_zorder = kwargs.pop('zorder', 2) 

881 

882 self.collections[:] = [ 

883 mcoll.PathCollection( 

884 self._make_paths(segs, kinds), 

885 facecolors="none", 

886 antialiaseds=aa, 

887 linewidths=width, 

888 linestyles=[lstyle], 

889 alpha=self.alpha, 

890 transform=self.get_transform(), 

891 zorder=self._contour_zorder, 

892 label='_nolegend_') 

893 for level, width, lstyle, segs, kinds 

894 in zip(self.levels, tlinewidths, tlinestyles, self.allsegs, 

895 self.allkinds)] 

896 

897 for col in self.collections: 

898 self.axes.add_collection(col, autolim=False) 

899 col.sticky_edges.x[:] = [self._mins[0], self._maxs[0]] 

900 col.sticky_edges.y[:] = [self._mins[1], self._maxs[1]] 

901 self.axes.update_datalim([self._mins, self._maxs]) 

902 self.axes.autoscale_view(tight=True) 

903 

904 self.changed() # set the colors 

905 

906 if kwargs: 

907 _api.warn_external( 

908 'The following kwargs were not used by contour: ' + 

909 ", ".join(map(repr, kwargs)) 

910 ) 

911 

912 def get_transform(self): 

913 """Return the `.Transform` instance used by this ContourSet.""" 

914 if self._transform is None: 

915 self._transform = self.axes.transData 

916 elif (not isinstance(self._transform, mtransforms.Transform) 

917 and hasattr(self._transform, '_as_mpl_transform')): 

918 self._transform = self._transform._as_mpl_transform(self.axes) 

919 return self._transform 

920 

921 def __getstate__(self): 

922 state = self.__dict__.copy() 

923 # the C object _contour_generator cannot currently be pickled. This 

924 # isn't a big issue as it is not actually used once the contour has 

925 # been calculated. 

926 state['_contour_generator'] = None 

927 return state 

928 

929 def legend_elements(self, variable_name='x', str_format=str): 

930 """ 

931 Return a list of artists and labels suitable for passing through 

932 to `~.Axes.legend` which represent this ContourSet. 

933 

934 The labels have the form "0 < x <= 1" stating the data ranges which 

935 the artists represent. 

936 

937 Parameters 

938 ---------- 

939 variable_name : str 

940 The string used inside the inequality used on the labels. 

941 str_format : function: float -> str 

942 Function used to format the numbers in the labels. 

943 

944 Returns 

945 ------- 

946 artists : list[`.Artist`] 

947 A list of the artists. 

948 labels : list[str] 

949 A list of the labels. 

950 """ 

951 artists = [] 

952 labels = [] 

953 

954 if self.filled: 

955 lowers, uppers = self._get_lowers_and_uppers() 

956 n_levels = len(self.collections) 

957 

958 for i, (collection, lower, upper) in enumerate( 

959 zip(self.collections, lowers, uppers)): 

960 patch = mpatches.Rectangle( 

961 (0, 0), 1, 1, 

962 facecolor=collection.get_facecolor()[0], 

963 hatch=collection.get_hatch(), 

964 alpha=collection.get_alpha()) 

965 artists.append(patch) 

966 

967 lower = str_format(lower) 

968 upper = str_format(upper) 

969 

970 if i == 0 and self.extend in ('min', 'both'): 

971 labels.append(fr'${variable_name} \leq {lower}s$') 

972 elif i == n_levels - 1 and self.extend in ('max', 'both'): 

973 labels.append(fr'${variable_name} > {upper}s$') 

974 else: 

975 labels.append(fr'${lower} < {variable_name} \leq {upper}$') 

976 else: 

977 for collection, level in zip(self.collections, self.levels): 

978 

979 patch = mcoll.LineCollection(None) 

980 patch.update_from(collection) 

981 

982 artists.append(patch) 

983 # format the level for insertion into the labels 

984 level = str_format(level) 

985 labels.append(fr'${variable_name} = {level}$') 

986 

987 return artists, labels 

988 

989 def _process_args(self, *args, **kwargs): 

990 """ 

991 Process *args* and *kwargs*; override in derived classes. 

992 

993 Must set self.levels, self.zmin and self.zmax, and update axes limits. 

994 """ 

995 self.levels = args[0] 

996 self.allsegs = args[1] 

997 self.allkinds = args[2] if len(args) > 2 else None 

998 self.zmax = np.max(self.levels) 

999 self.zmin = np.min(self.levels) 

1000 

1001 # Check lengths of levels and allsegs. 

1002 if self.filled: 

1003 if len(self.allsegs) != len(self.levels) - 1: 

1004 raise ValueError('must be one less number of segments as ' 

1005 'levels') 

1006 else: 

1007 if len(self.allsegs) != len(self.levels): 

1008 raise ValueError('must be same number of segments as levels') 

1009 

1010 # Check length of allkinds. 

1011 if (self.allkinds is not None and 

1012 len(self.allkinds) != len(self.allsegs)): 

1013 raise ValueError('allkinds has different length to allsegs') 

1014 

1015 # Determine x, y bounds and update axes data limits. 

1016 flatseglist = [s for seg in self.allsegs for s in seg] 

1017 points = np.concatenate(flatseglist, axis=0) 

1018 self._mins = points.min(axis=0) 

1019 self._maxs = points.max(axis=0) 

1020 

1021 return kwargs 

1022 

1023 def _get_allsegs_and_allkinds(self): 

1024 """Compute ``allsegs`` and ``allkinds`` using C extension.""" 

1025 allsegs = [] 

1026 allkinds = [] 

1027 if self.filled: 

1028 lowers, uppers = self._get_lowers_and_uppers() 

1029 for level, level_upper in zip(lowers, uppers): 

1030 vertices, kinds = \ 

1031 self._contour_generator.create_filled_contour( 

1032 level, level_upper) 

1033 allsegs.append(vertices) 

1034 allkinds.append(kinds) 

1035 else: 

1036 for level in self.levels: 

1037 vertices, kinds = self._contour_generator.create_contour(level) 

1038 allsegs.append(vertices) 

1039 allkinds.append(kinds) 

1040 return allsegs, allkinds 

1041 

1042 def _get_lowers_and_uppers(self): 

1043 """ 

1044 Return ``(lowers, uppers)`` for filled contours. 

1045 """ 

1046 lowers = self._levels[:-1] 

1047 if self.zmin == lowers[0]: 

1048 # Include minimum values in lowest interval 

1049 lowers = lowers.copy() # so we don't change self._levels 

1050 if self.logscale: 

1051 lowers[0] = 0.99 * self.zmin 

1052 else: 

1053 lowers[0] -= 1 

1054 uppers = self._levels[1:] 

1055 return (lowers, uppers) 

1056 

1057 def _make_paths(self, segs, kinds): 

1058 """ 

1059 Create and return Path objects for the specified segments and optional 

1060 kind codes. *segs* is a list of numpy arrays, each array is either a 

1061 closed line loop or open line strip of 2D points with a shape of 

1062 (npoints, 2). *kinds* is either None or a list (with the same length 

1063 as *segs*) of numpy arrays, each array is of shape (npoints,) and 

1064 contains the kind codes for the corresponding line in *segs*. If 

1065 *kinds* is None then the Path constructor creates the kind codes 

1066 assuming that the line is an open strip. 

1067 """ 

1068 if kinds is None: 

1069 return [mpath.Path(seg) for seg in segs] 

1070 else: 

1071 return [mpath.Path(seg, codes=kind) for seg, kind 

1072 in zip(segs, kinds)] 

1073 

1074 def changed(self): 

1075 if not hasattr(self, "cvalues"): 

1076 # Just return after calling the super() changed function 

1077 cm.ScalarMappable.changed(self) 

1078 return 

1079 # Force an autoscale immediately because self.to_rgba() calls 

1080 # autoscale_None() internally with the data passed to it, 

1081 # so if vmin/vmax are not set yet, this would override them with 

1082 # content from *cvalues* rather than levels like we want 

1083 self.norm.autoscale_None(self.levels) 

1084 tcolors = [(tuple(rgba),) 

1085 for rgba in self.to_rgba(self.cvalues, alpha=self.alpha)] 

1086 self.tcolors = tcolors 

1087 hatches = self.hatches * len(tcolors) 

1088 for color, hatch, collection in zip(tcolors, hatches, 

1089 self.collections): 

1090 if self.filled: 

1091 collection.set_facecolor(color) 

1092 # update the collection's hatch (may be None) 

1093 collection.set_hatch(hatch) 

1094 else: 

1095 collection.set_edgecolor(color) 

1096 for label, cv in zip(self.labelTexts, self.labelCValues): 

1097 label.set_alpha(self.alpha) 

1098 label.set_color(self.labelMappable.to_rgba(cv)) 

1099 # add label colors 

1100 cm.ScalarMappable.changed(self) 

1101 

1102 def _autolev(self, N): 

1103 """ 

1104 Select contour levels to span the data. 

1105 

1106 The target number of levels, *N*, is used only when the 

1107 scale is not log and default locator is used. 

1108 

1109 We need two more levels for filled contours than for 

1110 line contours, because for the latter we need to specify 

1111 the lower and upper boundary of each range. For example, 

1112 a single contour boundary, say at z = 0, requires only 

1113 one contour line, but two filled regions, and therefore 

1114 three levels to provide boundaries for both regions. 

1115 """ 

1116 if self.locator is None: 

1117 if self.logscale: 

1118 self.locator = ticker.LogLocator() 

1119 else: 

1120 self.locator = ticker.MaxNLocator(N + 1, min_n_ticks=1) 

1121 

1122 lev = self.locator.tick_values(self.zmin, self.zmax) 

1123 

1124 try: 

1125 if self.locator._symmetric: 

1126 return lev 

1127 except AttributeError: 

1128 pass 

1129 

1130 # Trim excess levels the locator may have supplied. 

1131 under = np.nonzero(lev < self.zmin)[0] 

1132 i0 = under[-1] if len(under) else 0 

1133 over = np.nonzero(lev > self.zmax)[0] 

1134 i1 = over[0] + 1 if len(over) else len(lev) 

1135 if self.extend in ('min', 'both'): 

1136 i0 += 1 

1137 if self.extend in ('max', 'both'): 

1138 i1 -= 1 

1139 

1140 if i1 - i0 < 3: 

1141 i0, i1 = 0, len(lev) 

1142 

1143 return lev[i0:i1] 

1144 

1145 def _process_contour_level_args(self, args): 

1146 """ 

1147 Determine the contour levels and store in self.levels. 

1148 """ 

1149 if self.levels is None: 

1150 if len(args) == 0: 

1151 levels_arg = 7 # Default, hard-wired. 

1152 else: 

1153 levels_arg = args[0] 

1154 else: 

1155 levels_arg = self.levels 

1156 if isinstance(levels_arg, Integral): 

1157 self.levels = self._autolev(levels_arg) 

1158 else: 

1159 self.levels = np.asarray(levels_arg).astype(np.float64) 

1160 

1161 if not self.filled: 

1162 inside = (self.levels > self.zmin) & (self.levels < self.zmax) 

1163 levels_in = self.levels[inside] 

1164 if len(levels_in) == 0: 

1165 self.levels = [self.zmin] 

1166 _api.warn_external( 

1167 "No contour levels were found within the data range.") 

1168 

1169 if self.filled and len(self.levels) < 2: 

1170 raise ValueError("Filled contours require at least 2 levels.") 

1171 

1172 if len(self.levels) > 1 and np.min(np.diff(self.levels)) <= 0.0: 

1173 raise ValueError("Contour levels must be increasing") 

1174 

1175 def _process_levels(self): 

1176 """ 

1177 Assign values to :attr:`layers` based on :attr:`levels`, 

1178 adding extended layers as needed if contours are filled. 

1179 

1180 For line contours, layers simply coincide with levels; 

1181 a line is a thin layer. No extended levels are needed 

1182 with line contours. 

1183 """ 

1184 # Make a private _levels to include extended regions; we 

1185 # want to leave the original levels attribute unchanged. 

1186 # (Colorbar needs this even for line contours.) 

1187 self._levels = list(self.levels) 

1188 

1189 if self.logscale: 

1190 lower, upper = 1e-250, 1e250 

1191 else: 

1192 lower, upper = -1e250, 1e250 

1193 

1194 if self.extend in ('both', 'min'): 

1195 self._levels.insert(0, lower) 

1196 if self.extend in ('both', 'max'): 

1197 self._levels.append(upper) 

1198 self._levels = np.asarray(self._levels) 

1199 

1200 if not self.filled: 

1201 self.layers = self.levels 

1202 return 

1203 

1204 # Layer values are mid-way between levels in screen space. 

1205 if self.logscale: 

1206 # Avoid overflow by taking sqrt before multiplying. 

1207 self.layers = (np.sqrt(self._levels[:-1]) 

1208 * np.sqrt(self._levels[1:])) 

1209 else: 

1210 self.layers = 0.5 * (self._levels[:-1] + self._levels[1:]) 

1211 

1212 def _process_colors(self): 

1213 """ 

1214 Color argument processing for contouring. 

1215 

1216 Note that we base the colormapping on the contour levels 

1217 and layers, not on the actual range of the Z values. This 

1218 means we don't have to worry about bad values in Z, and we 

1219 always have the full dynamic range available for the selected 

1220 levels. 

1221 

1222 The color is based on the midpoint of the layer, except for 

1223 extended end layers. By default, the norm vmin and vmax 

1224 are the extreme values of the non-extended levels. Hence, 

1225 the layer color extremes are not the extreme values of 

1226 the colormap itself, but approach those values as the number 

1227 of levels increases. An advantage of this scheme is that 

1228 line contours, when added to filled contours, take on 

1229 colors that are consistent with those of the filled regions; 

1230 for example, a contour line on the boundary between two 

1231 regions will have a color intermediate between those 

1232 of the regions. 

1233 

1234 """ 

1235 self.monochrome = self.cmap.monochrome 

1236 if self.colors is not None: 

1237 # Generate integers for direct indexing. 

1238 i0, i1 = 0, len(self.levels) 

1239 if self.filled: 

1240 i1 -= 1 

1241 # Out of range indices for over and under: 

1242 if self.extend in ('both', 'min'): 

1243 i0 -= 1 

1244 if self.extend in ('both', 'max'): 

1245 i1 += 1 

1246 self.cvalues = list(range(i0, i1)) 

1247 self.set_norm(mcolors.NoNorm()) 

1248 else: 

1249 self.cvalues = self.layers 

1250 self.set_array(self.levels) 

1251 self.autoscale_None() 

1252 if self.extend in ('both', 'max', 'min'): 

1253 self.norm.clip = False 

1254 

1255 # self.tcolors are set by the "changed" method 

1256 

1257 def _process_linewidths(self): 

1258 linewidths = self.linewidths 

1259 Nlev = len(self.levels) 

1260 if linewidths is None: 

1261 default_linewidth = mpl.rcParams['contour.linewidth'] 

1262 if default_linewidth is None: 

1263 default_linewidth = mpl.rcParams['lines.linewidth'] 

1264 tlinewidths = [(default_linewidth,)] * Nlev 

1265 else: 

1266 if not np.iterable(linewidths): 

1267 linewidths = [linewidths] * Nlev 

1268 else: 

1269 linewidths = list(linewidths) 

1270 if len(linewidths) < Nlev: 

1271 nreps = int(np.ceil(Nlev / len(linewidths))) 

1272 linewidths = linewidths * nreps 

1273 if len(linewidths) > Nlev: 

1274 linewidths = linewidths[:Nlev] 

1275 tlinewidths = [(w,) for w in linewidths] 

1276 return tlinewidths 

1277 

1278 def _process_linestyles(self): 

1279 linestyles = self.linestyles 

1280 Nlev = len(self.levels) 

1281 if linestyles is None: 

1282 tlinestyles = ['solid'] * Nlev 

1283 if self.monochrome: 

1284 eps = - (self.zmax - self.zmin) * 1e-15 

1285 for i, lev in enumerate(self.levels): 

1286 if lev < eps: 

1287 tlinestyles[i] = self.negative_linestyles 

1288 else: 

1289 if isinstance(linestyles, str): 

1290 tlinestyles = [linestyles] * Nlev 

1291 elif np.iterable(linestyles): 

1292 tlinestyles = list(linestyles) 

1293 if len(tlinestyles) < Nlev: 

1294 nreps = int(np.ceil(Nlev / len(linestyles))) 

1295 tlinestyles = tlinestyles * nreps 

1296 if len(tlinestyles) > Nlev: 

1297 tlinestyles = tlinestyles[:Nlev] 

1298 else: 

1299 raise ValueError("Unrecognized type for linestyles kwarg") 

1300 return tlinestyles 

1301 

1302 def get_alpha(self): 

1303 """Return alpha to be applied to all ContourSet artists.""" 

1304 return self.alpha 

1305 

1306 def set_alpha(self, alpha): 

1307 """ 

1308 Set the alpha blending value for all ContourSet artists. 

1309 *alpha* must be between 0 (transparent) and 1 (opaque). 

1310 """ 

1311 self.alpha = alpha 

1312 self.changed() 

1313 

1314 def find_nearest_contour(self, x, y, indices=None, pixel=True): 

1315 """ 

1316 Find the point in the contour plot that is closest to ``(x, y)``. 

1317 

1318 This method does not support filled contours. 

1319 

1320 Parameters 

1321 ---------- 

1322 x, y : float 

1323 The reference point. 

1324 indices : list of int or None, default: None 

1325 Indices of contour levels to consider. If None (the default), all 

1326 levels are considered. 

1327 pixel : bool, default: True 

1328 If *True*, measure distance in pixel (screen) space, which is 

1329 useful for manual contour labeling; else, measure distance in axes 

1330 space. 

1331 

1332 Returns 

1333 ------- 

1334 contour : `.Collection` 

1335 The contour that is closest to ``(x, y)``. 

1336 segment : int 

1337 The index of the `.Path` in *contour* that is closest to 

1338 ``(x, y)``. 

1339 index : int 

1340 The index of the path segment in *segment* that is closest to 

1341 ``(x, y)``. 

1342 xmin, ymin : float 

1343 The point in the contour plot that is closest to ``(x, y)``. 

1344 d2 : float 

1345 The squared distance from ``(xmin, ymin)`` to ``(x, y)``. 

1346 """ 

1347 

1348 # This function uses a method that is probably quite 

1349 # inefficient based on converting each contour segment to 

1350 # pixel coordinates and then comparing the given point to 

1351 # those coordinates for each contour. This will probably be 

1352 # quite slow for complex contours, but for normal use it works 

1353 # sufficiently well that the time is not noticeable. 

1354 # Nonetheless, improvements could probably be made. 

1355 

1356 if self.filled: 

1357 raise ValueError("Method does not support filled contours.") 

1358 

1359 if indices is None: 

1360 indices = range(len(self.collections)) 

1361 

1362 d2min = np.inf 

1363 conmin = None 

1364 segmin = None 

1365 imin = None 

1366 xmin = None 

1367 ymin = None 

1368 

1369 point = np.array([x, y]) 

1370 

1371 for icon in indices: 

1372 con = self.collections[icon] 

1373 trans = con.get_transform() 

1374 paths = con.get_paths() 

1375 

1376 for segNum, linepath in enumerate(paths): 

1377 lc = linepath.vertices 

1378 # transfer all data points to screen coordinates if desired 

1379 if pixel: 

1380 lc = trans.transform(lc) 

1381 

1382 d2, xc, leg = _find_closest_point_on_path(lc, point) 

1383 if d2 < d2min: 

1384 d2min = d2 

1385 conmin = icon 

1386 segmin = segNum 

1387 imin = leg[1] 

1388 xmin = xc[0] 

1389 ymin = xc[1] 

1390 

1391 return (conmin, segmin, imin, xmin, ymin, d2min) 

1392 

1393 

1394@_docstring.dedent_interpd 

1395class QuadContourSet(ContourSet): 

1396 """ 

1397 Create and store a set of contour lines or filled regions. 

1398 

1399 This class is typically not instantiated directly by the user but by 

1400 `~.Axes.contour` and `~.Axes.contourf`. 

1401 

1402 %(contour_set_attributes)s 

1403 """ 

1404 

1405 def _process_args(self, *args, corner_mask=None, algorithm=None, **kwargs): 

1406 """ 

1407 Process args and kwargs. 

1408 """ 

1409 if isinstance(args[0], QuadContourSet): 

1410 if self.levels is None: 

1411 self.levels = args[0].levels 

1412 self.zmin = args[0].zmin 

1413 self.zmax = args[0].zmax 

1414 self._corner_mask = args[0]._corner_mask 

1415 contour_generator = args[0]._contour_generator 

1416 self._mins = args[0]._mins 

1417 self._maxs = args[0]._maxs 

1418 self._algorithm = args[0]._algorithm 

1419 else: 

1420 import contourpy 

1421 

1422 if algorithm is None: 

1423 algorithm = mpl.rcParams['contour.algorithm'] 

1424 mpl.rcParams.validate["contour.algorithm"](algorithm) 

1425 self._algorithm = algorithm 

1426 

1427 if corner_mask is None: 

1428 if self._algorithm == "mpl2005": 

1429 # mpl2005 does not support corner_mask=True so if not 

1430 # specifically requested then disable it. 

1431 corner_mask = False 

1432 else: 

1433 corner_mask = mpl.rcParams['contour.corner_mask'] 

1434 self._corner_mask = corner_mask 

1435 

1436 x, y, z = self._contour_args(args, kwargs) 

1437 

1438 contour_generator = contourpy.contour_generator( 

1439 x, y, z, name=self._algorithm, corner_mask=self._corner_mask, 

1440 line_type=contourpy.LineType.SeparateCode, 

1441 fill_type=contourpy.FillType.OuterCode, 

1442 chunk_size=self.nchunk) 

1443 

1444 t = self.get_transform() 

1445 

1446 # if the transform is not trans data, and some part of it 

1447 # contains transData, transform the xs and ys to data coordinates 

1448 if (t != self.axes.transData and 

1449 any(t.contains_branch_seperately(self.axes.transData))): 

1450 trans_to_data = t - self.axes.transData 

1451 pts = np.vstack([x.flat, y.flat]).T 

1452 transformed_pts = trans_to_data.transform(pts) 

1453 x = transformed_pts[..., 0] 

1454 y = transformed_pts[..., 1] 

1455 

1456 self._mins = [ma.min(x), ma.min(y)] 

1457 self._maxs = [ma.max(x), ma.max(y)] 

1458 

1459 self._contour_generator = contour_generator 

1460 

1461 return kwargs 

1462 

1463 def _contour_args(self, args, kwargs): 

1464 if self.filled: 

1465 fn = 'contourf' 

1466 else: 

1467 fn = 'contour' 

1468 Nargs = len(args) 

1469 if Nargs <= 2: 

1470 z = ma.asarray(args[0], dtype=np.float64) 

1471 x, y = self._initialize_x_y(z) 

1472 args = args[1:] 

1473 elif Nargs <= 4: 

1474 x, y, z = self._check_xyz(args[:3], kwargs) 

1475 args = args[3:] 

1476 else: 

1477 raise TypeError("Too many arguments to %s; see help(%s)" % 

1478 (fn, fn)) 

1479 z = ma.masked_invalid(z, copy=False) 

1480 self.zmax = float(z.max()) 

1481 self.zmin = float(z.min()) 

1482 if self.logscale and self.zmin <= 0: 

1483 z = ma.masked_where(z <= 0, z) 

1484 _api.warn_external('Log scale: values of z <= 0 have been masked') 

1485 self.zmin = float(z.min()) 

1486 self._process_contour_level_args(args) 

1487 return (x, y, z) 

1488 

1489 def _check_xyz(self, args, kwargs): 

1490 """ 

1491 Check that the shapes of the input arrays match; if x and y are 1D, 

1492 convert them to 2D using meshgrid. 

1493 """ 

1494 x, y = args[:2] 

1495 x, y = self.axes._process_unit_info([("x", x), ("y", y)], kwargs) 

1496 

1497 x = np.asarray(x, dtype=np.float64) 

1498 y = np.asarray(y, dtype=np.float64) 

1499 z = ma.asarray(args[2], dtype=np.float64) 

1500 

1501 if z.ndim != 2: 

1502 raise TypeError(f"Input z must be 2D, not {z.ndim}D") 

1503 if z.shape[0] < 2 or z.shape[1] < 2: 

1504 raise TypeError(f"Input z must be at least a (2, 2) shaped array, " 

1505 f"but has shape {z.shape}") 

1506 Ny, Nx = z.shape 

1507 

1508 if x.ndim != y.ndim: 

1509 raise TypeError(f"Number of dimensions of x ({x.ndim}) and y " 

1510 f"({y.ndim}) do not match") 

1511 if x.ndim == 1: 

1512 nx, = x.shape 

1513 ny, = y.shape 

1514 if nx != Nx: 

1515 raise TypeError(f"Length of x ({nx}) must match number of " 

1516 f"columns in z ({Nx})") 

1517 if ny != Ny: 

1518 raise TypeError(f"Length of y ({ny}) must match number of " 

1519 f"rows in z ({Ny})") 

1520 x, y = np.meshgrid(x, y) 

1521 elif x.ndim == 2: 

1522 if x.shape != z.shape: 

1523 raise TypeError( 

1524 f"Shapes of x {x.shape} and z {z.shape} do not match") 

1525 if y.shape != z.shape: 

1526 raise TypeError( 

1527 f"Shapes of y {y.shape} and z {z.shape} do not match") 

1528 else: 

1529 raise TypeError(f"Inputs x and y must be 1D or 2D, not {x.ndim}D") 

1530 

1531 return x, y, z 

1532 

1533 def _initialize_x_y(self, z): 

1534 """ 

1535 Return X, Y arrays such that contour(Z) will match imshow(Z) 

1536 if origin is not None. 

1537 The center of pixel Z[i, j] depends on origin: 

1538 if origin is None, x = j, y = i; 

1539 if origin is 'lower', x = j + 0.5, y = i + 0.5; 

1540 if origin is 'upper', x = j + 0.5, y = Nrows - i - 0.5 

1541 If extent is not None, x and y will be scaled to match, 

1542 as in imshow. 

1543 If origin is None and extent is not None, then extent 

1544 will give the minimum and maximum values of x and y. 

1545 """ 

1546 if z.ndim != 2: 

1547 raise TypeError(f"Input z must be 2D, not {z.ndim}D") 

1548 elif z.shape[0] < 2 or z.shape[1] < 2: 

1549 raise TypeError(f"Input z must be at least a (2, 2) shaped array, " 

1550 f"but has shape {z.shape}") 

1551 else: 

1552 Ny, Nx = z.shape 

1553 if self.origin is None: # Not for image-matching. 

1554 if self.extent is None: 

1555 return np.meshgrid(np.arange(Nx), np.arange(Ny)) 

1556 else: 

1557 x0, x1, y0, y1 = self.extent 

1558 x = np.linspace(x0, x1, Nx) 

1559 y = np.linspace(y0, y1, Ny) 

1560 return np.meshgrid(x, y) 

1561 # Match image behavior: 

1562 if self.extent is None: 

1563 x0, x1, y0, y1 = (0, Nx, 0, Ny) 

1564 else: 

1565 x0, x1, y0, y1 = self.extent 

1566 dx = (x1 - x0) / Nx 

1567 dy = (y1 - y0) / Ny 

1568 x = x0 + (np.arange(Nx) + 0.5) * dx 

1569 y = y0 + (np.arange(Ny) + 0.5) * dy 

1570 if self.origin == 'upper': 

1571 y = y[::-1] 

1572 return np.meshgrid(x, y) 

1573 

1574 

1575_docstring.interpd.update(contour_doc=""" 

1576`.contour` and `.contourf` draw contour lines and filled contours, 

1577respectively. Except as noted, function signatures and return values 

1578are the same for both versions. 

1579 

1580Parameters 

1581---------- 

1582X, Y : array-like, optional 

1583 The coordinates of the values in *Z*. 

1584 

1585 *X* and *Y* must both be 2D with the same shape as *Z* (e.g. 

1586 created via `numpy.meshgrid`), or they must both be 1-D such 

1587 that ``len(X) == N`` is the number of columns in *Z* and 

1588 ``len(Y) == M`` is the number of rows in *Z*. 

1589 

1590 *X* and *Y* must both be ordered monotonically. 

1591 

1592 If not given, they are assumed to be integer indices, i.e. 

1593 ``X = range(N)``, ``Y = range(M)``. 

1594 

1595Z : (M, N) array-like 

1596 The height values over which the contour is drawn. Color-mapping is 

1597 controlled by *cmap*, *norm*, *vmin*, and *vmax*. 

1598 

1599levels : int or array-like, optional 

1600 Determines the number and positions of the contour lines / regions. 

1601 

1602 If an int *n*, use `~matplotlib.ticker.MaxNLocator`, which tries 

1603 to automatically choose no more than *n+1* "nice" contour levels 

1604 between minimum and maximum numeric values of *Z*. 

1605 

1606 If array-like, draw contour lines at the specified levels. 

1607 The values must be in increasing order. 

1608 

1609Returns 

1610------- 

1611`~.contour.QuadContourSet` 

1612 

1613Other Parameters 

1614---------------- 

1615corner_mask : bool, default: :rc:`contour.corner_mask` 

1616 Enable/disable corner masking, which only has an effect if *Z* is 

1617 a masked array. If ``False``, any quad touching a masked point is 

1618 masked out. If ``True``, only the triangular corners of quads 

1619 nearest those points are always masked out, other triangular 

1620 corners comprising three unmasked points are contoured as usual. 

1621 

1622colors : color string or sequence of colors, optional 

1623 The colors of the levels, i.e. the lines for `.contour` and the 

1624 areas for `.contourf`. 

1625 

1626 The sequence is cycled for the levels in ascending order. If the 

1627 sequence is shorter than the number of levels, it's repeated. 

1628 

1629 As a shortcut, single color strings may be used in place of 

1630 one-element lists, i.e. ``'red'`` instead of ``['red']`` to color 

1631 all levels with the same color. This shortcut does only work for 

1632 color strings, not for other ways of specifying colors. 

1633 

1634 By default (value *None*), the colormap specified by *cmap* 

1635 will be used. 

1636 

1637alpha : float, default: 1 

1638 The alpha blending value, between 0 (transparent) and 1 (opaque). 

1639 

1640%(cmap_doc)s 

1641 

1642 This parameter is ignored if *colors* is set. 

1643 

1644%(norm_doc)s 

1645 

1646 This parameter is ignored if *colors* is set. 

1647 

1648%(vmin_vmax_doc)s 

1649 

1650 If *vmin* or *vmax* are not given, the default color scaling is based on 

1651 *levels*. 

1652 

1653 This parameter is ignored if *colors* is set. 

1654 

1655origin : {*None*, 'upper', 'lower', 'image'}, default: None 

1656 Determines the orientation and exact position of *Z* by specifying 

1657 the position of ``Z[0, 0]``. This is only relevant, if *X*, *Y* 

1658 are not given. 

1659 

1660 - *None*: ``Z[0, 0]`` is at X=0, Y=0 in the lower left corner. 

1661 - 'lower': ``Z[0, 0]`` is at X=0.5, Y=0.5 in the lower left corner. 

1662 - 'upper': ``Z[0, 0]`` is at X=N+0.5, Y=0.5 in the upper left 

1663 corner. 

1664 - 'image': Use the value from :rc:`image.origin`. 

1665 

1666extent : (x0, x1, y0, y1), optional 

1667 If *origin* is not *None*, then *extent* is interpreted as in 

1668 `.imshow`: it gives the outer pixel boundaries. In this case, the 

1669 position of Z[0, 0] is the center of the pixel, not a corner. If 

1670 *origin* is *None*, then (*x0*, *y0*) is the position of Z[0, 0], 

1671 and (*x1*, *y1*) is the position of Z[-1, -1]. 

1672 

1673 This argument is ignored if *X* and *Y* are specified in the call 

1674 to contour. 

1675 

1676locator : ticker.Locator subclass, optional 

1677 The locator is used to determine the contour levels if they 

1678 are not given explicitly via *levels*. 

1679 Defaults to `~.ticker.MaxNLocator`. 

1680 

1681extend : {'neither', 'both', 'min', 'max'}, default: 'neither' 

1682 Determines the ``contourf``-coloring of values that are outside the 

1683 *levels* range. 

1684 

1685 If 'neither', values outside the *levels* range are not colored. 

1686 If 'min', 'max' or 'both', color the values below, above or below 

1687 and above the *levels* range. 

1688 

1689 Values below ``min(levels)`` and above ``max(levels)`` are mapped 

1690 to the under/over values of the `.Colormap`. Note that most 

1691 colormaps do not have dedicated colors for these by default, so 

1692 that the over and under values are the edge values of the colormap. 

1693 You may want to set these values explicitly using 

1694 `.Colormap.set_under` and `.Colormap.set_over`. 

1695 

1696 .. note:: 

1697 

1698 An existing `.QuadContourSet` does not get notified if 

1699 properties of its colormap are changed. Therefore, an explicit 

1700 call `.QuadContourSet.changed()` is needed after modifying the 

1701 colormap. The explicit call can be left out, if a colorbar is 

1702 assigned to the `.QuadContourSet` because it internally calls 

1703 `.QuadContourSet.changed()`. 

1704 

1705 Example:: 

1706 

1707 x = np.arange(1, 10) 

1708 y = x.reshape(-1, 1) 

1709 h = x * y 

1710 

1711 cs = plt.contourf(h, levels=[10, 30, 50], 

1712 colors=['#808080', '#A0A0A0', '#C0C0C0'], extend='both') 

1713 cs.cmap.set_over('red') 

1714 cs.cmap.set_under('blue') 

1715 cs.changed() 

1716 

1717xunits, yunits : registered units, optional 

1718 Override axis units by specifying an instance of a 

1719 :class:`matplotlib.units.ConversionInterface`. 

1720 

1721antialiased : bool, optional 

1722 Enable antialiasing, overriding the defaults. For 

1723 filled contours, the default is *True*. For line contours, 

1724 it is taken from :rc:`lines.antialiased`. 

1725 

1726nchunk : int >= 0, optional 

1727 If 0, no subdivision of the domain. Specify a positive integer to 

1728 divide the domain into subdomains of *nchunk* by *nchunk* quads. 

1729 Chunking reduces the maximum length of polygons generated by the 

1730 contouring algorithm which reduces the rendering workload passed 

1731 on to the backend and also requires slightly less RAM. It can 

1732 however introduce rendering artifacts at chunk boundaries depending 

1733 on the backend, the *antialiased* flag and value of *alpha*. 

1734 

1735linewidths : float or array-like, default: :rc:`contour.linewidth` 

1736 *Only applies to* `.contour`. 

1737 

1738 The line width of the contour lines. 

1739 

1740 If a number, all levels will be plotted with this linewidth. 

1741 

1742 If a sequence, the levels in ascending order will be plotted with 

1743 the linewidths in the order specified. 

1744 

1745 If None, this falls back to :rc:`lines.linewidth`. 

1746 

1747linestyles : {*None*, 'solid', 'dashed', 'dashdot', 'dotted'}, optional 

1748 *Only applies to* `.contour`. 

1749 

1750 If *linestyles* is *None*, the default is 'solid' unless the lines are 

1751 monochrome. In that case, negative contours will instead take their 

1752 linestyle from the *negative_linestyles* argument. 

1753 

1754 *linestyles* can also be an iterable of the above strings specifying a set 

1755 of linestyles to be used. If this iterable is shorter than the number of 

1756 contour levels it will be repeated as necessary. 

1757 

1758negative_linestyles : {*None*, 'solid', 'dashed', 'dashdot', 'dotted'}, \ 

1759 optional 

1760 *Only applies to* `.contour`. 

1761 

1762 If *linestyles* is *None* and the lines are monochrome, this argument 

1763 specifies the line style for negative contours. 

1764 

1765 If *negative_linestyles* is *None*, the default is taken from 

1766 :rc:`contour.negative_linestyles`. 

1767 

1768 *negative_linestyles* can also be an iterable of the above strings 

1769 specifying a set of linestyles to be used. If this iterable is shorter than 

1770 the number of contour levels it will be repeated as necessary. 

1771 

1772hatches : list[str], optional 

1773 *Only applies to* `.contourf`. 

1774 

1775 A list of cross hatch patterns to use on the filled areas. 

1776 If None, no hatching will be added to the contour. 

1777 Hatching is supported in the PostScript, PDF, SVG and Agg 

1778 backends only. 

1779 

1780algorithm : {'mpl2005', 'mpl2014', 'serial', 'threaded'}, optional 

1781 Which contouring algorithm to use to calculate the contour lines and 

1782 polygons. The algorithms are implemented in 

1783 `ContourPy <https://github.com/contourpy/contourpy>`_, consult the 

1784 `ContourPy documentation <https://contourpy.readthedocs.io>`_ for 

1785 further information. 

1786 

1787 The default is taken from :rc:`contour.algorithm`. 

1788 

1789data : indexable object, optional 

1790 DATA_PARAMETER_PLACEHOLDER 

1791 

1792Notes 

1793----- 

17941. `.contourf` differs from the MATLAB version in that it does not draw 

1795 the polygon edges. To draw edges, add line contours with calls to 

1796 `.contour`. 

1797 

17982. `.contourf` fills intervals that are closed at the top; that is, for 

1799 boundaries *z1* and *z2*, the filled region is:: 

1800 

1801 z1 < Z <= z2 

1802 

1803 except for the lowest interval, which is closed on both sides (i.e. 

1804 it includes the lowest value). 

1805 

18063. `.contour` and `.contourf` use a `marching squares 

1807 <https://en.wikipedia.org/wiki/Marching_squares>`_ algorithm to 

1808 compute contour locations. More information can be found in 

1809 `ContourPy documentation <https://contourpy.readthedocs.io>`_. 

1810""" % _docstring.interpd.params)