| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702 |
- """
- Classes to support contour plotting and labelling for the Axes class.
- """
- from contextlib import ExitStack
- import functools
- import math
- from numbers import Integral
- import numpy as np
- from numpy import ma
- import matplotlib as mpl
- from matplotlib import _api, _docstring
- from matplotlib.backend_bases import MouseButton
- from matplotlib.lines import Line2D
- from matplotlib.path import Path
- from matplotlib.text import Text
- import matplotlib.ticker as ticker
- import matplotlib.cm as cm
- import matplotlib.colors as mcolors
- import matplotlib.collections as mcoll
- import matplotlib.font_manager as font_manager
- import matplotlib.cbook as cbook
- import matplotlib.patches as mpatches
- import matplotlib.transforms as mtransforms
- def _contour_labeler_event_handler(cs, inline, inline_spacing, event):
- canvas = cs.axes.get_figure(root=True).canvas
- is_button = event.name == "button_press_event"
- is_key = event.name == "key_press_event"
- # Quit (even if not in infinite mode; this is consistent with
- # MATLAB and sometimes quite useful, but will require the user to
- # test how many points were actually returned before using data).
- if (is_button and event.button == MouseButton.MIDDLE
- or is_key and event.key in ["escape", "enter"]):
- canvas.stop_event_loop()
- # Pop last click.
- elif (is_button and event.button == MouseButton.RIGHT
- or is_key and event.key in ["backspace", "delete"]):
- # Unfortunately, if one is doing inline labels, then there is currently
- # no way to fix the broken contour - once humpty-dumpty is broken, he
- # can't be put back together. In inline mode, this does nothing.
- if not inline:
- cs.pop_label()
- canvas.draw()
- # Add new click.
- elif (is_button and event.button == MouseButton.LEFT
- # On macOS/gtk, some keys return None.
- or is_key and event.key is not None):
- if cs.axes.contains(event)[0]:
- cs.add_label_near(event.x, event.y, transform=False,
- inline=inline, inline_spacing=inline_spacing)
- canvas.draw()
- class ContourLabeler:
- """Mixin to provide labelling capability to `.ContourSet`."""
- def clabel(self, levels=None, *,
- fontsize=None, inline=True, inline_spacing=5, fmt=None,
- colors=None, use_clabeltext=False, manual=False,
- rightside_up=True, zorder=None):
- """
- Label a contour plot.
- Adds labels to line contours in this `.ContourSet` (which inherits from
- this mixin class).
- Parameters
- ----------
- levels : array-like, optional
- A list of level values, that should be labeled. The list must be
- a subset of ``cs.levels``. If not given, all levels are labeled.
- fontsize : str or float, default: :rc:`font.size`
- Size in points or relative size e.g., 'smaller', 'x-large'.
- See `.Text.set_size` for accepted string values.
- colors : :mpltype:`color` or colors or None, default: None
- The label colors:
- - If *None*, the color of each label matches the color of
- the corresponding contour.
- - If one string color, e.g., *colors* = 'r' or *colors* =
- 'red', all labels will be plotted in this color.
- - If a tuple of colors (string, float, RGB, etc), different labels
- will be plotted in different colors in the order specified.
- inline : bool, default: True
- If ``True`` the underlying contour is removed where the label is
- placed.
- inline_spacing : float, default: 5
- Space in pixels to leave on each side of label when placing inline.
- This spacing will be exact for labels at locations where the
- contour is straight, less so for labels on curved contours.
- fmt : `.Formatter` or str or callable or dict, optional
- How the levels are formatted:
- - If a `.Formatter`, it is used to format all levels at once, using
- its `.Formatter.format_ticks` method.
- - If a str, it is interpreted as a %-style format string.
- - If a callable, it is called with one level at a time and should
- return the corresponding label.
- - If a dict, it should directly map levels to labels.
- The default is to use a standard `.ScalarFormatter`.
- manual : bool or iterable, default: False
- If ``True``, contour labels will be placed manually using
- mouse clicks. Click the first button near a contour to
- add a label, click the second button (or potentially both
- mouse buttons at once) to finish adding labels. The third
- button can be used to remove the last label added, but
- only if labels are not inline. Alternatively, the keyboard
- can be used to select label locations (enter to end label
- placement, delete or backspace act like the third mouse button,
- and any other key will select a label location).
- *manual* can also be an iterable object of (x, y) tuples.
- Contour labels will be created as if mouse is clicked at each
- (x, y) position.
- rightside_up : bool, default: True
- If ``True``, label rotations will always be plus
- or minus 90 degrees from level.
- use_clabeltext : bool, default: False
- If ``True``, use `.Text.set_transform_rotates_text` to ensure that
- label rotation is updated whenever the Axes aspect changes.
- zorder : float or None, default: ``(2 + contour.get_zorder())``
- zorder of the contour labels.
- Returns
- -------
- labels
- A list of `.Text` instances for the labels.
- """
- # Based on the input arguments, clabel() adds a list of "label
- # specific" attributes to the ContourSet object. These attributes are
- # all of the form label* and names should be fairly self explanatory.
- #
- # Once these attributes are set, clabel passes control to the labels()
- # method (for automatic label placement) or blocking_input_loop and
- # _contour_labeler_event_handler (for manual label placement).
- if fmt is None:
- fmt = ticker.ScalarFormatter(useOffset=False)
- fmt.create_dummy_axis()
- self.labelFmt = fmt
- self._use_clabeltext = use_clabeltext
- self.labelManual = manual
- self.rightside_up = rightside_up
- self._clabel_zorder = 2 + self.get_zorder() if zorder is None else zorder
- if levels is None:
- levels = self.levels
- indices = list(range(len(self.cvalues)))
- else:
- levlabs = list(levels)
- indices, levels = [], []
- for i, lev in enumerate(self.levels):
- if lev in levlabs:
- indices.append(i)
- levels.append(lev)
- if len(levels) < len(levlabs):
- raise ValueError(f"Specified levels {levlabs} don't match "
- f"available levels {self.levels}")
- self.labelLevelList = levels
- self.labelIndiceList = indices
- self._label_font_props = font_manager.FontProperties(size=fontsize)
- if colors is None:
- self.labelMappable = self
- self.labelCValueList = np.take(self.cvalues, self.labelIndiceList)
- else:
- cmap = mcolors.ListedColormap(colors, N=len(self.labelLevelList))
- self.labelCValueList = list(range(len(self.labelLevelList)))
- self.labelMappable = cm.ScalarMappable(cmap=cmap,
- norm=mcolors.NoNorm())
- self.labelXYs = []
- if np.iterable(manual):
- for x, y in manual:
- self.add_label_near(x, y, inline, inline_spacing)
- elif manual:
- print('Select label locations manually using first mouse button.')
- print('End manual selection with second mouse button.')
- if not inline:
- print('Remove last label by clicking third mouse button.')
- mpl._blocking_input.blocking_input_loop(
- self.axes.get_figure(root=True),
- ["button_press_event", "key_press_event"],
- timeout=-1, handler=functools.partial(
- _contour_labeler_event_handler,
- self, inline, inline_spacing))
- else:
- self.labels(inline, inline_spacing)
- return cbook.silent_list('text.Text', self.labelTexts)
- def print_label(self, linecontour, labelwidth):
- """Return whether a contour is long enough to hold a label."""
- return (len(linecontour) > 10 * labelwidth
- or (len(linecontour)
- and (np.ptp(linecontour, axis=0) > 1.2 * labelwidth).any()))
- def too_close(self, x, y, lw):
- """Return whether a label is already near this location."""
- thresh = (1.2 * lw) ** 2
- return any((x - loc[0]) ** 2 + (y - loc[1]) ** 2 < thresh
- for loc in self.labelXYs)
- def _get_nth_label_width(self, nth):
- """Return the width of the *nth* label, in pixels."""
- fig = self.axes.get_figure(root=False)
- renderer = fig.get_figure(root=True)._get_renderer()
- return (Text(0, 0,
- self.get_text(self.labelLevelList[nth], self.labelFmt),
- figure=fig, fontproperties=self._label_font_props)
- .get_window_extent(renderer).width)
- def get_text(self, lev, fmt):
- """Get the text of the label."""
- if isinstance(lev, str):
- return lev
- elif isinstance(fmt, dict):
- return fmt.get(lev, '%1.3f')
- elif callable(getattr(fmt, "format_ticks", None)):
- return fmt.format_ticks([*self.labelLevelList, lev])[-1]
- elif callable(fmt):
- return fmt(lev)
- else:
- return fmt % lev
- def locate_label(self, linecontour, labelwidth):
- """
- Find good place to draw a label (relatively flat part of the contour).
- """
- ctr_size = len(linecontour)
- n_blocks = int(np.ceil(ctr_size / labelwidth)) if labelwidth > 1 else 1
- block_size = ctr_size if n_blocks == 1 else int(labelwidth)
- # Split contour into blocks of length ``block_size``, filling the last
- # block by cycling the contour start (per `np.resize` semantics). (Due
- # to cycling, the index returned is taken modulo ctr_size.)
- xx = np.resize(linecontour[:, 0], (n_blocks, block_size))
- yy = np.resize(linecontour[:, 1], (n_blocks, block_size))
- yfirst = yy[:, :1]
- ylast = yy[:, -1:]
- xfirst = xx[:, :1]
- xlast = xx[:, -1:]
- s = (yfirst - yy) * (xlast - xfirst) - (xfirst - xx) * (ylast - yfirst)
- l = np.hypot(xlast - xfirst, ylast - yfirst)
- # Ignore warning that divide by zero throws, as this is a valid option
- with np.errstate(divide='ignore', invalid='ignore'):
- distances = (abs(s) / l).sum(axis=-1)
- # Labels are drawn in the middle of the block (``hbsize``) where the
- # contour is the closest (per ``distances``) to a straight line, but
- # not `too_close()` to a preexisting label.
- hbsize = block_size // 2
- adist = np.argsort(distances)
- # If all candidates are `too_close()`, go back to the straightest part
- # (``adist[0]``).
- for idx in np.append(adist, adist[0]):
- x, y = xx[idx, hbsize], yy[idx, hbsize]
- if not self.too_close(x, y, labelwidth):
- break
- return x, y, (idx * block_size + hbsize) % ctr_size
- def _split_path_and_get_label_rotation(self, path, idx, screen_pos, lw, spacing=5):
- """
- Prepare for insertion of a label at index *idx* of *path*.
- Parameters
- ----------
- path : Path
- The path where the label will be inserted, in data space.
- idx : int
- The vertex index after which the label will be inserted.
- screen_pos : (float, float)
- The position where the label will be inserted, in screen space.
- lw : float
- The label width, in screen space.
- spacing : float
- Extra spacing around the label, in screen space.
- Returns
- -------
- path : Path
- The path, broken so that the label can be drawn over it.
- angle : float
- The rotation of the label.
- Notes
- -----
- Both tasks are done together to avoid calculating path lengths multiple times,
- which is relatively costly.
- The method used here involves computing the path length along the contour in
- pixel coordinates and then looking (label width / 2) away from central point to
- determine rotation and then to break contour if desired. The extra spacing is
- taken into account when breaking the path, but not when computing the angle.
- """
- xys = path.vertices
- codes = path.codes
- # Insert a vertex at idx/pos (converting back to data space), if there isn't yet
- # a vertex there. With infinite precision one could also always insert the
- # extra vertex (it will get masked out by the label below anyways), but floating
- # point inaccuracies (the point can have undergone a data->screen->data
- # transform loop) can slightly shift the point and e.g. shift the angle computed
- # below from exactly zero to nonzero.
- pos = self.get_transform().inverted().transform(screen_pos)
- if not np.allclose(pos, xys[idx]):
- xys = np.insert(xys, idx, pos, axis=0)
- codes = np.insert(codes, idx, Path.LINETO)
- # Find the connected component where the label will be inserted. Note that a
- # path always starts with a MOVETO, and we consider there's an implicit
- # MOVETO (closing the last path) at the end.
- movetos = (codes == Path.MOVETO).nonzero()[0]
- start = movetos[movetos <= idx][-1]
- try:
- stop = movetos[movetos > idx][0]
- except IndexError:
- stop = len(codes)
- # Restrict ourselves to the connected component.
- cc_xys = xys[start:stop]
- idx -= start
- # If the path is closed, rotate it s.t. it starts at the label.
- is_closed_path = codes[stop - 1] == Path.CLOSEPOLY
- if is_closed_path:
- cc_xys = np.concatenate([cc_xys[idx:-1], cc_xys[:idx+1]])
- idx = 0
- # Like np.interp, but additionally vectorized over fp.
- def interp_vec(x, xp, fp): return [np.interp(x, xp, col) for col in fp.T]
- # Use cumulative path lengths ("cpl") as curvilinear coordinate along contour.
- screen_xys = self.get_transform().transform(cc_xys)
- path_cpls = np.insert(
- np.cumsum(np.hypot(*np.diff(screen_xys, axis=0).T)), 0, 0)
- path_cpls -= path_cpls[idx]
- # Use linear interpolation to get end coordinates of label.
- target_cpls = np.array([-lw/2, lw/2])
- if is_closed_path: # For closed paths, target from the other end.
- target_cpls[0] += (path_cpls[-1] - path_cpls[0])
- (sx0, sx1), (sy0, sy1) = interp_vec(target_cpls, path_cpls, screen_xys)
- angle = np.rad2deg(np.arctan2(sy1 - sy0, sx1 - sx0)) # Screen space.
- if self.rightside_up: # Fix angle so text is never upside-down
- angle = (angle + 90) % 180 - 90
- target_cpls += [-spacing, +spacing] # Expand range by spacing.
- # Get indices near points of interest; use -1 as out of bounds marker.
- i0, i1 = np.interp(target_cpls, path_cpls, range(len(path_cpls)),
- left=-1, right=-1)
- i0 = math.floor(i0)
- i1 = math.ceil(i1)
- (x0, x1), (y0, y1) = interp_vec(target_cpls, path_cpls, cc_xys)
- # Actually break contours (dropping zero-len parts).
- new_xy_blocks = []
- new_code_blocks = []
- if is_closed_path:
- if i0 != -1 and i1 != -1:
- # This is probably wrong in the case that the entire contour would
- # be discarded, but ensures that a valid path is returned and is
- # consistent with behavior of mpl <3.8
- points = cc_xys[i1:i0+1]
- new_xy_blocks.extend([[(x1, y1)], points, [(x0, y0)]])
- nlines = len(points) + 1
- new_code_blocks.extend([[Path.MOVETO], [Path.LINETO] * nlines])
- else:
- if i0 != -1:
- new_xy_blocks.extend([cc_xys[:i0 + 1], [(x0, y0)]])
- new_code_blocks.extend([[Path.MOVETO], [Path.LINETO] * (i0 + 1)])
- if i1 != -1:
- new_xy_blocks.extend([[(x1, y1)], cc_xys[i1:]])
- new_code_blocks.extend([
- [Path.MOVETO], [Path.LINETO] * (len(cc_xys) - i1)])
- # Back to the full path.
- xys = np.concatenate([xys[:start], *new_xy_blocks, xys[stop:]])
- codes = np.concatenate([codes[:start], *new_code_blocks, codes[stop:]])
- return angle, Path(xys, codes)
- def add_label(self, x, y, rotation, lev, cvalue):
- """Add a contour label, respecting whether *use_clabeltext* was set."""
- data_x, data_y = self.axes.transData.inverted().transform((x, y))
- t = Text(
- data_x, data_y,
- text=self.get_text(lev, self.labelFmt),
- rotation=rotation,
- horizontalalignment='center', verticalalignment='center',
- zorder=self._clabel_zorder,
- color=self.labelMappable.to_rgba(cvalue, alpha=self.get_alpha()),
- fontproperties=self._label_font_props,
- clip_box=self.axes.bbox)
- if self._use_clabeltext:
- data_rotation, = self.axes.transData.inverted().transform_angles(
- [rotation], [[x, y]])
- t.set(rotation=data_rotation, transform_rotates_text=True)
- self.labelTexts.append(t)
- self.labelCValues.append(cvalue)
- self.labelXYs.append((x, y))
- # Add label to plot here - useful for manual mode label selection
- self.axes.add_artist(t)
- def add_label_near(self, x, y, inline=True, inline_spacing=5,
- transform=None):
- """
- Add a label near the point ``(x, y)``.
- Parameters
- ----------
- x, y : float
- The approximate location of the label.
- inline : bool, default: True
- If *True* remove the segment of the contour beneath the label.
- inline_spacing : int, default: 5
- Space in pixels to leave on each side of label when placing
- inline. This spacing will be exact for labels at locations where
- the contour is straight, less so for labels on curved contours.
- transform : `.Transform` or `False`, default: ``self.axes.transData``
- A transform applied to ``(x, y)`` before labeling. The default
- causes ``(x, y)`` to be interpreted as data coordinates. `False`
- is a synonym for `.IdentityTransform`; i.e. ``(x, y)`` should be
- interpreted as display coordinates.
- """
- if transform is None:
- transform = self.axes.transData
- if transform:
- x, y = transform.transform((x, y))
- idx_level_min, idx_vtx_min, proj = self._find_nearest_contour(
- (x, y), self.labelIndiceList)
- path = self._paths[idx_level_min]
- level = self.labelIndiceList.index(idx_level_min)
- label_width = self._get_nth_label_width(level)
- rotation, path = self._split_path_and_get_label_rotation(
- path, idx_vtx_min, proj, label_width, inline_spacing)
- self.add_label(*proj, rotation, self.labelLevelList[idx_level_min],
- self.labelCValueList[idx_level_min])
- if inline:
- self._paths[idx_level_min] = path
- def pop_label(self, index=-1):
- """Defaults to removing last label, but any index can be supplied"""
- self.labelCValues.pop(index)
- t = self.labelTexts.pop(index)
- t.remove()
- def labels(self, inline, inline_spacing):
- for idx, (icon, lev, cvalue) in enumerate(zip(
- self.labelIndiceList,
- self.labelLevelList,
- self.labelCValueList,
- )):
- trans = self.get_transform()
- label_width = self._get_nth_label_width(idx)
- additions = []
- for subpath in self._paths[icon]._iter_connected_components():
- screen_xys = trans.transform(subpath.vertices)
- # Check if long enough for a label
- if self.print_label(screen_xys, label_width):
- x, y, idx = self.locate_label(screen_xys, label_width)
- rotation, path = self._split_path_and_get_label_rotation(
- subpath, idx, (x, y),
- label_width, inline_spacing)
- self.add_label(x, y, rotation, lev, cvalue) # Really add label.
- if inline: # If inline, add new contours
- additions.append(path)
- else: # If not adding label, keep old path
- additions.append(subpath)
- # After looping over all segments on a contour, replace old path by new one
- # if inlining.
- if inline:
- self._paths[icon] = Path.make_compound_path(*additions)
- def remove(self):
- super().remove()
- for text in self.labelTexts:
- text.remove()
- def _find_closest_point_on_path(xys, p):
- """
- Parameters
- ----------
- xys : (N, 2) array-like
- Coordinates of vertices.
- p : (float, float)
- Coordinates of point.
- Returns
- -------
- d2min : float
- Minimum square distance of *p* to *xys*.
- proj : (float, float)
- Projection of *p* onto *xys*.
- imin : (int, int)
- Consecutive indices of vertices of segment in *xys* where *proj* is.
- Segments are considered as including their end-points; i.e. if the
- closest point on the path is a node in *xys* with index *i*, this
- returns ``(i-1, i)``. For the special case where *xys* is a single
- point, this returns ``(0, 0)``.
- """
- if len(xys) == 1:
- return (((p - xys[0]) ** 2).sum(), xys[0], (0, 0))
- dxys = xys[1:] - xys[:-1] # Individual segment vectors.
- norms = (dxys ** 2).sum(axis=1)
- norms[norms == 0] = 1 # For zero-length segment, replace 0/0 by 0/1.
- rel_projs = np.clip( # Project onto each segment in relative 0-1 coords.
- ((p - xys[:-1]) * dxys).sum(axis=1) / norms,
- 0, 1)[:, None]
- projs = xys[:-1] + rel_projs * dxys # Projs. onto each segment, in (x, y).
- d2s = ((projs - p) ** 2).sum(axis=1) # Squared distances.
- imin = np.argmin(d2s)
- return (d2s[imin], projs[imin], (imin, imin+1))
- _docstring.interpd.register(contour_set_attributes=r"""
- Attributes
- ----------
- levels : array
- The values of the contour levels.
- layers : array
- Same as levels for line contours; half-way between
- levels for filled contours. See ``ContourSet._process_colors``.
- """)
- @_docstring.interpd
- class ContourSet(ContourLabeler, mcoll.Collection):
- """
- Store a set of contour lines or filled regions.
- User-callable method: `~.Axes.clabel`
- Parameters
- ----------
- ax : `~matplotlib.axes.Axes`
- levels : [level0, level1, ..., leveln]
- A list of floating point numbers indicating the contour levels.
- allsegs : [level0segs, level1segs, ...]
- List of all the polygon segments for all the *levels*.
- For contour lines ``len(allsegs) == len(levels)``, and for
- filled contour regions ``len(allsegs) = len(levels)-1``. The lists
- should look like ::
- level0segs = [polygon0, polygon1, ...]
- polygon0 = [[x0, y0], [x1, y1], ...]
- allkinds : ``None`` or [level0kinds, level1kinds, ...]
- Optional list of all the polygon vertex kinds (code types), as
- described and used in Path. This is used to allow multiply-
- connected paths such as holes within filled polygons.
- If not ``None``, ``len(allkinds) == len(allsegs)``. The lists
- should look like ::
- level0kinds = [polygon0kinds, ...]
- polygon0kinds = [vertexcode0, vertexcode1, ...]
- If *allkinds* is not ``None``, usually all polygons for a
- particular contour level are grouped together so that
- ``level0segs = [polygon0]`` and ``level0kinds = [polygon0kinds]``.
- **kwargs
- Keyword arguments are as described in the docstring of
- `~.Axes.contour`.
- %(contour_set_attributes)s
- """
- def __init__(self, ax, *args,
- levels=None, filled=False, linewidths=None, linestyles=None,
- hatches=(None,), alpha=None, origin=None, extent=None,
- cmap=None, colors=None, norm=None, vmin=None, vmax=None,
- colorizer=None, extend='neither', antialiased=None, nchunk=0,
- locator=None, transform=None, negative_linestyles=None, clip_path=None,
- **kwargs):
- """
- Draw contour lines or filled regions, depending on
- whether keyword arg *filled* is ``False`` (default) or ``True``.
- Call signature::
- ContourSet(ax, levels, allsegs, [allkinds], **kwargs)
- Parameters
- ----------
- ax : `~matplotlib.axes.Axes`
- The `~.axes.Axes` object to draw on.
- levels : [level0, level1, ..., leveln]
- A list of floating point numbers indicating the contour
- levels.
- allsegs : [level0segs, level1segs, ...]
- List of all the polygon segments for all the *levels*.
- For contour lines ``len(allsegs) == len(levels)``, and for
- filled contour regions ``len(allsegs) = len(levels)-1``. The lists
- should look like ::
- level0segs = [polygon0, polygon1, ...]
- polygon0 = [[x0, y0], [x1, y1], ...]
- allkinds : [level0kinds, level1kinds, ...], optional
- Optional list of all the polygon vertex kinds (code types), as
- described and used in Path. This is used to allow multiply-
- connected paths such as holes within filled polygons.
- If not ``None``, ``len(allkinds) == len(allsegs)``. The lists
- should look like ::
- level0kinds = [polygon0kinds, ...]
- polygon0kinds = [vertexcode0, vertexcode1, ...]
- If *allkinds* is not ``None``, usually all polygons for a
- particular contour level are grouped together so that
- ``level0segs = [polygon0]`` and ``level0kinds = [polygon0kinds]``.
- **kwargs
- Keyword arguments are as described in the docstring of
- `~.Axes.contour`.
- """
- if antialiased is None and filled:
- # Eliminate artifacts; we are not stroking the boundaries.
- antialiased = False
- # The default for line contours will be taken from the
- # LineCollection default, which uses :rc:`lines.antialiased`.
- super().__init__(
- antialiaseds=antialiased,
- alpha=alpha,
- clip_path=clip_path,
- transform=transform,
- colorizer=colorizer,
- )
- self.axes = ax
- self.levels = levels
- self.filled = filled
- self.hatches = hatches
- self.origin = origin
- self.extent = extent
- self.colors = colors
- self.extend = extend
- self.nchunk = nchunk
- self.locator = locator
- if colorizer:
- self._set_colorizer_check_keywords(colorizer, cmap=cmap,
- norm=norm, vmin=vmin,
- vmax=vmax, colors=colors)
- norm = colorizer.norm
- cmap = colorizer.cmap
- if (isinstance(norm, mcolors.LogNorm)
- or isinstance(self.locator, ticker.LogLocator)):
- self.logscale = True
- if norm is None:
- norm = mcolors.LogNorm()
- else:
- self.logscale = False
- _api.check_in_list([None, 'lower', 'upper', 'image'], origin=origin)
- if self.extent is not None and len(self.extent) != 4:
- raise ValueError(
- "If given, 'extent' must be None or (x0, x1, y0, y1)")
- if self.colors is not None and cmap is not None:
- raise ValueError('Either colors or cmap must be None')
- if self.origin == 'image':
- self.origin = mpl.rcParams['image.origin']
- self._orig_linestyles = linestyles # Only kept for user access.
- self.negative_linestyles = negative_linestyles
- # If negative_linestyles was not defined as a keyword argument, define
- # negative_linestyles with rcParams
- if self.negative_linestyles is None:
- self.negative_linestyles = \
- mpl.rcParams['contour.negative_linestyle']
- kwargs = self._process_args(*args, **kwargs)
- self._process_levels()
- self._extend_min = self.extend in ['min', 'both']
- self._extend_max = self.extend in ['max', 'both']
- if self.colors is not None:
- if mcolors.is_color_like(self.colors):
- color_sequence = [self.colors]
- else:
- color_sequence = self.colors
- ncolors = len(self.levels)
- if self.filled:
- ncolors -= 1
- i0 = 0
- # Handle the case where colors are given for the extended
- # parts of the contour.
- use_set_under_over = False
- # if we are extending the lower end, and we've been given enough
- # colors then skip the first color in the resulting cmap. For the
- # extend_max case we don't need to worry about passing more colors
- # than ncolors as ListedColormap will clip.
- total_levels = (ncolors +
- int(self._extend_min) +
- int(self._extend_max))
- if (len(color_sequence) == total_levels and
- (self._extend_min or self._extend_max)):
- use_set_under_over = True
- if self._extend_min:
- i0 = 1
- cmap = mcolors.ListedColormap(color_sequence[i0:None], N=ncolors)
- if use_set_under_over:
- if self._extend_min:
- cmap.set_under(color_sequence[0])
- if self._extend_max:
- cmap.set_over(color_sequence[-1])
- # label lists must be initialized here
- self.labelTexts = []
- self.labelCValues = []
- self.set_cmap(cmap)
- if norm is not None:
- self.set_norm(norm)
- with self.norm.callbacks.blocked(signal="changed"):
- if vmin is not None:
- self.norm.vmin = vmin
- if vmax is not None:
- self.norm.vmax = vmax
- self.norm._changed()
- self._process_colors()
- if self._paths is None:
- self._paths = self._make_paths_from_contour_generator()
- if self.filled:
- if linewidths is not None:
- _api.warn_external('linewidths is ignored by contourf')
- # Lower and upper contour levels.
- lowers, uppers = self._get_lowers_and_uppers()
- self.set(
- edgecolor="none",
- # Default zorder taken from Collection
- zorder=kwargs.pop("zorder", 1),
- )
- else:
- self.set(
- facecolor="none",
- linewidths=self._process_linewidths(linewidths),
- linestyle=self._process_linestyles(linestyles),
- # Default zorder taken from LineCollection, which is higher
- # than for filled contours so that lines are displayed on top.
- zorder=kwargs.pop("zorder", 2),
- label="_nolegend_",
- )
- self.axes.add_collection(self, autolim=False)
- self.sticky_edges.x[:] = [self._mins[0], self._maxs[0]]
- self.sticky_edges.y[:] = [self._mins[1], self._maxs[1]]
- self.axes.update_datalim([self._mins, self._maxs])
- self.axes.autoscale_view(tight=True)
- self.changed() # set the colors
- if kwargs:
- _api.warn_external(
- 'The following kwargs were not used by contour: ' +
- ", ".join(map(repr, kwargs))
- )
- allsegs = property(lambda self: [
- [subp.vertices for subp in p._iter_connected_components()]
- for p in self.get_paths()])
- allkinds = property(lambda self: [
- [subp.codes for subp in p._iter_connected_components()]
- for p in self.get_paths()])
- alpha = property(lambda self: self.get_alpha())
- linestyles = property(lambda self: self._orig_linestyles)
- def get_transform(self):
- """Return the `.Transform` instance used by this ContourSet."""
- if self._transform is None:
- self._transform = self.axes.transData
- elif (not isinstance(self._transform, mtransforms.Transform)
- and hasattr(self._transform, '_as_mpl_transform')):
- self._transform = self._transform._as_mpl_transform(self.axes)
- return self._transform
- def __getstate__(self):
- state = self.__dict__.copy()
- # the C object _contour_generator cannot currently be pickled. This
- # isn't a big issue as it is not actually used once the contour has
- # been calculated.
- state['_contour_generator'] = None
- return state
- def legend_elements(self, variable_name='x', str_format=str):
- """
- Return a list of artists and labels suitable for passing through
- to `~.Axes.legend` which represent this ContourSet.
- The labels have the form "0 < x <= 1" stating the data ranges which
- the artists represent.
- Parameters
- ----------
- variable_name : str
- The string used inside the inequality used on the labels.
- str_format : function: float -> str
- Function used to format the numbers in the labels.
- Returns
- -------
- artists : list[`.Artist`]
- A list of the artists.
- labels : list[str]
- A list of the labels.
- """
- artists = []
- labels = []
- if self.filled:
- lowers, uppers = self._get_lowers_and_uppers()
- n_levels = len(self._paths)
- for idx in range(n_levels):
- artists.append(mpatches.Rectangle(
- (0, 0), 1, 1,
- facecolor=self.get_facecolor()[idx],
- hatch=self.hatches[idx % len(self.hatches)],
- ))
- lower = str_format(lowers[idx])
- upper = str_format(uppers[idx])
- if idx == 0 and self.extend in ('min', 'both'):
- labels.append(fr'${variable_name} \leq {lower}s$')
- elif idx == n_levels - 1 and self.extend in ('max', 'both'):
- labels.append(fr'${variable_name} > {upper}s$')
- else:
- labels.append(fr'${lower} < {variable_name} \leq {upper}$')
- else:
- for idx, level in enumerate(self.levels):
- artists.append(Line2D(
- [], [],
- color=self.get_edgecolor()[idx],
- linewidth=self.get_linewidths()[idx],
- linestyle=self.get_linestyles()[idx],
- ))
- labels.append(fr'${variable_name} = {str_format(level)}$')
- return artists, labels
- def _process_args(self, *args, **kwargs):
- """
- Process *args* and *kwargs*; override in derived classes.
- Must set self.levels, self.zmin and self.zmax, and update Axes limits.
- """
- self.levels = args[0]
- allsegs = args[1]
- allkinds = args[2] if len(args) > 2 else None
- self.zmax = np.max(self.levels)
- self.zmin = np.min(self.levels)
- if allkinds is None:
- allkinds = [[None] * len(segs) for segs in allsegs]
- # Check lengths of levels and allsegs.
- if self.filled:
- if len(allsegs) != len(self.levels) - 1:
- raise ValueError('must be one less number of segments as '
- 'levels')
- else:
- if len(allsegs) != len(self.levels):
- raise ValueError('must be same number of segments as levels')
- # Check length of allkinds.
- if len(allkinds) != len(allsegs):
- raise ValueError('allkinds has different length to allsegs')
- # Determine x, y bounds and update axes data limits.
- flatseglist = [s for seg in allsegs for s in seg]
- points = np.concatenate(flatseglist, axis=0)
- self._mins = points.min(axis=0)
- self._maxs = points.max(axis=0)
- # Each entry in (allsegs, allkinds) is a list of (segs, kinds): segs is a list
- # of (N, 2) arrays of xy coordinates, kinds is a list of arrays of corresponding
- # pathcodes. However, kinds can also be None; in which case all paths in that
- # list are codeless (this case is normalized above). These lists are used to
- # construct paths, which then get concatenated.
- self._paths = [Path.make_compound_path(*map(Path, segs, kinds))
- for segs, kinds in zip(allsegs, allkinds)]
- return kwargs
- def _make_paths_from_contour_generator(self):
- """Compute ``paths`` using C extension."""
- if self._paths is not None:
- return self._paths
- cg = self._contour_generator
- empty_path = Path(np.empty((0, 2)))
- vertices_and_codes = (
- map(cg.create_filled_contour, *self._get_lowers_and_uppers())
- if self.filled else
- map(cg.create_contour, self.levels))
- return [Path(np.concatenate(vs), np.concatenate(cs)) if len(vs) else empty_path
- for vs, cs in vertices_and_codes]
- def _get_lowers_and_uppers(self):
- """
- Return ``(lowers, uppers)`` for filled contours.
- """
- lowers = self._levels[:-1]
- if self.zmin == lowers[0]:
- # Include minimum values in lowest interval
- lowers = lowers.copy() # so we don't change self._levels
- if self.logscale:
- lowers[0] = 0.99 * self.zmin
- else:
- lowers[0] -= 1
- uppers = self._levels[1:]
- return (lowers, uppers)
- def changed(self):
- if not hasattr(self, "cvalues"):
- self._process_colors() # Sets cvalues.
- # Force an autoscale immediately because self.to_rgba() calls
- # autoscale_None() internally with the data passed to it,
- # so if vmin/vmax are not set yet, this would override them with
- # content from *cvalues* rather than levels like we want
- self.norm.autoscale_None(self.levels)
- self.set_array(self.cvalues)
- self.update_scalarmappable()
- alphas = np.broadcast_to(self.get_alpha(), len(self.cvalues))
- for label, cv, alpha in zip(self.labelTexts, self.labelCValues, alphas):
- label.set_alpha(alpha)
- label.set_color(self.labelMappable.to_rgba(cv))
- super().changed()
- def _autolev(self, N):
- """
- Select contour levels to span the data.
- The target number of levels, *N*, is used only when the
- scale is not log and default locator is used.
- We need two more levels for filled contours than for
- line contours, because for the latter we need to specify
- the lower and upper boundary of each range. For example,
- a single contour boundary, say at z = 0, requires only
- one contour line, but two filled regions, and therefore
- three levels to provide boundaries for both regions.
- """
- if self.locator is None:
- if self.logscale:
- self.locator = ticker.LogLocator()
- else:
- self.locator = ticker.MaxNLocator(N + 1, min_n_ticks=1)
- lev = self.locator.tick_values(self.zmin, self.zmax)
- try:
- if self.locator._symmetric:
- return lev
- except AttributeError:
- pass
- # Trim excess levels the locator may have supplied.
- under = np.nonzero(lev < self.zmin)[0]
- i0 = under[-1] if len(under) else 0
- over = np.nonzero(lev > self.zmax)[0]
- i1 = over[0] + 1 if len(over) else len(lev)
- if self.extend in ('min', 'both'):
- i0 += 1
- if self.extend in ('max', 'both'):
- i1 -= 1
- if i1 - i0 < 3:
- i0, i1 = 0, len(lev)
- return lev[i0:i1]
- def _process_contour_level_args(self, args, z_dtype):
- """
- Determine the contour levels and store in self.levels.
- """
- if self.levels is None:
- if args:
- levels_arg = args[0]
- elif np.issubdtype(z_dtype, bool):
- if self.filled:
- levels_arg = [0, .5, 1]
- else:
- levels_arg = [.5]
- else:
- levels_arg = 7 # Default, hard-wired.
- else:
- levels_arg = self.levels
- if isinstance(levels_arg, Integral):
- self.levels = self._autolev(levels_arg)
- else:
- self.levels = np.asarray(levels_arg, np.float64)
- if self.filled and len(self.levels) < 2:
- raise ValueError("Filled contours require at least 2 levels.")
- if len(self.levels) > 1 and np.min(np.diff(self.levels)) <= 0.0:
- raise ValueError("Contour levels must be increasing")
- def _process_levels(self):
- """
- Assign values to :attr:`layers` based on :attr:`levels`,
- adding extended layers as needed if contours are filled.
- For line contours, layers simply coincide with levels;
- a line is a thin layer. No extended levels are needed
- with line contours.
- """
- # Make a private _levels to include extended regions; we
- # want to leave the original levels attribute unchanged.
- # (Colorbar needs this even for line contours.)
- self._levels = list(self.levels)
- if self.logscale:
- lower, upper = 1e-250, 1e250
- else:
- lower, upper = -1e250, 1e250
- if self.extend in ('both', 'min'):
- self._levels.insert(0, lower)
- if self.extend in ('both', 'max'):
- self._levels.append(upper)
- self._levels = np.asarray(self._levels)
- if not self.filled:
- self.layers = self.levels
- return
- # Layer values are mid-way between levels in screen space.
- if self.logscale:
- # Avoid overflow by taking sqrt before multiplying.
- self.layers = (np.sqrt(self._levels[:-1])
- * np.sqrt(self._levels[1:]))
- else:
- self.layers = 0.5 * (self._levels[:-1] + self._levels[1:])
- def _process_colors(self):
- """
- Color argument processing for contouring.
- Note that we base the colormapping on the contour levels
- and layers, not on the actual range of the Z values. This
- means we don't have to worry about bad values in Z, and we
- always have the full dynamic range available for the selected
- levels.
- The color is based on the midpoint of the layer, except for
- extended end layers. By default, the norm vmin and vmax
- are the extreme values of the non-extended levels. Hence,
- the layer color extremes are not the extreme values of
- the colormap itself, but approach those values as the number
- of levels increases. An advantage of this scheme is that
- line contours, when added to filled contours, take on
- colors that are consistent with those of the filled regions;
- for example, a contour line on the boundary between two
- regions will have a color intermediate between those
- of the regions.
- """
- self.monochrome = self.cmap.monochrome
- if self.colors is not None:
- # Generate integers for direct indexing.
- i0, i1 = 0, len(self.levels)
- if self.filled:
- i1 -= 1
- # Out of range indices for over and under:
- if self.extend in ('both', 'min'):
- i0 -= 1
- if self.extend in ('both', 'max'):
- i1 += 1
- self.cvalues = list(range(i0, i1))
- self.set_norm(mcolors.NoNorm())
- else:
- self.cvalues = self.layers
- self.norm.autoscale_None(self.levels)
- self.set_array(self.cvalues)
- self.update_scalarmappable()
- if self.extend in ('both', 'max', 'min'):
- self.norm.clip = False
- def _process_linewidths(self, linewidths):
- Nlev = len(self.levels)
- if linewidths is None:
- default_linewidth = mpl.rcParams['contour.linewidth']
- if default_linewidth is None:
- default_linewidth = mpl.rcParams['lines.linewidth']
- return [default_linewidth] * Nlev
- elif not np.iterable(linewidths):
- return [linewidths] * Nlev
- else:
- linewidths = list(linewidths)
- return (linewidths * math.ceil(Nlev / len(linewidths)))[:Nlev]
- def _process_linestyles(self, linestyles):
- Nlev = len(self.levels)
- if linestyles is None:
- tlinestyles = ['solid'] * Nlev
- if self.monochrome:
- eps = - (self.zmax - self.zmin) * 1e-15
- for i, lev in enumerate(self.levels):
- if lev < eps:
- tlinestyles[i] = self.negative_linestyles
- else:
- if isinstance(linestyles, str):
- tlinestyles = [linestyles] * Nlev
- elif np.iterable(linestyles):
- tlinestyles = list(linestyles)
- if len(tlinestyles) < Nlev:
- nreps = int(np.ceil(Nlev / len(linestyles)))
- tlinestyles = tlinestyles * nreps
- if len(tlinestyles) > Nlev:
- tlinestyles = tlinestyles[:Nlev]
- else:
- raise ValueError("Unrecognized type for linestyles kwarg")
- return tlinestyles
- def _find_nearest_contour(self, xy, indices=None):
- """
- Find the point in the unfilled contour plot that is closest (in screen
- space) to point *xy*.
- Parameters
- ----------
- xy : tuple[float, float]
- The reference point (in screen space).
- indices : list of int or None, default: None
- Indices of contour levels to consider. If None (the default), all levels
- are considered.
- Returns
- -------
- idx_level_min : int
- The index of the contour level closest to *xy*.
- idx_vtx_min : int
- The index of the `.Path` segment closest to *xy* (at that level).
- proj : (float, float)
- The point in the contour plot closest to *xy*.
- """
- # Convert each contour segment to pixel coordinates and then compare the given
- # point to those coordinates for each contour. This is fast enough in normal
- # cases, but speedups may be possible.
- if self.filled:
- raise ValueError("Method does not support filled contours")
- if indices is None:
- indices = range(len(self._paths))
- d2min = np.inf
- idx_level_min = idx_vtx_min = proj_min = None
- for idx_level in indices:
- path = self._paths[idx_level]
- idx_vtx_start = 0
- for subpath in path._iter_connected_components():
- if not len(subpath.vertices):
- continue
- lc = self.get_transform().transform(subpath.vertices)
- d2, proj, leg = _find_closest_point_on_path(lc, xy)
- if d2 < d2min:
- d2min = d2
- idx_level_min = idx_level
- idx_vtx_min = leg[1] + idx_vtx_start
- proj_min = proj
- idx_vtx_start += len(subpath)
- return idx_level_min, idx_vtx_min, proj_min
- def find_nearest_contour(self, x, y, indices=None, pixel=True):
- """
- Find the point in the contour plot that is closest to ``(x, y)``.
- This method does not support filled contours.
- Parameters
- ----------
- x, y : float
- The reference point.
- indices : list of int or None, default: None
- Indices of contour levels to consider. If None (the default), all
- levels are considered.
- pixel : bool, default: True
- If *True*, measure distance in pixel (screen) space, which is
- useful for manual contour labeling; else, measure distance in axes
- space.
- Returns
- -------
- path : int
- The index of the path that is closest to ``(x, y)``. Each path corresponds
- to one contour level.
- subpath : int
- The index within that closest path of the subpath that is closest to
- ``(x, y)``. Each subpath corresponds to one unbroken contour line.
- index : int
- The index of the vertices within that subpath that are closest to
- ``(x, y)``.
- xmin, ymin : float
- The point in the contour plot that is closest to ``(x, y)``.
- d2 : float
- The squared distance from ``(xmin, ymin)`` to ``(x, y)``.
- """
- segment = index = d2 = None
- with ExitStack() as stack:
- if not pixel:
- # _find_nearest_contour works in pixel space. We want axes space, so
- # effectively disable the transformation here by setting to identity.
- stack.enter_context(self._cm_set(
- transform=mtransforms.IdentityTransform()))
- i_level, i_vtx, (xmin, ymin) = self._find_nearest_contour((x, y), indices)
- if i_level is not None:
- cc_cumlens = np.cumsum(
- [*map(len, self._paths[i_level]._iter_connected_components())])
- segment = cc_cumlens.searchsorted(i_vtx, "right")
- index = i_vtx if segment == 0 else i_vtx - cc_cumlens[segment - 1]
- d2 = (xmin-x)**2 + (ymin-y)**2
- return (i_level, segment, index, xmin, ymin, d2)
- def draw(self, renderer):
- paths = self._paths
- n_paths = len(paths)
- if not self.filled or all(hatch is None for hatch in self.hatches):
- super().draw(renderer)
- return
- # In presence of hatching, draw contours one at a time.
- edgecolors = self.get_edgecolors()
- if edgecolors.size == 0:
- edgecolors = ("none",)
- for idx in range(n_paths):
- with cbook._setattr_cm(self, _paths=[paths[idx]]), self._cm_set(
- hatch=self.hatches[idx % len(self.hatches)],
- array=[self.get_array()[idx]],
- linewidths=[self.get_linewidths()[idx % len(self.get_linewidths())]],
- linestyles=[self.get_linestyles()[idx % len(self.get_linestyles())]],
- edgecolors=edgecolors[idx % len(edgecolors)],
- ):
- super().draw(renderer)
- @_docstring.interpd
- class QuadContourSet(ContourSet):
- """
- Create and store a set of contour lines or filled regions.
- This class is typically not instantiated directly by the user but by
- `~.Axes.contour` and `~.Axes.contourf`.
- %(contour_set_attributes)s
- """
- def _process_args(self, *args, corner_mask=None, algorithm=None, **kwargs):
- """
- Process args and kwargs.
- """
- if args and isinstance(args[0], QuadContourSet):
- if self.levels is None:
- self.levels = args[0].levels
- self.zmin = args[0].zmin
- self.zmax = args[0].zmax
- self._corner_mask = args[0]._corner_mask
- contour_generator = args[0]._contour_generator
- self._mins = args[0]._mins
- self._maxs = args[0]._maxs
- self._algorithm = args[0]._algorithm
- else:
- import contourpy
- if algorithm is None:
- algorithm = mpl.rcParams['contour.algorithm']
- mpl.rcParams.validate["contour.algorithm"](algorithm)
- self._algorithm = algorithm
- if corner_mask is None:
- if self._algorithm == "mpl2005":
- # mpl2005 does not support corner_mask=True so if not
- # specifically requested then disable it.
- corner_mask = False
- else:
- corner_mask = mpl.rcParams['contour.corner_mask']
- self._corner_mask = corner_mask
- x, y, z = self._contour_args(args, kwargs)
- contour_generator = contourpy.contour_generator(
- x, y, z, name=self._algorithm, corner_mask=self._corner_mask,
- line_type=contourpy.LineType.SeparateCode,
- fill_type=contourpy.FillType.OuterCode,
- chunk_size=self.nchunk)
- t = self.get_transform()
- # if the transform is not trans data, and some part of it
- # contains transData, transform the xs and ys to data coordinates
- if (t != self.axes.transData and
- any(t.contains_branch_seperately(self.axes.transData))):
- trans_to_data = t - self.axes.transData
- pts = np.vstack([x.flat, y.flat]).T
- transformed_pts = trans_to_data.transform(pts)
- x = transformed_pts[..., 0]
- y = transformed_pts[..., 1]
- self._mins = [ma.min(x), ma.min(y)]
- self._maxs = [ma.max(x), ma.max(y)]
- self._contour_generator = contour_generator
- return kwargs
- def _contour_args(self, args, kwargs):
- if self.filled:
- fn = 'contourf'
- else:
- fn = 'contour'
- nargs = len(args)
- if 0 < nargs <= 2:
- z, *args = args
- z = ma.asarray(z)
- x, y = self._initialize_x_y(z)
- elif 2 < nargs <= 4:
- x, y, z_orig, *args = args
- x, y, z = self._check_xyz(x, y, z_orig, kwargs)
- else:
- raise _api.nargs_error(fn, takes="from 1 to 4", given=nargs)
- z = ma.masked_invalid(z, copy=False)
- self.zmax = z.max().astype(float)
- self.zmin = z.min().astype(float)
- if self.logscale and self.zmin <= 0:
- z = ma.masked_where(z <= 0, z)
- _api.warn_external('Log scale: values of z <= 0 have been masked')
- self.zmin = z.min().astype(float)
- self._process_contour_level_args(args, z.dtype)
- return (x, y, z)
- def _check_xyz(self, x, y, z, kwargs):
- """
- Check that the shapes of the input arrays match; if x and y are 1D,
- convert them to 2D using meshgrid.
- """
- x, y = self.axes._process_unit_info([("x", x), ("y", y)], kwargs)
- x = np.asarray(x, dtype=np.float64)
- y = np.asarray(y, dtype=np.float64)
- z = ma.asarray(z)
- if z.ndim != 2:
- raise TypeError(f"Input z must be 2D, not {z.ndim}D")
- if z.shape[0] < 2 or z.shape[1] < 2:
- raise TypeError(f"Input z must be at least a (2, 2) shaped array, "
- f"but has shape {z.shape}")
- Ny, Nx = z.shape
- if x.ndim != y.ndim:
- raise TypeError(f"Number of dimensions of x ({x.ndim}) and y "
- f"({y.ndim}) do not match")
- if x.ndim == 1:
- nx, = x.shape
- ny, = y.shape
- if nx != Nx:
- raise TypeError(f"Length of x ({nx}) must match number of "
- f"columns in z ({Nx})")
- if ny != Ny:
- raise TypeError(f"Length of y ({ny}) must match number of "
- f"rows in z ({Ny})")
- x, y = np.meshgrid(x, y)
- elif x.ndim == 2:
- if x.shape != z.shape:
- raise TypeError(
- f"Shapes of x {x.shape} and z {z.shape} do not match")
- if y.shape != z.shape:
- raise TypeError(
- f"Shapes of y {y.shape} and z {z.shape} do not match")
- else:
- raise TypeError(f"Inputs x and y must be 1D or 2D, not {x.ndim}D")
- return x, y, z
- def _initialize_x_y(self, z):
- """
- Return X, Y arrays such that contour(Z) will match imshow(Z)
- if origin is not None.
- The center of pixel Z[i, j] depends on origin:
- if origin is None, x = j, y = i;
- if origin is 'lower', x = j + 0.5, y = i + 0.5;
- if origin is 'upper', x = j + 0.5, y = Nrows - i - 0.5
- If extent is not None, x and y will be scaled to match,
- as in imshow.
- If origin is None and extent is not None, then extent
- will give the minimum and maximum values of x and y.
- """
- if z.ndim != 2:
- raise TypeError(f"Input z must be 2D, not {z.ndim}D")
- elif z.shape[0] < 2 or z.shape[1] < 2:
- raise TypeError(f"Input z must be at least a (2, 2) shaped array, "
- f"but has shape {z.shape}")
- else:
- Ny, Nx = z.shape
- if self.origin is None: # Not for image-matching.
- if self.extent is None:
- return np.meshgrid(np.arange(Nx), np.arange(Ny))
- else:
- x0, x1, y0, y1 = self.extent
- x = np.linspace(x0, x1, Nx)
- y = np.linspace(y0, y1, Ny)
- return np.meshgrid(x, y)
- # Match image behavior:
- if self.extent is None:
- x0, x1, y0, y1 = (0, Nx, 0, Ny)
- else:
- x0, x1, y0, y1 = self.extent
- dx = (x1 - x0) / Nx
- dy = (y1 - y0) / Ny
- x = x0 + (np.arange(Nx) + 0.5) * dx
- y = y0 + (np.arange(Ny) + 0.5) * dy
- if self.origin == 'upper':
- y = y[::-1]
- return np.meshgrid(x, y)
- _docstring.interpd.register(contour_doc="""
- `.contour` and `.contourf` draw contour lines and filled contours,
- respectively. Except as noted, function signatures and return values
- are the same for both versions.
- Parameters
- ----------
- X, Y : array-like, optional
- The coordinates of the values in *Z*.
- *X* and *Y* must both be 2D with the same shape as *Z* (e.g.
- created via `numpy.meshgrid`), or they must both be 1-D such
- that ``len(X) == N`` is the number of columns in *Z* and
- ``len(Y) == M`` is the number of rows in *Z*.
- *X* and *Y* must both be ordered monotonically.
- If not given, they are assumed to be integer indices, i.e.
- ``X = range(N)``, ``Y = range(M)``.
- Z : (M, N) array-like
- The height values over which the contour is drawn. Color-mapping is
- controlled by *cmap*, *norm*, *vmin*, and *vmax*.
- levels : int or array-like, optional
- Determines the number and positions of the contour lines / regions.
- If an int *n*, use `~matplotlib.ticker.MaxNLocator`, which tries
- to automatically choose no more than *n+1* "nice" contour levels
- between minimum and maximum numeric values of *Z*.
- If array-like, draw contour lines at the specified levels.
- The values must be in increasing order.
- Returns
- -------
- `~.contour.QuadContourSet`
- Other Parameters
- ----------------
- corner_mask : bool, default: :rc:`contour.corner_mask`
- Enable/disable corner masking, which only has an effect if *Z* is
- a masked array. If ``False``, any quad touching a masked point is
- masked out. If ``True``, only the triangular corners of quads
- nearest those points are always masked out, other triangular
- corners comprising three unmasked points are contoured as usual.
- colors : :mpltype:`color` or list of :mpltype:`color`, optional
- The colors of the levels, i.e. the lines for `.contour` and the
- areas for `.contourf`.
- The sequence is cycled for the levels in ascending order. If the
- sequence is shorter than the number of levels, it's repeated.
- As a shortcut, a single color may be used in place of one-element lists, i.e.
- ``'red'`` instead of ``['red']`` to color all levels with the same color.
- .. versionchanged:: 3.10
- Previously a single color had to be expressed as a string, but now any
- valid color format may be passed.
- By default (value *None*), the colormap specified by *cmap*
- will be used.
- alpha : float, default: 1
- The alpha blending value, between 0 (transparent) and 1 (opaque).
- %(cmap_doc)s
- This parameter is ignored if *colors* is set.
- %(norm_doc)s
- This parameter is ignored if *colors* is set.
- %(vmin_vmax_doc)s
- If *vmin* or *vmax* are not given, the default color scaling is based on
- *levels*.
- This parameter is ignored if *colors* is set.
- %(colorizer_doc)s
- This parameter is ignored if *colors* is set.
- origin : {*None*, 'upper', 'lower', 'image'}, default: None
- Determines the orientation and exact position of *Z* by specifying
- the position of ``Z[0, 0]``. This is only relevant, if *X*, *Y*
- are not given.
- - *None*: ``Z[0, 0]`` is at X=0, Y=0 in the lower left corner.
- - 'lower': ``Z[0, 0]`` is at X=0.5, Y=0.5 in the lower left corner.
- - 'upper': ``Z[0, 0]`` is at X=N+0.5, Y=0.5 in the upper left
- corner.
- - 'image': Use the value from :rc:`image.origin`.
- extent : (x0, x1, y0, y1), optional
- If *origin* is not *None*, then *extent* is interpreted as in
- `.imshow`: it gives the outer pixel boundaries. In this case, the
- position of Z[0, 0] is the center of the pixel, not a corner. If
- *origin* is *None*, then (*x0*, *y0*) is the position of Z[0, 0],
- and (*x1*, *y1*) is the position of Z[-1, -1].
- This argument is ignored if *X* and *Y* are specified in the call
- to contour.
- locator : ticker.Locator subclass, optional
- The locator is used to determine the contour levels if they
- are not given explicitly via *levels*.
- Defaults to `~.ticker.MaxNLocator`.
- extend : {'neither', 'both', 'min', 'max'}, default: 'neither'
- Determines the ``contourf``-coloring of values that are outside the
- *levels* range.
- If 'neither', values outside the *levels* range are not colored.
- If 'min', 'max' or 'both', color the values below, above or below
- and above the *levels* range.
- Values below ``min(levels)`` and above ``max(levels)`` are mapped
- to the under/over values of the `.Colormap`. Note that most
- colormaps do not have dedicated colors for these by default, so
- that the over and under values are the edge values of the colormap.
- You may want to set these values explicitly using
- `.Colormap.set_under` and `.Colormap.set_over`.
- .. note::
- An existing `.QuadContourSet` does not get notified if
- properties of its colormap are changed. Therefore, an explicit
- call `~.ContourSet.changed()` is needed after modifying the
- colormap. The explicit call can be left out, if a colorbar is
- assigned to the `.QuadContourSet` because it internally calls
- `~.ContourSet.changed()`.
- Example::
- x = np.arange(1, 10)
- y = x.reshape(-1, 1)
- h = x * y
- cs = plt.contourf(h, levels=[10, 30, 50],
- colors=['#808080', '#A0A0A0', '#C0C0C0'], extend='both')
- cs.cmap.set_over('red')
- cs.cmap.set_under('blue')
- cs.changed()
- xunits, yunits : registered units, optional
- Override axis units by specifying an instance of a
- :class:`matplotlib.units.ConversionInterface`.
- antialiased : bool, optional
- Enable antialiasing, overriding the defaults. For
- filled contours, the default is *False*. For line contours,
- it is taken from :rc:`lines.antialiased`.
- nchunk : int >= 0, optional
- If 0, no subdivision of the domain. Specify a positive integer to
- divide the domain into subdomains of *nchunk* by *nchunk* quads.
- Chunking reduces the maximum length of polygons generated by the
- contouring algorithm which reduces the rendering workload passed
- on to the backend and also requires slightly less RAM. It can
- however introduce rendering artifacts at chunk boundaries depending
- on the backend, the *antialiased* flag and value of *alpha*.
- linewidths : float or array-like, default: :rc:`contour.linewidth`
- *Only applies to* `.contour`.
- The line width of the contour lines.
- If a number, all levels will be plotted with this linewidth.
- If a sequence, the levels in ascending order will be plotted with
- the linewidths in the order specified.
- If None, this falls back to :rc:`lines.linewidth`.
- linestyles : {*None*, 'solid', 'dashed', 'dashdot', 'dotted'}, optional
- *Only applies to* `.contour`.
- If *linestyles* is *None*, the default is 'solid' unless the lines are
- monochrome. In that case, negative contours will instead take their
- linestyle from the *negative_linestyles* argument.
- *linestyles* can also be an iterable of the above strings specifying a set
- of linestyles to be used. If this iterable is shorter than the number of
- contour levels it will be repeated as necessary.
- negative_linestyles : {*None*, 'solid', 'dashed', 'dashdot', 'dotted'}, \
- optional
- *Only applies to* `.contour`.
- If *linestyles* is *None* and the lines are monochrome, this argument
- specifies the line style for negative contours.
- If *negative_linestyles* is *None*, the default is taken from
- :rc:`contour.negative_linestyle`.
- *negative_linestyles* can also be an iterable of the above strings
- specifying a set of linestyles to be used. If this iterable is shorter than
- the number of contour levels it will be repeated as necessary.
- hatches : list[str], optional
- *Only applies to* `.contourf`.
- A list of cross hatch patterns to use on the filled areas.
- If None, no hatching will be added to the contour.
- algorithm : {'mpl2005', 'mpl2014', 'serial', 'threaded'}, optional
- Which contouring algorithm to use to calculate the contour lines and
- polygons. The algorithms are implemented in
- `ContourPy <https://github.com/contourpy/contourpy>`_, consult the
- `ContourPy documentation <https://contourpy.readthedocs.io>`_ for
- further information.
- The default is taken from :rc:`contour.algorithm`.
- clip_path : `~matplotlib.patches.Patch` or `.Path` or `.TransformedPath`
- Set the clip path. See `~matplotlib.artist.Artist.set_clip_path`.
- .. versionadded:: 3.8
- data : indexable object, optional
- DATA_PARAMETER_PLACEHOLDER
- Notes
- -----
- 1. `.contourf` differs from the MATLAB version in that it does not draw
- the polygon edges. To draw edges, add line contours with calls to
- `.contour`.
- 2. `.contourf` fills intervals that are closed at the top; that is, for
- boundaries *z1* and *z2*, the filled region is::
- z1 < Z <= z2
- except for the lowest interval, which is closed on both sides (i.e.
- it includes the lowest value).
- 3. `.contour` and `.contourf` use a `marching squares
- <https://en.wikipedia.org/wiki/Marching_squares>`_ algorithm to
- compute contour locations. More information can be found in
- `ContourPy documentation <https://contourpy.readthedocs.io>`_.
- """ % _docstring.interpd.params)
|