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
« 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"""
5import functools
6from numbers import Integral
8import numpy as np
9from numpy import ma
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
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.
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 """
41 def get_rotation(self):
42 new_angle, = self.get_transform().transform_angles(
43 [super().get_rotation()], [self.get_position()])
44 return new_angle
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()
76class ContourLabeler:
77 """Mixin to provide labelling capability to `.ContourSet`."""
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.
86 Adds labels to line contours in this `.ContourSet` (which inherits from
87 this mixin class).
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.
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.
99 colors : color or colors or None, default: None
100 The label colors:
102 - If *None*, the color of each label matches the color of
103 the corresponding contour.
105 - If one string color, e.g., *colors* = 'r' or *colors* =
106 'red', all labels will be plotted in this color.
108 - If a tuple of colors (string, float, rgb, etc), different labels
109 will be plotted in different colors in the order specified.
111 inline : bool, default: True
112 If ``True`` the underlying contour is removed where the label is
113 placed.
115 inline_spacing : float, default: 5
116 Space in pixels to leave on each side of label when placing inline.
118 This spacing will be exact for labels at locations where the
119 contour is straight, less so for labels on curved contours.
121 fmt : `.Formatter` or str or callable or dict, optional
122 How the levels are formatted:
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.
131 The default is to use a standard `.ScalarFormatter`.
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).
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.
148 rightside_up : bool, default: True
149 If ``True``, label rotations will always be plus
150 or minus 90 degrees from level.
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.
158 zorder : float or None, default: ``(2 + contour.get_zorder())``
159 zorder of the contour labels.
161 Returns
162 -------
163 labels
164 A list of `.Text` instances for the labels.
165 """
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).
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
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
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)
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())
219 self.labelXYs = []
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)
237 self.labelTextsList = cbook.silent_list('text.Text', self.labelTexts)
238 return self.labelTextsList
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())
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)
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)
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
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)
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
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
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.
336 If *lc* is not None or empty, also break contours and compute
337 inlining.
339 *spacing* is the empty space to leave around the label, in pixels.
341 Both tasks are done together to avoid calculating path lengths
342 multiple times, which is relatively costly.
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 """
350 if lc is None:
351 lc = []
352 # Half the label width
353 hlw = lw / 2.0
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
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]
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)
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))
382 if self.rightside_up:
383 # Fix angle so text is never upside-down
384 rotation = (rotation + 90) % 180 - 90
386 # Break contour if desired
387 nlc = []
388 if len(lc):
389 # Expand range by spacing
390 xi = dp + xi + np.array([-spacing, spacing])
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]
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]:]]))
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 ]
418 return rotation, nlc
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
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)
440 return t
442 def _add_label(self, t, x, y, lev, cvalue):
443 color = self.labelMappable.to_rgba(cvalue, alpha=self.alpha)
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))
451 # Add label to plot here - useful for manual mode label selection
452 self.axes.add_artist(t)
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)
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)
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)``.
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 """
494 if transform is None:
495 transform = self.axes.transData
496 if transform:
497 x, y = transform.transform((x, y))
499 # find the nearest contour _in screen units_
500 conmin, segmin, imin, xmin, ymin = self.find_nearest_contour(
501 x, y, self.labelIndiceList)[:5]
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)
515 # Get index of nearest level in subset of levels used for labeling
516 lmin = self.labelIndiceList.index(conmin)
518 # Get label width for rotating labels and breaking contours
519 lw = self._get_nth_label_width(lmin)
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)
526 self.add_label(xmin, ymin, rotation, self.labelLevelList[lmin],
527 self.labelCValueList[lmin])
529 if inline:
530 # Remove old, not looping over paths so we can do this up front
531 paths.pop(segmin)
533 # Add paths if not empty or single point
534 paths.extend([mpath.Path(n) for n in nlc if len(n) > 1])
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()
542 def labels(self, inline, inline_spacing):
544 if self._use_clabeltext:
545 add_label = self.add_label_clabeltext
546 else:
547 add_label = self.add_label
549 for idx, (icon, lev, cvalue) in enumerate(zip(
550 self.labelIndiceList,
551 self.labelLevelList,
552 self.labelCValueList,
553 )):
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
564 # Check if long enough for a label
565 if self.print_label(slc, lw):
566 x, y, ind = self.locate_label(slc, lw)
568 rotation, new = self.calc_label_rot_and_inline(
569 slc, ind, lw, lc if inline else None, inline_spacing)
571 # Actually add the label
572 add_label(x, y, rotation, lev, cvalue)
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)
583 # After looping over all segments on a contour, replace old paths
584 # by new ones if inlining.
585 if inline:
586 paths[:] = additions
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)
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.
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))
634_docstring.interpd.update(contour_set_attributes=r"""
635Attributes
636----------
637ax : `~matplotlib.axes.Axes`
638 The Axes object in which the contours are drawn.
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.
644levels : array
645 The values of the contour levels.
647layers : array
648 Same as levels for line contours; half-way between
649 levels for filled contours. See ``ContourSet._process_colors``.
650""")
653@_docstring.dedent_interpd
654class ContourSet(cm.ScalarMappable, ContourLabeler):
655 """
656 Store a set of contour lines or filled regions.
658 User-callable method: `~.Axes.clabel`
660 Parameters
661 ----------
662 ax : `~.axes.Axes`
664 levels : [level0, level1, ..., leveln]
665 A list of floating point numbers indicating the contour levels.
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 ::
673 level0segs = [polygon0, polygon1, ...]
674 polygon0 = [[x0, y0], [x1, y1], ...]
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 ::
683 level0kinds = [polygon0kinds, ...]
684 polygon0kinds = [vertexcode0, vertexcode1, ...]
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]``.
690 **kwargs
691 Keyword arguments are as described in the docstring of
692 `~.Axes.contour`.
694 %(contour_set_attributes)s
695 """
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``.
708 Call signature::
710 ContourSet(ax, levels, allsegs, [allkinds], **kwargs)
712 Parameters
713 ----------
714 ax : `~.axes.Axes`
715 The `~.axes.Axes` object to draw on.
717 levels : [level0, level1, ..., leveln]
718 A list of floating point numbers indicating the contour
719 levels.
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 ::
727 level0segs = [polygon0, polygon1, ...]
728 polygon0 = [[x0, y0], [x1, y1], ...]
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 ::
737 level0kinds = [polygon0kinds, ...]
738 polygon0kinds = [vertexcode0, vertexcode1, ...]
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]``.
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`.
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
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']
785 self._transform = transform
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']
794 kwargs = self._process_args(*args, **kwargs)
795 self._process_levels()
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
805 # Handle the case where colors are given for the extended
806 # parts of the contour.
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
822 cmap = mcolors.ListedColormap(self.colors[i0:None], N=ncolors)
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])
830 self.collections = cbook.silent_list(None)
832 # label lists must be initialized here
833 self.labelTexts = []
834 self.labelCValues = []
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()
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)
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)
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)
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)]
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)
904 self.changed() # set the colors
906 if kwargs:
907 _api.warn_external(
908 'The following kwargs were not used by contour: ' +
909 ", ".join(map(repr, kwargs))
910 )
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
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
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.
934 The labels have the form "0 < x <= 1" stating the data ranges which
935 the artists represent.
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.
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 = []
954 if self.filled:
955 lowers, uppers = self._get_lowers_and_uppers()
956 n_levels = len(self.collections)
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)
967 lower = str_format(lower)
968 upper = str_format(upper)
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):
979 patch = mcoll.LineCollection(None)
980 patch.update_from(collection)
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}$')
987 return artists, labels
989 def _process_args(self, *args, **kwargs):
990 """
991 Process *args* and *kwargs*; override in derived classes.
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)
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')
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')
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)
1021 return kwargs
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
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)
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)]
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)
1102 def _autolev(self, N):
1103 """
1104 Select contour levels to span the data.
1106 The target number of levels, *N*, is used only when the
1107 scale is not log and default locator is used.
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)
1122 lev = self.locator.tick_values(self.zmin, self.zmax)
1124 try:
1125 if self.locator._symmetric:
1126 return lev
1127 except AttributeError:
1128 pass
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
1140 if i1 - i0 < 3:
1141 i0, i1 = 0, len(lev)
1143 return lev[i0:i1]
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)
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.")
1169 if self.filled and len(self.levels) < 2:
1170 raise ValueError("Filled contours require at least 2 levels.")
1172 if len(self.levels) > 1 and np.min(np.diff(self.levels)) <= 0.0:
1173 raise ValueError("Contour levels must be increasing")
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.
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)
1189 if self.logscale:
1190 lower, upper = 1e-250, 1e250
1191 else:
1192 lower, upper = -1e250, 1e250
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)
1200 if not self.filled:
1201 self.layers = self.levels
1202 return
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:])
1212 def _process_colors(self):
1213 """
1214 Color argument processing for contouring.
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.
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.
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
1255 # self.tcolors are set by the "changed" method
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
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
1302 def get_alpha(self):
1303 """Return alpha to be applied to all ContourSet artists."""
1304 return self.alpha
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()
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)``.
1318 This method does not support filled contours.
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.
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 """
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.
1356 if self.filled:
1357 raise ValueError("Method does not support filled contours.")
1359 if indices is None:
1360 indices = range(len(self.collections))
1362 d2min = np.inf
1363 conmin = None
1364 segmin = None
1365 imin = None
1366 xmin = None
1367 ymin = None
1369 point = np.array([x, y])
1371 for icon in indices:
1372 con = self.collections[icon]
1373 trans = con.get_transform()
1374 paths = con.get_paths()
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)
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]
1391 return (conmin, segmin, imin, xmin, ymin, d2min)
1394@_docstring.dedent_interpd
1395class QuadContourSet(ContourSet):
1396 """
1397 Create and store a set of contour lines or filled regions.
1399 This class is typically not instantiated directly by the user but by
1400 `~.Axes.contour` and `~.Axes.contourf`.
1402 %(contour_set_attributes)s
1403 """
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
1422 if algorithm is None:
1423 algorithm = mpl.rcParams['contour.algorithm']
1424 mpl.rcParams.validate["contour.algorithm"](algorithm)
1425 self._algorithm = algorithm
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
1436 x, y, z = self._contour_args(args, kwargs)
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)
1444 t = self.get_transform()
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]
1456 self._mins = [ma.min(x), ma.min(y)]
1457 self._maxs = [ma.max(x), ma.max(y)]
1459 self._contour_generator = contour_generator
1461 return kwargs
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)
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)
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)
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
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")
1531 return x, y, z
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)
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.
1580Parameters
1581----------
1582X, Y : array-like, optional
1583 The coordinates of the values in *Z*.
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*.
1590 *X* and *Y* must both be ordered monotonically.
1592 If not given, they are assumed to be integer indices, i.e.
1593 ``X = range(N)``, ``Y = range(M)``.
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*.
1599levels : int or array-like, optional
1600 Determines the number and positions of the contour lines / regions.
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*.
1606 If array-like, draw contour lines at the specified levels.
1607 The values must be in increasing order.
1609Returns
1610-------
1611`~.contour.QuadContourSet`
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.
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`.
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.
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.
1634 By default (value *None*), the colormap specified by *cmap*
1635 will be used.
1637alpha : float, default: 1
1638 The alpha blending value, between 0 (transparent) and 1 (opaque).
1640%(cmap_doc)s
1642 This parameter is ignored if *colors* is set.
1644%(norm_doc)s
1646 This parameter is ignored if *colors* is set.
1648%(vmin_vmax_doc)s
1650 If *vmin* or *vmax* are not given, the default color scaling is based on
1651 *levels*.
1653 This parameter is ignored if *colors* is set.
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.
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`.
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].
1673 This argument is ignored if *X* and *Y* are specified in the call
1674 to contour.
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`.
1681extend : {'neither', 'both', 'min', 'max'}, default: 'neither'
1682 Determines the ``contourf``-coloring of values that are outside the
1683 *levels* range.
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.
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`.
1696 .. note::
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()`.
1705 Example::
1707 x = np.arange(1, 10)
1708 y = x.reshape(-1, 1)
1709 h = x * y
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()
1717xunits, yunits : registered units, optional
1718 Override axis units by specifying an instance of a
1719 :class:`matplotlib.units.ConversionInterface`.
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`.
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*.
1735linewidths : float or array-like, default: :rc:`contour.linewidth`
1736 *Only applies to* `.contour`.
1738 The line width of the contour lines.
1740 If a number, all levels will be plotted with this linewidth.
1742 If a sequence, the levels in ascending order will be plotted with
1743 the linewidths in the order specified.
1745 If None, this falls back to :rc:`lines.linewidth`.
1747linestyles : {*None*, 'solid', 'dashed', 'dashdot', 'dotted'}, optional
1748 *Only applies to* `.contour`.
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.
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.
1758negative_linestyles : {*None*, 'solid', 'dashed', 'dashdot', 'dotted'}, \
1759 optional
1760 *Only applies to* `.contour`.
1762 If *linestyles* is *None* and the lines are monochrome, this argument
1763 specifies the line style for negative contours.
1765 If *negative_linestyles* is *None*, the default is taken from
1766 :rc:`contour.negative_linestyles`.
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.
1772hatches : list[str], optional
1773 *Only applies to* `.contourf`.
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.
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.
1787 The default is taken from :rc:`contour.algorithm`.
1789data : indexable object, optional
1790 DATA_PARAMETER_PLACEHOLDER
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`.
17982. `.contourf` fills intervals that are closed at the top; that is, for
1799 boundaries *z1* and *z2*, the filled region is::
1801 z1 < Z <= z2
1803 except for the lowest interval, which is closed on both sides (i.e.
1804 it includes the lowest value).
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)