1"""
2Classes to support contour plotting and labelling for the Axes class.
3"""
4
5from contextlib import ExitStack
6import functools
7import math
8from numbers import Integral
9
10import numpy as np
11from numpy import ma
12
13import matplotlib as mpl
14from matplotlib import _api, _docstring
15from matplotlib.backend_bases import MouseButton
16from matplotlib.lines import Line2D
17from matplotlib.path import Path
18from matplotlib.text import Text
19import matplotlib.ticker as ticker
20import matplotlib.cm as cm
21import matplotlib.colors as mcolors
22import matplotlib.collections as mcoll
23import matplotlib.font_manager as font_manager
24import matplotlib.cbook as cbook
25import matplotlib.patches as mpatches
26import matplotlib.transforms as mtransforms
27
28
29def _contour_labeler_event_handler(cs, inline, inline_spacing, event):
30 canvas = cs.axes.figure.canvas
31 is_button = event.name == "button_press_event"
32 is_key = event.name == "key_press_event"
33 # Quit (even if not in infinite mode; this is consistent with
34 # MATLAB and sometimes quite useful, but will require the user to
35 # test how many points were actually returned before using data).
36 if (is_button and event.button == MouseButton.MIDDLE
37 or is_key and event.key in ["escape", "enter"]):
38 canvas.stop_event_loop()
39 # Pop last click.
40 elif (is_button and event.button == MouseButton.RIGHT
41 or is_key and event.key in ["backspace", "delete"]):
42 # Unfortunately, if one is doing inline labels, then there is currently
43 # no way to fix the broken contour - once humpty-dumpty is broken, he
44 # can't be put back together. In inline mode, this does nothing.
45 if not inline:
46 cs.pop_label()
47 canvas.draw()
48 # Add new click.
49 elif (is_button and event.button == MouseButton.LEFT
50 # On macOS/gtk, some keys return None.
51 or is_key and event.key is not None):
52 if cs.axes.contains(event)[0]:
53 cs.add_label_near(event.x, event.y, transform=False,
54 inline=inline, inline_spacing=inline_spacing)
55 canvas.draw()
56
57
58class ContourLabeler:
59 """Mixin to provide labelling capability to `.ContourSet`."""
60
61 def clabel(self, levels=None, *,
62 fontsize=None, inline=True, inline_spacing=5, fmt=None,
63 colors=None, use_clabeltext=False, manual=False,
64 rightside_up=True, zorder=None):
65 """
66 Label a contour plot.
67
68 Adds labels to line contours in this `.ContourSet` (which inherits from
69 this mixin class).
70
71 Parameters
72 ----------
73 levels : array-like, optional
74 A list of level values, that should be labeled. The list must be
75 a subset of ``cs.levels``. If not given, all levels are labeled.
76
77 fontsize : str or float, default: :rc:`font.size`
78 Size in points or relative size e.g., 'smaller', 'x-large'.
79 See `.Text.set_size` for accepted string values.
80
81 colors : :mpltype:`color` or colors or None, default: None
82 The label colors:
83
84 - If *None*, the color of each label matches the color of
85 the corresponding contour.
86
87 - If one string color, e.g., *colors* = 'r' or *colors* =
88 'red', all labels will be plotted in this color.
89
90 - If a tuple of colors (string, float, RGB, etc), different labels
91 will be plotted in different colors in the order specified.
92
93 inline : bool, default: True
94 If ``True`` the underlying contour is removed where the label is
95 placed.
96
97 inline_spacing : float, default: 5
98 Space in pixels to leave on each side of label when placing inline.
99
100 This spacing will be exact for labels at locations where the
101 contour is straight, less so for labels on curved contours.
102
103 fmt : `.Formatter` or str or callable or dict, optional
104 How the levels are formatted:
105
106 - If a `.Formatter`, it is used to format all levels at once, using
107 its `.Formatter.format_ticks` method.
108 - If a str, it is interpreted as a %-style format string.
109 - If a callable, it is called with one level at a time and should
110 return the corresponding label.
111 - If a dict, it should directly map levels to labels.
112
113 The default is to use a standard `.ScalarFormatter`.
114
115 manual : bool or iterable, default: False
116 If ``True``, contour labels will be placed manually using
117 mouse clicks. Click the first button near a contour to
118 add a label, click the second button (or potentially both
119 mouse buttons at once) to finish adding labels. The third
120 button can be used to remove the last label added, but
121 only if labels are not inline. Alternatively, the keyboard
122 can be used to select label locations (enter to end label
123 placement, delete or backspace act like the third mouse button,
124 and any other key will select a label location).
125
126 *manual* can also be an iterable object of (x, y) tuples.
127 Contour labels will be created as if mouse is clicked at each
128 (x, y) position.
129
130 rightside_up : bool, default: True
131 If ``True``, label rotations will always be plus
132 or minus 90 degrees from level.
133
134 use_clabeltext : bool, default: False
135 If ``True``, use `.Text.set_transform_rotates_text` to ensure that
136 label rotation is updated whenever the Axes aspect changes.
137
138 zorder : float or None, default: ``(2 + contour.get_zorder())``
139 zorder of the contour labels.
140
141 Returns
142 -------
143 labels
144 A list of `.Text` instances for the labels.
145 """
146
147 # Based on the input arguments, clabel() adds a list of "label
148 # specific" attributes to the ContourSet object. These attributes are
149 # all of the form label* and names should be fairly self explanatory.
150 #
151 # Once these attributes are set, clabel passes control to the labels()
152 # method (for automatic label placement) or blocking_input_loop and
153 # _contour_labeler_event_handler (for manual label placement).
154
155 if fmt is None:
156 fmt = ticker.ScalarFormatter(useOffset=False)
157 fmt.create_dummy_axis()
158 self.labelFmt = fmt
159 self._use_clabeltext = use_clabeltext
160 self.labelManual = manual
161 self.rightside_up = rightside_up
162 self._clabel_zorder = 2 + self.get_zorder() if zorder is None else zorder
163
164 if levels is None:
165 levels = self.levels
166 indices = list(range(len(self.cvalues)))
167 else:
168 levlabs = list(levels)
169 indices, levels = [], []
170 for i, lev in enumerate(self.levels):
171 if lev in levlabs:
172 indices.append(i)
173 levels.append(lev)
174 if len(levels) < len(levlabs):
175 raise ValueError(f"Specified levels {levlabs} don't match "
176 f"available levels {self.levels}")
177 self.labelLevelList = levels
178 self.labelIndiceList = indices
179
180 self._label_font_props = font_manager.FontProperties(size=fontsize)
181
182 if colors is None:
183 self.labelMappable = self
184 self.labelCValueList = np.take(self.cvalues, self.labelIndiceList)
185 else:
186 cmap = mcolors.ListedColormap(colors, N=len(self.labelLevelList))
187 self.labelCValueList = list(range(len(self.labelLevelList)))
188 self.labelMappable = cm.ScalarMappable(cmap=cmap,
189 norm=mcolors.NoNorm())
190
191 self.labelXYs = []
192
193 if np.iterable(manual):
194 for x, y in manual:
195 self.add_label_near(x, y, inline, inline_spacing)
196 elif manual:
197 print('Select label locations manually using first mouse button.')
198 print('End manual selection with second mouse button.')
199 if not inline:
200 print('Remove last label by clicking third mouse button.')
201 mpl._blocking_input.blocking_input_loop(
202 self.axes.figure, ["button_press_event", "key_press_event"],
203 timeout=-1, handler=functools.partial(
204 _contour_labeler_event_handler,
205 self, inline, inline_spacing))
206 else:
207 self.labels(inline, inline_spacing)
208
209 return cbook.silent_list('text.Text', self.labelTexts)
210
211 def print_label(self, linecontour, labelwidth):
212 """Return whether a contour is long enough to hold a label."""
213 return (len(linecontour) > 10 * labelwidth
214 or (len(linecontour)
215 and (np.ptp(linecontour, axis=0) > 1.2 * labelwidth).any()))
216
217 def too_close(self, x, y, lw):
218 """Return whether a label is already near this location."""
219 thresh = (1.2 * lw) ** 2
220 return any((x - loc[0]) ** 2 + (y - loc[1]) ** 2 < thresh
221 for loc in self.labelXYs)
222
223 def _get_nth_label_width(self, nth):
224 """Return the width of the *nth* label, in pixels."""
225 fig = self.axes.figure
226 renderer = fig._get_renderer()
227 return (Text(0, 0,
228 self.get_text(self.labelLevelList[nth], self.labelFmt),
229 figure=fig, fontproperties=self._label_font_props)
230 .get_window_extent(renderer).width)
231
232 def get_text(self, lev, fmt):
233 """Get the text of the label."""
234 if isinstance(lev, str):
235 return lev
236 elif isinstance(fmt, dict):
237 return fmt.get(lev, '%1.3f')
238 elif callable(getattr(fmt, "format_ticks", None)):
239 return fmt.format_ticks([*self.labelLevelList, lev])[-1]
240 elif callable(fmt):
241 return fmt(lev)
242 else:
243 return fmt % lev
244
245 def locate_label(self, linecontour, labelwidth):
246 """
247 Find good place to draw a label (relatively flat part of the contour).
248 """
249 ctr_size = len(linecontour)
250 n_blocks = int(np.ceil(ctr_size / labelwidth)) if labelwidth > 1 else 1
251 block_size = ctr_size if n_blocks == 1 else int(labelwidth)
252 # Split contour into blocks of length ``block_size``, filling the last
253 # block by cycling the contour start (per `np.resize` semantics). (Due
254 # to cycling, the index returned is taken modulo ctr_size.)
255 xx = np.resize(linecontour[:, 0], (n_blocks, block_size))
256 yy = np.resize(linecontour[:, 1], (n_blocks, block_size))
257 yfirst = yy[:, :1]
258 ylast = yy[:, -1:]
259 xfirst = xx[:, :1]
260 xlast = xx[:, -1:]
261 s = (yfirst - yy) * (xlast - xfirst) - (xfirst - xx) * (ylast - yfirst)
262 l = np.hypot(xlast - xfirst, ylast - yfirst)
263 # Ignore warning that divide by zero throws, as this is a valid option
264 with np.errstate(divide='ignore', invalid='ignore'):
265 distances = (abs(s) / l).sum(axis=-1)
266 # Labels are drawn in the middle of the block (``hbsize``) where the
267 # contour is the closest (per ``distances``) to a straight line, but
268 # not `too_close()` to a preexisting label.
269 hbsize = block_size // 2
270 adist = np.argsort(distances)
271 # If all candidates are `too_close()`, go back to the straightest part
272 # (``adist[0]``).
273 for idx in np.append(adist, adist[0]):
274 x, y = xx[idx, hbsize], yy[idx, hbsize]
275 if not self.too_close(x, y, labelwidth):
276 break
277 return x, y, (idx * block_size + hbsize) % ctr_size
278
279 def _split_path_and_get_label_rotation(self, path, idx, screen_pos, lw, spacing=5):
280 """
281 Prepare for insertion of a label at index *idx* of *path*.
282
283 Parameters
284 ----------
285 path : Path
286 The path where the label will be inserted, in data space.
287 idx : int
288 The vertex index after which the label will be inserted.
289 screen_pos : (float, float)
290 The position where the label will be inserted, in screen space.
291 lw : float
292 The label width, in screen space.
293 spacing : float
294 Extra spacing around the label, in screen space.
295
296 Returns
297 -------
298 path : Path
299 The path, broken so that the label can be drawn over it.
300 angle : float
301 The rotation of the label.
302
303 Notes
304 -----
305 Both tasks are done together to avoid calculating path lengths multiple times,
306 which is relatively costly.
307
308 The method used here involves computing the path length along the contour in
309 pixel coordinates and then looking (label width / 2) away from central point to
310 determine rotation and then to break contour if desired. The extra spacing is
311 taken into account when breaking the path, but not when computing the angle.
312 """
313 if hasattr(self, "_old_style_split_collections"):
314 vis = False
315 for coll in self._old_style_split_collections:
316 vis |= coll.get_visible()
317 coll.remove()
318 self.set_visible(vis)
319 del self._old_style_split_collections # Invalidate them.
320
321 xys = path.vertices
322 codes = path.codes
323
324 # Insert a vertex at idx/pos (converting back to data space), if there isn't yet
325 # a vertex there. With infinite precision one could also always insert the
326 # extra vertex (it will get masked out by the label below anyways), but floating
327 # point inaccuracies (the point can have undergone a data->screen->data
328 # transform loop) can slightly shift the point and e.g. shift the angle computed
329 # below from exactly zero to nonzero.
330 pos = self.get_transform().inverted().transform(screen_pos)
331 if not np.allclose(pos, xys[idx]):
332 xys = np.insert(xys, idx, pos, axis=0)
333 codes = np.insert(codes, idx, Path.LINETO)
334
335 # Find the connected component where the label will be inserted. Note that a
336 # path always starts with a MOVETO, and we consider there's an implicit
337 # MOVETO (closing the last path) at the end.
338 movetos = (codes == Path.MOVETO).nonzero()[0]
339 start = movetos[movetos <= idx][-1]
340 try:
341 stop = movetos[movetos > idx][0]
342 except IndexError:
343 stop = len(codes)
344
345 # Restrict ourselves to the connected component.
346 cc_xys = xys[start:stop]
347 idx -= start
348
349 # If the path is closed, rotate it s.t. it starts at the label.
350 is_closed_path = codes[stop - 1] == Path.CLOSEPOLY
351 if is_closed_path:
352 cc_xys = np.concatenate([cc_xys[idx:-1], cc_xys[:idx+1]])
353 idx = 0
354
355 # Like np.interp, but additionally vectorized over fp.
356 def interp_vec(x, xp, fp): return [np.interp(x, xp, col) for col in fp.T]
357
358 # Use cumulative path lengths ("cpl") as curvilinear coordinate along contour.
359 screen_xys = self.get_transform().transform(cc_xys)
360 path_cpls = np.insert(
361 np.cumsum(np.hypot(*np.diff(screen_xys, axis=0).T)), 0, 0)
362 path_cpls -= path_cpls[idx]
363
364 # Use linear interpolation to get end coordinates of label.
365 target_cpls = np.array([-lw/2, lw/2])
366 if is_closed_path: # For closed paths, target from the other end.
367 target_cpls[0] += (path_cpls[-1] - path_cpls[0])
368 (sx0, sx1), (sy0, sy1) = interp_vec(target_cpls, path_cpls, screen_xys)
369 angle = np.rad2deg(np.arctan2(sy1 - sy0, sx1 - sx0)) # Screen space.
370 if self.rightside_up: # Fix angle so text is never upside-down
371 angle = (angle + 90) % 180 - 90
372
373 target_cpls += [-spacing, +spacing] # Expand range by spacing.
374
375 # Get indices near points of interest; use -1 as out of bounds marker.
376 i0, i1 = np.interp(target_cpls, path_cpls, range(len(path_cpls)),
377 left=-1, right=-1)
378 i0 = math.floor(i0)
379 i1 = math.ceil(i1)
380 (x0, x1), (y0, y1) = interp_vec(target_cpls, path_cpls, cc_xys)
381
382 # Actually break contours (dropping zero-len parts).
383 new_xy_blocks = []
384 new_code_blocks = []
385 if is_closed_path:
386 if i0 != -1 and i1 != -1:
387 # This is probably wrong in the case that the entire contour would
388 # be discarded, but ensures that a valid path is returned and is
389 # consistent with behavior of mpl <3.8
390 points = cc_xys[i1:i0+1]
391 new_xy_blocks.extend([[(x1, y1)], points, [(x0, y0)]])
392 nlines = len(points) + 1
393 new_code_blocks.extend([[Path.MOVETO], [Path.LINETO] * nlines])
394 else:
395 if i0 != -1:
396 new_xy_blocks.extend([cc_xys[:i0 + 1], [(x0, y0)]])
397 new_code_blocks.extend([[Path.MOVETO], [Path.LINETO] * (i0 + 1)])
398 if i1 != -1:
399 new_xy_blocks.extend([[(x1, y1)], cc_xys[i1:]])
400 new_code_blocks.extend([
401 [Path.MOVETO], [Path.LINETO] * (len(cc_xys) - i1)])
402
403 # Back to the full path.
404 xys = np.concatenate([xys[:start], *new_xy_blocks, xys[stop:]])
405 codes = np.concatenate([codes[:start], *new_code_blocks, codes[stop:]])
406
407 return angle, Path(xys, codes)
408
409 @_api.deprecated("3.8")
410 def calc_label_rot_and_inline(self, slc, ind, lw, lc=None, spacing=5):
411 """
412 Calculate the appropriate label rotation given the linecontour
413 coordinates in screen units, the index of the label location and the
414 label width.
415
416 If *lc* is not None or empty, also break contours and compute
417 inlining.
418
419 *spacing* is the empty space to leave around the label, in pixels.
420
421 Both tasks are done together to avoid calculating path lengths
422 multiple times, which is relatively costly.
423
424 The method used here involves computing the path length along the
425 contour in pixel coordinates and then looking approximately (label
426 width / 2) away from central point to determine rotation and then to
427 break contour if desired.
428 """
429
430 if lc is None:
431 lc = []
432 # Half the label width
433 hlw = lw / 2.0
434
435 # Check if closed and, if so, rotate contour so label is at edge
436 closed = _is_closed_polygon(slc)
437 if closed:
438 slc = np.concatenate([slc[ind:-1], slc[:ind + 1]])
439 if len(lc): # Rotate lc also if not empty
440 lc = np.concatenate([lc[ind:-1], lc[:ind + 1]])
441 ind = 0
442
443 # Calculate path lengths
444 pl = np.zeros(slc.shape[0], dtype=float)
445 dx = np.diff(slc, axis=0)
446 pl[1:] = np.cumsum(np.hypot(dx[:, 0], dx[:, 1]))
447 pl = pl - pl[ind]
448
449 # Use linear interpolation to get points around label
450 xi = np.array([-hlw, hlw])
451 if closed: # Look at end also for closed contours
452 dp = np.array([pl[-1], 0])
453 else:
454 dp = np.zeros_like(xi)
455
456 # Get angle of vector between the two ends of the label - must be
457 # calculated in pixel space for text rotation to work correctly.
458 (dx,), (dy,) = (np.diff(np.interp(dp + xi, pl, slc_col))
459 for slc_col in slc.T)
460 rotation = np.rad2deg(np.arctan2(dy, dx))
461
462 if self.rightside_up:
463 # Fix angle so text is never upside-down
464 rotation = (rotation + 90) % 180 - 90
465
466 # Break contour if desired
467 nlc = []
468 if len(lc):
469 # Expand range by spacing
470 xi = dp + xi + np.array([-spacing, spacing])
471
472 # Get (integer) indices near points of interest; use -1 as marker
473 # for out of bounds.
474 I = np.interp(xi, pl, np.arange(len(pl)), left=-1, right=-1)
475 I = [np.floor(I[0]).astype(int), np.ceil(I[1]).astype(int)]
476 if I[0] != -1:
477 xy1 = [np.interp(xi[0], pl, lc_col) for lc_col in lc.T]
478 if I[1] != -1:
479 xy2 = [np.interp(xi[1], pl, lc_col) for lc_col in lc.T]
480
481 # Actually break contours
482 if closed:
483 # This will remove contour if shorter than label
484 if all(i != -1 for i in I):
485 nlc.append(np.vstack([xy2, lc[I[1]:I[0]+1], xy1]))
486 else:
487 # These will remove pieces of contour if they have length zero
488 if I[0] != -1:
489 nlc.append(np.vstack([lc[:I[0]+1], xy1]))
490 if I[1] != -1:
491 nlc.append(np.vstack([xy2, lc[I[1]:]]))
492
493 # The current implementation removes contours completely
494 # covered by labels. Uncomment line below to keep
495 # original contour if this is the preferred behavior.
496 # if not len(nlc): nlc = [lc]
497
498 return rotation, nlc
499
500 def add_label(self, x, y, rotation, lev, cvalue):
501 """Add a contour label, respecting whether *use_clabeltext* was set."""
502 data_x, data_y = self.axes.transData.inverted().transform((x, y))
503 t = Text(
504 data_x, data_y,
505 text=self.get_text(lev, self.labelFmt),
506 rotation=rotation,
507 horizontalalignment='center', verticalalignment='center',
508 zorder=self._clabel_zorder,
509 color=self.labelMappable.to_rgba(cvalue, alpha=self.get_alpha()),
510 fontproperties=self._label_font_props,
511 clip_box=self.axes.bbox)
512 if self._use_clabeltext:
513 data_rotation, = self.axes.transData.inverted().transform_angles(
514 [rotation], [[x, y]])
515 t.set(rotation=data_rotation, transform_rotates_text=True)
516 self.labelTexts.append(t)
517 self.labelCValues.append(cvalue)
518 self.labelXYs.append((x, y))
519 # Add label to plot here - useful for manual mode label selection
520 self.axes.add_artist(t)
521
522 @_api.deprecated("3.8", alternative="add_label")
523 def add_label_clabeltext(self, x, y, rotation, lev, cvalue):
524 """Add contour label with `.Text.set_transform_rotates_text`."""
525 with cbook._setattr_cm(self, _use_clabeltext=True):
526 self.add_label(x, y, rotation, lev, cvalue)
527
528 def add_label_near(self, x, y, inline=True, inline_spacing=5,
529 transform=None):
530 """
531 Add a label near the point ``(x, y)``.
532
533 Parameters
534 ----------
535 x, y : float
536 The approximate location of the label.
537 inline : bool, default: True
538 If *True* remove the segment of the contour beneath the label.
539 inline_spacing : int, default: 5
540 Space in pixels to leave on each side of label when placing
541 inline. This spacing will be exact for labels at locations where
542 the contour is straight, less so for labels on curved contours.
543 transform : `.Transform` or `False`, default: ``self.axes.transData``
544 A transform applied to ``(x, y)`` before labeling. The default
545 causes ``(x, y)`` to be interpreted as data coordinates. `False`
546 is a synonym for `.IdentityTransform`; i.e. ``(x, y)`` should be
547 interpreted as display coordinates.
548 """
549
550 if transform is None:
551 transform = self.axes.transData
552 if transform:
553 x, y = transform.transform((x, y))
554
555 idx_level_min, idx_vtx_min, proj = self._find_nearest_contour(
556 (x, y), self.labelIndiceList)
557 path = self._paths[idx_level_min]
558 level = self.labelIndiceList.index(idx_level_min)
559 label_width = self._get_nth_label_width(level)
560 rotation, path = self._split_path_and_get_label_rotation(
561 path, idx_vtx_min, proj, label_width, inline_spacing)
562 self.add_label(*proj, rotation, self.labelLevelList[idx_level_min],
563 self.labelCValueList[idx_level_min])
564
565 if inline:
566 self._paths[idx_level_min] = path
567
568 def pop_label(self, index=-1):
569 """Defaults to removing last label, but any index can be supplied"""
570 self.labelCValues.pop(index)
571 t = self.labelTexts.pop(index)
572 t.remove()
573
574 def labels(self, inline, inline_spacing):
575 for idx, (icon, lev, cvalue) in enumerate(zip(
576 self.labelIndiceList,
577 self.labelLevelList,
578 self.labelCValueList,
579 )):
580 trans = self.get_transform()
581 label_width = self._get_nth_label_width(idx)
582 additions = []
583 for subpath in self._paths[icon]._iter_connected_components():
584 screen_xys = trans.transform(subpath.vertices)
585 # Check if long enough for a label
586 if self.print_label(screen_xys, label_width):
587 x, y, idx = self.locate_label(screen_xys, label_width)
588 rotation, path = self._split_path_and_get_label_rotation(
589 subpath, idx, (x, y),
590 label_width, inline_spacing)
591 self.add_label(x, y, rotation, lev, cvalue) # Really add label.
592 if inline: # If inline, add new contours
593 additions.append(path)
594 else: # If not adding label, keep old path
595 additions.append(subpath)
596 # After looping over all segments on a contour, replace old path by new one
597 # if inlining.
598 if inline:
599 self._paths[icon] = Path.make_compound_path(*additions)
600
601 def remove(self):
602 super().remove()
603 for text in self.labelTexts:
604 text.remove()
605
606
607def _is_closed_polygon(X):
608 """
609 Return whether first and last object in a sequence are the same. These are
610 presumably coordinates on a polygonal curve, in which case this function
611 tests if that curve is closed.
612 """
613 return np.allclose(X[0], X[-1], rtol=1e-10, atol=1e-13)
614
615
616def _find_closest_point_on_path(xys, p):
617 """
618 Parameters
619 ----------
620 xys : (N, 2) array-like
621 Coordinates of vertices.
622 p : (float, float)
623 Coordinates of point.
624
625 Returns
626 -------
627 d2min : float
628 Minimum square distance of *p* to *xys*.
629 proj : (float, float)
630 Projection of *p* onto *xys*.
631 imin : (int, int)
632 Consecutive indices of vertices of segment in *xys* where *proj* is.
633 Segments are considered as including their end-points; i.e. if the
634 closest point on the path is a node in *xys* with index *i*, this
635 returns ``(i-1, i)``. For the special case where *xys* is a single
636 point, this returns ``(0, 0)``.
637 """
638 if len(xys) == 1:
639 return (((p - xys[0]) ** 2).sum(), xys[0], (0, 0))
640 dxys = xys[1:] - xys[:-1] # Individual segment vectors.
641 norms = (dxys ** 2).sum(axis=1)
642 norms[norms == 0] = 1 # For zero-length segment, replace 0/0 by 0/1.
643 rel_projs = np.clip( # Project onto each segment in relative 0-1 coords.
644 ((p - xys[:-1]) * dxys).sum(axis=1) / norms,
645 0, 1)[:, None]
646 projs = xys[:-1] + rel_projs * dxys # Projs. onto each segment, in (x, y).
647 d2s = ((projs - p) ** 2).sum(axis=1) # Squared distances.
648 imin = np.argmin(d2s)
649 return (d2s[imin], projs[imin], (imin, imin+1))
650
651
652_docstring.interpd.update(contour_set_attributes=r"""
653Attributes
654----------
655ax : `~matplotlib.axes.Axes`
656 The Axes object in which the contours are drawn.
657
658collections : `.silent_list` of `.PathCollection`\s
659 The `.Artist`\s representing the contour. This is a list of
660 `.PathCollection`\s for both line and filled contours.
661
662levels : array
663 The values of the contour levels.
664
665layers : array
666 Same as levels for line contours; half-way between
667 levels for filled contours. See ``ContourSet._process_colors``.
668""")
669
670
671@_docstring.dedent_interpd
672class ContourSet(ContourLabeler, mcoll.Collection):
673 """
674 Store a set of contour lines or filled regions.
675
676 User-callable method: `~.Axes.clabel`
677
678 Parameters
679 ----------
680 ax : `~matplotlib.axes.Axes`
681
682 levels : [level0, level1, ..., leveln]
683 A list of floating point numbers indicating the contour levels.
684
685 allsegs : [level0segs, level1segs, ...]
686 List of all the polygon segments for all the *levels*.
687 For contour lines ``len(allsegs) == len(levels)``, and for
688 filled contour regions ``len(allsegs) = len(levels)-1``. The lists
689 should look like ::
690
691 level0segs = [polygon0, polygon1, ...]
692 polygon0 = [[x0, y0], [x1, y1], ...]
693
694 allkinds : ``None`` or [level0kinds, level1kinds, ...]
695 Optional list of all the polygon vertex kinds (code types), as
696 described and used in Path. This is used to allow multiply-
697 connected paths such as holes within filled polygons.
698 If not ``None``, ``len(allkinds) == len(allsegs)``. The lists
699 should look like ::
700
701 level0kinds = [polygon0kinds, ...]
702 polygon0kinds = [vertexcode0, vertexcode1, ...]
703
704 If *allkinds* is not ``None``, usually all polygons for a
705 particular contour level are grouped together so that
706 ``level0segs = [polygon0]`` and ``level0kinds = [polygon0kinds]``.
707
708 **kwargs
709 Keyword arguments are as described in the docstring of
710 `~.Axes.contour`.
711
712 %(contour_set_attributes)s
713 """
714
715 def __init__(self, ax, *args,
716 levels=None, filled=False, linewidths=None, linestyles=None,
717 hatches=(None,), alpha=None, origin=None, extent=None,
718 cmap=None, colors=None, norm=None, vmin=None, vmax=None,
719 extend='neither', antialiased=None, nchunk=0, locator=None,
720 transform=None, negative_linestyles=None, clip_path=None,
721 **kwargs):
722 """
723 Draw contour lines or filled regions, depending on
724 whether keyword arg *filled* is ``False`` (default) or ``True``.
725
726 Call signature::
727
728 ContourSet(ax, levels, allsegs, [allkinds], **kwargs)
729
730 Parameters
731 ----------
732 ax : `~matplotlib.axes.Axes`
733 The `~.axes.Axes` object to draw on.
734
735 levels : [level0, level1, ..., leveln]
736 A list of floating point numbers indicating the contour
737 levels.
738
739 allsegs : [level0segs, level1segs, ...]
740 List of all the polygon segments for all the *levels*.
741 For contour lines ``len(allsegs) == len(levels)``, and for
742 filled contour regions ``len(allsegs) = len(levels)-1``. The lists
743 should look like ::
744
745 level0segs = [polygon0, polygon1, ...]
746 polygon0 = [[x0, y0], [x1, y1], ...]
747
748 allkinds : [level0kinds, level1kinds, ...], optional
749 Optional list of all the polygon vertex kinds (code types), as
750 described and used in Path. This is used to allow multiply-
751 connected paths such as holes within filled polygons.
752 If not ``None``, ``len(allkinds) == len(allsegs)``. The lists
753 should look like ::
754
755 level0kinds = [polygon0kinds, ...]
756 polygon0kinds = [vertexcode0, vertexcode1, ...]
757
758 If *allkinds* is not ``None``, usually all polygons for a
759 particular contour level are grouped together so that
760 ``level0segs = [polygon0]`` and ``level0kinds = [polygon0kinds]``.
761
762 **kwargs
763 Keyword arguments are as described in the docstring of
764 `~.Axes.contour`.
765 """
766 if antialiased is None and filled:
767 # Eliminate artifacts; we are not stroking the boundaries.
768 antialiased = False
769 # The default for line contours will be taken from the
770 # LineCollection default, which uses :rc:`lines.antialiased`.
771 super().__init__(
772 antialiaseds=antialiased,
773 alpha=alpha,
774 clip_path=clip_path,
775 transform=transform,
776 )
777 self.axes = ax
778 self.levels = levels
779 self.filled = filled
780 self.hatches = hatches
781 self.origin = origin
782 self.extent = extent
783 self.colors = colors
784 self.extend = extend
785
786 self.nchunk = nchunk
787 self.locator = locator
788 if (isinstance(norm, mcolors.LogNorm)
789 or isinstance(self.locator, ticker.LogLocator)):
790 self.logscale = True
791 if norm is None:
792 norm = mcolors.LogNorm()
793 else:
794 self.logscale = False
795
796 _api.check_in_list([None, 'lower', 'upper', 'image'], origin=origin)
797 if self.extent is not None and len(self.extent) != 4:
798 raise ValueError(
799 "If given, 'extent' must be None or (x0, x1, y0, y1)")
800 if self.colors is not None and cmap is not None:
801 raise ValueError('Either colors or cmap must be None')
802 if self.origin == 'image':
803 self.origin = mpl.rcParams['image.origin']
804
805 self._orig_linestyles = linestyles # Only kept for user access.
806 self.negative_linestyles = negative_linestyles
807 # If negative_linestyles was not defined as a keyword argument, define
808 # negative_linestyles with rcParams
809 if self.negative_linestyles is None:
810 self.negative_linestyles = \
811 mpl.rcParams['contour.negative_linestyle']
812
813 kwargs = self._process_args(*args, **kwargs)
814 self._process_levels()
815
816 self._extend_min = self.extend in ['min', 'both']
817 self._extend_max = self.extend in ['max', 'both']
818 if self.colors is not None:
819 ncolors = len(self.levels)
820 if self.filled:
821 ncolors -= 1
822 i0 = 0
823
824 # Handle the case where colors are given for the extended
825 # parts of the contour.
826
827 use_set_under_over = False
828 # if we are extending the lower end, and we've been given enough
829 # colors then skip the first color in the resulting cmap. For the
830 # extend_max case we don't need to worry about passing more colors
831 # than ncolors as ListedColormap will clip.
832 total_levels = (ncolors +
833 int(self._extend_min) +
834 int(self._extend_max))
835 if (len(self.colors) == total_levels and
836 (self._extend_min or self._extend_max)):
837 use_set_under_over = True
838 if self._extend_min:
839 i0 = 1
840
841 cmap = mcolors.ListedColormap(self.colors[i0:None], N=ncolors)
842
843 if use_set_under_over:
844 if self._extend_min:
845 cmap.set_under(self.colors[0])
846 if self._extend_max:
847 cmap.set_over(self.colors[-1])
848
849 # label lists must be initialized here
850 self.labelTexts = []
851 self.labelCValues = []
852
853 self.set_cmap(cmap)
854 if norm is not None:
855 self.set_norm(norm)
856 with self.norm.callbacks.blocked(signal="changed"):
857 if vmin is not None:
858 self.norm.vmin = vmin
859 if vmax is not None:
860 self.norm.vmax = vmax
861 self.norm._changed()
862 self._process_colors()
863
864 if self._paths is None:
865 self._paths = self._make_paths_from_contour_generator()
866
867 if self.filled:
868 if linewidths is not None:
869 _api.warn_external('linewidths is ignored by contourf')
870 # Lower and upper contour levels.
871 lowers, uppers = self._get_lowers_and_uppers()
872 self.set(
873 edgecolor="none",
874 # Default zorder taken from Collection
875 zorder=kwargs.pop("zorder", 1),
876 )
877
878 else:
879 self.set(
880 facecolor="none",
881 linewidths=self._process_linewidths(linewidths),
882 linestyle=self._process_linestyles(linestyles),
883 # Default zorder taken from LineCollection, which is higher
884 # than for filled contours so that lines are displayed on top.
885 zorder=kwargs.pop("zorder", 2),
886 label="_nolegend_",
887 )
888
889 self.axes.add_collection(self, autolim=False)
890 self.sticky_edges.x[:] = [self._mins[0], self._maxs[0]]
891 self.sticky_edges.y[:] = [self._mins[1], self._maxs[1]]
892 self.axes.update_datalim([self._mins, self._maxs])
893 self.axes.autoscale_view(tight=True)
894
895 self.changed() # set the colors
896
897 if kwargs:
898 _api.warn_external(
899 'The following kwargs were not used by contour: ' +
900 ", ".join(map(repr, kwargs))
901 )
902
903 allsegs = property(lambda self: [
904 [subp.vertices for subp in p._iter_connected_components()]
905 for p in self.get_paths()])
906 allkinds = property(lambda self: [
907 [subp.codes for subp in p._iter_connected_components()]
908 for p in self.get_paths()])
909 tcolors = _api.deprecated("3.8")(property(lambda self: [
910 (tuple(rgba),) for rgba in self.to_rgba(self.cvalues, self.alpha)]))
911 tlinewidths = _api.deprecated("3.8")(property(lambda self: [
912 (w,) for w in self.get_linewidths()]))
913 alpha = property(lambda self: self.get_alpha())
914 linestyles = property(lambda self: self._orig_linestyles)
915
916 @_api.deprecated("3.8", alternative="set_antialiased or get_antialiased",
917 addendum="Note that get_antialiased returns an array.")
918 @property
919 def antialiased(self):
920 return all(self.get_antialiased())
921
922 @antialiased.setter
923 def antialiased(self, aa):
924 self.set_antialiased(aa)
925
926 @_api.deprecated("3.8")
927 @property
928 def collections(self):
929 # On access, make oneself invisible and instead add the old-style collections
930 # (one PathCollection per level). We do not try to further split contours into
931 # connected components as we already lost track of what pairs of contours need
932 # to be considered as single units to draw filled regions with holes.
933 if not hasattr(self, "_old_style_split_collections"):
934 self.set_visible(False)
935 fcs = self.get_facecolor()
936 ecs = self.get_edgecolor()
937 lws = self.get_linewidth()
938 lss = self.get_linestyle()
939 self._old_style_split_collections = []
940 for idx, path in enumerate(self._paths):
941 pc = mcoll.PathCollection(
942 [path] if len(path.vertices) else [],
943 alpha=self.get_alpha(),
944 antialiaseds=self._antialiaseds[idx % len(self._antialiaseds)],
945 transform=self.get_transform(),
946 zorder=self.get_zorder(),
947 label="_nolegend_",
948 facecolor=fcs[idx] if len(fcs) else "none",
949 edgecolor=ecs[idx] if len(ecs) else "none",
950 linewidths=[lws[idx % len(lws)]],
951 linestyles=[lss[idx % len(lss)]],
952 )
953 if self.filled:
954 pc.set(hatch=self.hatches[idx % len(self.hatches)])
955 self._old_style_split_collections.append(pc)
956 for col in self._old_style_split_collections:
957 self.axes.add_collection(col)
958 return self._old_style_split_collections
959
960 def get_transform(self):
961 """Return the `.Transform` instance used by this ContourSet."""
962 if self._transform is None:
963 self._transform = self.axes.transData
964 elif (not isinstance(self._transform, mtransforms.Transform)
965 and hasattr(self._transform, '_as_mpl_transform')):
966 self._transform = self._transform._as_mpl_transform(self.axes)
967 return self._transform
968
969 def __getstate__(self):
970 state = self.__dict__.copy()
971 # the C object _contour_generator cannot currently be pickled. This
972 # isn't a big issue as it is not actually used once the contour has
973 # been calculated.
974 state['_contour_generator'] = None
975 return state
976
977 def legend_elements(self, variable_name='x', str_format=str):
978 """
979 Return a list of artists and labels suitable for passing through
980 to `~.Axes.legend` which represent this ContourSet.
981
982 The labels have the form "0 < x <= 1" stating the data ranges which
983 the artists represent.
984
985 Parameters
986 ----------
987 variable_name : str
988 The string used inside the inequality used on the labels.
989 str_format : function: float -> str
990 Function used to format the numbers in the labels.
991
992 Returns
993 -------
994 artists : list[`.Artist`]
995 A list of the artists.
996 labels : list[str]
997 A list of the labels.
998 """
999 artists = []
1000 labels = []
1001
1002 if self.filled:
1003 lowers, uppers = self._get_lowers_and_uppers()
1004 n_levels = len(self._paths)
1005 for idx in range(n_levels):
1006 artists.append(mpatches.Rectangle(
1007 (0, 0), 1, 1,
1008 facecolor=self.get_facecolor()[idx],
1009 hatch=self.hatches[idx % len(self.hatches)],
1010 ))
1011 lower = str_format(lowers[idx])
1012 upper = str_format(uppers[idx])
1013 if idx == 0 and self.extend in ('min', 'both'):
1014 labels.append(fr'${variable_name} \leq {lower}s$')
1015 elif idx == n_levels - 1 and self.extend in ('max', 'both'):
1016 labels.append(fr'${variable_name} > {upper}s$')
1017 else:
1018 labels.append(fr'${lower} < {variable_name} \leq {upper}$')
1019 else:
1020 for idx, level in enumerate(self.levels):
1021 artists.append(Line2D(
1022 [], [],
1023 color=self.get_edgecolor()[idx],
1024 linewidth=self.get_linewidths()[idx],
1025 linestyle=self.get_linestyles()[idx],
1026 ))
1027 labels.append(fr'${variable_name} = {str_format(level)}$')
1028
1029 return artists, labels
1030
1031 def _process_args(self, *args, **kwargs):
1032 """
1033 Process *args* and *kwargs*; override in derived classes.
1034
1035 Must set self.levels, self.zmin and self.zmax, and update Axes limits.
1036 """
1037 self.levels = args[0]
1038 allsegs = args[1]
1039 allkinds = args[2] if len(args) > 2 else None
1040 self.zmax = np.max(self.levels)
1041 self.zmin = np.min(self.levels)
1042
1043 if allkinds is None:
1044 allkinds = [[None] * len(segs) for segs in allsegs]
1045
1046 # Check lengths of levels and allsegs.
1047 if self.filled:
1048 if len(allsegs) != len(self.levels) - 1:
1049 raise ValueError('must be one less number of segments as '
1050 'levels')
1051 else:
1052 if len(allsegs) != len(self.levels):
1053 raise ValueError('must be same number of segments as levels')
1054
1055 # Check length of allkinds.
1056 if len(allkinds) != len(allsegs):
1057 raise ValueError('allkinds has different length to allsegs')
1058
1059 # Determine x, y bounds and update axes data limits.
1060 flatseglist = [s for seg in allsegs for s in seg]
1061 points = np.concatenate(flatseglist, axis=0)
1062 self._mins = points.min(axis=0)
1063 self._maxs = points.max(axis=0)
1064
1065 # Each entry in (allsegs, allkinds) is a list of (segs, kinds): segs is a list
1066 # of (N, 2) arrays of xy coordinates, kinds is a list of arrays of corresponding
1067 # pathcodes. However, kinds can also be None; in which case all paths in that
1068 # list are codeless (this case is normalized above). These lists are used to
1069 # construct paths, which then get concatenated.
1070 self._paths = [Path.make_compound_path(*map(Path, segs, kinds))
1071 for segs, kinds in zip(allsegs, allkinds)]
1072
1073 return kwargs
1074
1075 def _make_paths_from_contour_generator(self):
1076 """Compute ``paths`` using C extension."""
1077 if self._paths is not None:
1078 return self._paths
1079 cg = self._contour_generator
1080 empty_path = Path(np.empty((0, 2)))
1081 vertices_and_codes = (
1082 map(cg.create_filled_contour, *self._get_lowers_and_uppers())
1083 if self.filled else
1084 map(cg.create_contour, self.levels))
1085 return [Path(np.concatenate(vs), np.concatenate(cs)) if len(vs) else empty_path
1086 for vs, cs in vertices_and_codes]
1087
1088 def _get_lowers_and_uppers(self):
1089 """
1090 Return ``(lowers, uppers)`` for filled contours.
1091 """
1092 lowers = self._levels[:-1]
1093 if self.zmin == lowers[0]:
1094 # Include minimum values in lowest interval
1095 lowers = lowers.copy() # so we don't change self._levels
1096 if self.logscale:
1097 lowers[0] = 0.99 * self.zmin
1098 else:
1099 lowers[0] -= 1
1100 uppers = self._levels[1:]
1101 return (lowers, uppers)
1102
1103 def changed(self):
1104 if not hasattr(self, "cvalues"):
1105 self._process_colors() # Sets cvalues.
1106 # Force an autoscale immediately because self.to_rgba() calls
1107 # autoscale_None() internally with the data passed to it,
1108 # so if vmin/vmax are not set yet, this would override them with
1109 # content from *cvalues* rather than levels like we want
1110 self.norm.autoscale_None(self.levels)
1111 self.set_array(self.cvalues)
1112 self.update_scalarmappable()
1113 alphas = np.broadcast_to(self.get_alpha(), len(self.cvalues))
1114 for label, cv, alpha in zip(self.labelTexts, self.labelCValues, alphas):
1115 label.set_alpha(alpha)
1116 label.set_color(self.labelMappable.to_rgba(cv))
1117 super().changed()
1118
1119 def _autolev(self, N):
1120 """
1121 Select contour levels to span the data.
1122
1123 The target number of levels, *N*, is used only when the
1124 scale is not log and default locator is used.
1125
1126 We need two more levels for filled contours than for
1127 line contours, because for the latter we need to specify
1128 the lower and upper boundary of each range. For example,
1129 a single contour boundary, say at z = 0, requires only
1130 one contour line, but two filled regions, and therefore
1131 three levels to provide boundaries for both regions.
1132 """
1133 if self.locator is None:
1134 if self.logscale:
1135 self.locator = ticker.LogLocator()
1136 else:
1137 self.locator = ticker.MaxNLocator(N + 1, min_n_ticks=1)
1138
1139 lev = self.locator.tick_values(self.zmin, self.zmax)
1140
1141 try:
1142 if self.locator._symmetric:
1143 return lev
1144 except AttributeError:
1145 pass
1146
1147 # Trim excess levels the locator may have supplied.
1148 under = np.nonzero(lev < self.zmin)[0]
1149 i0 = under[-1] if len(under) else 0
1150 over = np.nonzero(lev > self.zmax)[0]
1151 i1 = over[0] + 1 if len(over) else len(lev)
1152 if self.extend in ('min', 'both'):
1153 i0 += 1
1154 if self.extend in ('max', 'both'):
1155 i1 -= 1
1156
1157 if i1 - i0 < 3:
1158 i0, i1 = 0, len(lev)
1159
1160 return lev[i0:i1]
1161
1162 def _process_contour_level_args(self, args, z_dtype):
1163 """
1164 Determine the contour levels and store in self.levels.
1165 """
1166 if self.levels is None:
1167 if args:
1168 levels_arg = args[0]
1169 elif np.issubdtype(z_dtype, bool):
1170 if self.filled:
1171 levels_arg = [0, .5, 1]
1172 else:
1173 levels_arg = [.5]
1174 else:
1175 levels_arg = 7 # Default, hard-wired.
1176 else:
1177 levels_arg = self.levels
1178 if isinstance(levels_arg, Integral):
1179 self.levels = self._autolev(levels_arg)
1180 else:
1181 self.levels = np.asarray(levels_arg, np.float64)
1182 if self.filled and len(self.levels) < 2:
1183 raise ValueError("Filled contours require at least 2 levels.")
1184 if len(self.levels) > 1 and np.min(np.diff(self.levels)) <= 0.0:
1185 raise ValueError("Contour levels must be increasing")
1186
1187 def _process_levels(self):
1188 """
1189 Assign values to :attr:`layers` based on :attr:`levels`,
1190 adding extended layers as needed if contours are filled.
1191
1192 For line contours, layers simply coincide with levels;
1193 a line is a thin layer. No extended levels are needed
1194 with line contours.
1195 """
1196 # Make a private _levels to include extended regions; we
1197 # want to leave the original levels attribute unchanged.
1198 # (Colorbar needs this even for line contours.)
1199 self._levels = list(self.levels)
1200
1201 if self.logscale:
1202 lower, upper = 1e-250, 1e250
1203 else:
1204 lower, upper = -1e250, 1e250
1205
1206 if self.extend in ('both', 'min'):
1207 self._levels.insert(0, lower)
1208 if self.extend in ('both', 'max'):
1209 self._levels.append(upper)
1210 self._levels = np.asarray(self._levels)
1211
1212 if not self.filled:
1213 self.layers = self.levels
1214 return
1215
1216 # Layer values are mid-way between levels in screen space.
1217 if self.logscale:
1218 # Avoid overflow by taking sqrt before multiplying.
1219 self.layers = (np.sqrt(self._levels[:-1])
1220 * np.sqrt(self._levels[1:]))
1221 else:
1222 self.layers = 0.5 * (self._levels[:-1] + self._levels[1:])
1223
1224 def _process_colors(self):
1225 """
1226 Color argument processing for contouring.
1227
1228 Note that we base the colormapping on the contour levels
1229 and layers, not on the actual range of the Z values. This
1230 means we don't have to worry about bad values in Z, and we
1231 always have the full dynamic range available for the selected
1232 levels.
1233
1234 The color is based on the midpoint of the layer, except for
1235 extended end layers. By default, the norm vmin and vmax
1236 are the extreme values of the non-extended levels. Hence,
1237 the layer color extremes are not the extreme values of
1238 the colormap itself, but approach those values as the number
1239 of levels increases. An advantage of this scheme is that
1240 line contours, when added to filled contours, take on
1241 colors that are consistent with those of the filled regions;
1242 for example, a contour line on the boundary between two
1243 regions will have a color intermediate between those
1244 of the regions.
1245
1246 """
1247 self.monochrome = self.cmap.monochrome
1248 if self.colors is not None:
1249 # Generate integers for direct indexing.
1250 i0, i1 = 0, len(self.levels)
1251 if self.filled:
1252 i1 -= 1
1253 # Out of range indices for over and under:
1254 if self.extend in ('both', 'min'):
1255 i0 -= 1
1256 if self.extend in ('both', 'max'):
1257 i1 += 1
1258 self.cvalues = list(range(i0, i1))
1259 self.set_norm(mcolors.NoNorm())
1260 else:
1261 self.cvalues = self.layers
1262 self.norm.autoscale_None(self.levels)
1263 self.set_array(self.cvalues)
1264 self.update_scalarmappable()
1265 if self.extend in ('both', 'max', 'min'):
1266 self.norm.clip = False
1267
1268 def _process_linewidths(self, linewidths):
1269 Nlev = len(self.levels)
1270 if linewidths is None:
1271 default_linewidth = mpl.rcParams['contour.linewidth']
1272 if default_linewidth is None:
1273 default_linewidth = mpl.rcParams['lines.linewidth']
1274 return [default_linewidth] * Nlev
1275 elif not np.iterable(linewidths):
1276 return [linewidths] * Nlev
1277 else:
1278 linewidths = list(linewidths)
1279 return (linewidths * math.ceil(Nlev / len(linewidths)))[:Nlev]
1280
1281 def _process_linestyles(self, linestyles):
1282 Nlev = len(self.levels)
1283 if linestyles is None:
1284 tlinestyles = ['solid'] * Nlev
1285 if self.monochrome:
1286 eps = - (self.zmax - self.zmin) * 1e-15
1287 for i, lev in enumerate(self.levels):
1288 if lev < eps:
1289 tlinestyles[i] = self.negative_linestyles
1290 else:
1291 if isinstance(linestyles, str):
1292 tlinestyles = [linestyles] * Nlev
1293 elif np.iterable(linestyles):
1294 tlinestyles = list(linestyles)
1295 if len(tlinestyles) < Nlev:
1296 nreps = int(np.ceil(Nlev / len(linestyles)))
1297 tlinestyles = tlinestyles * nreps
1298 if len(tlinestyles) > Nlev:
1299 tlinestyles = tlinestyles[:Nlev]
1300 else:
1301 raise ValueError("Unrecognized type for linestyles kwarg")
1302 return tlinestyles
1303
1304 def _find_nearest_contour(self, xy, indices=None):
1305 """
1306 Find the point in the unfilled contour plot that is closest (in screen
1307 space) to point *xy*.
1308
1309 Parameters
1310 ----------
1311 xy : tuple[float, float]
1312 The reference point (in screen space).
1313 indices : list of int or None, default: None
1314 Indices of contour levels to consider. If None (the default), all levels
1315 are considered.
1316
1317 Returns
1318 -------
1319 idx_level_min : int
1320 The index of the contour level closest to *xy*.
1321 idx_vtx_min : int
1322 The index of the `.Path` segment closest to *xy* (at that level).
1323 proj : (float, float)
1324 The point in the contour plot closest to *xy*.
1325 """
1326
1327 # Convert each contour segment to pixel coordinates and then compare the given
1328 # point to those coordinates for each contour. This is fast enough in normal
1329 # cases, but speedups may be possible.
1330
1331 if self.filled:
1332 raise ValueError("Method does not support filled contours")
1333
1334 if indices is None:
1335 indices = range(len(self._paths))
1336
1337 d2min = np.inf
1338 idx_level_min = idx_vtx_min = proj_min = None
1339
1340 for idx_level in indices:
1341 path = self._paths[idx_level]
1342 idx_vtx_start = 0
1343 for subpath in path._iter_connected_components():
1344 if not len(subpath.vertices):
1345 continue
1346 lc = self.get_transform().transform(subpath.vertices)
1347 d2, proj, leg = _find_closest_point_on_path(lc, xy)
1348 if d2 < d2min:
1349 d2min = d2
1350 idx_level_min = idx_level
1351 idx_vtx_min = leg[1] + idx_vtx_start
1352 proj_min = proj
1353 idx_vtx_start += len(subpath)
1354
1355 return idx_level_min, idx_vtx_min, proj_min
1356
1357 def find_nearest_contour(self, x, y, indices=None, pixel=True):
1358 """
1359 Find the point in the contour plot that is closest to ``(x, y)``.
1360
1361 This method does not support filled contours.
1362
1363 Parameters
1364 ----------
1365 x, y : float
1366 The reference point.
1367 indices : list of int or None, default: None
1368 Indices of contour levels to consider. If None (the default), all
1369 levels are considered.
1370 pixel : bool, default: True
1371 If *True*, measure distance in pixel (screen) space, which is
1372 useful for manual contour labeling; else, measure distance in axes
1373 space.
1374
1375 Returns
1376 -------
1377 path : int
1378 The index of the path that is closest to ``(x, y)``. Each path corresponds
1379 to one contour level.
1380 subpath : int
1381 The index within that closest path of the subpath that is closest to
1382 ``(x, y)``. Each subpath corresponds to one unbroken contour line.
1383 index : int
1384 The index of the vertices within that subpath that are closest to
1385 ``(x, y)``.
1386 xmin, ymin : float
1387 The point in the contour plot that is closest to ``(x, y)``.
1388 d2 : float
1389 The squared distance from ``(xmin, ymin)`` to ``(x, y)``.
1390 """
1391 segment = index = d2 = None
1392
1393 with ExitStack() as stack:
1394 if not pixel:
1395 # _find_nearest_contour works in pixel space. We want axes space, so
1396 # effectively disable the transformation here by setting to identity.
1397 stack.enter_context(self._cm_set(
1398 transform=mtransforms.IdentityTransform()))
1399
1400 i_level, i_vtx, (xmin, ymin) = self._find_nearest_contour((x, y), indices)
1401
1402 if i_level is not None:
1403 cc_cumlens = np.cumsum(
1404 [*map(len, self._paths[i_level]._iter_connected_components())])
1405 segment = cc_cumlens.searchsorted(i_vtx, "right")
1406 index = i_vtx if segment == 0 else i_vtx - cc_cumlens[segment - 1]
1407 d2 = (xmin-x)**2 + (ymin-y)**2
1408
1409 return (i_level, segment, index, xmin, ymin, d2)
1410
1411 def draw(self, renderer):
1412 paths = self._paths
1413 n_paths = len(paths)
1414 if not self.filled or all(hatch is None for hatch in self.hatches):
1415 super().draw(renderer)
1416 return
1417 # In presence of hatching, draw contours one at a time.
1418 edgecolors = self.get_edgecolors()
1419 if edgecolors.size == 0:
1420 edgecolors = ("none",)
1421 for idx in range(n_paths):
1422 with cbook._setattr_cm(self, _paths=[paths[idx]]), self._cm_set(
1423 hatch=self.hatches[idx % len(self.hatches)],
1424 array=[self.get_array()[idx]],
1425 linewidths=[self.get_linewidths()[idx % len(self.get_linewidths())]],
1426 linestyles=[self.get_linestyles()[idx % len(self.get_linestyles())]],
1427 edgecolors=edgecolors[idx % len(edgecolors)],
1428 ):
1429 super().draw(renderer)
1430
1431
1432@_docstring.dedent_interpd
1433class QuadContourSet(ContourSet):
1434 """
1435 Create and store a set of contour lines or filled regions.
1436
1437 This class is typically not instantiated directly by the user but by
1438 `~.Axes.contour` and `~.Axes.contourf`.
1439
1440 %(contour_set_attributes)s
1441 """
1442
1443 def _process_args(self, *args, corner_mask=None, algorithm=None, **kwargs):
1444 """
1445 Process args and kwargs.
1446 """
1447 if args and isinstance(args[0], QuadContourSet):
1448 if self.levels is None:
1449 self.levels = args[0].levels
1450 self.zmin = args[0].zmin
1451 self.zmax = args[0].zmax
1452 self._corner_mask = args[0]._corner_mask
1453 contour_generator = args[0]._contour_generator
1454 self._mins = args[0]._mins
1455 self._maxs = args[0]._maxs
1456 self._algorithm = args[0]._algorithm
1457 else:
1458 import contourpy
1459
1460 if algorithm is None:
1461 algorithm = mpl.rcParams['contour.algorithm']
1462 mpl.rcParams.validate["contour.algorithm"](algorithm)
1463 self._algorithm = algorithm
1464
1465 if corner_mask is None:
1466 if self._algorithm == "mpl2005":
1467 # mpl2005 does not support corner_mask=True so if not
1468 # specifically requested then disable it.
1469 corner_mask = False
1470 else:
1471 corner_mask = mpl.rcParams['contour.corner_mask']
1472 self._corner_mask = corner_mask
1473
1474 x, y, z = self._contour_args(args, kwargs)
1475
1476 contour_generator = contourpy.contour_generator(
1477 x, y, z, name=self._algorithm, corner_mask=self._corner_mask,
1478 line_type=contourpy.LineType.SeparateCode,
1479 fill_type=contourpy.FillType.OuterCode,
1480 chunk_size=self.nchunk)
1481
1482 t = self.get_transform()
1483
1484 # if the transform is not trans data, and some part of it
1485 # contains transData, transform the xs and ys to data coordinates
1486 if (t != self.axes.transData and
1487 any(t.contains_branch_seperately(self.axes.transData))):
1488 trans_to_data = t - self.axes.transData
1489 pts = np.vstack([x.flat, y.flat]).T
1490 transformed_pts = trans_to_data.transform(pts)
1491 x = transformed_pts[..., 0]
1492 y = transformed_pts[..., 1]
1493
1494 self._mins = [ma.min(x), ma.min(y)]
1495 self._maxs = [ma.max(x), ma.max(y)]
1496
1497 self._contour_generator = contour_generator
1498
1499 return kwargs
1500
1501 def _contour_args(self, args, kwargs):
1502 if self.filled:
1503 fn = 'contourf'
1504 else:
1505 fn = 'contour'
1506 nargs = len(args)
1507
1508 if 0 < nargs <= 2:
1509 z, *args = args
1510 z = ma.asarray(z)
1511 x, y = self._initialize_x_y(z)
1512 elif 2 < nargs <= 4:
1513 x, y, z_orig, *args = args
1514 x, y, z = self._check_xyz(x, y, z_orig, kwargs)
1515
1516 else:
1517 raise _api.nargs_error(fn, takes="from 1 to 4", given=nargs)
1518 z = ma.masked_invalid(z, copy=False)
1519 self.zmax = z.max().astype(float)
1520 self.zmin = z.min().astype(float)
1521 if self.logscale and self.zmin <= 0:
1522 z = ma.masked_where(z <= 0, z)
1523 _api.warn_external('Log scale: values of z <= 0 have been masked')
1524 self.zmin = z.min().astype(float)
1525 self._process_contour_level_args(args, z.dtype)
1526 return (x, y, z)
1527
1528 def _check_xyz(self, x, y, z, kwargs):
1529 """
1530 Check that the shapes of the input arrays match; if x and y are 1D,
1531 convert them to 2D using meshgrid.
1532 """
1533 x, y = self.axes._process_unit_info([("x", x), ("y", y)], kwargs)
1534
1535 x = np.asarray(x, dtype=np.float64)
1536 y = np.asarray(y, dtype=np.float64)
1537 z = ma.asarray(z)
1538
1539 if z.ndim != 2:
1540 raise TypeError(f"Input z must be 2D, not {z.ndim}D")
1541 if z.shape[0] < 2 or z.shape[1] < 2:
1542 raise TypeError(f"Input z must be at least a (2, 2) shaped array, "
1543 f"but has shape {z.shape}")
1544 Ny, Nx = z.shape
1545
1546 if x.ndim != y.ndim:
1547 raise TypeError(f"Number of dimensions of x ({x.ndim}) and y "
1548 f"({y.ndim}) do not match")
1549 if x.ndim == 1:
1550 nx, = x.shape
1551 ny, = y.shape
1552 if nx != Nx:
1553 raise TypeError(f"Length of x ({nx}) must match number of "
1554 f"columns in z ({Nx})")
1555 if ny != Ny:
1556 raise TypeError(f"Length of y ({ny}) must match number of "
1557 f"rows in z ({Ny})")
1558 x, y = np.meshgrid(x, y)
1559 elif x.ndim == 2:
1560 if x.shape != z.shape:
1561 raise TypeError(
1562 f"Shapes of x {x.shape} and z {z.shape} do not match")
1563 if y.shape != z.shape:
1564 raise TypeError(
1565 f"Shapes of y {y.shape} and z {z.shape} do not match")
1566 else:
1567 raise TypeError(f"Inputs x and y must be 1D or 2D, not {x.ndim}D")
1568
1569 return x, y, z
1570
1571 def _initialize_x_y(self, z):
1572 """
1573 Return X, Y arrays such that contour(Z) will match imshow(Z)
1574 if origin is not None.
1575 The center of pixel Z[i, j] depends on origin:
1576 if origin is None, x = j, y = i;
1577 if origin is 'lower', x = j + 0.5, y = i + 0.5;
1578 if origin is 'upper', x = j + 0.5, y = Nrows - i - 0.5
1579 If extent is not None, x and y will be scaled to match,
1580 as in imshow.
1581 If origin is None and extent is not None, then extent
1582 will give the minimum and maximum values of x and y.
1583 """
1584 if z.ndim != 2:
1585 raise TypeError(f"Input z must be 2D, not {z.ndim}D")
1586 elif z.shape[0] < 2 or z.shape[1] < 2:
1587 raise TypeError(f"Input z must be at least a (2, 2) shaped array, "
1588 f"but has shape {z.shape}")
1589 else:
1590 Ny, Nx = z.shape
1591 if self.origin is None: # Not for image-matching.
1592 if self.extent is None:
1593 return np.meshgrid(np.arange(Nx), np.arange(Ny))
1594 else:
1595 x0, x1, y0, y1 = self.extent
1596 x = np.linspace(x0, x1, Nx)
1597 y = np.linspace(y0, y1, Ny)
1598 return np.meshgrid(x, y)
1599 # Match image behavior:
1600 if self.extent is None:
1601 x0, x1, y0, y1 = (0, Nx, 0, Ny)
1602 else:
1603 x0, x1, y0, y1 = self.extent
1604 dx = (x1 - x0) / Nx
1605 dy = (y1 - y0) / Ny
1606 x = x0 + (np.arange(Nx) + 0.5) * dx
1607 y = y0 + (np.arange(Ny) + 0.5) * dy
1608 if self.origin == 'upper':
1609 y = y[::-1]
1610 return np.meshgrid(x, y)
1611
1612
1613_docstring.interpd.update(contour_doc="""
1614`.contour` and `.contourf` draw contour lines and filled contours,
1615respectively. Except as noted, function signatures and return values
1616are the same for both versions.
1617
1618Parameters
1619----------
1620X, Y : array-like, optional
1621 The coordinates of the values in *Z*.
1622
1623 *X* and *Y* must both be 2D with the same shape as *Z* (e.g.
1624 created via `numpy.meshgrid`), or they must both be 1-D such
1625 that ``len(X) == N`` is the number of columns in *Z* and
1626 ``len(Y) == M`` is the number of rows in *Z*.
1627
1628 *X* and *Y* must both be ordered monotonically.
1629
1630 If not given, they are assumed to be integer indices, i.e.
1631 ``X = range(N)``, ``Y = range(M)``.
1632
1633Z : (M, N) array-like
1634 The height values over which the contour is drawn. Color-mapping is
1635 controlled by *cmap*, *norm*, *vmin*, and *vmax*.
1636
1637levels : int or array-like, optional
1638 Determines the number and positions of the contour lines / regions.
1639
1640 If an int *n*, use `~matplotlib.ticker.MaxNLocator`, which tries
1641 to automatically choose no more than *n+1* "nice" contour levels
1642 between minimum and maximum numeric values of *Z*.
1643
1644 If array-like, draw contour lines at the specified levels.
1645 The values must be in increasing order.
1646
1647Returns
1648-------
1649`~.contour.QuadContourSet`
1650
1651Other Parameters
1652----------------
1653corner_mask : bool, default: :rc:`contour.corner_mask`
1654 Enable/disable corner masking, which only has an effect if *Z* is
1655 a masked array. If ``False``, any quad touching a masked point is
1656 masked out. If ``True``, only the triangular corners of quads
1657 nearest those points are always masked out, other triangular
1658 corners comprising three unmasked points are contoured as usual.
1659
1660colors : :mpltype:`color` or list of :mpltype:`color`, optional
1661 The colors of the levels, i.e. the lines for `.contour` and the
1662 areas for `.contourf`.
1663
1664 The sequence is cycled for the levels in ascending order. If the
1665 sequence is shorter than the number of levels, it's repeated.
1666
1667 As a shortcut, single color strings may be used in place of
1668 one-element lists, i.e. ``'red'`` instead of ``['red']`` to color
1669 all levels with the same color. This shortcut does only work for
1670 color strings, not for other ways of specifying colors.
1671
1672 By default (value *None*), the colormap specified by *cmap*
1673 will be used.
1674
1675alpha : float, default: 1
1676 The alpha blending value, between 0 (transparent) and 1 (opaque).
1677
1678%(cmap_doc)s
1679
1680 This parameter is ignored if *colors* is set.
1681
1682%(norm_doc)s
1683
1684 This parameter is ignored if *colors* is set.
1685
1686%(vmin_vmax_doc)s
1687
1688 If *vmin* or *vmax* are not given, the default color scaling is based on
1689 *levels*.
1690
1691 This parameter is ignored if *colors* is set.
1692
1693origin : {*None*, 'upper', 'lower', 'image'}, default: None
1694 Determines the orientation and exact position of *Z* by specifying
1695 the position of ``Z[0, 0]``. This is only relevant, if *X*, *Y*
1696 are not given.
1697
1698 - *None*: ``Z[0, 0]`` is at X=0, Y=0 in the lower left corner.
1699 - 'lower': ``Z[0, 0]`` is at X=0.5, Y=0.5 in the lower left corner.
1700 - 'upper': ``Z[0, 0]`` is at X=N+0.5, Y=0.5 in the upper left
1701 corner.
1702 - 'image': Use the value from :rc:`image.origin`.
1703
1704extent : (x0, x1, y0, y1), optional
1705 If *origin* is not *None*, then *extent* is interpreted as in
1706 `.imshow`: it gives the outer pixel boundaries. In this case, the
1707 position of Z[0, 0] is the center of the pixel, not a corner. If
1708 *origin* is *None*, then (*x0*, *y0*) is the position of Z[0, 0],
1709 and (*x1*, *y1*) is the position of Z[-1, -1].
1710
1711 This argument is ignored if *X* and *Y* are specified in the call
1712 to contour.
1713
1714locator : ticker.Locator subclass, optional
1715 The locator is used to determine the contour levels if they
1716 are not given explicitly via *levels*.
1717 Defaults to `~.ticker.MaxNLocator`.
1718
1719extend : {'neither', 'both', 'min', 'max'}, default: 'neither'
1720 Determines the ``contourf``-coloring of values that are outside the
1721 *levels* range.
1722
1723 If 'neither', values outside the *levels* range are not colored.
1724 If 'min', 'max' or 'both', color the values below, above or below
1725 and above the *levels* range.
1726
1727 Values below ``min(levels)`` and above ``max(levels)`` are mapped
1728 to the under/over values of the `.Colormap`. Note that most
1729 colormaps do not have dedicated colors for these by default, so
1730 that the over and under values are the edge values of the colormap.
1731 You may want to set these values explicitly using
1732 `.Colormap.set_under` and `.Colormap.set_over`.
1733
1734 .. note::
1735
1736 An existing `.QuadContourSet` does not get notified if
1737 properties of its colormap are changed. Therefore, an explicit
1738 call `.QuadContourSet.changed()` is needed after modifying the
1739 colormap. The explicit call can be left out, if a colorbar is
1740 assigned to the `.QuadContourSet` because it internally calls
1741 `.QuadContourSet.changed()`.
1742
1743 Example::
1744
1745 x = np.arange(1, 10)
1746 y = x.reshape(-1, 1)
1747 h = x * y
1748
1749 cs = plt.contourf(h, levels=[10, 30, 50],
1750 colors=['#808080', '#A0A0A0', '#C0C0C0'], extend='both')
1751 cs.cmap.set_over('red')
1752 cs.cmap.set_under('blue')
1753 cs.changed()
1754
1755xunits, yunits : registered units, optional
1756 Override axis units by specifying an instance of a
1757 :class:`matplotlib.units.ConversionInterface`.
1758
1759antialiased : bool, optional
1760 Enable antialiasing, overriding the defaults. For
1761 filled contours, the default is *False*. For line contours,
1762 it is taken from :rc:`lines.antialiased`.
1763
1764nchunk : int >= 0, optional
1765 If 0, no subdivision of the domain. Specify a positive integer to
1766 divide the domain into subdomains of *nchunk* by *nchunk* quads.
1767 Chunking reduces the maximum length of polygons generated by the
1768 contouring algorithm which reduces the rendering workload passed
1769 on to the backend and also requires slightly less RAM. It can
1770 however introduce rendering artifacts at chunk boundaries depending
1771 on the backend, the *antialiased* flag and value of *alpha*.
1772
1773linewidths : float or array-like, default: :rc:`contour.linewidth`
1774 *Only applies to* `.contour`.
1775
1776 The line width of the contour lines.
1777
1778 If a number, all levels will be plotted with this linewidth.
1779
1780 If a sequence, the levels in ascending order will be plotted with
1781 the linewidths in the order specified.
1782
1783 If None, this falls back to :rc:`lines.linewidth`.
1784
1785linestyles : {*None*, 'solid', 'dashed', 'dashdot', 'dotted'}, optional
1786 *Only applies to* `.contour`.
1787
1788 If *linestyles* is *None*, the default is 'solid' unless the lines are
1789 monochrome. In that case, negative contours will instead take their
1790 linestyle from the *negative_linestyles* argument.
1791
1792 *linestyles* can also be an iterable of the above strings specifying a set
1793 of linestyles to be used. If this iterable is shorter than the number of
1794 contour levels it will be repeated as necessary.
1795
1796negative_linestyles : {*None*, 'solid', 'dashed', 'dashdot', 'dotted'}, \
1797 optional
1798 *Only applies to* `.contour`.
1799
1800 If *linestyles* is *None* and the lines are monochrome, this argument
1801 specifies the line style for negative contours.
1802
1803 If *negative_linestyles* is *None*, the default is taken from
1804 :rc:`contour.negative_linestyles`.
1805
1806 *negative_linestyles* can also be an iterable of the above strings
1807 specifying a set of linestyles to be used. If this iterable is shorter than
1808 the number of contour levels it will be repeated as necessary.
1809
1810hatches : list[str], optional
1811 *Only applies to* `.contourf`.
1812
1813 A list of cross hatch patterns to use on the filled areas.
1814 If None, no hatching will be added to the contour.
1815
1816algorithm : {'mpl2005', 'mpl2014', 'serial', 'threaded'}, optional
1817 Which contouring algorithm to use to calculate the contour lines and
1818 polygons. The algorithms are implemented in
1819 `ContourPy <https://github.com/contourpy/contourpy>`_, consult the
1820 `ContourPy documentation <https://contourpy.readthedocs.io>`_ for
1821 further information.
1822
1823 The default is taken from :rc:`contour.algorithm`.
1824
1825clip_path : `~matplotlib.patches.Patch` or `.Path` or `.TransformedPath`
1826 Set the clip path. See `~matplotlib.artist.Artist.set_clip_path`.
1827
1828 .. versionadded:: 3.8
1829
1830data : indexable object, optional
1831 DATA_PARAMETER_PLACEHOLDER
1832
1833Notes
1834-----
18351. `.contourf` differs from the MATLAB version in that it does not draw
1836 the polygon edges. To draw edges, add line contours with calls to
1837 `.contour`.
1838
18392. `.contourf` fills intervals that are closed at the top; that is, for
1840 boundaries *z1* and *z2*, the filled region is::
1841
1842 z1 < Z <= z2
1843
1844 except for the lowest interval, which is closed on both sides (i.e.
1845 it includes the lowest value).
1846
18473. `.contour` and `.contourf` use a `marching squares
1848 <https://en.wikipedia.org/wiki/Marching_squares>`_ algorithm to
1849 compute contour locations. More information can be found in
1850 `ContourPy documentation <https://contourpy.readthedocs.io>`_.
1851""" % _docstring.interpd.params)