From f4399ecec2fa283007253f9f315dff4efa681608 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Mon, 12 Jun 2017 17:54:20 -0700 Subject: [PATCH 1/5] First pass; seems to work. More testing needed --- lib/matplotlib/colorbar.py | 41 ++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 5894ecf123c2..f06cc34a6ab9 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -1166,7 +1166,7 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, @docstring.Substitution(make_axes_kw_doc) -def make_axes_gridspec(parent, **kw): +def make_axes_gridspec(parents, **kw): ''' Resize and reposition a parent axes, and return a child axes suitable for a colorbar. This function is similar to @@ -1213,13 +1213,35 @@ def make_axes_gridspec(parent, **kw): pad_s = (1. - shrink) * 0.5 wh_ratios = [pad_s, shrink, pad_s] + # make parents a 1-d ndarray if its not already... + parents = np.atleast_1d(parents).ravel() + # get the appropriate subplot spec. Loop through the parents. + gs0 = parents[0].get_subplotspec().get_gridspec() + minind = 10000 + maxind = -10000 + for parent in parents: + gs = parent.get_subplotspec().get_gridspec() + if gs == gs0: + ss = parent.get_subplotspec().get_geometry() + if ss[2]maxind: + maxind = ss[3] + else: + pass + print(minind) + print(maxind) + subspec = gridspec.SubplotSpec(gs0,minind,maxind) + print(subspec) + print(subspec.get_geometry()) + gs_from_subplotspec = gridspec.GridSpecFromSubplotSpec if orientation == 'vertical': pad = kw.pop('pad', 0.05) wh_space = 2 * pad / (1 - pad) gs = gs_from_subplotspec(1, 2, - subplot_spec=parent.get_subplotspec(), + subplot_spec=subspec, wspace=wh_space, width_ratios=[x1 - pad, fraction] ) @@ -1229,6 +1251,8 @@ def make_axes_gridspec(parent, **kw): hspace=0., height_ratios=wh_ratios, ) + print(gs) + print(gs2) anchor = (0.0, 0.5) panchor = (1.0, 0.5) @@ -1237,7 +1261,7 @@ def make_axes_gridspec(parent, **kw): wh_space = 2 * pad / (1 - pad) gs = gs_from_subplotspec(2, 1, - subplot_spec=parent.get_subplotspec(), + subplot_spec=subspec, hspace=wh_space, height_ratios=[x1 - pad, fraction] ) @@ -1252,12 +1276,13 @@ def make_axes_gridspec(parent, **kw): anchor = (0.5, 1.0) panchor = (0.5, 0.0) - parent.set_subplotspec(gs[0]) - parent.update_params() - parent.set_position(parent.figbox) - parent.set_anchor(panchor) + for parent in parents: + parent.set_subplotspec(gs[0]) + parent.update_params() + parent.set_position(parent.figbox) + parent.set_anchor(panchor) - fig = parent.get_figure() + fig = parents[0].get_figure() cax = fig.add_subplot(gs2[1]) cax.set_aspect(aspect, anchor=anchor, adjustable='box') return cax, kw From b9a07aca7961cbc049a07bd9c894a589d9ec0ddf Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Mon, 12 Jun 2017 17:55:03 -0700 Subject: [PATCH 2/5] First pass; seems to work. More testing needed --- lib/matplotlib/colorbar.py | 72 ++---- lib/matplotlib/figure.py | 507 +++++++++++-------------------------- 2 files changed, 162 insertions(+), 417 deletions(-) diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index f06cc34a6ab9..415da55b0ebd 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -39,7 +39,7 @@ import matplotlib.patches as mpatches import matplotlib.path as mpath import matplotlib.ticker as ticker -import matplotlib.transforms as mtransforms +import matplotlib.transforms as mtrans from matplotlib import docstring @@ -317,7 +317,7 @@ def __init__(self, ax, cmap=None, linthresh=self.norm.linthresh) else: self.formatter = ticker.ScalarFormatter() - elif isinstance(format, six.string_types): + elif cbook.is_string_like(format): self.formatter = ticker.FormatStrFormatter(format) else: self.formatter = format # Assume it is a Formatter @@ -344,7 +344,6 @@ def draw_all(self): Calculate any free parameters based on the current cmap and norm, and do all the drawing. ''' - self._process_values() self._find_range() X, Y = self._mesh() @@ -400,10 +399,6 @@ def set_ticks(self, ticks, update_ticks=True): self.update_ticks() self.stale = True - def get_ticks(self, minor=False): - """Return the x ticks as a list of locations""" - return self._tick_data_values - def set_ticklabels(self, ticklabels, update_ticks=True): """ set tick labels. Tick labels are updated immediately unless @@ -616,7 +611,6 @@ def _ticker(self): else: eps = (intv[1] - intv[0]) * 1e-10 b = b[(b <= intv[1] + eps) & (b >= intv[0] - eps)] - self._tick_data_values = b ticks = self._locate(b) formatter.set_locs(b) ticklabels = [formatter(t, i) for i, t in enumerate(b)] @@ -687,10 +681,9 @@ def _process_values(self, b=None): self.norm.vmin = 0 self.norm.vmax = 1 - self.norm.vmin, self.norm.vmax = mtransforms.nonsingular( - self.norm.vmin, - self.norm.vmax, - expander=0.1) + self.norm.vmin, self.norm.vmax = mtrans.nonsingular(self.norm.vmin, + self.norm.vmax, + expander=0.1) b = self.norm.inverse(self._uniform_y(self.cmap.N + 1)) @@ -1117,10 +1110,8 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, parent_anchor = kw.pop('panchor', loc_settings['panchor']) pad = kw.pop('pad', loc_settings['pad']) - # turn parents into a list if it is not already. We do this w/ np - # because `plt.subplots` can return an ndarray and is natural to - # pass to `colorbar`. - parents = np.atleast_1d(parents).ravel() + # turn parents into a list if it is not already + parents = np.atleast_1d(parents).ravel().tolist() fig = parents[0].get_figure() if not all(fig is ax.get_figure() for ax in parents): @@ -1128,8 +1119,8 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, 'parents share the same figure.') # take a bounding box around all of the given axes - parents_bbox = mtransforms.Bbox.union( - [ax.get_position(original=True).frozen() for ax in parents]) + parents_bbox = mtrans.Bbox.union([ax.get_position(original=True).frozen() + for ax in parents]) pb = parents_bbox if location in ('left', 'right'): @@ -1150,12 +1141,12 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, # define a transform which takes us from old axes coordinates to # new axes coordinates - shrinking_trans = mtransforms.BboxTransform(parents_bbox, pb1) + shrinking_trans = mtrans.BboxTransform(parents_bbox, pb1) # transform each of the axes in parents using the new transform for ax in parents: new_posn = shrinking_trans.transform(ax.get_position()) - new_posn = mtransforms.Bbox(new_posn) + new_posn = mtrans.Bbox(new_posn) ax.set_position(new_posn) if parent_anchor is not False: ax.set_anchor(parent_anchor) @@ -1166,7 +1157,7 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, @docstring.Substitution(make_axes_kw_doc) -def make_axes_gridspec(parents, **kw): +def make_axes_gridspec(parent, **kw): ''' Resize and reposition a parent axes, and return a child axes suitable for a colorbar. This function is similar to @@ -1213,35 +1204,13 @@ def make_axes_gridspec(parents, **kw): pad_s = (1. - shrink) * 0.5 wh_ratios = [pad_s, shrink, pad_s] - # make parents a 1-d ndarray if its not already... - parents = np.atleast_1d(parents).ravel() - # get the appropriate subplot spec. Loop through the parents. - gs0 = parents[0].get_subplotspec().get_gridspec() - minind = 10000 - maxind = -10000 - for parent in parents: - gs = parent.get_subplotspec().get_gridspec() - if gs == gs0: - ss = parent.get_subplotspec().get_geometry() - if ss[2]maxind: - maxind = ss[3] - else: - pass - print(minind) - print(maxind) - subspec = gridspec.SubplotSpec(gs0,minind,maxind) - print(subspec) - print(subspec.get_geometry()) - gs_from_subplotspec = gridspec.GridSpecFromSubplotSpec if orientation == 'vertical': pad = kw.pop('pad', 0.05) wh_space = 2 * pad / (1 - pad) gs = gs_from_subplotspec(1, 2, - subplot_spec=subspec, + subplot_spec=parent.get_subplotspec(), wspace=wh_space, width_ratios=[x1 - pad, fraction] ) @@ -1251,8 +1220,6 @@ def make_axes_gridspec(parents, **kw): hspace=0., height_ratios=wh_ratios, ) - print(gs) - print(gs2) anchor = (0.0, 0.5) panchor = (1.0, 0.5) @@ -1261,7 +1228,7 @@ def make_axes_gridspec(parents, **kw): wh_space = 2 * pad / (1 - pad) gs = gs_from_subplotspec(2, 1, - subplot_spec=subspec, + subplot_spec=parent.get_subplotspec(), hspace=wh_space, height_ratios=[x1 - pad, fraction] ) @@ -1276,13 +1243,12 @@ def make_axes_gridspec(parents, **kw): anchor = (0.5, 1.0) panchor = (0.5, 0.0) - for parent in parents: - parent.set_subplotspec(gs[0]) - parent.update_params() - parent.set_position(parent.figbox) - parent.set_anchor(panchor) + parent.set_subplotspec(gs[0]) + parent.update_params() + parent.set_position(parent.figbox) + parent.set_anchor(panchor) - fig = parents[0].get_figure() + fig = parent.get_figure() cax = fig.add_subplot(gs2[1]) cax.set_aspect(aspect, anchor=anchor, adjustable='box') return cax, kw diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 7034ea64c0d0..288ff450a25e 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -17,6 +17,7 @@ import six import warnings +from operator import itemgetter import numpy as np @@ -38,7 +39,6 @@ from matplotlib.axes import Axes, SubplotBase, subplot_class_factory from matplotlib.blocking_input import BlockingMouseInput, BlockingKeyMouseInput -from matplotlib.gridspec import GridSpec from matplotlib.legend import Legend from matplotlib.patches import Rectangle from matplotlib.projections import (get_projection_names, @@ -94,7 +94,7 @@ def get(self, key): return item[1] def _entry_from_axes(self, e): - ind, k = {a: (ind, k) for k, (ind, a) in self._elements}[e] + ind, k = dict([(a, (ind, k)) for (k, (ind, a)) in self._elements])[e] return (k, (ind, e)) def remove(self, a): @@ -118,18 +118,17 @@ def add(self, key, a): # All the error checking may be unnecessary; but this method # is called so seldom that the overhead is negligible. if not isinstance(a, Axes): - raise ValueError("second argument, {!r}, is not an Axes".format(a)) + raise ValueError("second argument, %s, is not an Axes" % a) try: hash(key) except TypeError: - raise ValueError( - "first argument, {!r}, is not a valid key".format(key)) + raise ValueError("first argument, %s, is not a valid key" % key) a_existing = self.get(key) if a_existing is not None: Stack.remove(self, (key, a_existing)) warnings.warn( - "key {!r} already existed; Axes is being replaced".format(key)) + "key %s already existed; Axes is being replaced" % key) # I don't think the above should ever happen. if a in self: @@ -324,11 +323,7 @@ def __init__(self, if frameon is None: frameon = rcParams['figure.frameon'] - if not np.isfinite(figsize).all(): - raise ValueError('figure size must be finite not ' - '{}'.format(figsize)) self.bbox_inches = Bbox.from_bounds(0, 0, *figsize) - self.dpi_scale_trans = Affine2D().scale(dpi, dpi) # do not use property as it will trigger self._dpi = dpi @@ -338,9 +333,12 @@ def __init__(self, self.transFigure = BboxTransformTo(self.bbox) - self.patch = Rectangle( + # the figurePatch name is deprecated + self.patch = self.figurePatch = Rectangle( xy=(0, 0), width=1, height=1, - facecolor=facecolor, edgecolor=edgecolor, linewidth=linewidth) + facecolor=facecolor, edgecolor=edgecolor, + linewidth=linewidth) + self._set_artist_props(self.patch) self.patch.set_aa(False) @@ -361,10 +359,6 @@ def __init__(self, self.clf() self._cachedRenderer = None - @cbook.deprecated("2.1", alternative="Figure.patch") - def figurePatch(self): - return self.patch - # TODO: I'd like to dynamically add the _repr_html_ method # to the figure in the right context, but then IPython doesn't # use it, for some reason. @@ -426,7 +420,7 @@ def _set_dpi(self, dpi): def get_tight_layout(self): """ - Return the Boolean flag, True to use :meth:`tight_layout` when drawing. + Return the Boolean flag, True to use :meth`tight_layout` when drawing. """ return self._tight @@ -447,7 +441,7 @@ def set_tight_layout(self, tight): self._tight_parameters = tight if isinstance(tight, dict) else {} self.stale = True - def autofmt_xdate(self, bottom=0.2, rotation=30, ha='right', which=None): + def autofmt_xdate(self, bottom=0.2, rotation=30, ha='right'): """ Date ticklabels often overlap, so it is useful to rotate them and right align them. Also, a common use case is a number of @@ -456,36 +450,30 @@ def autofmt_xdate(self, bottom=0.2, rotation=30, ha='right', which=None): bottom subplot and turn them off on other subplots, as well as turn off xlabels. - Parameters - ---------- - - bottom : scalar + *bottom* The bottom of the subplots for :meth:`subplots_adjust` - rotation : angle in degrees + *rotation* The rotation of the xtick labels - ha : string + *ha* The horizontal alignment of the xticklabels - - which : {None, 'major', 'minor', 'both'} - Selects which ticklabels to rotate (default is None which works - same as major) """ - allsubplots = all(hasattr(ax, 'is_last_row') for ax in self.axes) + allsubplots = np.alltrue([hasattr(ax, 'is_last_row') for ax + in self.axes]) if len(self.axes) == 1: - for label in self.axes[0].get_xticklabels(which=which): + for label in self.axes[0].get_xticklabels(): label.set_ha(ha) label.set_rotation(rotation) else: if allsubplots: for ax in self.get_axes(): if ax.is_last_row(): - for label in ax.get_xticklabels(which=which): + for label in ax.get_xticklabels(): label.set_ha(ha) label.set_rotation(rotation) else: - for label in ax.get_xticklabels(which=which): + for label in ax.get_xticklabels(): label.set_visible(False) ax.set_xlabel('') @@ -509,11 +497,13 @@ def contains(self, mouseevent): """ Test whether the mouse event occurred on the figure. - Returns True, {}. + Returns True,{} """ - if callable(self._contains): + if six.callable(self._contains): return self._contains(self, mouseevent) + # inside = mouseevent.x >= 0 and mouseevent.y >= 0 inside = self.bbox.contains(mouseevent.x, mouseevent.y) + return inside, {} def get_window_extent(self, *args, **kwargs): @@ -666,6 +656,9 @@ def figimage(self, X, An :class:`matplotlib.image.FigureImage` instance is returned. + .. plot:: mpl_examples/pylab_examples/figimage_demo.py + + Additional kwargs are Artist kwargs passed on to :class:`~matplotlib.image.FigureImage` """ @@ -714,9 +707,8 @@ def set_size_inches(self, w, h=None, forward=True): # argument, so unpack them if h is None: w, h = w - if not all(np.isfinite(_) for _ in (w, h)): - raise ValueError('figure size must be finite not ' - '({}, {})'.format(w, h)) + + dpival = self.dpi self.bbox_inches.p1 = w, h if forward: @@ -925,9 +917,6 @@ def add_axes(self, *args, **kwargs): raise ValueError(msg) else: rect = args[0] - if not np.isfinite(rect).all(): - raise ValueError('all entries in rect must be finite ' - 'not {}'.format(rect)) projection_class, kwargs, key = process_projection_requirements( self, *args, **kwargs) @@ -1038,130 +1027,6 @@ def add_subplot(self, *args, **kwargs): a.stale_callback = _stale_figure_callback return a - def subplots(self, nrows=1, ncols=1, sharex=False, sharey=False, - squeeze=True, subplot_kw=None, gridspec_kw=None): - """ - Add a set of subplots to this figure. - - Parameters - ---------- - nrows, ncols : int, default: 1 - Number of rows/cols of the subplot grid. - - sharex, sharey : bool or {'none', 'all', 'row', 'col'}, default: False - Controls sharing of properties among x (`sharex`) or y (`sharey`) - axes: - - - True or 'all': x- or y-axis will be shared among all - subplots. - - False or 'none': each subplot x- or y-axis will be - independent. - - 'row': each subplot row will share an x- or y-axis. - - 'col': each subplot column will share an x- or y-axis. - - When subplots have a shared x-axis along a column, only the x tick - labels of the bottom subplot are visible. Similarly, when - subplots have a shared y-axis along a row, only the y tick labels - of the first column subplot are visible. - - squeeze : bool, default: True - - If True, extra dimensions are squeezed out from the returned - axis object: - - - if only one subplot is constructed (nrows=ncols=1), the - resulting single Axes object is returned as a scalar. - - for Nx1 or 1xN subplots, the returned object is a 1D numpy - object array of Axes objects are returned as numpy 1D - arrays. - - for NxM, subplots with N>1 and M>1 are returned as a 2D - arrays. - - - If False, no squeezing at all is done: the returned Axes object - is always a 2D array containing Axes instances, even if it ends - up being 1x1. - - subplot_kw : dict, default: {} - Dict with keywords passed to the - :meth:`~matplotlib.figure.Figure.add_subplot` call used to create - each subplots. - - gridspec_kw : dict, default: {} - Dict with keywords passed to the - :class:`~matplotlib.gridspec.GridSpec` constructor used to create - the grid the subplots are placed on. - - Returns - ------- - ax : single Axes object or array of Axes objects - The added axes. The dimensions of the resulting array can be - controlled with the squeeze keyword, see above. - - See Also - -------- - pyplot.subplots : pyplot API; docstring includes examples. - """ - - # for backwards compatibility - if isinstance(sharex, bool): - sharex = "all" if sharex else "none" - if isinstance(sharey, bool): - sharey = "all" if sharey else "none" - share_values = ["all", "row", "col", "none"] - if sharex not in share_values: - # This check was added because it is very easy to type - # `subplots(1, 2, 1)` when `subplot(1, 2, 1)` was intended. - # In most cases, no error will ever occur, but mysterious behavior - # will result because what was intended to be the subplot index is - # instead treated as a bool for sharex. - if isinstance(sharex, int): - warnings.warn( - "sharex argument to subplots() was an integer. " - "Did you intend to use subplot() (without 's')?") - - raise ValueError("sharex [%s] must be one of %s" % - (sharex, share_values)) - if sharey not in share_values: - raise ValueError("sharey [%s] must be one of %s" % - (sharey, share_values)) - if subplot_kw is None: - subplot_kw = {} - if gridspec_kw is None: - gridspec_kw = {} - - gs = GridSpec(nrows, ncols, **gridspec_kw) - - # Create array to hold all axes. - axarr = np.empty((nrows, ncols), dtype=object) - for row in range(nrows): - for col in range(ncols): - shared_with = {"none": None, "all": axarr[0, 0], - "row": axarr[row, 0], "col": axarr[0, col]} - subplot_kw["sharex"] = shared_with[sharex] - subplot_kw["sharey"] = shared_with[sharey] - axarr[row, col] = self.add_subplot(gs[row, col], **subplot_kw) - - # turn off redundant tick labeling - if sharex in ["col", "all"]: - # turn off all but the bottom row - for ax in axarr[:-1, :].flat: - for label in ax.get_xticklabels(): - label.set_visible(False) - ax.xaxis.offsetText.set_visible(False) - if sharey in ["row", "all"]: - # turn off all but the first column - for ax in axarr[:, 1:].flat: - for label in ax.get_yticklabels(): - label.set_visible(False) - ax.yaxis.offsetText.set_visible(False) - - if squeeze: - # Discarding unneeded dimensions that equal 1. If we only have one - # subplot, just return it instead of a 1-element array. - return axarr.item() if axarr.size == 1 else axarr.squeeze() - else: - # Returned axis array will be always 2-d, even if nrows=ncols=1. - return axarr - def __remove_ax(self, ax): def _reset_loc_form(axis): axis.set_major_formatter(axis.get_major_formatter()) @@ -1217,11 +1082,11 @@ def clf(self, keep_observers=False): self._suptitle = None self.stale = True - def clear(self, keep_observers=False): + def clear(self): """ Clear the figure -- synonym for :meth:`clf`. """ - self.clf(keep_observers=keep_observers) + self.clf() @allow_rasterization def draw(self, renderer): @@ -1234,12 +1099,34 @@ def draw(self, renderer): if not self.get_visible(): return - artists = sorted( - (artist for artist in (self.patches + self.lines + self.artists - + self.images + self.axes + self.texts - + self.legends) - if not artist.get_animated()), - key=lambda artist: artist.get_zorder()) + # a list of (zorder, func_to_call, list_of_args) + dsu = [] + + for a in self.patches: + dsu.append((a.get_zorder(), a)) + + for a in self.lines: + dsu.append((a.get_zorder(), a)) + + for a in self.artists: + dsu.append((a.get_zorder(), a)) + + for a in self.images: + dsu.append((a.get_zorder(), a)) + + # render the axes + for a in self.axes: + dsu.append((a.get_zorder(), a)) + + # render the figure text + for a in self.texts: + dsu.append((a.get_zorder(), a)) + + for a in self.legends: + dsu.append((a.get_zorder(), a)) + + dsu = [row for row in dsu if not row[1].get_animated()] + dsu.sort(key=itemgetter(0)) try: renderer.open_group('figure') @@ -1254,7 +1141,7 @@ def draw(self, renderer): self.patch.draw(renderer) mimage._draw_list_compositing_images( - renderer, self, artists, self.suppressComposite) + renderer, self, dsu, self.suppressComposite) renderer.close_group('figure') finally: @@ -1277,214 +1164,127 @@ def draw_artist(self, a): def get_axes(self): return self.axes - def legend(self, *args, **kwargs): + def legend(self, handles, labels, *args, **kwargs): """ - Place a legend on the figure. - - To make a legend from existing artists on every axes:: - - legend() + Place a legend in the figure. Labels are a sequence of + strings, handles is a sequence of + :class:`~matplotlib.lines.Line2D` or + :class:`~matplotlib.patches.Patch` instances, and loc can be a + string or an integer specifying the legend location - To make a legend for a list of lines and labels:: + USAGE:: legend( (line1, line2, line3), ('label1', 'label2', 'label3'), 'upper right') - Parameters - ---------- - loc : string or integer - The location of the legend. Possible codes are: - - =============== ============= - Location String Location Code - =============== ============= - 'upper right' 1 - 'upper left' 2 - 'lower left' 3 - 'lower right' 4 - 'right' 5 - 'center left' 6 - 'center right' 7 - 'lower center' 8 - 'upper center' 9 - 'center' 10 - =============== ============= - - *loc* can also be an (x,y) tuple in figure coords, which specifies - the lower left of the legend box. In figure coords (0,0) is the - bottom left of the figure, and (1,1) is the top right. - - prop : None or FontProperties or dict - A :class:`matplotlib.font_manager.FontProperties` instance. If - *prop* is a dictionary, a new instance will be created with *prop*. - If *None*, use rc settings. - - numpoints : integer + The *loc* location codes are:: + + 'best' : 0, (currently not supported for figure legends) + 'upper right' : 1, + 'upper left' : 2, + 'lower left' : 3, + 'lower right' : 4, + 'right' : 5, + 'center left' : 6, + 'center right' : 7, + 'lower center' : 8, + 'upper center' : 9, + 'center' : 10, + + *loc* can also be an (x,y) tuple in figure coords, which + specifies the lower left of the legend box. figure coords are + (0,0) is the left, bottom of the figure and 1,1 is the right, + top. + + Keyword arguments: + + prop: [ *None* | FontProperties | dict ] + A :class:`matplotlib.font_manager.FontProperties` + instance. If *prop* is a dictionary, a new instance will be + created with *prop*. If *None*, use rc settings. + + numpoints: integer The number of points in the legend line, default is 4 - scatterpoints : integer + scatterpoints: integer The number of points in the legend line, default is 4 - scatteryoffsets : list of floats - A list of yoffsets for scatter symbols in legend + scatteryoffsets: list of floats + a list of yoffsets for scatter symbols in legend - markerscale : None or scalar + markerscale: [ *None* | scalar ] The relative size of legend markers vs. original. If *None*, use rc settings. - markerfirst : bool - If *True*, legend marker is placed to the left of the legend label. - If *False*, legend marker is placed to the right of the legend - label. - Default is *True*. + markerfirst: [ *True* | *False* ] + if *True*, legend marker is placed to the left of the legend label + if *False*, legend marker is placed to the right of the legend + label - frameon : None or bool + frameon: [ *None* | bool ] Control whether the legend should be drawn on a patch (frame). Default is *None* which will take the value from the ``legend.frameon`` :data:`rcParam`. - fancybox : None or bool - If *True*, draw a frame with a round fancybox. If *None*, use rc - settings. + fancybox: [ *None* | *False* | *True* ] + if *True*, draw a frame with a round fancybox. If *None*, use rc - shadow : None or bool + shadow: [ *None* | *False* | *True* ] If *True*, draw a shadow behind legend. If *None*, use rc settings. - framealpha : None or float + framealpha: [ *None* | float ] Control the alpha transparency of the legend's background. Default is *None* which will take the value from the ``legend.framealpha`` :data:`rcParam`. - facecolor : None or "inherit" or a color spec + facecolor: [ *None* | "inherit" | a color spec ] Control the legend's background color. Default is *None* which will take the value from the ``legend.facecolor`` :data:`rcParam`. If ``"inherit"``, it will take the ``axes.facecolor`` :data:`rcParam`. - edgecolor : None or "inherit" or a color spec + edgecolor: [ *None* | "inherit" | a color spec ] Control the legend's background patch edge color. Default is *None* which will take the value from the ``legend.edgecolor`` :data:`rcParam`. If ``"inherit"``, it will take the ``axes.edgecolor`` :data:`rcParam`. - ncol : integer - Number of columns. Default is 1. + ncol : integer + number of columns. default is 1 - mode : "expand" or None - If mode is "expand", the legend will be horizontally expanded + mode : [ "expand" | *None* ] + if mode is "expand", the legend will be horizontally expanded to fill the axes area (or *bbox_to_anchor*) - title : string - The legend title - - borderpad : float or None - The fractional whitespace inside the legend border, measured in - font-size units. - Default is *None* which will take the value from the - ``legend.borderpad`` :data:`rcParam`. - - labelspacing : float or None - The vertical space between the legend entries, measured in - font-size units. - Default is *None* which will take the value from the - ``legend.labelspacing`` :data:`rcParam`. - - handlelength : float or None - The length of the legend handles, measured in font-size units. - Default is *None* which will take the value from the - ``legend.handlelength`` :data:`rcParam`. + title : string + the legend title - handletextpad : float or None - The padding between the legend handle and text, measured in - font-size units. - Default is *None* which will take the value from the - ``legend.handletextpad`` :data:`rcParam`. + Padding and spacing between various elements use following keywords + parameters. The dimensions of these values are given as a fraction + of the fontsize. Values from rcParams will be used if None. - borderaxespad : float or None - The padding between the axes and legend border, measured in - font-size units. - Default is *None* which will take the value from the - ``legend.borderaxespad`` :data:`rcParam`. + ================ ==================================================== + Keyword Description + ================ ==================================================== + borderpad the fractional whitespace inside the legend border + labelspacing the vertical space between the legend entries + handlelength the length of the legend handles + handletextpad the pad between the legend handle and text + borderaxespad the pad between the axes and legend border + columnspacing the spacing between columns + ================ ==================================================== - columnspacing : float or None - The spacing between columns, measured in font-size units. - Default is *None* which will take the value from the - ``legend.columnspacing`` :data:`rcParam`. + .. Note:: Not all kinds of artist are supported by the legend. + See LINK (FIXME) for details. - Returns - ------- - :class:`matplotlib.legend.Legend` instance + **Example:** - Notes - ----- - Not all kinds of artist are supported by the legend command. See - :ref:`sphx_glr_tutorials_02_intermediate_legend_guide.py` for details. + .. plot:: mpl_examples/pylab_examples/figlegend_demo.py """ - - # If no arguments given, collect up all the artists on the figure - if len(args) == 0: - handles = [] - labels = [] - - def in_handles(h, l): - # Method to check if we already have a given handle and label. - # Consider two handles to be the same if they share a label, - # color, facecolor, and edgecolor. - - # Loop through each handle and label already collected - for f_h, f_l in zip(handles, labels): - if f_l != l: - continue - if type(f_h) != type(h): - continue - try: - if f_h.get_color() != h.get_color(): - continue - except AttributeError: - pass - try: - if f_h.get_facecolor() != h.get_facecolor(): - continue - except AttributeError: - pass - try: - if f_h.get_edgecolor() != h.get_edgecolor(): - continue - except AttributeError: - pass - return True - return False - - for ax in self.axes: - ax_handles, ax_labels = ax.get_legend_handles_labels() - for h, l in zip(ax_handles, ax_labels): - if not in_handles(h, l): - handles.append(h) - labels.append(l) - if len(handles) == 0: - warnings.warn("No labeled objects found. " - "Use label='...' kwarg on individual plots.") - return None - - elif len(args) == 2: - # LINES, LABELS - handles, labels = args - - elif len(args) == 3: - # LINES, LABELS, LOC - handles, labels, loc = args - kwargs['loc'] = loc - - else: - raise TypeError('Invalid number of arguments passed to legend. ' - 'Please specify either 0 args, 2 args ' - '(artist handles, figure labels) or 3 args ' - '(artist handles, figure labels, legend location)') - - l = Legend(self, handles, labels, **kwargs) + l = Legend(self, handles, labels, *args, **kwargs) self.legends.append(l) l._remove_method = lambda h: self.legends.remove(h) self.stale = True @@ -1564,10 +1364,6 @@ def gca(self, **kwargs): # continue and a new axes will be created if key == ckey and isinstance(cax, projection_class): return cax - else: - warnings.warn('Requested projection is different from ' - 'current axis projection, creating new axis ' - 'with requested projection.', stacklevel=2) # no axes found, so create one which spans the figure return self.add_subplot(1, 1, 1, **kwargs) @@ -1841,10 +1637,17 @@ def subplots_adjust(self, *args, **kwargs): def ginput(self, n=1, timeout=30, show_clicks=True, mouse_add=1, mouse_pop=3, mouse_stop=2): """ - Blocking call to interact with a figure. + Blocking call to interact with the figure. - Wait until the user clicks *n* times on the figure, and return the - coordinates of each click in a list. + This will wait for *n* clicks from the user and return a list of the + coordinates of each click. + + If *timeout* is zero or negative, does not timeout. + + If *n* is zero or negative, accumulate clicks until a middle click + (or potentially both mouse buttons at once) terminates the input. + + Right clicking cancels last input. The buttons used for the various actions (adding points, removing points, terminating the inputs) can be overriden via the @@ -1852,30 +1655,6 @@ def ginput(self, n=1, timeout=30, show_clicks=True, mouse_add=1, the associated mouse button: 1 for left, 2 for middle, 3 for right. - Parameters - ---------- - n : int, optional, default: 1 - Number of mouse clicks to accumulate. If negative, accumulate - clicks until the input is terminated manually. - timeout : scalar, optional, default: 30 - Number of seconds to wait before timing out. If zero or negative - will never timeout. - show_clicks : bool, optional, default: False - If True, show a red cross at the location of each click. - mouse_add : int, one of (1, 2, 3), optional, default: 1 (left click) - Mouse button used to add points. - mouse_pop : int, one of (1, 2, 3), optional, default: 3 (right click) - Mouse button used to remove the most recently added point. - mouse_stop : int, one of (1, 2, 3), optional, default: 2 (middle click) - Mouse button used to stop input. - - Returns - ------- - points : list of tuples - A list of the clicked (x, y) coordinates. - - Notes - ----- The keyboard can also be used to select points in case your mouse does not have one or more of the buttons. The delete and backspace keys act like right clicking (i.e., remove last point), the enter key From 2d0d1558dbd0289b2cfb371f764f385d83651873 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Tue, 13 Jun 2017 06:45:34 -0700 Subject: [PATCH 3/5] Changes to colorbar to allow list of SubplotSpec as the argument to --- lib/matplotlib/colorbar.py | 134 ++++++++-- lib/matplotlib/figure.py | 511 ++++++++++++++++++++++++++----------- 2 files changed, 475 insertions(+), 170 deletions(-) diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 415da55b0ebd..b2a69083c407 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -39,7 +39,7 @@ import matplotlib.patches as mpatches import matplotlib.path as mpath import matplotlib.ticker as ticker -import matplotlib.transforms as mtrans +import matplotlib.transforms as mtransforms from matplotlib import docstring @@ -317,7 +317,7 @@ def __init__(self, ax, cmap=None, linthresh=self.norm.linthresh) else: self.formatter = ticker.ScalarFormatter() - elif cbook.is_string_like(format): + elif isinstance(format, six.string_types): self.formatter = ticker.FormatStrFormatter(format) else: self.formatter = format # Assume it is a Formatter @@ -344,6 +344,7 @@ def draw_all(self): Calculate any free parameters based on the current cmap and norm, and do all the drawing. ''' + self._process_values() self._find_range() X, Y = self._mesh() @@ -399,6 +400,10 @@ def set_ticks(self, ticks, update_ticks=True): self.update_ticks() self.stale = True + def get_ticks(self, minor=False): + """Return the x ticks as a list of locations""" + return self._tick_data_values + def set_ticklabels(self, ticklabels, update_ticks=True): """ set tick labels. Tick labels are updated immediately unless @@ -611,6 +616,7 @@ def _ticker(self): else: eps = (intv[1] - intv[0]) * 1e-10 b = b[(b <= intv[1] + eps) & (b >= intv[0] - eps)] + self._tick_data_values = b ticks = self._locate(b) formatter.set_locs(b) ticklabels = [formatter(t, i) for i, t in enumerate(b)] @@ -681,9 +687,10 @@ def _process_values(self, b=None): self.norm.vmin = 0 self.norm.vmax = 1 - self.norm.vmin, self.norm.vmax = mtrans.nonsingular(self.norm.vmin, - self.norm.vmax, - expander=0.1) + self.norm.vmin, self.norm.vmax = mtransforms.nonsingular( + self.norm.vmin, + self.norm.vmax, + expander=0.1) b = self.norm.inverse(self._uniform_y(self.cmap.N + 1)) @@ -1110,8 +1117,10 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, parent_anchor = kw.pop('panchor', loc_settings['panchor']) pad = kw.pop('pad', loc_settings['pad']) - # turn parents into a list if it is not already - parents = np.atleast_1d(parents).ravel().tolist() + # turn parents into a list if it is not already. We do this w/ np + # because `plt.subplots` can return an ndarray and is natural to + # pass to `colorbar`. + parents = np.atleast_1d(parents).ravel() fig = parents[0].get_figure() if not all(fig is ax.get_figure() for ax in parents): @@ -1119,8 +1128,8 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, 'parents share the same figure.') # take a bounding box around all of the given axes - parents_bbox = mtrans.Bbox.union([ax.get_position(original=True).frozen() - for ax in parents]) + parents_bbox = mtransforms.Bbox.union( + [ax.get_position(original=True).frozen() for ax in parents]) pb = parents_bbox if location in ('left', 'right'): @@ -1141,12 +1150,12 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, # define a transform which takes us from old axes coordinates to # new axes coordinates - shrinking_trans = mtrans.BboxTransform(parents_bbox, pb1) + shrinking_trans = mtransforms.BboxTransform(parents_bbox, pb1) # transform each of the axes in parents using the new transform for ax in parents: new_posn = shrinking_trans.transform(ax.get_position()) - new_posn = mtrans.Bbox(new_posn) + new_posn = mtransforms.Bbox(new_posn) ax.set_position(new_posn) if parent_anchor is not False: ax.set_anchor(parent_anchor) @@ -1156,24 +1165,36 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, return cax, kw +# helper functions for row,col to index. +def index2rowcolunm(index, ncols): + col = index % ncols + 1 + row = int(np.floor(index / ncols ) + 1) + return row, col + + +def rowcolunm2index(row, col, ncols): + index = col - 1 + (row - 1) * ncols + return index + + @docstring.Substitution(make_axes_kw_doc) -def make_axes_gridspec(parent, **kw): +def make_axes_gridspec(parents, **kw): ''' - Resize and reposition a parent axes, and return a child axes + Resize and reposition a list of parent axes, and return a child axes suitable for a colorbar. This function is similar to - make_axes. Prmary differences are + make_axes. Primary differences are * *make_axes_gridspec* only handles the *orientation* keyword and cannot handle the "location" keyword. - * *make_axes_gridspec* should only be used with a subplot parent. + * *make_axes_gridspec* should only be used with subplot parents. * *make_axes* creates an instance of Axes. *make_axes_gridspec* creates an instance of Subplot. * *make_axes* updates the position of the - parent. *make_axes_gridspec* replaces the grid_spec attribute - of the parent with a new one. + parents. *make_axes_gridspec* replaces the grid_spec attribute + of the parents with new ones. While this function is meant to be compatible with *make_axes*, there could be some minor differences. @@ -1204,17 +1225,52 @@ def make_axes_gridspec(parent, **kw): pad_s = (1. - shrink) * 0.5 wh_ratios = [pad_s, shrink, pad_s] + # make parents a 1-d ndarray if its not already... + parents = np.atleast_1d(parents).ravel() + # For the subplotspec that these axes belong to, loop through and get + # the maximum and minimum index into the subplotspec. The result is the + # size the new gridspec that we will create must be. + gsp0 = parents[0].get_subplotspec().get_gridspec() + minind = 10000 + maxind = -10000 + for parent in parents: + gsp = parent.get_subplotspec().get_gridspec() + if gsp == gsp0: + ss = list(parent.get_subplotspec().get_geometry()) + if ss[3] is None: + ss[3] = ss[2] + if ss[2] < minind: + minind = ss[2] + if ss[3] > maxind: + maxind = ss[3] + else: + raise NotImplementedError('List of axes passed to colorbar ' + 'must be from the same gridspec or call to plt.subplots') + # map the extent of the gridspec from indices to rows and columns. + # We need this below to assign the parents into the new gridspec. + ncols0 = gsp0.get_geometry()[1] + minrow, mincol = index2rowcolunm(minind, ncols0) + maxrow, maxcol = index2rowcolunm(maxind, ncols0) + nrows = maxrow-minrow+1 + ncols = maxcol-mincol+1 + + # this is subplot spec the region that we need to resize and add + # a colorbar to. + subspec = gridspec.SubplotSpec(gsp0, minind, maxind) + gs_from_subplotspec = gridspec.GridSpecFromSubplotSpec if orientation == 'vertical': pad = kw.pop('pad', 0.05) wh_space = 2 * pad / (1 - pad) + # split the subplotspec containing parents into two, with one only + # `fraction` wide for the colorbar, and the other with some padding. gs = gs_from_subplotspec(1, 2, - subplot_spec=parent.get_subplotspec(), + subplot_spec=subspec, wspace=wh_space, width_ratios=[x1 - pad, fraction] ) - + # center the colorbar vertically. gs2 = gs_from_subplotspec(3, 1, subplot_spec=gs[1], hspace=0., @@ -1228,7 +1284,7 @@ def make_axes_gridspec(parent, **kw): wh_space = 2 * pad / (1 - pad) gs = gs_from_subplotspec(2, 1, - subplot_spec=parent.get_subplotspec(), + subplot_spec=subspec, hspace=wh_space, height_ratios=[x1 - pad, fraction] ) @@ -1243,12 +1299,40 @@ def make_axes_gridspec(parent, **kw): anchor = (0.5, 1.0) panchor = (0.5, 0.0) - parent.set_subplotspec(gs[0]) - parent.update_params() - parent.set_position(parent.figbox) - parent.set_anchor(panchor) + # we need to repackage the parent axes into gs. + # We need to know where they are in the new gs. + # the new gs has nrows by ncols. + gsnew = gs_from_subplotspec(nrows, ncols, subplot_spec=gs[0]) + + for parent in parents: + geo = parent.get_subplotspec().get_geometry() + ncol0 = geo[1] + + # remap the old min gridspec index (geo[2]) into a new + # index. + oldrow, oldcol = index2rowcolunm(geo[2], ncol0) + newrow = oldrow - minrow+1 + newcol = oldcol - mincol+1 + newminind = rowcolunm2index(newrow, newcol, ncols) + + # remap the old max gridspec index (geo[3]) into a new + # index. + if geo[3] is None: + newmaxind = newminind + else: + oldrow, oldcol = index2rowcolunm(geo[3], ncol0) + newrow = oldrow - minrow+1 + newcol = oldcol - mincol+1 + newmaxind = rowcolunm2index(newrow, newcol, ncols) + + # change the subplotspec for this parent. + parent.set_subplotspec( + gridspec.SubplotSpec(gsnew, newminind, newmaxind)) + parent.update_params() + parent.set_position(parent.figbox) + parent.set_anchor(panchor) - fig = parent.get_figure() + fig = parents[0].get_figure() cax = fig.add_subplot(gs2[1]) cax.set_aspect(aspect, anchor=anchor, adjustable='box') return cax, kw diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 288ff450a25e..1cb952b3d322 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -17,7 +17,6 @@ import six import warnings -from operator import itemgetter import numpy as np @@ -39,6 +38,7 @@ from matplotlib.axes import Axes, SubplotBase, subplot_class_factory from matplotlib.blocking_input import BlockingMouseInput, BlockingKeyMouseInput +from matplotlib.gridspec import GridSpec from matplotlib.legend import Legend from matplotlib.patches import Rectangle from matplotlib.projections import (get_projection_names, @@ -94,7 +94,7 @@ def get(self, key): return item[1] def _entry_from_axes(self, e): - ind, k = dict([(a, (ind, k)) for (k, (ind, a)) in self._elements])[e] + ind, k = {a: (ind, k) for k, (ind, a) in self._elements}[e] return (k, (ind, e)) def remove(self, a): @@ -118,17 +118,18 @@ def add(self, key, a): # All the error checking may be unnecessary; but this method # is called so seldom that the overhead is negligible. if not isinstance(a, Axes): - raise ValueError("second argument, %s, is not an Axes" % a) + raise ValueError("second argument, {!r}, is not an Axes".format(a)) try: hash(key) except TypeError: - raise ValueError("first argument, %s, is not a valid key" % key) + raise ValueError( + "first argument, {!r}, is not a valid key".format(key)) a_existing = self.get(key) if a_existing is not None: Stack.remove(self, (key, a_existing)) warnings.warn( - "key %s already existed; Axes is being replaced" % key) + "key {!r} already existed; Axes is being replaced".format(key)) # I don't think the above should ever happen. if a in self: @@ -323,7 +324,11 @@ def __init__(self, if frameon is None: frameon = rcParams['figure.frameon'] + if not np.isfinite(figsize).all(): + raise ValueError('figure size must be finite not ' + '{}'.format(figsize)) self.bbox_inches = Bbox.from_bounds(0, 0, *figsize) + self.dpi_scale_trans = Affine2D().scale(dpi, dpi) # do not use property as it will trigger self._dpi = dpi @@ -333,12 +338,9 @@ def __init__(self, self.transFigure = BboxTransformTo(self.bbox) - # the figurePatch name is deprecated - self.patch = self.figurePatch = Rectangle( + self.patch = Rectangle( xy=(0, 0), width=1, height=1, - facecolor=facecolor, edgecolor=edgecolor, - linewidth=linewidth) - + facecolor=facecolor, edgecolor=edgecolor, linewidth=linewidth) self._set_artist_props(self.patch) self.patch.set_aa(False) @@ -359,6 +361,10 @@ def __init__(self, self.clf() self._cachedRenderer = None + @cbook.deprecated("2.1", alternative="Figure.patch") + def figurePatch(self): + return self.patch + # TODO: I'd like to dynamically add the _repr_html_ method # to the figure in the right context, but then IPython doesn't # use it, for some reason. @@ -420,7 +426,7 @@ def _set_dpi(self, dpi): def get_tight_layout(self): """ - Return the Boolean flag, True to use :meth`tight_layout` when drawing. + Return the Boolean flag, True to use :meth:`tight_layout` when drawing. """ return self._tight @@ -441,7 +447,7 @@ def set_tight_layout(self, tight): self._tight_parameters = tight if isinstance(tight, dict) else {} self.stale = True - def autofmt_xdate(self, bottom=0.2, rotation=30, ha='right'): + def autofmt_xdate(self, bottom=0.2, rotation=30, ha='right', which=None): """ Date ticklabels often overlap, so it is useful to rotate them and right align them. Also, a common use case is a number of @@ -450,30 +456,36 @@ def autofmt_xdate(self, bottom=0.2, rotation=30, ha='right'): bottom subplot and turn them off on other subplots, as well as turn off xlabels. - *bottom* + Parameters + ---------- + + bottom : scalar The bottom of the subplots for :meth:`subplots_adjust` - *rotation* + rotation : angle in degrees The rotation of the xtick labels - *ha* + ha : string The horizontal alignment of the xticklabels + + which : {None, 'major', 'minor', 'both'} + Selects which ticklabels to rotate (default is None which works + same as major) """ - allsubplots = np.alltrue([hasattr(ax, 'is_last_row') for ax - in self.axes]) + allsubplots = all(hasattr(ax, 'is_last_row') for ax in self.axes) if len(self.axes) == 1: - for label in self.axes[0].get_xticklabels(): + for label in self.axes[0].get_xticklabels(which=which): label.set_ha(ha) label.set_rotation(rotation) else: if allsubplots: for ax in self.get_axes(): if ax.is_last_row(): - for label in ax.get_xticklabels(): + for label in ax.get_xticklabels(which=which): label.set_ha(ha) label.set_rotation(rotation) else: - for label in ax.get_xticklabels(): + for label in ax.get_xticklabels(which=which): label.set_visible(False) ax.set_xlabel('') @@ -497,13 +509,11 @@ def contains(self, mouseevent): """ Test whether the mouse event occurred on the figure. - Returns True,{} + Returns True, {}. """ - if six.callable(self._contains): + if callable(self._contains): return self._contains(self, mouseevent) - # inside = mouseevent.x >= 0 and mouseevent.y >= 0 inside = self.bbox.contains(mouseevent.x, mouseevent.y) - return inside, {} def get_window_extent(self, *args, **kwargs): @@ -656,9 +666,6 @@ def figimage(self, X, An :class:`matplotlib.image.FigureImage` instance is returned. - .. plot:: mpl_examples/pylab_examples/figimage_demo.py - - Additional kwargs are Artist kwargs passed on to :class:`~matplotlib.image.FigureImage` """ @@ -707,8 +714,9 @@ def set_size_inches(self, w, h=None, forward=True): # argument, so unpack them if h is None: w, h = w - - dpival = self.dpi + if not all(np.isfinite(_) for _ in (w, h)): + raise ValueError('figure size must be finite not ' + '({}, {})'.format(w, h)) self.bbox_inches.p1 = w, h if forward: @@ -917,6 +925,9 @@ def add_axes(self, *args, **kwargs): raise ValueError(msg) else: rect = args[0] + if not np.isfinite(rect).all(): + raise ValueError('all entries in rect must be finite ' + 'not {}'.format(rect)) projection_class, kwargs, key = process_projection_requirements( self, *args, **kwargs) @@ -1027,6 +1038,130 @@ def add_subplot(self, *args, **kwargs): a.stale_callback = _stale_figure_callback return a + def subplots(self, nrows=1, ncols=1, sharex=False, sharey=False, + squeeze=True, subplot_kw=None, gridspec_kw=None): + """ + Add a set of subplots to this figure. + + Parameters + ---------- + nrows, ncols : int, default: 1 + Number of rows/cols of the subplot grid. + + sharex, sharey : bool or {'none', 'all', 'row', 'col'}, default: False + Controls sharing of properties among x (`sharex`) or y (`sharey`) + axes: + + - True or 'all': x- or y-axis will be shared among all + subplots. + - False or 'none': each subplot x- or y-axis will be + independent. + - 'row': each subplot row will share an x- or y-axis. + - 'col': each subplot column will share an x- or y-axis. + + When subplots have a shared x-axis along a column, only the x tick + labels of the bottom subplot are visible. Similarly, when + subplots have a shared y-axis along a row, only the y tick labels + of the first column subplot are visible. + + squeeze : bool, default: True + - If True, extra dimensions are squeezed out from the returned + axis object: + + - if only one subplot is constructed (nrows=ncols=1), the + resulting single Axes object is returned as a scalar. + - for Nx1 or 1xN subplots, the returned object is a 1D numpy + object array of Axes objects are returned as numpy 1D + arrays. + - for NxM, subplots with N>1 and M>1 are returned as a 2D + arrays. + + - If False, no squeezing at all is done: the returned Axes object + is always a 2D array containing Axes instances, even if it ends + up being 1x1. + + subplot_kw : dict, default: {} + Dict with keywords passed to the + :meth:`~matplotlib.figure.Figure.add_subplot` call used to create + each subplots. + + gridspec_kw : dict, default: {} + Dict with keywords passed to the + :class:`~matplotlib.gridspec.GridSpec` constructor used to create + the grid the subplots are placed on. + + Returns + ------- + ax : single Axes object or array of Axes objects + The added axes. The dimensions of the resulting array can be + controlled with the squeeze keyword, see above. + + See Also + -------- + pyplot.subplots : pyplot API; docstring includes examples. + """ + + # for backwards compatibility + if isinstance(sharex, bool): + sharex = "all" if sharex else "none" + if isinstance(sharey, bool): + sharey = "all" if sharey else "none" + share_values = ["all", "row", "col", "none"] + if sharex not in share_values: + # This check was added because it is very easy to type + # `subplots(1, 2, 1)` when `subplot(1, 2, 1)` was intended. + # In most cases, no error will ever occur, but mysterious behavior + # will result because what was intended to be the subplot index is + # instead treated as a bool for sharex. + if isinstance(sharex, int): + warnings.warn( + "sharex argument to subplots() was an integer. " + "Did you intend to use subplot() (without 's')?") + + raise ValueError("sharex [%s] must be one of %s" % + (sharex, share_values)) + if sharey not in share_values: + raise ValueError("sharey [%s] must be one of %s" % + (sharey, share_values)) + if subplot_kw is None: + subplot_kw = {} + if gridspec_kw is None: + gridspec_kw = {} + + gs = GridSpec(nrows, ncols, **gridspec_kw) + + # Create array to hold all axes. + axarr = np.empty((nrows, ncols), dtype=object) + for row in range(nrows): + for col in range(ncols): + shared_with = {"none": None, "all": axarr[0, 0], + "row": axarr[row, 0], "col": axarr[0, col]} + subplot_kw["sharex"] = shared_with[sharex] + subplot_kw["sharey"] = shared_with[sharey] + axarr[row, col] = self.add_subplot(gs[row, col], **subplot_kw) + + # turn off redundant tick labeling + if sharex in ["col", "all"]: + # turn off all but the bottom row + for ax in axarr[:-1, :].flat: + for label in ax.get_xticklabels(): + label.set_visible(False) + ax.xaxis.offsetText.set_visible(False) + if sharey in ["row", "all"]: + # turn off all but the first column + for ax in axarr[:, 1:].flat: + for label in ax.get_yticklabels(): + label.set_visible(False) + ax.yaxis.offsetText.set_visible(False) + + if squeeze: + # Discarding unneeded dimensions that equal 1. If we only have one + # subplot, just return it instead of a 1-element array. + return axarr.item() if axarr.size == 1 else axarr.squeeze() + else: + # Returned axis array will be always 2-d, even if nrows=ncols=1. + return axarr + def __remove_ax(self, ax): def _reset_loc_form(axis): axis.set_major_formatter(axis.get_major_formatter()) @@ -1082,11 +1217,11 @@ def clf(self, keep_observers=False): self._suptitle = None self.stale = True - def clear(self): + def clear(self, keep_observers=False): """ Clear the figure -- synonym for :meth:`clf`. """ - self.clf() + self.clf(keep_observers=keep_observers) @allow_rasterization def draw(self, renderer): @@ -1099,34 +1234,12 @@ def draw(self, renderer): if not self.get_visible(): return - # a list of (zorder, func_to_call, list_of_args) - dsu = [] - - for a in self.patches: - dsu.append((a.get_zorder(), a)) - - for a in self.lines: - dsu.append((a.get_zorder(), a)) - - for a in self.artists: - dsu.append((a.get_zorder(), a)) - - for a in self.images: - dsu.append((a.get_zorder(), a)) - - # render the axes - for a in self.axes: - dsu.append((a.get_zorder(), a)) - - # render the figure text - for a in self.texts: - dsu.append((a.get_zorder(), a)) - - for a in self.legends: - dsu.append((a.get_zorder(), a)) - - dsu = [row for row in dsu if not row[1].get_animated()] - dsu.sort(key=itemgetter(0)) + artists = sorted( + (artist for artist in (self.patches + self.lines + self.artists + + self.images + self.axes + self.texts + + self.legends) + if not artist.get_animated()), + key=lambda artist: artist.get_zorder()) try: renderer.open_group('figure') @@ -1141,7 +1254,7 @@ def draw(self, renderer): self.patch.draw(renderer) mimage._draw_list_compositing_images( - renderer, self, dsu, self.suppressComposite) + renderer, self, artists, self.suppressComposite) renderer.close_group('figure') finally: @@ -1164,127 +1277,214 @@ def draw_artist(self, a): def get_axes(self): return self.axes - def legend(self, handles, labels, *args, **kwargs): + def legend(self, *args, **kwargs): """ - Place a legend in the figure. Labels are a sequence of - strings, handles is a sequence of - :class:`~matplotlib.lines.Line2D` or - :class:`~matplotlib.patches.Patch` instances, and loc can be a - string or an integer specifying the legend location + Place a legend on the figure. + + To make a legend from existing artists on every axes:: + + legend() - USAGE:: + To make a legend for a list of lines and labels:: legend( (line1, line2, line3), ('label1', 'label2', 'label3'), 'upper right') - The *loc* location codes are:: - - 'best' : 0, (currently not supported for figure legends) - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4, - 'right' : 5, - 'center left' : 6, - 'center right' : 7, - 'lower center' : 8, - 'upper center' : 9, - 'center' : 10, - - *loc* can also be an (x,y) tuple in figure coords, which - specifies the lower left of the legend box. figure coords are - (0,0) is the left, bottom of the figure and 1,1 is the right, - top. - - Keyword arguments: - - prop: [ *None* | FontProperties | dict ] - A :class:`matplotlib.font_manager.FontProperties` - instance. If *prop* is a dictionary, a new instance will be - created with *prop*. If *None*, use rc settings. - - numpoints: integer + Parameters + ---------- + loc : string or integer + The location of the legend. Possible codes are: + + =============== ============= + Location String Location Code + =============== ============= + 'upper right' 1 + 'upper left' 2 + 'lower left' 3 + 'lower right' 4 + 'right' 5 + 'center left' 6 + 'center right' 7 + 'lower center' 8 + 'upper center' 9 + 'center' 10 + =============== ============= + + *loc* can also be an (x,y) tuple in figure coords, which specifies + the lower left of the legend box. In figure coords (0,0) is the + bottom left of the figure, and (1,1) is the top right. + + prop : None or FontProperties or dict + A :class:`matplotlib.font_manager.FontProperties` instance. If + *prop* is a dictionary, a new instance will be created with *prop*. + If *None*, use rc settings. + + numpoints : integer The number of points in the legend line, default is 4 - scatterpoints: integer + scatterpoints : integer The number of points in the legend line, default is 4 - scatteryoffsets: list of floats - a list of yoffsets for scatter symbols in legend + scatteryoffsets : list of floats + A list of yoffsets for scatter symbols in legend - markerscale: [ *None* | scalar ] + markerscale : None or scalar The relative size of legend markers vs. original. If *None*, use rc settings. - markerfirst: [ *True* | *False* ] - if *True*, legend marker is placed to the left of the legend label - if *False*, legend marker is placed to the right of the legend - label + markerfirst : bool + If *True*, legend marker is placed to the left of the legend label. + If *False*, legend marker is placed to the right of the legend + label. + Default is *True*. - frameon: [ *None* | bool ] + frameon : None or bool Control whether the legend should be drawn on a patch (frame). Default is *None* which will take the value from the ``legend.frameon`` :data:`rcParam`. - fancybox: [ *None* | *False* | *True* ] - if *True*, draw a frame with a round fancybox. If *None*, use rc + fancybox : None or bool + If *True*, draw a frame with a round fancybox. If *None*, use rc + settings. - shadow: [ *None* | *False* | *True* ] + shadow : None or bool If *True*, draw a shadow behind legend. If *None*, use rc settings. - framealpha: [ *None* | float ] + framealpha : None or float Control the alpha transparency of the legend's background. Default is *None* which will take the value from the ``legend.framealpha`` :data:`rcParam`. - facecolor: [ *None* | "inherit" | a color spec ] + facecolor : None or "inherit" or a color spec Control the legend's background color. Default is *None* which will take the value from the ``legend.facecolor`` :data:`rcParam`. If ``"inherit"``, it will take the ``axes.facecolor`` :data:`rcParam`. - edgecolor: [ *None* | "inherit" | a color spec ] + edgecolor : None or "inherit" or a color spec Control the legend's background patch edge color. Default is *None* which will take the value from the ``legend.edgecolor`` :data:`rcParam`. If ``"inherit"``, it will take the ``axes.edgecolor`` :data:`rcParam`. - ncol : integer - number of columns. default is 1 + ncol : integer + Number of columns. Default is 1. - mode : [ "expand" | *None* ] - if mode is "expand", the legend will be horizontally expanded + mode : "expand" or None + If mode is "expand", the legend will be horizontally expanded to fill the axes area (or *bbox_to_anchor*) - title : string - the legend title + title : string + The legend title + + borderpad : float or None + The fractional whitespace inside the legend border, measured in + font-size units. + Default is *None* which will take the value from the + ``legend.borderpad`` :data:`rcParam`. + + labelspacing : float or None + The vertical space between the legend entries, measured in + font-size units. + Default is *None* which will take the value from the + ``legend.labelspacing`` :data:`rcParam`. + + handlelength : float or None + The length of the legend handles, measured in font-size units. + Default is *None* which will take the value from the + ``legend.handlelength`` :data:`rcParam`. - Padding and spacing between various elements use following keywords - parameters. The dimensions of these values are given as a fraction - of the fontsize. Values from rcParams will be used if None. + handletextpad : float or None + The padding between the legend handle and text, measured in + font-size units. + Default is *None* which will take the value from the + ``legend.handletextpad`` :data:`rcParam`. - ================ ==================================================== - Keyword Description - ================ ==================================================== - borderpad the fractional whitespace inside the legend border - labelspacing the vertical space between the legend entries - handlelength the length of the legend handles - handletextpad the pad between the legend handle and text - borderaxespad the pad between the axes and legend border - columnspacing the spacing between columns - ================ ==================================================== + borderaxespad : float or None + The padding between the axes and legend border, measured in + font-size units. + Default is *None* which will take the value from the + ``legend.borderaxespad`` :data:`rcParam`. - .. Note:: Not all kinds of artist are supported by the legend. - See LINK (FIXME) for details. + columnspacing : float or None + The spacing between columns, measured in font-size units. + Default is *None* which will take the value from the + ``legend.columnspacing`` :data:`rcParam`. - **Example:** + Returns + ------- + :class:`matplotlib.legend.Legend` instance - .. plot:: mpl_examples/pylab_examples/figlegend_demo.py + Notes + ----- + Not all kinds of artist are supported by the legend command. See + :ref:`sphx_glr_tutorials_02_intermediate_legend_guide.py` for details. """ - l = Legend(self, handles, labels, *args, **kwargs) + + # If no arguments given, collect up all the artists on the figure + if len(args) == 0: + handles = [] + labels = [] + + def in_handles(h, l): + # Method to check if we already have a given handle and label. + # Consider two handles to be the same if they share a label, + # color, facecolor, and edgecolor. + + # Loop through each handle and label already collected + for f_h, f_l in zip(handles, labels): + if f_l != l: + continue + if type(f_h) != type(h): + continue + try: + if f_h.get_color() != h.get_color(): + continue + except AttributeError: + pass + try: + if f_h.get_facecolor() != h.get_facecolor(): + continue + except AttributeError: + pass + try: + if f_h.get_edgecolor() != h.get_edgecolor(): + continue + except AttributeError: + pass + return True + return False + + for ax in self.axes: + ax_handles, ax_labels = ax.get_legend_handles_labels() + for h, l in zip(ax_handles, ax_labels): + if not in_handles(h, l): + handles.append(h) + labels.append(l) + if len(handles) == 0: + warnings.warn("No labeled objects found. " + "Use label='...' kwarg on individual plots.") + return None + + elif len(args) == 2: + # LINES, LABELS + handles, labels = args + + elif len(args) == 3: + # LINES, LABELS, LOC + handles, labels, loc = args + kwargs['loc'] = loc + + else: + raise TypeError('Invalid number of arguments passed to legend. ' + 'Please specify either 0 args, 2 args ' + '(artist handles, figure labels) or 3 args ' + '(artist handles, figure labels, legend location)') + + l = Legend(self, handles, labels, **kwargs) self.legends.append(l) l._remove_method = lambda h: self.legends.remove(h) self.stale = True @@ -1364,6 +1564,10 @@ def gca(self, **kwargs): # continue and a new axes will be created if key == ckey and isinstance(cax, projection_class): return cax + else: + warnings.warn('Requested projection is different from ' + 'current axis projection, creating new axis ' + 'with requested projection.', stacklevel=2) # no axes found, so create one which spans the figure return self.add_subplot(1, 1, 1, **kwargs) @@ -1590,12 +1794,12 @@ def colorbar(self, mappable, cax=None, ax=None, use_gridspec=True, **kw): """ if ax is None: ax = self.gca() + ax = np.atleast_1d(ax).ravel() # Store the value of gca so that we can set it back later on. current_ax = self.gca() - if cax is None: - if use_gridspec and isinstance(ax, SubplotBase): + if use_gridspec and isinstance(ax[0], SubplotBase): cax, kw = cbar.make_axes_gridspec(ax, **kw) else: cax, kw = cbar.make_axes(ax, **kw) @@ -1637,17 +1841,10 @@ def subplots_adjust(self, *args, **kwargs): def ginput(self, n=1, timeout=30, show_clicks=True, mouse_add=1, mouse_pop=3, mouse_stop=2): """ - Blocking call to interact with the figure. - - This will wait for *n* clicks from the user and return a list of the - coordinates of each click. - - If *timeout* is zero or negative, does not timeout. - - If *n* is zero or negative, accumulate clicks until a middle click - (or potentially both mouse buttons at once) terminates the input. + Blocking call to interact with a figure. - Right clicking cancels last input. + Wait until the user clicks *n* times on the figure, and return the + coordinates of each click in a list. The buttons used for the various actions (adding points, removing points, terminating the inputs) can be overriden via the @@ -1655,6 +1852,30 @@ def ginput(self, n=1, timeout=30, show_clicks=True, mouse_add=1, the associated mouse button: 1 for left, 2 for middle, 3 for right. + Parameters + ---------- + n : int, optional, default: 1 + Number of mouse clicks to accumulate. If negative, accumulate + clicks until the input is terminated manually. + timeout : scalar, optional, default: 30 + Number of seconds to wait before timing out. If zero or negative + will never timeout. + show_clicks : bool, optional, default: False + If True, show a red cross at the location of each click. + mouse_add : int, one of (1, 2, 3), optional, default: 1 (left click) + Mouse button used to add points. + mouse_pop : int, one of (1, 2, 3), optional, default: 3 (right click) + Mouse button used to remove the most recently added point. + mouse_stop : int, one of (1, 2, 3), optional, default: 2 (middle click) + Mouse button used to stop input. + + Returns + ------- + points : list of tuples + A list of the clicked (x, y) coordinates. + + Notes + ----- The keyboard can also be used to select points in case your mouse does not have one or more of the buttons. The delete and backspace keys act like right clicking (i.e., remove last point), the enter key From 233211468b82af262a570d81370d0a2c43c28faa Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Tue, 13 Jun 2017 06:45:46 -0700 Subject: [PATCH 4/5] Changes to colorbar to allow list of SubplotSpec as the argument to --- lib/matplotlib/colorbar.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index b2a69083c407..daeb23642a8a 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -1178,7 +1178,7 @@ def rowcolunm2index(row, col, ncols): @docstring.Substitution(make_axes_kw_doc) -def make_axes_gridspec(parents, **kw): +def make_axes_gridspec(parent, **kw): ''' Resize and reposition a list of parent axes, and return a child axes suitable for a colorbar. This function is similar to @@ -1226,13 +1226,13 @@ def make_axes_gridspec(parents, **kw): wh_ratios = [pad_s, shrink, pad_s] # make parents a 1-d ndarray if its not already... - parents = np.atleast_1d(parents).ravel() + parents = np.atleast_1d(parent).ravel() # For the subplotspec that these axes belong to, loop through and get # the maximum and minimum index into the subplotspec. The result is the # size the new gridspec that we will create must be. gsp0 = parents[0].get_subplotspec().get_gridspec() - minind = 10000 - maxind = -10000 + minind = np.inf + maxind = -np.inf for parent in parents: gsp = parent.get_subplotspec().get_gridspec() if gsp == gsp0: From 6b5971d506fc8a19e86cf6e8690db55565da2fd4 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Tue, 13 Jun 2017 07:25:09 -0700 Subject: [PATCH 5/5] Changes to colorbar to allow list of SubplotSpec as the argument to ax --- lib/matplotlib/colorbar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index daeb23642a8a..a563991f6c12 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -1168,7 +1168,7 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, # helper functions for row,col to index. def index2rowcolunm(index, ncols): col = index % ncols + 1 - row = int(np.floor(index / ncols ) + 1) + row = int(np.floor(index / ncols) + 1) return row, col