From cbda376993149cd1fddd780cc84b64245b0153f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Sat, 28 Feb 2026 00:23:21 +0100 Subject: [PATCH 1/4] Initial bivar_colorbar --- doc/api/figure_api.rst | 2 + doc/api/pyplot_summary.rst | 1 + lib/matplotlib/backend_bases.py | 3 +- lib/matplotlib/colorbar.py | 633 ++++++++++++++++-- lib/matplotlib/colorbar.pyi | 38 +- lib/matplotlib/colorizer.py | 3 +- lib/matplotlib/colors.py | 7 +- lib/matplotlib/figure.py | 101 +++ lib/matplotlib/figure.pyi | 10 +- lib/matplotlib/pyplot.py | 20 +- .../test_colorbar/bivar_cbar_locationing.png | Bin 0 -> 19383 bytes .../bivar_cbar_not_rasterized.png | Bin 0 -> 1615 bytes .../test_colorbar/bivar_cbar_sharing.png | Bin 0 -> 10809 bytes lib/matplotlib/tests/test_colorbar.py | 369 ++++++++++ 14 files changed, 1105 insertions(+), 82 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_locationing.png create mode 100644 lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_not_rasterized.png create mode 100644 lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_sharing.png diff --git a/doc/api/figure_api.rst b/doc/api/figure_api.rst index 5dd3adbfec9f..d369fa3e706f 100644 --- a/doc/api/figure_api.rst +++ b/doc/api/figure_api.rst @@ -60,6 +60,7 @@ Annotating :nosignatures: Figure.colorbar + Figure.bivar_colorbar Figure.legend Figure.text Figure.suptitle @@ -254,6 +255,7 @@ Annotating :nosignatures: SubFigure.colorbar + SubFigure.bivar_colorbar SubFigure.legend SubFigure.text SubFigure.suptitle diff --git a/doc/api/pyplot_summary.rst b/doc/api/pyplot_summary.rst index 97d9c576cc86..6b6b4e93eb21 100644 --- a/doc/api/pyplot_summary.rst +++ b/doc/api/pyplot_summary.rst @@ -263,6 +263,7 @@ Colormapping clim colorbar + bivar_colorbar gci sci get_cmap diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 384987e3d036..00a8e585d3b1 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3275,7 +3275,8 @@ def press_zoom(self, event): # to the edge of the Axes bbox in the other dimension. To do that we # store the orientation of the colorbar for later. parent_ax = axes[0] - if hasattr(parent_ax, "_colorbar"): + if hasattr(parent_ax, "_colorbar") and hasattr(parent_ax._colorbar, + "orientation"): cbar = parent_ax._colorbar.orientation else: cbar = None diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 100807dd7f18..583fc1e6c55a 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -28,6 +28,8 @@ _log = logging.getLogger(__name__) + + _docstring.interpd.register( _make_axes_kw_doc=""" location : None or {'left', 'right', 'top', 'bottom'} @@ -114,7 +116,38 @@ spacing : {'uniform', 'proportional'} For discrete colorbars (`.BoundaryNorm` or contours), 'uniform' gives each color the same space; 'proportional' makes the space proportional to the - data interval.""") + data interval.""", + _make_bivar_axes_kw_doc=""" +location : None or {'left', 'right', 'top', 'bottom'} + The location, relative to the parent Axes, where the colorbar Axes + is created. Also determines the position of the ticks and labels, + which will favour being away from the parent axes. + +fraction : float, default: 0.15 + Fraction of original Axes to use for colorbar. + +shrink : float, default: 1.0 + Fraction by which to multiply the size of the colorbar. + +pad : float, default: 0.05 if left or right, 0.15 if top or bottom + Fraction of original Axes between colorbar and new image Axes. + +anchor : (float, float), optional + The anchor point of the colorbar Axes. + Defaults to (0.0, 0.5) if left or right; (0.5, 1.0) if top or bottom. + +panchor : (float, float), or *False*, optional + The anchor point of the colorbar parent Axes. If *False*, the parent + axes' anchor will be unchanged. + Defaults to (1.0, 0.5) if left or right; (0.5, 0.0) if top or bottom. +""", + _bivar_colormap_kw_doc=""" +ticklocations : tuple describing the ticklocation of the y and x axis + The first element must be {'auto', 'left', 'right'} + The second element must be {'auto', 'top', 'bottom''}. + If 'auto', the ticklocations are determined by the *location*. + +""") def _set_ticks_on_axis_warn(*args, **kwargs): @@ -531,7 +564,6 @@ def update_normal(self, mappable=None): if self.mappable.norm != self.norm: self.norm = self.mappable.norm self._reset_locator_formatter_scale() - self._draw_all() if isinstance(self.mappable, contour.ContourSet): CS = self.mappable @@ -1063,6 +1095,8 @@ def remove(self): try: ax = self.mappable.axes + if ax is None: + return except AttributeError: return try: @@ -1357,9 +1391,324 @@ def drag_pan(self, button, key, x, y): ColorbarBase = Colorbar # Backcompat API +class BivarColorbar: + r""" + Draw a bivariate colorbar in an existing Axes. + + Typically, bivariate colorbars are created using `.Figure.bivar_colorbar` + and associated with `.ColorizingArtist`\s (such as an + `.AxesImage` generated via `~.axes.Axes.imshow`). + + Unlike `Colorbar`, `BivarColorbar` does not support + customizing the ticks, and ticks must be customized on the axes instead. + """ + + n_rasterize = 256 # rasterize solids if number of colors >= n_rasterize + + def __init__( + self, ax, mappable, + *, + alpha=None, + location=None, + ticklocations=['auto', 'auto'], + aspect=1.0, + ): + """ + Parameters + ---------- + ax : `~matplotlib.axes.Axes` + The `~.axes.Axes` instance in which the colorbar is drawn. + + mappable : `.ColorizingArtist` + The mappable whose colormap and norm will be used. + + To show the colors versus index instead of on a 0-1 scale, set the + mappable's norm to ``colors.NoNorm()``. + + alpha : float + The colorbars transparency between 0 (transparent) and 1 (opaque). + + location : None or {'left', 'right', 'top', 'bottom'} + Set the bivariate colorbars's location + + Other Parameters + ---------------- + ticklocations : tuple describing the ticklocation of the y and x axis + The first element must be {'auto', 'left', 'right'} + The second element must be {'auto', 'top', 'bottom''}. + If 'auto', the ticklocations are determined by the *location*. + """ + + self.ax = ax + self.location = location + if isinstance(mappable, mpl.colorizer.Colorizer): + mappable = mcolorizer.ColorizingArtist(mappable) + + self.mappable = mappable + self.aspect = aspect + self.colorizer = mappable.colorizer + + mappable.colorbar = self + mappable.colorbar_cid = mappable.callbacks.connect( + 'changed', self.update_normals) + + ticklocations = list(ticklocations) + if len(ticklocations) != 2: + raise ValueError("ticklocations must be a tuple of length 2") + _api.check_in_list(['auto', 'left', 'right'], + ticklocation=ticklocations[0]) + _api.check_in_list(['auto', 'top', 'bottom'], + ticklocation=ticklocations[1]) + + location_ticklocs = _get_bivar_ticklocations_from_location(location) + for i in range(2): + if ticklocations[i] == 'auto': + ticklocations[i] = location_ticklocs[i] + self.ticklocations = ticklocations + self.ax.yaxis.set(label_position=self.ticklocations[0], + ticks_position=self.ticklocations[0]) + self.ax.xaxis.set(label_position=self.ticklocations[1], + ticks_position=self.ticklocations[1]) + + self._image = None + self.alpha = None + # Call set_alpha to handle array-like alphas properly + self.set_alpha(alpha) + + self.update_normals() # also calls _draw_all() + self.ax._colorbar = self + self._interactive_funcs = ["_get_view", "_set_view", + "_set_view_from_bbox", "drag_pan"] + for x in self._interactive_funcs: + setattr(self.ax, x, getattr(self, x)) + self.ax.cla = self._cbar_cla + + """self._colorbar_xax_cid = ax.callbacks.connect( + 'xlim_changed', self._x_limits_changed) + self._colorbar_yax_cid = ax.callbacks.connect( + 'ylim_changed', self._y_limits_changed) + """ + + @property + def aspect(self): + return self._aspect + + @aspect.setter + def aspect(self, aspect): + aspect = float(aspect) + self._aspect = aspect + self.ax.set_box_aspect(aspect) + if hasattr(self.ax, "_colorbar_info"): + self.ax._colorbar_info["aspect"] = aspect + + def _draw_all(self): + """ + Calculate any free parameters based on the current cmap and norm, + and do all the drawing. + """ + + # transform from 0-1 to vmin-vmax: + if self.mappable.get_array() is not None: + self.mappable.autoscale_None() + if not self.colorizer.norm.scaled(): + # If we still aren't scaled after autoscaling, use 0, 1 as default + self._set_view([0, 1, 0, 1]) + n, m = self.colorizer.cmap.N, self.colorizer.cmap.M + x = self.colorizer.norm.norms[1].inverse(np.linspace(0, 1, m + 1)) + y = self.colorizer.norm.norms[0].inverse(np.linspace(0, 1, n + 1)) + X, Y = np.meshgrid(x, y) + + if self.alpha is None: + lut = self.colorizer.cmap.lut + else: + lut = np.copy(self.colorizer.cmap.lut) + lut[:, :, 3] *= self.alpha + + if n * m > self.n_rasterize: + rasterized = True + else: + rasterized = False + + if self._image is not None: + self._image.remove() + self._image = self.ax.pcolormesh( + X, Y, lut, + alpha=self.alpha, + rasterized=rasterized, + edgecolors='none', shading='flat') + # Apply norm scaling (supports LogNorm etc.) + try: + self.ax.set_yscale(self.colorizer.norm.norms[0]._scale) + except TypeError: + # fallback for custom norms + self.ax.set_yscale( + 'function', + functions=( + self.colorizer.norm.norms[0], + self.colorizer.norm.norms[0].inverse + ) + ) + try: + self.ax.set_xscale(self.colorizer.norm.norms[1]._scale) + except TypeError: + # fallback for custom norms + self.ax.set_xscale( + 'function', + functions=( + self.colorizer.norm.norms[1], + self.colorizer.norm.norms[1].inverse + ) + ) + # Manually set limits (image is in Axes coordinates) + extent = [ + self.colorizer.norm.norms[1].vmin, + self.colorizer.norm.norms[1].vmax, + self.colorizer.norm.norms[0].vmin, + self.colorizer.norm.norms[0].vmax, + ] + self.ax.set_ylim(extent[2:4]) + self.ax.set_xlim(extent[0:2]) + + def set_xlabel(self, label): + self.ax.set_xlabel(label) + + def set_ylabel(self, label): + self.ax.set_ylabel(label) + + @property + def xaxis(self): + return self.ax.xaxis + + @property + def yaxis(self): + return self.ax.yaxis + + """ + def _y_limits_changed(self, ax=None): + if not self._allow_change_limits: + ylim = self.ax.get_ylim() + if self.colorizer.norm.norms[0].vmin != ylim[0]: + self.colorizer.norm.norms[0].vmin = ylim[0] + if self.colorizer.norm.norms[0].vmax != ylim[1]: + self.colorizer.norm.norms[0].vmax = ylim[1] + + def _x_limits_changed(self, ax=None): + if not self._allow_change_limits: + xlim = self.ax.get_xlim() + if self.colorizer.norm.norms[1].vmin != xlim[0]: + self.colorizer.norm.norms[1].vmin = xlim[0] + if self.colorizer.norm.norms[1].vmax != xlim[1]: + self.colorizer.norm.norms[1].vmax = xlim[1] + """ + """ + def _update_limits(self, ax=None): + if not self._allow_change_limits: + ylim = self.ax.get_ylim() + xlim = self.ax.get_xlim() + if (self.colorizer.norm.norms[0].vmin != ylim[0] or + self.colorizer.norm.norms[0].vmax != ylim[1] or + self.colorizer.norm.norms[1].vmin != xlim[0] or + self.colorizer.norm.norms[1].vmax != xlim[1]): + raise ValueError("Changing the limits of this axis must be done " + "using the norm attached to the bivariate colorbar.") + """ + + def update_normals(self, mappable=None): + self.set_alpha(self.mappable.get_alpha()) + #self._draw_all() + + def set_alpha(self, alpha): + """ + Set the transparency between 0 (transparent) and 1 (opaque). + + If an array is provided, *alpha* will be set to None to use the + transparency values associated with the colormap. + """ + self.alpha = None if isinstance(alpha, np.ndarray) else alpha + self._draw_all() + + def remove(self): + """ + Remove this colorbar from the figure. + + If the colorbar was created with ``use_gridspec=True`` the previous + gridspec is restored. + """ + if hasattr(self.ax, '_colorbar_info'): + parents = self.ax._colorbar_info['parents'] + for a in parents: + if self.ax in a._colorbars: + a._colorbars.remove(self.ax) + self.ax.remove() + self.mappable.callbacks.disconnect(self.mappable.colorbar_cid) + self.mappable.colorbar = None + self.mappable.colorbar_cid = None + # self.ax.callbacks.disconnect(self._colorbar_xax_cid) + # self.ax.callbacks.disconnect(self._colorbar_yax_cid) + + ax = self.mappable.axes + if ax is None: + return + try: + subplotspec = self.ax.get_subplotspec().get_gridspec()._subplot_spec + except AttributeError: # use_gridspec was False + pos = ax.get_position(original=True) + ax._set_position(pos) + else: # use_gridspec was True + ax.set_subplotspec(subplotspec) + + def _get_view(self): + ynorm, xnorm = self.colorizer.norm.norms + return ynorm.vmin, ynorm.vmax, xnorm.vmin, xnorm.vmax + + def _set_view(self, view): + ynorm, xnorm = self.colorizer.norm.norms + if (view[0] != ynorm.vmin + or view[1] != ynorm.vmax + or view[2] != xnorm.vmin + or view[3] != xnorm.vmax): + with self.colorizer.norm.callbacks.blocked(): + ynorm.vmin, ynorm.vmax, xnorm.vmin, xnorm.vmax = view + self.colorizer.norm._changed() + + def _set_view_from_bbox(self, bbox, direction='in', + mode=None, twinx=False, twiny=False): + new_xbound, new_ybound = self.ax._prepare_view_from_bbox( + bbox, direction=direction, mode=mode, twinx=twinx, twiny=twiny) + ynorm, xnorm = self.colorizer.norm.norms + ynorm.vmin, ynorm.vmax = new_ybound + xnorm.vmin, xnorm.vmax = new_xbound + + def drag_pan(self, button, key, x, y): + points = self.ax._get_pan_points(button, key, x, y) + if points is not None: + ynorm, xnorm = self.colorizer.norm.norms + xnorm.vmin, xnorm.vmax = points[:, 0] + ynorm.vmin, ynorm.vmax = points[:, 1] + + def _cbar_cla(self): + """Function to clear the interactive colorbar state.""" + for x in self._interactive_funcs: + delattr(self.ax, x) + # We now restore the old cla() back and can call it directly + del self.ax.cla + self.ax.cla() + + def _normalize_location_orientation(location, orientation): if location is None: location = _get_ticklocation_from_orientation(orientation) + loc_settings = _normalize_location(location) + loc_settings["orientation"] = _get_orientation_from_location(location) + if orientation is not None and orientation != loc_settings["orientation"]: + # Allow the user to pass both if they are consistent. + raise TypeError("location and orientation are mutually exclusive") + return loc_settings + + +def _normalize_location(location): + if location is None: + location = 'right' loc_settings = _api.getitem_checked({ "left": {"location": "left", "anchor": (1.0, 0.5), "panchor": (0.0, 0.5), "pad": 0.10}, @@ -1370,10 +1719,6 @@ def _normalize_location_orientation(location, orientation): "bottom": {"location": "bottom", "anchor": (0.5, 1.0), "panchor": (0.5, 0.0), "pad": 0.15}, }, location=location) - loc_settings["orientation"] = _get_orientation_from_location(location) - if orientation is not None and orientation != loc_settings["orientation"]: - # Allow the user to pass both if they are consistent. - raise TypeError("location and orientation are mutually exclusive") return loc_settings @@ -1389,39 +1734,18 @@ def _get_ticklocation_from_orientation(orientation): orientation=orientation) -@_docstring.interpd -def make_axes(parents, location=None, orientation=None, fraction=0.15, - shrink=1.0, aspect=20, **kwargs): - """ - Create an `~.axes.Axes` suitable for a colorbar. +def _get_bivar_ticklocations_from_location(location): + loc_0 = _api.getitem_checked( + {None: "right", "left": "left", "right": "right", + "top": "left", "bottom": "left"}, location=location) + loc_1 = _api.getitem_checked( + {None: "bottom", "left": "bottom", "right": "bottom", + "top": "top", "bottom": "bottom"}, location=location) + return (loc_0, loc_1) - The Axes is placed in the figure of the *parents* Axes, by resizing and - repositioning *parents*. - Parameters - ---------- - parents : `~matplotlib.axes.Axes` or iterable or `numpy.ndarray` of `~.axes.Axes` - The Axes to use as parents for placing the colorbar. - %(_make_axes_kw_doc)s - - Returns - ------- - cax : `~matplotlib.axes.Axes` - The child Axes. - kwargs : dict - The reduced keyword dictionary to be passed when creating the colorbar - instance. - """ - loc_settings = _normalize_location_orientation(location, orientation) - # put appropriate values into the kwargs dict for passing back to - # the Colorbar class - kwargs['orientation'] = loc_settings['orientation'] - location = kwargs['ticklocation'] = loc_settings['location'] - - anchor = kwargs.pop('anchor', loc_settings['anchor']) - panchor = kwargs.pop('panchor', loc_settings['panchor']) - aspect0 = aspect - # turn parents into a list if it is not already. Note we cannot +def _normalize_parents(parents): + # Turn parents into a list if it is not already. Note we cannot # use .flatten or .ravel as these copy the references rather than # reuse them, leading to a memory leak if isinstance(parents, np.ndarray): @@ -1433,18 +1757,20 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, fig = parents[0].get_figure() - pad0 = 0.05 if fig.get_constrained_layout() else loc_settings['pad'] - pad = kwargs.pop('pad', pad0) - if not all(fig is ax.get_figure() for ax in parents): raise ValueError('Unable to create a colorbar Axes as not all ' 'parents share the same figure.') + return parents, fig + +def _get_bbox_shrink_parents(parents, location, fraction, + pad, shrink, anchor, panchor): + """Shrink parents and get the bbox for the colorbar.""" # take a bounding box around all of the given Axes - parents_bbox = mtransforms.Bbox.union( + pb = mtransforms.Bbox.union( [ax.get_position(original=True).frozen() for ax in parents]) - pb = parents_bbox + # calculate the new bounding boxes if location in ('left', 'right'): if location == 'left': pbcb, _, pb1 = pb.splitx(fraction, fraction + pad) @@ -1458,24 +1784,49 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, pb1, _, pbcb = pb.splity(1 - fraction - pad, 1 - fraction) pbcb = pbcb.shrunk(shrink, 1.0).anchored(anchor, pbcb) - # define the aspect ratio in terms of y's per x rather than x's per y - aspect = 1.0 / aspect - # define a transform which takes us from old axes coordinates to # new axes coordinates - shrinking_trans = mtransforms.BboxTransform(parents_bbox, pb1) + shrinking_trans = mtransforms.BboxTransform(pb, pb1) - # transform each of the Axes in parents using the new transform for ax in parents: new_posn = shrinking_trans.transform(ax.get_position(original=True)) new_posn = mtransforms.Bbox(new_posn) ax._set_position(new_posn) if panchor is not False: ax.set_anchor(panchor) + return pbcb + + +def _make_axes_helper(loc_settings, fraction, shrink, aspect, kwargs, parents): + """ + Help function for `make_axes` and `make_bivar_axes`. + + `make_axes` and `make_bivar_axes` are identical + except for the fact that `make_axes` also deals with: + 1. the aspect + 2. orientation. + + `make_bivar_axes` on the other hand has no concept of orientation + and the aspect is handled by the `BivarColormap` instance, not during + creation of the axes. + """ + location = loc_settings['location'] + kwargs['location'] = location + + anchor = kwargs.pop('anchor', loc_settings['anchor']) + panchor = kwargs.pop('panchor', loc_settings['panchor']) + parents, fig = _normalize_parents(parents) + pad0 = 0.05 if fig.get_constrained_layout() else loc_settings['pad'] + pad = kwargs.pop('pad', pad0) + + # shrink the parents and get the bbox for the colorbar + pbcb = _get_bbox_shrink_parents(parents, location, fraction, + pad, shrink, anchor, panchor) cax = fig.add_axes(pbcb, label="") for a in parents: a._colorbars.append(cax) # tell the parent it has a colorbar + cax._colorbar_info = dict( parents=parents, location=location, @@ -1483,39 +1834,67 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, anchor=anchor, panchor=panchor, fraction=fraction, - aspect=aspect0, + aspect=aspect, pad=pad) - # and we need to set the aspect ratio by hand... + cax.set_anchor(anchor) + return cax + + +@_docstring.interpd +def make_axes(parents, location=None, orientation=None, fraction=0.15, + shrink=1.0, aspect=20, **kwargs): + """ + Create an `~.axes.Axes` suitable for a colorbar. + + The Axes is placed in the figure of the *parents* Axes, by resizing and + repositioning *parents*. + + Parameters + ---------- + parents : `~matplotlib.axes.Axes` or iterable or `numpy.ndarray` of `~.axes.Axes` + The Axes to use as parents for placing the colorbar. + %(_make_axes_kw_doc)s + + Returns + ------- + cax : `~matplotlib.axes.Axes` + The child Axes. + kwargs : dict + The reduced keyword dictionary to be passed when creating the colorbar + instance. + """ + loc_settings = _normalize_location_orientation(location, orientation) + # put appropriate values into the kwargs dict for passing back to + # the Colorbar class + kwargs['orientation'] = loc_settings['orientation'] + kwargs['ticklocation'] = loc_settings['location'] + + cax = _make_axes_helper(loc_settings, fraction, shrink, aspect, kwargs, parents) + + if loc_settings["location"] in ('top', 'bottom'): + aspect = 1.0 / aspect cax.set_box_aspect(aspect) cax.set_aspect('auto') + # and we need to set the aspect ratio by hand... return cax, kwargs @_docstring.interpd -def make_axes_gridspec(parent, *, location=None, orientation=None, - fraction=0.15, shrink=1.0, aspect=20, **kwargs): +def make_bivar_axes(parents, location=None, fraction=0.15, + shrink=1.0, aspect=1.0, **kwargs): """ - Create an `~.axes.Axes` suitable for a colorbar. - - The Axes is placed in the figure of the *parent* Axes, by resizing and - repositioning *parent*. - - This function is similar to `.make_axes` and mostly compatible with it. - Primary differences are + Create an `~.axes.Axes` suitable for a bivar_colorbar. - - `.make_axes_gridspec` requires the *parent* to have a subplotspec. - - `.make_axes` positions the Axes in figure coordinates; - `.make_axes_gridspec` positions it using a subplotspec. - - `.make_axes` updates the position of the parent. `.make_axes_gridspec` - replaces the parent gridspec with a new one. + The Axes is placed in the figure of the *parents* Axes, by resizing and + repositioning *parents*. Parameters ---------- - parent : `~matplotlib.axes.Axes` - The Axes to use as parent for placing the colorbar. - %(_make_axes_kw_doc)s + parents : `~matplotlib.axes.Axes` or iterable or `numpy.ndarray` of `~.axes.Axes` + The Axes to use as parents for placing the colorbar. + %(_make_bivar_axes_kw_doc)s Returns ------- @@ -1525,12 +1904,30 @@ def make_axes_gridspec(parent, *, location=None, orientation=None, The reduced keyword dictionary to be passed when creating the colorbar instance. """ + loc_settings = _normalize_location(location) + cax = _make_axes_helper(loc_settings, fraction, shrink, aspect, kwargs, parents) + # need to add aspect to kwargs so it propagates to the BivarColorbar + kwargs["aspect"] = aspect + + return cax, kwargs - loc_settings = _normalize_location_orientation(location, orientation) - kwargs['orientation'] = loc_settings['orientation'] - location = kwargs['ticklocation'] = loc_settings['location'] - aspect0 = aspect +def _make_axes_gridspec_helper(loc_settings, fraction, shrink, aspect, kwargs, parent): + """ + Help function for `make_axes_gridspec` and `make_bivar_axes_gridspec`. + + `make_axes_gridspec` and `make_bivar_axes_gridspec` are identical + except for the fact that `make_axes_gridspec` also deals with: + 1. the aspect + 2. orientation. + + `make_bivar_axes_gridspec` on the other hand has no concept of orientation + and the aspect is handled by the `BivarColormap` instance, not during creation + of the axes. + """ + location = loc_settings['location'] + kwargs['location'] = location + anchor = kwargs.pop('anchor', loc_settings['anchor']) panchor = kwargs.pop('panchor', loc_settings['panchor']) pad = kwargs.pop('pad', loc_settings["pad"]) @@ -1560,7 +1957,6 @@ def make_axes_gridspec(parent, *, location=None, orientation=None, gs.set_height_ratios([1 - fraction - pad, fraction]) ss_main = gs[0, :] ss_cb = gs[1, 1] - aspect = 1 / aspect parent.set_subplotspec(ss_main) if panchor is not False: @@ -1570,8 +1966,7 @@ def make_axes_gridspec(parent, *, location=None, orientation=None, cax = fig.add_subplot(ss_cb, label="") parent._colorbars.append(cax) # tell the parent it has a colorbar cax.set_anchor(anchor) - cax.set_box_aspect(aspect) - cax.set_aspect('auto') + cax._colorbar_info = dict( location=location, parents=[parent], @@ -1579,7 +1974,97 @@ def make_axes_gridspec(parent, *, location=None, orientation=None, anchor=anchor, panchor=panchor, fraction=fraction, - aspect=aspect0, + aspect=aspect, pad=pad) + return cax + + +@_docstring.interpd +def make_axes_gridspec(parent, *, location=None, orientation=None, + fraction=0.15, shrink=1.0, aspect=20, **kwargs): + """ + Create an `~.axes.Axes` suitable for a colorbar. + + The Axes is placed in the figure of the *parent* Axes, by resizing and + repositioning *parent*. + + This function is similar to `.make_axes` and mostly compatible with it. + Primary differences are + + - `.make_axes_gridspec` requires the *parent* to have a subplotspec. + - `.make_axes` positions the Axes in figure coordinates; + `.make_axes_gridspec` positions it using a subplotspec. + - `.make_axes` updates the position of the parent. `.make_axes_gridspec` + replaces the parent gridspec with a new one. + + Parameters + ---------- + parent : `~matplotlib.axes.Axes` + The Axes to use as parent for placing the colorbar. + %(_make_axes_kw_doc)s + + Returns + ------- + cax : `~matplotlib.axes.Axes` + The child Axes. + kwargs : dict + The reduced keyword dictionary to be passed when creating the colorbar + instance. + """ + loc_settings = _normalize_location_orientation(location, orientation) + kwargs['orientation'] = loc_settings['orientation'] + kwargs['ticklocation'] = loc_settings['location'] + + cax = _make_axes_gridspec_helper(loc_settings, fraction, shrink, + aspect, kwargs, parent) + + if loc_settings["location"] in ('top', 'bottom'): + aspect = 1 / aspect + cax.set_box_aspect(aspect) + cax.set_aspect('auto') + + return cax, kwargs + + +@_docstring.interpd +def make_bivar_axes_gridspec(parent, *, location=None, + fraction=0.15, shrink=1.0, aspect=1.0, **kwargs): + """ + Create an `~.axes.Axes` suitable for a bivariate colorbar. + + The Axes is placed in the figure of the *parent* Axes, by resizing and + repositioning *parent*. + + This function is similar to `.make_axes` and mostly compatible with it. + Primary differences are + + - `.make_axes_gridspec` requires the *parent* to have a subplotspec. + - `.make_axes` positions the Axes in figure coordinates; + `.make_axes_gridspec` positions it using a subplotspec. + - `.make_axes` updates the position of the parent. `.make_axes_gridspec` + replaces the parent gridspec with a new one. + + Parameters + ---------- + parent : `~matplotlib.axes.Axes` + The Axes to use as parent for placing the colorbar. + %(_make_bivar_axes_kw_doc)s + + Returns + ------- + cax : `~matplotlib.axes.Axes` + The child Axes. + kwargs : dict + The reduced keyword dictionary to be passed when creating the colorbar + instance. + """ + loc_settings = _normalize_location(location) + + cax = _make_axes_gridspec_helper(loc_settings, fraction, shrink, + aspect, kwargs, parent) + + # need to add aspect to kwargs so it propagates to the BivarColorbar + kwargs["aspect"] = aspect + return cax, kwargs diff --git a/lib/matplotlib/colorbar.pyi b/lib/matplotlib/colorbar.pyi index d1401b238793..49169c2c24ee 100644 --- a/lib/matplotlib/colorbar.pyi +++ b/lib/matplotlib/colorbar.pyi @@ -1,5 +1,5 @@ import matplotlib.spines as mspines -from matplotlib import cm, collections, colors, contour, colorizer +from matplotlib import cm, collections, colors, contour, colorizer as mcolorizer from matplotlib.axes import Axes from matplotlib.axis import Axis from matplotlib.backend_bases import RendererBase @@ -21,7 +21,7 @@ class _ColorbarSpine(mspines.Spine): class Colorbar: n_rasterize: int - mappable: cm.ScalarMappable | colorizer.ColorizingArtist + mappable: cm.ScalarMappable | mcolorizer.ColorizingArtist ax: Axes alpha: float | None cmap: colors.Colormap @@ -43,7 +43,7 @@ class Colorbar: def __init__( self, ax: Axes, - mappable: cm.ScalarMappable | colorizer.ColorizingArtist | None = ..., + mappable: cm.ScalarMappable | mcolorizer.ColorizingArtist | None = ..., *, cmap: str | colors.Colormap | None = ..., norm: colors.Normalize | None = ..., @@ -115,8 +115,40 @@ class Colorbar: def remove(self) -> None: ... def drag_pan(self, button: Any, key: Any, x: float, y: float) -> None: ... + ColorbarBase = Colorbar +class BivarColorbar: + mappable: mcolorizer.ColorizingArtist + ax: Axes + alpha: float | None + colorizer: mcolorizer.Colorizer + ticklocations: tuple[Literal["auto", "left", "right"], Literal["auto", "top", "bottom"]] + def __init__( + self, + ax: Axes, + mappable: mcolorizer.ColorizingArtist | mcolorizer.Colorizer, + *, + alpha: float | None = ..., + ticklocations: tuple[Literal["auto", "left", "right"], Literal["auto", "top", "bottom"]] = ..., + location: Literal["left", "right", "top", "bottom"] | None = ..., + aspect: float = ..., + ) -> None: ... + @property + def aspect(self) -> float: ... + @aspect.setter + def aspect(self, aspect: float) -> None: ... + def set_xlabel(self, label: str) -> None: ... + def set_ylabel(self, label: str) -> None: ... + @property + def xaxis(self) -> Axis: ... + @property + def yaxis(self) -> Axis: ... + def update_normals(self, mappable: mcolorizer.ColorizingArtist | None = ...) -> None: ... + def set_alpha(self, alpha: float | None) -> None: ... + def remove(self) -> None: ... + def drag_pan(self, button: Any, key: Any, x: float, y: float) -> None: ... + def make_axes( parents: Axes | list[Axes] | np.ndarray, location: Literal["left", "right", "top", "bottom"] | None = ..., diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py index 99cbcf157db4..e2fb7e3b31d0 100644 --- a/lib/matplotlib/colorizer.py +++ b/lib/matplotlib/colorizer.py @@ -805,7 +805,8 @@ def _ensure_norm(norm, n_components=1): _api.check_isinstance((colors.MultiNorm, None, tuple), norm=norm) if norm is None: norm = colors.MultiNorm(['linear']*n_components) - else: # iterable, i.e. multiple strings or Normalize objects + elif not isinstance(norm, colors.MultiNorm): + # iterable, i.e. multiple strings or Normalize objects norm = colors.MultiNorm(norm) if isinstance(norm, colors.MultiNorm) and norm.n_components == n_components: return norm diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 51b440a3a99d..5d9561b95d62 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -3573,11 +3573,16 @@ def autoscale_None(self, A): - If structured array, must have `n_components` fields. Each field is used for the limits of one constituent norm. """ + changed = False with self.callbacks.blocked(): A = self._iterable_components_in_data(A, self.n_components) for n, a in zip(self.norms, A): + vmin, vmax = n.vmin, n.vmax n.autoscale_None(a) - self._changed() + if vmin != n.vmin or vmax != n.vmax: + changed = True + if changed: + self._changed() def scaled(self): """Return whether both *vmin* and *vmax* are set on all constituent norms.""" diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 9920f6d908b3..6c96afce92bb 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -46,6 +46,8 @@ import matplotlib._api as _api import matplotlib.cbook as cbook import matplotlib.colorbar as cbar +import matplotlib.colors as mcolors +import matplotlib.colorizer as mcolorizer import matplotlib.image as mimage from matplotlib.axes import Axes @@ -1353,6 +1355,13 @@ def colorbar( therefore, this workaround is not used by default (see issue #1188). """ + if isinstance(mappable.cmap, mcolors.BivarColormap): + raise ValueError("`Figure.colorbar` can only be together with a " + "scalar colormap, please use `Figure.bivar_colorbar` " + "when working with a bivariate colormap") + if isinstance(mappable.cmap, mcolors.MultivarColormap): + raise ValueError("colorbar can only be together with a" + "scalar colormap") if ax is None: ax = getattr(mappable, "axes", None) @@ -1399,6 +1408,98 @@ def colorbar( cax.get_figure(root=False).stale = True return cb + @_docstring.interpd + def bivar_colorbar( + self, mappable, *, cax=None, ax=None, use_gridspec=True, **kwargs): + """ + Add a bivariate colorbar to a plot. + + Parameters + ---------- + mappable + The `matplotlib.colorizer.ColorizingArtist` (i.e., `.AxesImage`, + `.ContourSet`, etc.) described by this bivariate colorbar. + This argument is mandatory for the `.Figure.bivar_colorbar` method + but optional for the`.pyplot.bivar_colorbar` function, which sets + the default to the current image. + + cax : `~matplotlib.axes.Axes`, optional + Axes into which the colorbar will be drawn. If `None`, then a new + Axes is created and the space for it will be stolen from the Axes(s) + specified in *ax*. + + ax : `~matplotlib.axes.Axes` or iterable or `numpy.ndarray` of Axes, optional + The one or more parent Axes from which space for a new colorbar Axes + will be stolen. This parameter is only used if *cax* is not set. + + Defaults to the Axes that contains the mappable used to create the + colorbar. + + use_gridspec : bool, optional + If *cax* is ``None``, a new *cax* is created as an instance of + Axes. If *ax* is positioned with a subplotspec and *use_gridspec* + is ``True``, then *cax* is also positioned with a subplotspec. + + Returns + ------- + bivariate_colorbar : `~matplotlib.colorbar.BivarColorbar` + + Other Parameters + ---------------- + %(_make_bivar_axes_kw_doc)s + %(_bivar_colormap_kw_doc)s + + """ + + if isinstance(mappable, mpl.colorizer.Colorizer): + mappable = mcolorizer.ColorizingArtist(mappable) + if not isinstance(mappable.colorizer.cmap, mcolors.BivarColormap): + raise ValueError("A bivariate colorbar can only be used together with a " + f"bivariate colormap, not {type(mappable.colorizer.cmap)}") + if ax is None: + ax = getattr(mappable, "axes", None) + + if cax is None: + if ax is None: + raise ValueError( + 'Unable to determine Axes to steal space for Colorbar. ' + 'Either provide the *cax* argument to use as the Axes for ' + 'the Colorbar, provide the *ax* argument to steal space ' + 'from it, or add *mappable* to an Axes.') + fig = ( # Figure of first Axes; logic copied from make_axes. + [*ax.flat] if isinstance(ax, np.ndarray) + else [*ax] if np.iterable(ax) + else [ax])[0].get_figure(root=False) + current_ax = fig.gca() + if (fig.get_layout_engine() is not None and + not fig.get_layout_engine().colorbar_gridspec): + use_gridspec = False + if (use_gridspec + and isinstance(ax, mpl.axes._base._AxesBase) + and ax.get_subplotspec()): + cax, kwargs = cbar.make_bivar_axes_gridspec(ax, **kwargs) + else: + cax, kwargs = cbar.make_bivar_axes(ax, **kwargs) + # make_axes calls add_{axes,subplot} which changes gca; undo that. + fig.sca(current_ax) + cax.grid(visible=False, which='both', axis='both') + + if (hasattr(mappable, "get_figure") and + (mappable_host_fig := mappable.get_figure(root=True)) is not None): + # Warn in case of mismatch + if mappable_host_fig is not self._root_figure: + _api.warn_external( + f'Adding colorbar to a different Figure ' + f'{repr(mappable_host_fig)} than ' + f'{repr(self._root_figure)} which ' + f'fig.colorbar is called on.') + NON_COLORBAR_KEYS = [ # remove kws that cannot be passed to Colorbar + 'fraction', 'pad', 'shrink', 'anchor', 'panchor'] + cb = cbar.BivarColorbar(cax, mappable, **{ + k: v for k, v in kwargs.items() if k not in NON_COLORBAR_KEYS}) + cax.get_figure(root=False).stale = True + return cb + def subplots_adjust(self, left=None, bottom=None, right=None, top=None, wspace=None, hspace=None): """ diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi index cf17f4694dbd..b4aa4e44649e 100644 --- a/lib/matplotlib/figure.pyi +++ b/lib/matplotlib/figure.pyi @@ -14,7 +14,7 @@ from matplotlib.backend_bases import ( RendererBase, ) from matplotlib.colors import Colormap, Normalize -from matplotlib.colorbar import Colorbar +from matplotlib.colorbar import Colorbar, BivarColorbar from matplotlib.colorizer import ColorizingArtist, Colorizer from matplotlib.cm import ScalarMappable from matplotlib.gridspec import GridSpec, SubplotSpec, SubplotParams as SubplotParams @@ -185,6 +185,14 @@ class FigureBase(Artist): use_gridspec: bool = ..., **kwargs ) -> Colorbar: ... + def bivar_colorbar( + self, + mappable: ColorizingArtist, + cax: Axes | None = ..., + ax: Axes | Iterable[Axes] | None = ..., + use_gridspec: bool = ..., + **kwargs + ) -> BivarColorbar: ... def subplots_adjust( self, left: float | None = ..., diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 8315056c81a2..5aff242c9af4 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -126,7 +126,7 @@ EventCollection, QuadMesh, ) - from matplotlib.colorbar import Colorbar + from matplotlib.colorbar import Colorbar, BivarColorbar from matplotlib.container import ( BarContainer, ErrorbarContainer, @@ -2684,6 +2684,24 @@ def colorbar( return ret +@_copy_docstring_and_deprecators(Figure.bivar_colorbar) +def bivar_colorbar( + mappable: ColorizingArtist | None = None, + cax: matplotlib.axes.Axes | None = None, + ax: matplotlib.axes.Axes | Iterable[matplotlib.axes.Axes] | None = None, + **kwargs +) -> BivarColorbar: + if mappable is None: + mappable = gci() + if mappable is None: + raise RuntimeError('No mappable was found to use for colorbar ' + 'creation. First define a mappable such as ' + 'an image (with imshow) or a contour set (' + 'with contourf).') + ret = gcf().bivar_colorbar(mappable, cax=cax, ax=ax, **kwargs) + return ret + + def clim(vmin: float | None = None, vmax: float | None = None) -> None: """ Set the color limits of the current image. diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_locationing.png b/lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_locationing.png new file mode 100644 index 0000000000000000000000000000000000000000..7cf5b9be70f5376f027c7cee18c33f0974c07589 GIT binary patch literal 19383 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 Ql*s~FnA)0@U3828Cl=XJ-~a#s literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_not_rasterized.png b/lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_not_rasterized.png new file mode 100644 index 0000000000000000000000000000000000000000..4bf995fe8c8ee121523d534c6ddf3201084262f8 GIT binary patch literal 1615 zcmeAS@N?(olHy`uVBq!ia0y~yU~~YoKX9-C$wJ+|*+441#5JNMI6tkVJh3R1!8fs_ zASb^hCo@T*EVZaOGe6H*&(KiMKrbb=%+SKXLOVUp&@#!yC^^+cFU82f$jrdlz+6Wm zqokyu*h*hN7p`3|zbIY*qVBQjKr1*4JR*x37}(T7n9(=n#&HG))_I;Tjv*Cu-rn7x z6`d|~{Gqw3Gl!&$Nl6E9xiju-*~U zmNvnX?^s1J`)SP?EHx?L_gMb2OY&!;~P9s`cyRw#X%G zf2|1h(oFrAa;sv}+bfT!GHeodlRWUV#_rbQ^BQB;UOR&cPt(81Jovf$^!4;;ox%e7|zd;Uj0X5v&Z*z@uk(23iUG5p4| zqV)7}zjNl>v*rEl|NRS|zt-$^?Aq~r$S4Fj@VlFA8#yJ*(lDH$jJki z&0p8OW^9o2%X-aO)};*0>&gxilaGJCCj3Uk9VJZ2E()+&N`Be0m?vTF)~_6MdVZuA z1Rb~k?|QweYp%g52B$TXqrAR{zt$`}w)bn*-aOkQX7lXp${IGdTU^v0LqBE%MZYUfPB|L0_EC%qs+ZxhkUv9fyFn2r>mdKI;Vst05}~~4FCWD literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_sharing.png b/lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_sharing.png new file mode 100644 index 0000000000000000000000000000000000000000..8fe857c2bad9e4ba3f7e8083d0e5fa6c52732151 GIT binary patch literal 10809 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 literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index 33174a6df45f..ea2e332b92e4 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -1254,3 +1254,372 @@ def test_colorbar_format_string_and_old(): plt.imshow([[0, 1]]) cb = plt.colorbar(format="{x}%") assert isinstance(cb._formatter, StrMethodFormatter) + + +@pytest.mark.parametrize('use_gridspec', [True]) +@image_comparison(['bivar_cbar_locationing.png', + ], + remove_text=True, savefig_kwarg={'dpi': 40}, tol=0.05) +def test_bivar_colorbar_location(use_gridspec): + data = (np.arange(12).reshape((3, 4)) % 4, + np.arange(12).reshape((3, 4)) % 3) + # ------------------- + locations = ['left', 'right', 'top', 'bottom'] + fig, axes = plt.subplots(2, 2) + for i, ax in enumerate(axes.ravel()): + mim = ax.imshow(data, cmap='BiOrangeBlue') + fig.bivar_colorbar(mim, location=locations[i], use_gridspec=use_gridspec) + + +@pytest.mark.parametrize('use_gridspec', [True, False]) +@image_comparison(['bivar_cbar_sharing.png', + ], + remove_text=True, savefig_kwarg={'dpi': 40}, tol=0.05) +def test_bivar_colorbar_sharing(use_gridspec): + data = (np.arange(12).reshape((3, 4)) % 4, + np.arange(12).reshape((3, 4)) % 3) + # ------------------- + plt.figure() + ax1 = plt.subplot(211, anchor='NE', aspect='equal') + plt.imshow(data, cmap='BiOrangeBlue') + ax2 = plt.subplot(223) + plt.imshow(data, cmap='BiOrangeBlue') + ax3 = plt.subplot(224) + plt.imshow(data, cmap='BiOrangeBlue') + + plt.bivar_colorbar(ax=[ax2, ax3, ax1], location='right', pad=0.0, shrink=0.5, + panchor=False, use_gridspec=use_gridspec) + plt.bivar_colorbar(ax=[ax2, ax3, ax1], location='left', shrink=0.5, + panchor=False, use_gridspec=use_gridspec) + plt.bivar_colorbar(ax=[ax1], location='bottom', panchor=False, + anchor=(0.8, 0.5), shrink=0.6, use_gridspec=use_gridspec) + + +@pytest.mark.parametrize('constrained', [False, True], + ids=['standard', 'constrained']) +def test_bivar_cbar_single_ax_panchor_east(constrained): + fig, ax = plt.subplots(constrained_layout=constrained) + ax.set_anchor('N') + assert ax.get_anchor() == 'N' + mp = ax.imshow([[[0, 1], [1, 2]], [[2, 1], [0, 1]]], cmap='BiOrangeBlue') + fig.bivar_colorbar(mp, panchor='E') + assert ax.get_anchor() == 'E' + + +def test_bivar_cbar_set_xylabel(): + fig, axes = plt.subplots(1, 2) + mp = axes[0].imshow([[[0, 1], [1, 2]], [[2, 1], [0, 1]]], cmap='BiOrangeBlue') + cb = fig.bivar_colorbar(mp, cax=axes[1]) + cb.set_ylabel('y') + cb.set_xlabel('x') + assert axes[1].get_ylabel() == 'y' + assert axes[1].get_xlabel() == 'x' + assert cb.xaxis is axes[1].xaxis + assert cb.yaxis is axes[1].yaxis + + +def test_bivar_cbar_log_no_scale(): + fig, axes = plt.subplots(1, 2) + mp = axes[0].imshow([[[100, 1], [10, 1]], [[0.5, 0], [0.3, 1]]], + cmap='BiOrangeBlue', + norm=['log', mcolors.NoNorm()], + ) + assert axes[1].get_yscale() == 'linear' + assert axes[1].get_xscale() == 'linear' + fig.bivar_colorbar(mp, cax=axes[1]) + assert axes[1].get_yscale() == 'log' + assert axes[1].get_xscale() == 'function' + + +def test_bivar_cbar_change_vmin_vmax(): + fig, axes = plt.subplots(1, 2) + mp = axes[0].imshow([[[100, 1], [10, 1]], [[0.5, 0], [0.3, 1]]], + cmap='BiOrangeBlue', + ) + fig.bivar_colorbar(mp, cax=axes[1]) + assert np.all(axes[1].get_ylim() == np.array([1, 100])) + assert np.all(axes[1].get_xlim() == np.array([0, 1])) + mp.colorizer.norm.vmin = [-1, -2] + mp.colorizer.norm.vmax = [3, 5] + assert np.all(axes[1].get_ylim() == np.array([-1, 3])) + assert np.all(axes[1].get_xlim() == np.array([-2, 5])) + + +def test_bivar_cbar_change_norms(): + fig, axes = plt.subplots(1, 2) + mp = axes[0].imshow([[[100, 1], [10, 1]], [[0.5, 0.2], [0.3, 1]]], + cmap='BiOrangeBlue', + ) + fig.bivar_colorbar(mp, cax=axes[1]) + assert axes[1].get_yscale() == 'linear' + assert axes[1].get_xscale() == 'linear' + mp.colorizer.norm = ['log', mcolors.NoNorm()] + assert axes[1].get_yscale() == 'log' + assert axes[1].get_xscale() == 'function' + + +def test_bivar_cbar_anchor(): + # right + fig, ax = plt.subplots(figsize=(6, 2)) + mp = ax.imshow([[[0, 1], [1, 2]], [[2, 1], [0, 1]]], cmap='BiOrangeBlue') + anchor = (1, 0.3) + shrink = 0.3 + cbar = fig.bivar_colorbar(mp, anchor=anchor, shrink=shrink) + + x0, y0, x1, y1 = ax.get_position().extents + cx0, cy0, cx1, cy1 = cbar.ax.get_position().extents + p0 = (y1 - y0) * anchor[1] + y0 + np.testing.assert_allclose( + [cy1, cy0], + [y1 * shrink + (1 - shrink) * p0, p0 * (1 - shrink) + y0 * shrink]) + + # left + fig, ax = plt.subplots(figsize=(6, 2)) + mp = ax.imshow([[[0, 1], [1, 2]], [[2, 1], [0, 1]]], cmap='BiOrangeBlue') + anchor = (1, 0.7) + shrink = 0.3 + cbar = fig.bivar_colorbar(mp, anchor=anchor, shrink=shrink, location='left') + + x0, y0, x1, y1 = ax.get_position().extents + cx0, cy0, cx1, cy1 = cbar.ax.get_position().extents + p0 = (y1 - y0) * anchor[1] + y0 + np.testing.assert_allclose( + [cy1, cy0], + [y1 * shrink + (1 - shrink) * p0, p0 * (1 - shrink) + y0 * shrink]) + + # top + fig, ax = plt.subplots(figsize=(2, 6)) + mp = ax.imshow([[[0, 1], [1, 2]], [[2, 1], [0, 1]]], cmap='BiOrangeBlue') + anchor = (0.3, 1) + shrink = 0.3 + cbar = fig.bivar_colorbar(mp, anchor=anchor, shrink=shrink, location='top') + + x0, y0, x1, y1 = ax.get_position().extents + cx0, cy0, cx1, cy1 = cbar.ax.get_position().extents + p0 = (x1 - x0) * anchor[0] + x0 + np.testing.assert_allclose( + [cx1, cx0], + [x1 * shrink + (1 - shrink) * p0, p0 * (1 - shrink) + x0 * shrink]) + + # bottom + fig, ax = plt.subplots(figsize=(2, 6)) + mp = ax.imshow([[[0, 1], [1, 2]], [[2, 1], [0, 1]]], cmap='BiOrangeBlue') + anchor = (0.3, 1) + shrink = 0.3 + cbar = fig.bivar_colorbar(mp, anchor=anchor, shrink=shrink, location='bottom') + + x0, y0, x1, y1 = ax.get_position().extents + cx0, cy0, cx1, cy1 = cbar.ax.get_position().extents + p0 = (x1 - x0) * anchor[0] + x0 + np.testing.assert_allclose( + [cx1, cx0], + [x1 * shrink + (1 - shrink) * p0, p0 * (1 - shrink) + x0 * shrink]) + + +def test_bivar_cbar_aspect(): + fig, ax = plt.subplots(1, 1) + mp = ax.imshow([[[100, 1], [10, 1]], [[0.5, 0.2], [0.3, 1]]], + cmap='BiOrangeBlue', + ) + cbar = fig.bivar_colorbar(mp, aspect=0.3) + assert cbar.ax.get_box_aspect() == 0.3 + assert cbar.ax._colorbar_info["aspect"] == 0.3 + assert cbar.aspect == 0.3 + cbar.aspect = 4 + assert cbar.ax.get_box_aspect() == 4 + assert cbar.ax._colorbar_info["aspect"] == 4 + assert cbar.aspect == 4 + + +def test_bivar_cbar_alpha(): + fig, ax = plt.subplots(1, 1) + mp = ax.imshow([[[100, 1], [10, 1]], [[0.5, 0.2], [0.3, 1]]], + cmap='BiOrangeBlue', + alpha=0.5, + ) + cbar = fig.bivar_colorbar(mp) + assert cbar._image._alpha == 0.5 + + +# If we decide in the future to disallow calling colorbar() on the "wrong" figure, +# just delete this test. +def test_bivar_cbar_wrong_figure(): + fig0, ax0 = plt.subplots() + fig1, ax1 = plt.subplots() + mp = ax0.imshow([[[100, 1], [10, 1]], [[0.5, 0.2], [0.3, 1]]], + cmap='BiOrangeBlue', + ) + with pytest.warns(UserWarning, match="different Figure"): + fig1.bivar_colorbar(mp) + + +@pytest.mark.parametrize('use_gridspec', [True, False]) +@pytest.mark.parametrize('nested_gridspecs', [True, False]) +def test_bivar_cbar_remove_from_figure(nested_gridspecs, use_gridspec): + """Test `remove` with the specified ``use_gridspec`` setting.""" + fig = plt.figure() + if nested_gridspecs: + gs = fig.add_gridspec(2, 2)[1, 1].subgridspec(2, 2) + ax = fig.add_subplot(gs[1, 1]) + else: + ax = fig.add_subplot() + mp = ax.pcolormesh([[[100, 1], [10, 1]], [[0.5, 0.2], [0.3, 1]]], + cmap='BiOrangeBlue', + ) + pre_position = ax.get_position() + cb = fig.bivar_colorbar(mp, use_gridspec=use_gridspec) + fig.subplots_adjust() + cb.remove() + fig.subplots_adjust() + post_position = ax.get_position() + assert (pre_position.get_points() == post_position.get_points()).all() + + +def test_bivar_cbar_remove_with_no_mappable(): + fig, ax = plt.subplots() + norm = mpl.colors.MultiNorm(['linear', 'linear']) + ca = mpl.colorizer.Colorizer('BiOrangeBlue', norm) + cb = mpl.colorbar.BivarColorbar(ax, ca) + cb.remove() + + +def test_bivar_cbar_ticklocations(): + norm = mpl.colors.MultiNorm(['linear', 'linear']) + ca = mpl.colorizer.Colorizer('BiOrangeBlue', norm) + fig, ax = plt.subplots() + with pytest.raises(ValueError, match='ticklocations must be a tuple of'): + cbar = fig.bivar_colorbar(ca, cax=ax, ticklocations=['left']) + cbar = fig.bivar_colorbar(ca, cax=ax, ticklocations=['left', 'top']) + cbar.ax.yaxis.get_label_position() == 'left' + cbar.ax.xaxis.get_label_position() == 'top' + + +def test_wrong_kind_colorbar(): + fig, ax = plt.subplots(1, 1) + mp = ax.imshow([[[100, 1], [10, 1]], [[0.5, 0.2], [0.3, 1]]], + cmap='BiOrangeBlue', + ) + with pytest.raises(ValueError, match='can only be together with a scalar colormap'): + fig.colorbar(mp, ax=ax) + + fig, ax = plt.subplots(1, 1) + mp = ax.imshow([[100, 1], [10, 1]]) + with pytest.raises(ValueError, match='bivariate colorbar can only be used '): + fig.bivar_colorbar(mp, ax=ax) + + +def test_bivar_colorbar_set_get_view(): + # maybe not the best way to test this, this is normally used + # interacitively ._get_view() and ._set_view() are normally + # used in interactive mode + fig, axes = plt.subplots(1, 1) + mp = axes.imshow([[[100, 1], [10, 1]], [[0.5, 0.2], [0.3, 1]]], + cmap='BiOrangeBlue', + ) + cb = fig.bivar_colorbar(mp, ax=axes) + # limits are x: (0.2, 1), y: (1, 100) + view = cb._get_view() + view = np.array(view) + 1 + cb._set_view(view) + # limits are x: (1.2, 2), y: (2, 101) + assert mp.colorizer.norm.norms[1].vmin == 1.2 + assert mp.colorizer.norm.norms[1].vmax == 2 + assert mp.colorizer.norm.norms[0].vmin == 2 + assert mp.colorizer.norm.norms[0].vmax == 101 + + +def test_bivar_colorbar_no_ax(): + norm = mpl.colors.MultiNorm(['linear', 'linear']) + ca = mpl.colorizer.Colorizer('BiOrangeBlue', norm) + fig, ax = plt.subplots() + with pytest.raises(ValueError, match='Unable to determine Axes'): + fig.bivar_colorbar(ca) + + +def test_bivar_colorbar_custom_norm(): + """ + This tests setting the correct scale for norms that do not have a + ._scale property. + + In the normal case, bivar_colorbar uses the ._scale property + of each norm to set the transform on each axis. + """ + class CustomHalfNorm(mcolors.Normalize): + def __init__(self): + super().__init__() + + @property + def vmin(self): + return 0 + + @property + def vmax(self): + return 1 + + @property + def clip(self): + return False + + def __call__(self, value, clip=None): + return value / 2 + + def inverse(self, value): + return 2 * value + + def autoscale(self, A): + pass + + def autoscale_None(self, A): + pass + + def scaled(self): + return True + + @property + def n_components(self): + return 1 + + norm = mpl.colors.MultiNorm([CustomHalfNorm(), CustomHalfNorm()]) + ca = mpl.colorizer.Colorizer('BiOrangeBlue', norm) + fig, ax = plt.subplots() + fig.bivar_colorbar(ca, cax=ax) + assert ax.get_yscale() == 'function' + assert ax.get_xscale() == 'function' + + +def test_bivar_colorbar_not_via_fig(): + # This test makes a colorbar without calling + # fig.bivar_colorbar() and without a ColorizingArtist + norm = mpl.colors.MultiNorm(['linear', 'linear']) + ca = mpl.colorizer.Colorizer('BiOrangeBlue', norm) + fig, ax = plt.subplots() + cb = mpl.colorbar.BivarColorbar(ax, ca) + assert cb.colorizer is ca + + +def test_remove_colorbar_with_no_mappable(): + fig, ax = plt.subplots() + cb = mpl.colorbar.Colorbar(ax) + cb.remove() + + +@image_comparison(['bivar_cbar_not_rasterized.png', + ], + remove_text=True, savefig_kwarg={'dpi': 40}, tol=0.05) +def test_bivar_cbar_not_rasterized(): + cm = mpl.bivar_colormaps['BiOrangeBlue'].resampled((5,3)) + fig, axes = plt.subplots(1, 1) + mp = axes.imshow([[[100, 1], [10, 1]], [[0.5, 0.2], [0.3, 1]]], + cmap=cm, + interpolation='nearest' + ) + fig.bivar_colorbar(mp) + + +def test_cbar_wrong_figures(): + fig0, ax0 = plt.subplots() + fig1, ax1 = plt.subplots() + im0 = ax0.imshow([[0, 1], [2, 3]]) + im1 = ax1.imshow([[0, 1], [2, 3]]) + with pytest.raises(ValueError, match="not all parents share"): + fig0.colorbar(im0, ax=[im0, im1]) From 84860f6017d6ef92624a5562ab63e4c0caca5b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Sun, 8 Mar 2026 22:03:12 +0100 Subject: [PATCH 2/4] initial multivar colorbar --- lib/matplotlib/colorbar.py | 209 +++++++++++++++++++++++++++++++++---- lib/matplotlib/figure.py | 66 ++++++++++++ 2 files changed, 257 insertions(+), 18 deletions(-) diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 583fc1e6c55a..248b63c2fc2e 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -15,6 +15,7 @@ import logging import numpy as np +from collections.abc import Sequence import matplotlib as mpl from matplotlib import _api, cbook, collections, colors, contour, ticker @@ -1695,6 +1696,73 @@ def _cbar_cla(self): self.ax.cla() +class MultivarColorbar(Sequence): + def __init__(self, axes, mappable=None, **kwargs): + if isinstance(mappable, mpl.colorizer.Colorizer): + mappable = mcolorizer.ColorizingArtist(mappable) + + self.mappable = mappable + self.colorizer = mappable.colorizer + cmap = self.colorizer.cmap + norm = self.colorizer.norm + n = cmap.n_variates + + self._colorbars = [Colorbar(axes[i], + norm=norm.norms[i], + cmap=cmap[i], + **kwargs, + ) for i in range(n)] + + mappable.colorbar = self + mappable.colorbar_cid = mappable.callbacks.connect( + 'changed', self.update_normals) + + self.axes = axes + + def _set_colorbar_info(self, colorbar_info): + if colorbar_info is not None: + parents = colorbar_info['parents'] + for a in parents: + a._colorbars.append(self) + + def update_normals(self, mappable=None): + [c.update_normal() for c in self._colorbars] + + def remove(self): + if hasattr(self, '_colorbar_info'): + parents = self._colorbar_info['parents'] + for a in parents: + if self in a._colorbars: + a._colorbars.remove(self) + + for ax in self.axes: + ax.remove() + + self.mappable.callbacks.disconnect(self.mappable.colorbar_cid) + self.mappable.colorbar = None + self.mappable.colorbar_cid = None + + try: + ax = self.mappable.axes + if ax is None: + return + except AttributeError: + return + try: + subplotspec = self.ax.get_subplotspec().get_gridspec()._subplot_spec + except AttributeError: # use_gridspec was False + pos = ax.get_position(original=True) + ax._set_position(pos) + else: # use_gridspec was True + ax.set_subplotspec(subplotspec) + + def __getitem__(self, index): + return self._colorbars[index] + + def __len__(self): + return len(self._colorbars) + + def _normalize_location_orientation(location, orientation): if location is None: location = _get_ticklocation_from_orientation(orientation) @@ -1797,19 +1865,8 @@ def _get_bbox_shrink_parents(parents, location, fraction, return pbcb -def _make_axes_helper(loc_settings, fraction, shrink, aspect, kwargs, parents): - """ - Help function for `make_axes` and `make_bivar_axes`. - - `make_axes` and `make_bivar_axes` are identical - except for the fact that `make_axes` also deals with: - 1. the aspect - 2. orientation. +def _make_axes_get_pbcb(loc_settings, fraction, shrink, aspect, kwargs, parents): - `make_bivar_axes` on the other hand has no concept of orientation - and the aspect is handled by the `BivarColormap` instance, not during - creation of the axes. - """ location = loc_settings['location'] kwargs['location'] = location @@ -1823,11 +1880,7 @@ def _make_axes_helper(loc_settings, fraction, shrink, aspect, kwargs, parents): # shrink the parents and get the bbox for the colorbar pbcb = _get_bbox_shrink_parents(parents, location, fraction, pad, shrink, anchor, panchor) - cax = fig.add_axes(pbcb, label="") - for a in parents: - a._colorbars.append(cax) # tell the parent it has a colorbar - - cax._colorbar_info = dict( + colorbar_info = dict( parents=parents, location=location, shrink=shrink, @@ -1837,7 +1890,30 @@ def _make_axes_helper(loc_settings, fraction, shrink, aspect, kwargs, parents): aspect=aspect, pad=pad) - cax.set_anchor(anchor) + return fig, pbcb, colorbar_info + + +def _make_axes_helper(loc_settings, fraction, shrink, aspect, kwargs, parents): + """ + Help function for `make_axes` and `make_bivar_axes`. + + `make_axes` and `make_bivar_axes` are identical + except for the fact that `make_axes` also deals with: + 1. the aspect + 2. orientation. + + `make_bivar_axes` on the other hand has no concept of orientation + and the aspect is handled by the `BivarColormap` instance, not during + creation of the axes. + """ + fig, pbcb, colorbar_info = _make_axes_get_pbcb(loc_settings, fraction, shrink, + aspect, kwargs, parents) + cax = fig.add_axes(pbcb, label="") + for a in colorbar_info["parents"]: + a._colorbars.append(cax) # tell the parent it has a colorbar + + cax._colorbar_info = colorbar_info + cax.set_anchor(colorbar_info["anchor"]) return cax @@ -1912,6 +1988,103 @@ def make_bivar_axes(parents, location=None, fraction=0.15, return cax, kwargs +@_docstring.interpd +def make_multivar_axes(parents, n_variates, n_major, location=None, orientation=None, + fraction=0.15, shrink=1.0, aspect=20, **kwargs): + """ + Create an `~.axes.Axes` suitable for a mulitvariate colorbar. + + The Axes is placed in the figure of the *parents* Axes, by resizing and + repositioning *parents*. + + Parameters + ---------- + parents : `~matplotlib.axes.Axes` or iterable or `numpy.ndarray` of `~.axes.Axes` + The Axes to use as parents for placing the colorbar. + %(_make_axes_kw_doc)s + + Returns + ------- + cax : `~matplotlib.axes.Axes` + The child Axes. + kwargs : dict + The reduced keyword dictionary to be passed when creating the colorbar + instance. + """ + loc_settings = _normalize_location_orientation(location, orientation) + # put appropriate values into the kwargs dict for passing back to + # the Colorbar class + orientation = loc_settings['orientation'] + kwargs['orientation'] = orientation + kwargs['ticklocation'] = loc_settings['location'] + fig, pbcb, colorbar_info = _make_axes_get_pbcb(loc_settings, fraction, shrink, + aspect, kwargs, parents) + major_pad = 0.1 + minor_pad = 0.6 + + # get the shape of the grid of new colorbars + if n_major == -1: + n_major = n_variates + n_minor = 1 + else: + n_minor = n_variates // n_major + if n_major * n_minor < n_variates: + n_minor += 1 + + # adjust aspect + aspect = aspect/n_major + if loc_settings["location"] in ('top', 'bottom'): + aspect = 1.0 / aspect + + # define how the bbox needs to be split + major_width = (1-major_pad) / n_major + if n_major > 1: + major_space = major_pad / (n_major - 1) + else: + major_space = 0 + major_split = np.empty(2 * (n_major - 1)) + major_split[0::2] = major_width + major_split[1::2] = major_space + major_split = np.cumsum(major_split) + + minor_width = (1-minor_pad) / n_minor + if n_minor > 1: + minor_space = minor_pad / (n_minor - 1) + else: + minor_space = 0 + minor_split = np.empty(2 * (n_minor - 1)) + minor_split[0::2] = minor_width + minor_split[1::2] = minor_space + minor_split = np.cumsum(minor_split) + + # make the colorbar axes + caxes = [] + v = orientation == "vertical" + pbcbs = pbcb.splitx(*minor_split) if v else pbcb.splity(*minor_split)[::-1] + for i, mpcbc in enumerate(pbcbs): + if i % 2 == 1: + continue + fbcbs = mpcbc.splity(*major_split)[::-1] if v else mpcbc.splitx(*major_split) + for j, bbox in enumerate(fbcbs): + if j % 2 == 1: + continue + if len(caxes) < n_variates: + cax = fig.add_axes(bbox, label="") + caxes.append(cax) + + for cax in caxes: + for a in colorbar_info["parents"]: + a._colorbars.append(cax) # tell the parent it has a colorbar + + cax.set_anchor(colorbar_info["anchor"]) + + cax.set_box_aspect(aspect) + cax.set_aspect('auto') + cax._colorbar_info = colorbar_info + + return caxes, kwargs, colorbar_info + + def _make_axes_gridspec_helper(loc_settings, fraction, shrink, aspect, kwargs, parent): """ Help function for `make_axes_gridspec` and `make_bivar_axes_gridspec`. diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 6c96afce92bb..7d13a75db3c4 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1500,6 +1500,72 @@ def bivar_colorbar( cax.get_figure(root=False).stale = True return cb + @_docstring.interpd + def multivar_colorbar( + self, mappable, *, caxes=None, ax=None, use_gridspec=True, + n_major=-1, **kwargs): + + if isinstance(mappable, mpl.colorizer.Colorizer): + mappable = mcolorizer.ColorizingArtist(mappable) + if not isinstance(mappable.colorizer.cmap, mcolors.MultivarColormap): + raise ValueError("A multivariate colorbar can only be used together " + "with a multivariate colormap, not " + f"{type(mappable.colorizer.cmap)}") + + n_variates = mappable.colorizer.cmap.n_variates + + if ax is None: + ax = getattr(mappable, "axes", None) + + cbar_info = None + if caxes is None: + if ax is None: + raise ValueError( + 'Unable to determine Axes to steal space for Colorbar. ' + 'Either provide the *cax* argument to use as the Axes for ' + 'the Colorbar, provide the *ax* argument to steal space ' + 'from it, or add *mappable* to an Axes.') + fig = ( # Figure of first Axes; logic copied from make_axes. + [*ax.flat] if isinstance(ax, np.ndarray) + else [*ax] if np.iterable(ax) + else [ax])[0].get_figure(root=False) + current_ax = fig.gca() + if (fig.get_layout_engine() is not None and + not fig.get_layout_engine().colorbar_gridspec): + use_gridspec = False + if (use_gridspec + and isinstance(ax, mpl.axes._base._AxesBase) + and ax.get_subplotspec()): + caxes, kwargs, cbar_info = cbar.make_multivar_axes_gridspec(ax, + n_variates, n_major, **kwargs) + else: + caxes, kwargs, cbar_info = cbar.make_multivar_axes(ax, + n_variates, n_major, **kwargs) + # make_axes calls add_{axes,subplot} which changes gca; undo that. + fig.sca(current_ax) + for cax in caxes: + cax.grid(visible=False, which='both', axis='both') + + if (hasattr(mappable, "get_figure") and + (mappable_host_fig := mappable.get_figure(root=True)) is not None): + # Warn in case of mismatch + if mappable_host_fig is not self._root_figure: + _api.warn_external( + f'Adding colorbar to a different Figure ' + f'{repr(mappable_host_fig)} than ' + f'{repr(self._root_figure)} which ' + f'fig.colorbar is called on.') + NON_COLORBAR_KEYS = [ # remove kws that cannot be passed to Colorbar + 'fraction', 'pad', 'shrink', 'anchor', 'panchor'] + + cb = cbar.MultivarColorbar(caxes, mappable, **{ + k: v for k, v in kwargs.items() if k not in NON_COLORBAR_KEYS}) + cb._set_colorbar_info(cbar_info) + + for cax in caxes: + cax.get_figure(root=False).stale = True + return cb + def subplots_adjust(self, left=None, bottom=None, right=None, top=None, wspace=None, hspace=None): """ From 29b7796652aec17557408bc5da62638553c0b84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Mon, 13 Apr 2026 18:57:13 +0200 Subject: [PATCH 3/4] working on constrained_layout --- doc/api/figure_api.rst | 3 +- doc/api/pyplot_summary.rst | 3 +- lib/matplotlib/_constrained_layout.py | 61 +++-- lib/matplotlib/colorbar.py | 338 +++++++++++++++++++++----- lib/matplotlib/colorbar.pyi | 54 +++- lib/matplotlib/figure.py | 25 +- lib/matplotlib/figure.pyi | 14 +- lib/matplotlib/pyplot.py | 30 ++- lib/matplotlib/tests/test_colorbar.py | 76 +++--- 9 files changed, 472 insertions(+), 132 deletions(-) diff --git a/doc/api/figure_api.rst b/doc/api/figure_api.rst index d369fa3e706f..e08d23b8475b 100644 --- a/doc/api/figure_api.rst +++ b/doc/api/figure_api.rst @@ -60,7 +60,8 @@ Annotating :nosignatures: Figure.colorbar - Figure.bivar_colorbar + Figure.colorbar_bivar + Figure.colorbar_multivar Figure.legend Figure.text Figure.suptitle diff --git a/doc/api/pyplot_summary.rst b/doc/api/pyplot_summary.rst index 6b6b4e93eb21..5885e747bed2 100644 --- a/doc/api/pyplot_summary.rst +++ b/doc/api/pyplot_summary.rst @@ -263,7 +263,8 @@ Colormapping clim colorbar - bivar_colorbar + colorbar_bivar + colorbar_multivar gci sci get_cmap diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index ce488d555898..9eef1d3937b6 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -398,12 +398,18 @@ def make_layout_margins(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0, # make margin for colorbars. These margins go in the # padding margin, versus the margin for Axes decorators. for cbax in ax._colorbars: + if cbax._colorbar_info["type"] == "MultivarColorbar": + # a matplotlib.colorbar.MultivarColorbar object + fig = cbax.axes[0].get_figure(root=False) + tightbbox = cbax.get_tightbbox(renderer, for_layout_only=True) + cbbbox = tightbbox.transformed(fig.transFigure.inverted()) + else: + cbpos, cbbbox = get_pos_and_bbox(cbax, renderer) # note pad is a fraction of the parent width... pad = colorbar_get_pad(layoutgrids, cbax) # colorbars can be child of more than one subplot spec: cbp_rspan, cbp_cspan = get_cb_parent_spans(cbax) loc = cbax._colorbar_info['location'] - cbpos, cbbbox = get_pos_and_bbox(cbax, renderer) if loc == 'right': if cbp_cspan.stop == ss.colspan.stop: # only increase if the colorbar is on the right edge @@ -691,7 +697,7 @@ def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None, compress=Fa Parameters ---------- layoutgrids : dict - cbax : `~matplotlib.axes.Axes` + cbax : `~matplotlib.colorbar.MultiColorbars` or `~matplotlib.axes.Axes` Axes for the colorbar. renderer : `~matplotlib.backend_bases.RendererBase` subclass. The renderer to use. @@ -701,10 +707,18 @@ def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None, compress=Fa compress : bool Whether we're in compressed layout mode. """ + cb_info = cbax._colorbar_info + if cb_info["type"] == "MultivarColorbar": + multi_cbar = cbax + cbaxes = multi_cbar.axes + multi = True + else: + multi = False + cbaxes = [cbax] - parents = cbax._colorbar_info['parents'] + parents = cb_info['parents'] gs = parents[0].get_gridspec() - fig = cbax.get_figure(root=False) + fig = cbaxes[0].get_figure(root=False) trans_fig_to_subfig = fig.transFigure - fig.transSubfigure cb_rspans, cb_cspans = get_cb_parent_spans(cbax) @@ -712,11 +726,11 @@ def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None, compress=Fa cols=cb_cspans) pb = layoutgrids[gs].get_inner_bbox(rows=cb_rspans, cols=cb_cspans) - location = cbax._colorbar_info['location'] - anchor = cbax._colorbar_info['anchor'] - fraction = cbax._colorbar_info['fraction'] - aspect = cbax._colorbar_info['aspect'] - shrink = cbax._colorbar_info['shrink'] + location = cb_info['location'] + anchor = cb_info['anchor'] + fraction = cb_info['fraction'] + aspect = cb_info['aspect'] + shrink = cb_info['shrink'] # For colorbars with a single parent in compressed layout, # use the actual visual size of the parent axis after apply_aspect() @@ -743,7 +757,13 @@ def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None, compress=Fa pb = Bbox.from_extents(actual_pos_fig.x0, pb.y0, actual_pos_fig.x1, pb.y1) - cbpos, cbbbox = get_pos_and_bbox(cbax, renderer) + if multi: + tightbbox = martist._get_tightbbox_for_layout_only(multi_cbar, renderer) + cbbbox = tightbbox.transformed(fig.transFigure.inverted()) + cbpos = multi_cbar._get_original_position() + cbpos = cbpos.transformed(fig.transSubfigure - fig.transFigure) + else: + cbpos, cbbbox = get_pos_and_bbox(cbax, renderer) # Colorbar gets put at extreme edge of outer bbox of the subplotspec # It needs to be moved in by: 1) a pad 2) its "margin" 3) by @@ -754,6 +774,8 @@ def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None, compress=Fa pbcb = pb.shrunk(fraction, shrink).anchored(anchor, pb) # The colorbar is at the left side of the parent. Need # to translate to right (or left) + if multi: + pbcb.x1 = pbcb.x0 + cbbbox.width if location == 'right': lmargin = cbpos.x0 - cbbbox.x0 dx = bboxparent.x1 - pbcb.x0 + offset['right'] @@ -768,6 +790,8 @@ def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None, compress=Fa pbcb = pbcb.translated(dx, 0) else: # horizontal axes: pbcb = pb.shrunk(shrink, fraction).anchored(anchor, pb) + if multi: + pbcb.y1 = pbcb.y0 + cbbbox.height if location == 'top': bmargin = cbpos.y0 - cbbbox.y0 dy = bboxparent.y1 - pbcb.y0 + offset['top'] @@ -781,14 +805,19 @@ def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None, compress=Fa offset['bottom'] += cbbbox.height + cbpad pbcb = pbcb.translated(0, dy) - pbcb = trans_fig_to_subfig.transform_bbox(pbcb) - cbax.set_transform(fig.transSubfigure) - cbax._set_position(pbcb) - cbax.set_anchor(anchor) if location in ['bottom', 'top']: aspect = 1 / aspect - cbax.set_box_aspect(aspect) - cbax.set_aspect('auto') + if multi: + new_bboxs = multi_cbar._get_tight_packing_inside(pbcb) + else: + new_bboxs = [pbcb] + for cbax, new_bbox in zip(cbaxes, new_bboxs): + transformed_bbox = trans_fig_to_subfig.transform_bbox(new_bbox) + cbax.set_transform(fig.transSubfigure) + cbax._set_position(transformed_bbox) + cbax.set_anchor(anchor) + cbax.set_box_aspect(aspect) + cbax.set_aspect('auto') return offset diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 248b63c2fc2e..4463c38479fe 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -148,6 +148,47 @@ The second element must be {'auto', 'top', 'bottom''}. If 'auto', the ticklocations are determined by the *location*. +""", + _make_multivar_axes_kw_doc=""" +location : None or {'left', 'right', 'top', 'bottom'} + The location, relative to the parent Axes, where the colorbar Axes + is created. It also determines the *orientation* of the colorbar + (colorbars on the left and right are vertical, colorbars at the top + and bottom are horizontal). If None, the location will come from the + *orientation* if it is set (vertical colorbars on the right, horizontal + ones at the bottom), or default to 'right' if *orientation* is unset. + +orientation : None or {'vertical', 'horizontal'} + The orientation of the colorbars. It is preferable to set the *location* + of the colorbar, as that also determines the *orientation*; passing + incompatible values for *location* and *orientation* raises an exception. + +fraction : float, default: 0.15 + Fraction of original Axes to use for colorbars. + +shrink : float, default: 1.0 + Fraction by which to multiply the size. + +aspect : float, default: 20 + Ratio of long to short dimensions. + +pad : float, default: 0.05 if vertical, 0.15 if horizontal + Fraction of original Axes between colorbars and new image Axes. + +anchor : (float, float), optional + The anchor point of the colorbars Axes. + Defaults to (0.0, 0.5) if vertical; (0.5, 1.0) if horizontal. + +panchor : (float, float), or *False*, optional + The anchor point of the colorbars parent Axes. If *False*, the parent + axes' anchor will be unchanged. + Defaults to (1.0, 0.5) if vertical; (0.5, 0.0) if horizontal. + +major_pad : float + Spacing between colorbars along the long axis + +major_pad : float + Spacing between colorbars along the short axis """) @@ -1411,7 +1452,7 @@ def __init__( *, alpha=None, location=None, - ticklocations=['auto', 'auto'], + ticklocations=('auto', 'auto'), aspect=1.0, ): """ @@ -1720,6 +1761,7 @@ def __init__(self, axes, mappable=None, **kwargs): self.axes = axes def _set_colorbar_info(self, colorbar_info): + self._colorbar_info = colorbar_info if colorbar_info is not None: parents = colorbar_info['parents'] for a in parents: @@ -1762,6 +1804,187 @@ def __getitem__(self, index): def __len__(self): return len(self._colorbars) + def get_tightbbox(self, renderer=None, for_layout_only=False): + if for_layout_only and hasattr(self, '_colorbar_info'): + # figure out the maximum size of the tight boxes + # then multiply that up to the correct size + bounds = self.axes[0].get_tightbbox(renderer=renderer, + for_layout_only=True).bounds + x0, y0, width, height = bounds + for ax in self.axes[1:]: + bb = ax.get_tightbbox(renderer=renderer, + for_layout_only=True).bounds + x0 = min(x0, bb[0]) + y0 = min(y0, bb[1]) + width = max(width, bb[2]) + height = max(height, bb[3]) + n_major = self._colorbar_info["n_major"] + n_minor = self._colorbar_info["n_minor"] + + maj_p = self._colorbar_info["major_pad"] + min_p = self._colorbar_info["minor_pad"] + + if self._colorbar_info["orientation"] == 'vertical': + # height *= n_major + width *= n_minor + min_p * (n_minor - 1) + height *= n_major + maj_p * (n_major - 1) + else: + width *= n_major + maj_p * (n_major - 1) + height *= n_minor + min_p * (n_minor - 1) + + bbox = mtransforms.Bbox.from_bounds(x0, y0, width, height) + else: + # calculate the minimum size of the bbox that + # fits the current distribution of colormaps + # colorbars in the required grid + bbox = self.axes[0].get_tightbbox(renderer=renderer, + for_layout_only=for_layout_only) + for ax in self.axes[1:]: + bb = ax.get_tightbbox(renderer=renderer, + for_layout_only=for_layout_only) + if bb.x0 < bbox.x0: + bbox.x0 = bb.x0 + if bb.y0 < bbox.y0: + bbox.y0 = bb.y0 + if bb.x1 > bbox.x1: + bbox.x1 = bb.x1 + if bb.y1 > bbox.y1: + bbox.y1 = bb.y1 + return bbox + + def _get_tight_packing_inside(self, bbox): + """ + Positions the colorbars in a grid contained in bbox + + This function relates to get_tightbbox(for_layout_only=True) + It requires that self._colorbar_info exists + + It will position colorbars so that the bboxes + of each cmponent spans the assigned bbox + + The procedure for vertical colorbars is as follows: + 1. Calculate the height of each colorbar, based on n_minor and the padding + 2. From the height calculate the width using the aspect + 3. With both the width and height known, position the colorbars equispaced. + + Horizontal colorbars follow the same procedure but the height and width + are swapped. + """ + x_b, y_b, width_b, height_b = bbox.bounds # b for box + if hasattr(self, '_colorbar_info'): + n_major = self._colorbar_info["n_major"] + n_minor = self._colorbar_info["n_minor"] + aspect = self._colorbar_info["aspect"] + major_pad = self._colorbar_info["major_pad"] + # pad_minor = 0.6 + if self._colorbar_info["orientation"] == 'vertical': + if n_major > 1: + bar_height = height_b * (1 - major_pad) / n_major + y_spacing = (height_b - bar_height * n_major)/(n_major-1) + else: + bar_height = height_b + y_spacing = 0 + bar_width = bar_height / aspect + if n_minor > 1: + x_spacing = (width_b - bar_width * n_minor)/(n_minor) + else: + x_spacing = 0 + else: + if n_major > 1: + bar_width = width_b * (1 - major_pad) / n_major + x_spacing = (width_b - bar_width * n_major)/(n_major-1) + else: + bar_width = width_b + x_spacing = 0 + bar_height = bar_width / aspect + if n_minor > 1: + y_spacing = (height_b - bar_height * n_minor)/(n_minor) + else: + y_spacing = 0 + + x_step = bar_width + x_spacing + y_step = bar_height + y_spacing + bboxes = [] + if self._colorbar_info["orientation"] == 'vertical': + for i in range(n_minor): + for j in range(n_major): + bboxes.append(mtransforms.Bbox([[x_b + i * x_step, + y_b + j * y_step], + [x_b + i * x_step + bar_width, + y_b + j * y_step + bar_height], + ])) + else: + for i in range(n_minor): + for j in range(n_major): + bboxes.append(mtransforms.Bbox([[x_b + j * x_step, + y_b + i * y_step], + [x_b + j * x_step + bar_width, + y_b + i * y_step + bar_height], + ])) + return bboxes + else: + raise ValueError("_set_tight_packing cannot be called " + "unless the MultivarColorbar was created " + "by fig.multicolorbar(mappable).") + + def _get_original_position(self): + # comparable to axes.get_position(original=True) + bbox = self.axes[0].get_position(original=True) + for ax in self.axes[1:]: + bb = ax.get_position(original=True) + if bb.x0 < bbox.x0: + bbox.x0 = bb.x0 + if bb.y0 < bbox.y0: + bbox.y0 = bb.y0 + if bb.x1 > bbox.x1: + bbox.x1 = bb.x1 + if bb.y1 > bbox.y1: + bbox.y1 = bb.y1 + return bbox + + @staticmethod + def _subdivide_bbox(bbox, + n_major, + n_minor, + orientation, + major_pad=0.1, + minor_pad=0.6, + ): + major_width = (1-major_pad) / n_major + if n_major > 1: + major_space = major_pad / (n_major - 1) + else: + major_space = 0 + major_split = np.empty(2 * (n_major - 1)) + major_split[0::2] = major_width + major_split[1::2] = major_space + major_split = np.cumsum(major_split) + + minor_width = (1-minor_pad) / n_minor + if n_minor > 1: + minor_space = minor_pad / (n_minor - 1) + else: + minor_space = 0 + minor_split = np.empty(2 * (n_minor - 1)) + minor_split[0::2] = minor_width + minor_split[1::2] = minor_space + minor_split = np.cumsum(minor_split) + + # make the colorbar bboxes + sub_bboxs = [] + v = orientation == "vertical" + bboxs = bbox.splitx(*minor_split) if v else bbox.splity(*minor_split)[::-1] + for i, bboxs_i in enumerate(bboxs): + if i % 2 == 1: + continue + bboxs_j = bboxs_i.splity(*major_split + )[::-1] if v else bboxs_i.splitx(*major_split) + for j, sub_bbox in enumerate(bboxs_j): + if j % 2 == 1: + continue + sub_bboxs.append(sub_bbox) + return sub_bboxs + def _normalize_location_orientation(location, orientation): if location is None: @@ -1947,7 +2170,7 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, kwargs['ticklocation'] = loc_settings['location'] cax = _make_axes_helper(loc_settings, fraction, shrink, aspect, kwargs, parents) - + cax._colorbar_info["type"] = 'Colorbar' if loc_settings["location"] in ('top', 'bottom'): aspect = 1.0 / aspect cax.set_box_aspect(aspect) @@ -1982,6 +2205,7 @@ def make_bivar_axes(parents, location=None, fraction=0.15, """ loc_settings = _normalize_location(location) cax = _make_axes_helper(loc_settings, fraction, shrink, aspect, kwargs, parents) + cax._colorbar_info["type"] = 'BivarColorbar' # need to add aspect to kwargs so it propagates to the BivarColorbar kwargs["aspect"] = aspect @@ -1990,7 +2214,8 @@ def make_bivar_axes(parents, location=None, fraction=0.15, @_docstring.interpd def make_multivar_axes(parents, n_variates, n_major, location=None, orientation=None, - fraction=0.15, shrink=1.0, aspect=20, **kwargs): + fraction=0.15, shrink=1.0, aspect=20, major_pad=0.2, + minor_pad=0.6, **kwargs): """ Create an `~.axes.Axes` suitable for a mulitvariate colorbar. @@ -2001,7 +2226,13 @@ def make_multivar_axes(parents, n_variates, n_major, location=None, orientation= ---------- parents : `~matplotlib.axes.Axes` or iterable or `numpy.ndarray` of `~.axes.Axes` The Axes to use as parents for placing the colorbar. - %(_make_axes_kw_doc)s + + n_variates : int + The number of colorbars to be made + + n_major : int + Number of colorbars along the long axis of the colorbars + %(_make_multivar_axes_kw_doc)s Returns ------- @@ -2011,77 +2242,67 @@ def make_multivar_axes(parents, n_variates, n_major, location=None, orientation= The reduced keyword dictionary to be passed when creating the colorbar instance. """ + if n_major == -1: + n_major = n_variates + n_minor = 1 + else: + n_minor = n_variates // n_major + if n_major * n_minor < n_variates: + n_minor += 1 + + aspect = aspect / n_major + fraction = fraction * n_minor + loc_settings = _normalize_location_orientation(location, orientation) + # put appropriate values into the kwargs dict for passing back to # the Colorbar class orientation = loc_settings['orientation'] kwargs['orientation'] = orientation kwargs['ticklocation'] = loc_settings['location'] - fig, pbcb, colorbar_info = _make_axes_get_pbcb(loc_settings, fraction, shrink, - aspect, kwargs, parents) - major_pad = 0.1 - minor_pad = 0.6 # get the shape of the grid of new colorbars - if n_major == -1: - n_major = n_variates - n_minor = 1 - else: - n_minor = n_variates // n_major - if n_major * n_minor < n_variates: - n_minor += 1 + + if n_minor > 1: + location = loc_settings["location"] + if location == 'left': + loc_settings["anchor"] = (1, 0.5) + elif location == 'right': + loc_settings["anchor"] = (0, 0.5) + elif location == 'top': + loc_settings["anchor"] = (0.5, 0) + else: + loc_settings["anchor"] = (0.5, 1) + + fig, pbcb, colorbar_info = _make_axes_get_pbcb(loc_settings, fraction, shrink, + aspect, kwargs, parents) + colorbar_info["type"] = 'MultivarColorbar' + + # split pbcb into the required parts + sub_bboxes = MultivarColorbar._subdivide_bbox(pbcb, + n_major, + n_minor, + orientation, + major_pad=major_pad, + minor_pad=minor_pad, + )[:n_variates] + caxes = [fig.add_axes(sub_bbox, label="") + for sub_bbox in sub_bboxes] # adjust aspect - aspect = aspect/n_major if loc_settings["location"] in ('top', 'bottom'): aspect = 1.0 / aspect - # define how the bbox needs to be split - major_width = (1-major_pad) / n_major - if n_major > 1: - major_space = major_pad / (n_major - 1) - else: - major_space = 0 - major_split = np.empty(2 * (n_major - 1)) - major_split[0::2] = major_width - major_split[1::2] = major_space - major_split = np.cumsum(major_split) - - minor_width = (1-minor_pad) / n_minor - if n_minor > 1: - minor_space = minor_pad / (n_minor - 1) - else: - minor_space = 0 - minor_split = np.empty(2 * (n_minor - 1)) - minor_split[0::2] = minor_width - minor_split[1::2] = minor_space - minor_split = np.cumsum(minor_split) - - # make the colorbar axes - caxes = [] - v = orientation == "vertical" - pbcbs = pbcb.splitx(*minor_split) if v else pbcb.splity(*minor_split)[::-1] - for i, mpcbc in enumerate(pbcbs): - if i % 2 == 1: - continue - fbcbs = mpcbc.splity(*major_split)[::-1] if v else mpcbc.splitx(*major_split) - for j, bbox in enumerate(fbcbs): - if j % 2 == 1: - continue - if len(caxes) < n_variates: - cax = fig.add_axes(bbox, label="") - caxes.append(cax) - for cax in caxes: - for a in colorbar_info["parents"]: - a._colorbars.append(cax) # tell the parent it has a colorbar - cax.set_anchor(colorbar_info["anchor"]) - cax.set_box_aspect(aspect) cax.set_aspect('auto') - cax._colorbar_info = colorbar_info + colorbar_info["n_major"] = n_major + colorbar_info["n_minor"] = n_minor + colorbar_info["major_pad"] = major_pad + colorbar_info["minor_pad"] = minor_pad + colorbar_info["orientation"] = orientation return caxes, kwargs, colorbar_info @@ -2191,7 +2412,7 @@ def make_axes_gridspec(parent, *, location=None, orientation=None, cax = _make_axes_gridspec_helper(loc_settings, fraction, shrink, aspect, kwargs, parent) - + cax._colorbar_info["type"] = 'Colorbar' if loc_settings["location"] in ('top', 'bottom'): aspect = 1 / aspect cax.set_box_aspect(aspect) @@ -2236,6 +2457,7 @@ def make_bivar_axes_gridspec(parent, *, location=None, cax = _make_axes_gridspec_helper(loc_settings, fraction, shrink, aspect, kwargs, parent) + cax._colorbar_info["type"] = 'BivarColorbar' # need to add aspect to kwargs so it propagates to the BivarColorbar kwargs["aspect"] = aspect diff --git a/lib/matplotlib/colorbar.pyi b/lib/matplotlib/colorbar.pyi index 49169c2c24ee..8afb2e317fa9 100644 --- a/lib/matplotlib/colorbar.pyi +++ b/lib/matplotlib/colorbar.pyi @@ -119,6 +119,7 @@ class Colorbar: ColorbarBase = Colorbar class BivarColorbar: + n_rasterize: int mappable: mcolorizer.ColorizingArtist ax: Axes alpha: float | None @@ -130,8 +131,8 @@ class BivarColorbar: mappable: mcolorizer.ColorizingArtist | mcolorizer.Colorizer, *, alpha: float | None = ..., - ticklocations: tuple[Literal["auto", "left", "right"], Literal["auto", "top", "bottom"]] = ..., location: Literal["left", "right", "top", "bottom"] | None = ..., + ticklocations: tuple[Literal["auto", "left", "right"], Literal["auto", "top", "bottom"]] = ..., aspect: float = ..., ) -> None: ... @property @@ -149,6 +150,27 @@ class BivarColorbar: def remove(self) -> None: ... def drag_pan(self, button: Any, key: Any, x: float, y: float) -> None: ... +class MultivarColorbar(Sequence[Colorbar]): + mappable: mcolorizer.ColorizingArtist + colorizer: mcolorizer.Colorizer + axes: Sequence[Axes] + _colorbars: list[Colorbar] + def __init__( + self, + axes: Sequence[Axes], + mappable: mcolorizer.ColorizingArtist | mcolorizer.Colorizer | None = ..., + **kwargs: Any, + ) -> None: ... + + def update_normals(self, mappable: mcolorizer.ColorizingArtist | None = ...) -> None: ... + def remove(self) -> None: ... + @overload + def __getitem__(self, index: int, /) -> Colorbar: ... + @overload + def __getitem__(self, index: slice[int | None, int | None, int | None], /) -> Sequence[Colorbar]: ... + def __len__(self) -> int: ... + def get_tightbbox(self, renderer: RendererBase | None = ..., for_layout_only: bool = ...) -> Bbox: ... + def make_axes( parents: Axes | list[Axes] | np.ndarray, location: Literal["left", "right", "top", "bottom"] | None = ..., @@ -158,6 +180,27 @@ def make_axes( aspect: float = ..., **kwargs ) -> tuple[Axes, dict[str, Any]]: ... +def make_bivar_axes( + parents: Axes | list[Axes] | np.ndarray, + location: Literal["left", "right", "top", "bottom"] | None = ..., + fraction: float = ..., + shrink: float = ..., + aspect: float = ..., + **kwargs +) -> tuple[Axes, dict[str, Any]]: ... +def make_multivar_axes( + parents: Axes | list[Axes] | np.ndarray, + n_variates: int, + n_major: int, + location: Literal["left", "right", "top", "bottom"] | None = ..., + orientation: Literal["vertical", "horizontal"] | None = ..., + fraction: float = ..., + shrink: float = ..., + aspect: float = ..., + major_pad: float = ..., + minor_pad: float = ..., + **kwargs +) -> tuple[Axes, dict[str, Any], dict[str, Any]]: ... def make_axes_gridspec( parent: Axes, *, @@ -168,3 +211,12 @@ def make_axes_gridspec( aspect: float = ..., **kwargs ) -> tuple[Axes, dict[str, Any]]: ... +def make_bivar_axes_gridspec( + parent: Axes | list[Axes] | np.ndarray, + *, + location: Literal["left", "right", "top", "bottom"] | None = ..., + fraction: float = ..., + shrink: float = ..., + aspect: float = ..., + **kwargs +) -> tuple[Axes, dict[str, Any]]: ... diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 7d13a75db3c4..f308ffc1908d 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1357,7 +1357,7 @@ def colorbar( """ if isinstance(mappable.cmap, mcolors.BivarColormap): raise ValueError("`Figure.colorbar` can only be together with a " - "scalar colormap, please use `Figure.bivar_colorbar` " + "scalar colormap, please use `Figure.colorbar_bivar` " "when working with a bivariate colormap") if isinstance(mappable.cmap, mcolors.MultivarColormap): raise ValueError("colorbar can only be together with a" @@ -1409,7 +1409,7 @@ def colorbar( return cb @_docstring.interpd - def bivar_colorbar( + def colorbar_bivar( self, mappable, *, cax=None, ax=None, use_gridspec=True, **kwargs): """ Add a bivariate colorbar to a plot. @@ -1419,8 +1419,8 @@ def bivar_colorbar( mappable The `matplotlib.colorizer.ColorizingArtist` (i.e., `.AxesImage`, `.ContourSet`, etc.) described by this bivariate colorbar. - This argument is mandatory for the `.Figure.bivar_colorbar` method - but optional for the`.pyplot.bivar_colorbar` function, which sets + This argument is mandatory for the `.Figure.colorbar_bivar` method + but optional for the`.pyplot.colorbar_bivar` function, which sets the default to the current image. cax : `~matplotlib.axes.Axes`, optional @@ -1501,8 +1501,8 @@ def bivar_colorbar( return cb @_docstring.interpd - def multivar_colorbar( - self, mappable, *, caxes=None, ax=None, use_gridspec=True, + def colorbar_multivar( + self, mappable, *, caxes=None, ax=None, n_major=-1, **kwargs): if isinstance(mappable, mpl.colorizer.Colorizer): @@ -1530,17 +1530,8 @@ def multivar_colorbar( else [*ax] if np.iterable(ax) else [ax])[0].get_figure(root=False) current_ax = fig.gca() - if (fig.get_layout_engine() is not None and - not fig.get_layout_engine().colorbar_gridspec): - use_gridspec = False - if (use_gridspec - and isinstance(ax, mpl.axes._base._AxesBase) - and ax.get_subplotspec()): - caxes, kwargs, cbar_info = cbar.make_multivar_axes_gridspec(ax, - n_variates, n_major, **kwargs) - else: - caxes, kwargs, cbar_info = cbar.make_multivar_axes(ax, - n_variates, n_major, **kwargs) + caxes, kwargs, cbar_info = cbar.make_multivar_axes(ax, n_variates, + n_major, **kwargs) # make_axes calls add_{axes,subplot} which changes gca; undo that. fig.sca(current_ax) for cax in caxes: diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi index b4aa4e44649e..148c2420b2b8 100644 --- a/lib/matplotlib/figure.pyi +++ b/lib/matplotlib/figure.pyi @@ -14,7 +14,7 @@ from matplotlib.backend_bases import ( RendererBase, ) from matplotlib.colors import Colormap, Normalize -from matplotlib.colorbar import Colorbar, BivarColorbar +from matplotlib.colorbar import Colorbar, BivarColorbar, MultivarColorbar from matplotlib.colorizer import ColorizingArtist, Colorizer from matplotlib.cm import ScalarMappable from matplotlib.gridspec import GridSpec, SubplotSpec, SubplotParams as SubplotParams @@ -185,14 +185,24 @@ class FigureBase(Artist): use_gridspec: bool = ..., **kwargs ) -> Colorbar: ... - def bivar_colorbar( + def colorbar_bivar( self, mappable: ColorizingArtist, + *, cax: Axes | None = ..., ax: Axes | Iterable[Axes] | None = ..., use_gridspec: bool = ..., **kwargs ) -> BivarColorbar: ... + def colorbar_multivar( + self, + mappable: ColorizingArtist, + *, + caxes: Iterable[Axes] | None = ..., + ax: Axes | Iterable[Axes] | None = ..., + n_major: int = ..., + **kwargs + ) -> MultivarColorbar: ... def subplots_adjust( self, left: float | None = ..., diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 5aff242c9af4..2ac20f6d55e2 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -126,7 +126,11 @@ EventCollection, QuadMesh, ) - from matplotlib.colorbar import Colorbar, BivarColorbar + from matplotlib.colorbar import ( + Colorbar, + BivarColorbar, + MultivarColorbar, + ) from matplotlib.container import ( BarContainer, ErrorbarContainer, @@ -2684,8 +2688,8 @@ def colorbar( return ret -@_copy_docstring_and_deprecators(Figure.bivar_colorbar) -def bivar_colorbar( +@_copy_docstring_and_deprecators(Figure.colorbar_bivar) +def colorbar_bivar( mappable: ColorizingArtist | None = None, cax: matplotlib.axes.Axes | None = None, ax: matplotlib.axes.Axes | Iterable[matplotlib.axes.Axes] | None = None, @@ -2698,7 +2702,25 @@ def bivar_colorbar( 'creation. First define a mappable such as ' 'an image (with imshow) or a contour set (' 'with contourf).') - ret = gcf().bivar_colorbar(mappable, cax=cax, ax=ax, **kwargs) + ret = gcf().colorbar_bivar(mappable, cax=cax, ax=ax, **kwargs) + return ret + + +@_copy_docstring_and_deprecators(Figure.colorbar_multivar) +def colorbar_multivar( + mappable: ColorizingArtist | None = None, + caxes: Iterable[matplotlib.axes.Axes] | None = None, + ax: matplotlib.axes.Axes | Iterable[matplotlib.axes.Axes] | None = None, + **kwargs +) -> MultivarColorbar: + if mappable is None: + mappable = gci() + if mappable is None: + raise RuntimeError('No mappable was found to use for colorbar ' + 'creation. First define a mappable such as ' + 'an image (with imshow) or a contour set (' + 'with contourf).') + ret = gcf().colorbar_multivar(mappable, caxes=caxes, ax=ax, **kwargs) return ret diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index ea2e332b92e4..87d02e13787f 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -1260,7 +1260,7 @@ def test_colorbar_format_string_and_old(): @image_comparison(['bivar_cbar_locationing.png', ], remove_text=True, savefig_kwarg={'dpi': 40}, tol=0.05) -def test_bivar_colorbar_location(use_gridspec): +def test_colorbar_bivar_location(use_gridspec): data = (np.arange(12).reshape((3, 4)) % 4, np.arange(12).reshape((3, 4)) % 3) # ------------------- @@ -1268,14 +1268,14 @@ def test_bivar_colorbar_location(use_gridspec): fig, axes = plt.subplots(2, 2) for i, ax in enumerate(axes.ravel()): mim = ax.imshow(data, cmap='BiOrangeBlue') - fig.bivar_colorbar(mim, location=locations[i], use_gridspec=use_gridspec) + fig.colorbar_bivar(mim, location=locations[i], use_gridspec=use_gridspec) @pytest.mark.parametrize('use_gridspec', [True, False]) @image_comparison(['bivar_cbar_sharing.png', ], remove_text=True, savefig_kwarg={'dpi': 40}, tol=0.05) -def test_bivar_colorbar_sharing(use_gridspec): +def test_colorbar_bivar_sharing(use_gridspec): data = (np.arange(12).reshape((3, 4)) % 4, np.arange(12).reshape((3, 4)) % 3) # ------------------- @@ -1287,11 +1287,11 @@ def test_bivar_colorbar_sharing(use_gridspec): ax3 = plt.subplot(224) plt.imshow(data, cmap='BiOrangeBlue') - plt.bivar_colorbar(ax=[ax2, ax3, ax1], location='right', pad=0.0, shrink=0.5, + plt.colorbar_bivar(ax=[ax2, ax3, ax1], location='right', pad=0.0, shrink=0.5, panchor=False, use_gridspec=use_gridspec) - plt.bivar_colorbar(ax=[ax2, ax3, ax1], location='left', shrink=0.5, + plt.colorbar_bivar(ax=[ax2, ax3, ax1], location='left', shrink=0.5, panchor=False, use_gridspec=use_gridspec) - plt.bivar_colorbar(ax=[ax1], location='bottom', panchor=False, + plt.colorbar_bivar(ax=[ax1], location='bottom', panchor=False, anchor=(0.8, 0.5), shrink=0.6, use_gridspec=use_gridspec) @@ -1302,14 +1302,14 @@ def test_bivar_cbar_single_ax_panchor_east(constrained): ax.set_anchor('N') assert ax.get_anchor() == 'N' mp = ax.imshow([[[0, 1], [1, 2]], [[2, 1], [0, 1]]], cmap='BiOrangeBlue') - fig.bivar_colorbar(mp, panchor='E') + fig.colorbar_bivar(mp, panchor='E') assert ax.get_anchor() == 'E' def test_bivar_cbar_set_xylabel(): fig, axes = plt.subplots(1, 2) mp = axes[0].imshow([[[0, 1], [1, 2]], [[2, 1], [0, 1]]], cmap='BiOrangeBlue') - cb = fig.bivar_colorbar(mp, cax=axes[1]) + cb = fig.colorbar_bivar(mp, cax=axes[1]) cb.set_ylabel('y') cb.set_xlabel('x') assert axes[1].get_ylabel() == 'y' @@ -1326,7 +1326,7 @@ def test_bivar_cbar_log_no_scale(): ) assert axes[1].get_yscale() == 'linear' assert axes[1].get_xscale() == 'linear' - fig.bivar_colorbar(mp, cax=axes[1]) + fig.colorbar_bivar(mp, cax=axes[1]) assert axes[1].get_yscale() == 'log' assert axes[1].get_xscale() == 'function' @@ -1336,7 +1336,7 @@ def test_bivar_cbar_change_vmin_vmax(): mp = axes[0].imshow([[[100, 1], [10, 1]], [[0.5, 0], [0.3, 1]]], cmap='BiOrangeBlue', ) - fig.bivar_colorbar(mp, cax=axes[1]) + fig.colorbar_bivar(mp, cax=axes[1]) assert np.all(axes[1].get_ylim() == np.array([1, 100])) assert np.all(axes[1].get_xlim() == np.array([0, 1])) mp.colorizer.norm.vmin = [-1, -2] @@ -1350,7 +1350,7 @@ def test_bivar_cbar_change_norms(): mp = axes[0].imshow([[[100, 1], [10, 1]], [[0.5, 0.2], [0.3, 1]]], cmap='BiOrangeBlue', ) - fig.bivar_colorbar(mp, cax=axes[1]) + fig.colorbar_bivar(mp, cax=axes[1]) assert axes[1].get_yscale() == 'linear' assert axes[1].get_xscale() == 'linear' mp.colorizer.norm = ['log', mcolors.NoNorm()] @@ -1364,7 +1364,7 @@ def test_bivar_cbar_anchor(): mp = ax.imshow([[[0, 1], [1, 2]], [[2, 1], [0, 1]]], cmap='BiOrangeBlue') anchor = (1, 0.3) shrink = 0.3 - cbar = fig.bivar_colorbar(mp, anchor=anchor, shrink=shrink) + cbar = fig.colorbar_bivar(mp, anchor=anchor, shrink=shrink) x0, y0, x1, y1 = ax.get_position().extents cx0, cy0, cx1, cy1 = cbar.ax.get_position().extents @@ -1378,7 +1378,7 @@ def test_bivar_cbar_anchor(): mp = ax.imshow([[[0, 1], [1, 2]], [[2, 1], [0, 1]]], cmap='BiOrangeBlue') anchor = (1, 0.7) shrink = 0.3 - cbar = fig.bivar_colorbar(mp, anchor=anchor, shrink=shrink, location='left') + cbar = fig.colorbar_bivar(mp, anchor=anchor, shrink=shrink, location='left') x0, y0, x1, y1 = ax.get_position().extents cx0, cy0, cx1, cy1 = cbar.ax.get_position().extents @@ -1392,7 +1392,7 @@ def test_bivar_cbar_anchor(): mp = ax.imshow([[[0, 1], [1, 2]], [[2, 1], [0, 1]]], cmap='BiOrangeBlue') anchor = (0.3, 1) shrink = 0.3 - cbar = fig.bivar_colorbar(mp, anchor=anchor, shrink=shrink, location='top') + cbar = fig.colorbar_bivar(mp, anchor=anchor, shrink=shrink, location='top') x0, y0, x1, y1 = ax.get_position().extents cx0, cy0, cx1, cy1 = cbar.ax.get_position().extents @@ -1406,7 +1406,7 @@ def test_bivar_cbar_anchor(): mp = ax.imshow([[[0, 1], [1, 2]], [[2, 1], [0, 1]]], cmap='BiOrangeBlue') anchor = (0.3, 1) shrink = 0.3 - cbar = fig.bivar_colorbar(mp, anchor=anchor, shrink=shrink, location='bottom') + cbar = fig.colorbar_bivar(mp, anchor=anchor, shrink=shrink, location='bottom') x0, y0, x1, y1 = ax.get_position().extents cx0, cy0, cx1, cy1 = cbar.ax.get_position().extents @@ -1421,7 +1421,7 @@ def test_bivar_cbar_aspect(): mp = ax.imshow([[[100, 1], [10, 1]], [[0.5, 0.2], [0.3, 1]]], cmap='BiOrangeBlue', ) - cbar = fig.bivar_colorbar(mp, aspect=0.3) + cbar = fig.colorbar_bivar(mp, aspect=0.3) assert cbar.ax.get_box_aspect() == 0.3 assert cbar.ax._colorbar_info["aspect"] == 0.3 assert cbar.aspect == 0.3 @@ -1437,7 +1437,7 @@ def test_bivar_cbar_alpha(): cmap='BiOrangeBlue', alpha=0.5, ) - cbar = fig.bivar_colorbar(mp) + cbar = fig.colorbar_bivar(mp) assert cbar._image._alpha == 0.5 @@ -1450,7 +1450,7 @@ def test_bivar_cbar_wrong_figure(): cmap='BiOrangeBlue', ) with pytest.warns(UserWarning, match="different Figure"): - fig1.bivar_colorbar(mp) + fig1.colorbar_bivar(mp) @pytest.mark.parametrize('use_gridspec', [True, False]) @@ -1467,7 +1467,7 @@ def test_bivar_cbar_remove_from_figure(nested_gridspecs, use_gridspec): cmap='BiOrangeBlue', ) pre_position = ax.get_position() - cb = fig.bivar_colorbar(mp, use_gridspec=use_gridspec) + cb = fig.colorbar_bivar(mp, use_gridspec=use_gridspec) fig.subplots_adjust() cb.remove() fig.subplots_adjust() @@ -1488,8 +1488,8 @@ def test_bivar_cbar_ticklocations(): ca = mpl.colorizer.Colorizer('BiOrangeBlue', norm) fig, ax = plt.subplots() with pytest.raises(ValueError, match='ticklocations must be a tuple of'): - cbar = fig.bivar_colorbar(ca, cax=ax, ticklocations=['left']) - cbar = fig.bivar_colorbar(ca, cax=ax, ticklocations=['left', 'top']) + cbar = fig.colorbar_bivar(ca, cax=ax, ticklocations=['left']) + cbar = fig.colorbar_bivar(ca, cax=ax, ticklocations=['left', 'top']) cbar.ax.yaxis.get_label_position() == 'left' cbar.ax.xaxis.get_label_position() == 'top' @@ -1505,10 +1505,10 @@ def test_wrong_kind_colorbar(): fig, ax = plt.subplots(1, 1) mp = ax.imshow([[100, 1], [10, 1]]) with pytest.raises(ValueError, match='bivariate colorbar can only be used '): - fig.bivar_colorbar(mp, ax=ax) + fig.colorbar_bivar(mp, ax=ax) -def test_bivar_colorbar_set_get_view(): +def test_colorbar_bivar_set_get_view(): # maybe not the best way to test this, this is normally used # interacitively ._get_view() and ._set_view() are normally # used in interactive mode @@ -1516,7 +1516,7 @@ def test_bivar_colorbar_set_get_view(): mp = axes.imshow([[[100, 1], [10, 1]], [[0.5, 0.2], [0.3, 1]]], cmap='BiOrangeBlue', ) - cb = fig.bivar_colorbar(mp, ax=axes) + cb = fig.colorbar_bivar(mp, ax=axes) # limits are x: (0.2, 1), y: (1, 100) view = cb._get_view() view = np.array(view) + 1 @@ -1528,20 +1528,20 @@ def test_bivar_colorbar_set_get_view(): assert mp.colorizer.norm.norms[0].vmax == 101 -def test_bivar_colorbar_no_ax(): +def test_colorbar_bivar_no_ax(): norm = mpl.colors.MultiNorm(['linear', 'linear']) ca = mpl.colorizer.Colorizer('BiOrangeBlue', norm) fig, ax = plt.subplots() with pytest.raises(ValueError, match='Unable to determine Axes'): - fig.bivar_colorbar(ca) + fig.colorbar_bivar(ca) -def test_bivar_colorbar_custom_norm(): +def test_colorbar_bivar_custom_norm(): """ This tests setting the correct scale for norms that do not have a ._scale property. - In the normal case, bivar_colorbar uses the ._scale property + In the normal case, colorbar_bivar uses the ._scale property of each norm to set the transform on each axis. """ class CustomHalfNorm(mcolors.Normalize): @@ -1552,14 +1552,26 @@ def __init__(self): def vmin(self): return 0 + @vmin.setter + def vmin(self, val): + ... + @property def vmax(self): return 1 + @vmax.setter + def vmax(self, val): + ... + @property def clip(self): return False + @clip.setter + def clip(self, val): + ... + def __call__(self, value, clip=None): return value / 2 @@ -1582,14 +1594,14 @@ def n_components(self): norm = mpl.colors.MultiNorm([CustomHalfNorm(), CustomHalfNorm()]) ca = mpl.colorizer.Colorizer('BiOrangeBlue', norm) fig, ax = plt.subplots() - fig.bivar_colorbar(ca, cax=ax) + fig.colorbar_bivar(ca, cax=ax) assert ax.get_yscale() == 'function' assert ax.get_xscale() == 'function' -def test_bivar_colorbar_not_via_fig(): +def test_colorbar_bivar_not_via_fig(): # This test makes a colorbar without calling - # fig.bivar_colorbar() and without a ColorizingArtist + # fig.colorbar_bivar() and without a ColorizingArtist norm = mpl.colors.MultiNorm(['linear', 'linear']) ca = mpl.colorizer.Colorizer('BiOrangeBlue', norm) fig, ax = plt.subplots() @@ -1613,7 +1625,7 @@ def test_bivar_cbar_not_rasterized(): cmap=cm, interpolation='nearest' ) - fig.bivar_colorbar(mp) + fig.colorbar_bivar(mp) def test_cbar_wrong_figures(): From 65db0556788497e82595899a24d9c6ce1bf0ab54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Thu, 23 Apr 2026 00:00:31 +0200 Subject: [PATCH 4/4] tests for colorbar_multivar multivar_colorbar: updates after rebase multivar_colorbar: fix for docs --- doc/api/figure_api.rst | 3 +- lib/matplotlib/_constrained_layout.py | 2 +- lib/matplotlib/colorbar.py | 107 +++++++------- lib/matplotlib/colorbar.pyi | 1 - lib/matplotlib/figure.py | 37 ++++- .../test_colorbar/bivar_cbar_locationing.png | Bin 19383 -> 2884 bytes .../bivar_cbar_locationing_constrained.png | Bin 0 -> 2847 bytes .../bivar_cbar_locationing_gridspec.png | Bin 0 -> 19383 bytes .../bivar_cbar_not_rasterized.png | Bin 1615 -> 1374 bytes .../test_colorbar/bivar_cbar_sharing.png | Bin 10809 -> 2870 bytes .../multivar_cbar_locationing.png | Bin 0 -> 3668 bytes .../multivar_cbar_locationing_constrained.png | Bin 0 -> 3532 bytes .../test_colorbar/multivar_cbar_n_major.png | Bin 0 -> 11111 bytes .../test_colorbar/multivar_cbar_sharing.png | Bin 0 -> 3818 bytes lib/matplotlib/tests/test_colorbar.py | 136 +++++++++++++++++- lib/mpl_toolkits/mplot3d/axes3d.py | 4 +- 16 files changed, 226 insertions(+), 64 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_locationing_constrained.png create mode 100644 lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_locationing_gridspec.png create mode 100644 lib/matplotlib/tests/baseline_images/test_colorbar/multivar_cbar_locationing.png create mode 100644 lib/matplotlib/tests/baseline_images/test_colorbar/multivar_cbar_locationing_constrained.png create mode 100644 lib/matplotlib/tests/baseline_images/test_colorbar/multivar_cbar_n_major.png create mode 100644 lib/matplotlib/tests/baseline_images/test_colorbar/multivar_cbar_sharing.png diff --git a/doc/api/figure_api.rst b/doc/api/figure_api.rst index e08d23b8475b..9d00bad65d93 100644 --- a/doc/api/figure_api.rst +++ b/doc/api/figure_api.rst @@ -256,7 +256,8 @@ Annotating :nosignatures: SubFigure.colorbar - SubFigure.bivar_colorbar + SubFigure.colorbar_bivar + SubFigure.colorbar_multivar SubFigure.legend SubFigure.text SubFigure.suptitle diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index 9eef1d3937b6..507204f3acc9 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -750,7 +750,7 @@ def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None, compress=Fa # Keep the pb x-coordinates but use actual y-coordinates pb = Bbox.from_extents(pb.x0, actual_pos_fig.y0, pb.x1, actual_pos_fig.y1) - elif location in ('top', 'bottom'): + else: # location in ('top', 'bottom'): # For horizontal colorbars, use the actual parent bbox width # for colorbar sizing # Keep the pb y-coordinates but use actual x-coordinates diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 4463c38479fe..fb72637897ae 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -30,7 +30,6 @@ _log = logging.getLogger(__name__) - _docstring.interpd.register( _make_axes_kw_doc=""" location : None or {'left', 'right', 'top', 'bottom'} @@ -1437,7 +1436,7 @@ class BivarColorbar: r""" Draw a bivariate colorbar in an existing Axes. - Typically, bivariate colorbars are created using `.Figure.bivar_colorbar` + Typically, bivariate colorbars are created using `.Figure.colorbar_bivar` and associated with `.ColorizingArtist`\s (such as an `.AxesImage` generated via `~.axes.Axes.imshow`). @@ -1464,9 +1463,6 @@ def __init__( mappable : `.ColorizingArtist` The mappable whose colormap and norm will be used. - To show the colors versus index instead of on a 0-1 scale, set the - mappable's norm to ``colors.NoNorm()``. - alpha : float The colorbars transparency between 0 (transparent) and 1 (opaque). @@ -1525,12 +1521,6 @@ def __init__( setattr(self.ax, x, getattr(self, x)) self.ax.cla = self._cbar_cla - """self._colorbar_xax_cid = ax.callbacks.connect( - 'xlim_changed', self._x_limits_changed) - self._colorbar_yax_cid = ax.callbacks.connect( - 'ylim_changed', self._y_limits_changed) - """ - @property def aspect(self): return self._aspect @@ -1579,10 +1569,11 @@ def _draw_all(self): rasterized=rasterized, edgecolors='none', shading='flat') # Apply norm scaling (supports LogNorm etc.) - try: + if getattr(self.colorizer.norm.norms[0], '_scale', None): + # use the norm's scale (if it exists and is not None): self.ax.set_yscale(self.colorizer.norm.norms[0]._scale) - except TypeError: - # fallback for custom norms + else: + # fallback for custom norms, or NoNorm() self.ax.set_yscale( 'function', functions=( @@ -1590,10 +1581,12 @@ def _draw_all(self): self.colorizer.norm.norms[0].inverse ) ) - try: + + if getattr(self.colorizer.norm.norms[1], '_scale', None): + # use the norm's scale (if it exists and is not None): self.ax.set_xscale(self.colorizer.norm.norms[1]._scale) - except TypeError: - # fallback for custom norms + else: + # fallback for custom norms, or NoNorm() self.ax.set_xscale( 'function', functions=( @@ -1625,39 +1618,9 @@ def xaxis(self): def yaxis(self): return self.ax.yaxis - """ - def _y_limits_changed(self, ax=None): - if not self._allow_change_limits: - ylim = self.ax.get_ylim() - if self.colorizer.norm.norms[0].vmin != ylim[0]: - self.colorizer.norm.norms[0].vmin = ylim[0] - if self.colorizer.norm.norms[0].vmax != ylim[1]: - self.colorizer.norm.norms[0].vmax = ylim[1] - - def _x_limits_changed(self, ax=None): - if not self._allow_change_limits: - xlim = self.ax.get_xlim() - if self.colorizer.norm.norms[1].vmin != xlim[0]: - self.colorizer.norm.norms[1].vmin = xlim[0] - if self.colorizer.norm.norms[1].vmax != xlim[1]: - self.colorizer.norm.norms[1].vmax = xlim[1] - """ - """ - def _update_limits(self, ax=None): - if not self._allow_change_limits: - ylim = self.ax.get_ylim() - xlim = self.ax.get_xlim() - if (self.colorizer.norm.norms[0].vmin != ylim[0] or - self.colorizer.norm.norms[0].vmax != ylim[1] or - self.colorizer.norm.norms[1].vmin != xlim[0] or - self.colorizer.norm.norms[1].vmax != xlim[1]): - raise ValueError("Changing the limits of this axis must be done " - "using the norm attached to the bivariate colorbar.") - """ - def update_normals(self, mappable=None): self.set_alpha(self.mappable.get_alpha()) - #self._draw_all() + self._draw_all() def set_alpha(self, alpha): """ @@ -1667,7 +1630,6 @@ def set_alpha(self, alpha): transparency values associated with the colormap. """ self.alpha = None if isinstance(alpha, np.ndarray) else alpha - self._draw_all() def remove(self): """ @@ -1685,8 +1647,6 @@ def remove(self): self.mappable.callbacks.disconnect(self.mappable.colorbar_cid) self.mappable.colorbar = None self.mappable.colorbar_cid = None - # self.ax.callbacks.disconnect(self._colorbar_xax_cid) - # self.ax.callbacks.disconnect(self._colorbar_yax_cid) ax = self.mappable.axes if ax is None: @@ -1738,7 +1698,48 @@ def _cbar_cla(self): class MultivarColorbar(Sequence): + r""" + Draw a multivariate colorbar in existing Axes. + + Typically, multivariate colorbars are created using `.Figure.colorbar_multivar` + and associated with `.ColorizingArtist`\s (such as an + `.AxesImage` generated via `~.axes.Axes.imshow`). + + MultivarColorbar is iterable, and the constituent Colorbar objects can be accessed + by index. + """ + def __init__(self, axes, mappable=None, **kwargs): + """ + Parameters + ---------- + axes : list of `~matplotlib.axes.Axes` + The `~.axes.Axes` instances in which the colorbars are drawn. + + mappable : `.ColorizingArtist` + The mappable whose colormap and norm will be used. + + alpha : float + The colorbars transparency between 0 (transparent) and 1 (opaque). + + location : None or {'left', 'right', 'top', 'bottom'} + Set the multivariate colorbars's location + + Other Parameters + ---------------- + orientation : None or {'vertical', 'horizontal'} + If None, use the value determined by *location*. If both + *orientation* and *location* are None then defaults to 'vertical'. + + ticklocation : {'auto', 'left', 'right', 'top', 'bottom'} + The location of the colorbar ticks. The *ticklocation* must match + *orientation*. For example, a horizontal colorbar can only have ticks + at the top or the bottom. If 'auto', the ticks will be the same as + *location*, so a colorbar to the left will have ticks to the left. If + *location* is None, the ticks will be at the bottom for a horizontal + colorbar and at the right for a vertical. + """ + if isinstance(mappable, mpl.colorizer.Colorizer): mappable = mcolorizer.ColorizingArtist(mappable) @@ -2184,7 +2185,7 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, def make_bivar_axes(parents, location=None, fraction=0.15, shrink=1.0, aspect=1.0, **kwargs): """ - Create an `~.axes.Axes` suitable for a bivar_colorbar. + Create an `~.axes.Axes` suitable for a bivariate colorbar. The Axes is placed in the figure of the *parents* Axes, by resizing and repositioning *parents*. @@ -2246,6 +2247,8 @@ def make_multivar_axes(parents, n_variates, n_major, location=None, orientation= n_major = n_variates n_minor = 1 else: + if n_major == 0: + raise ValueError("n_major cannot be zero") n_minor = n_variates // n_major if n_major * n_minor < n_variates: n_minor += 1 diff --git a/lib/matplotlib/colorbar.pyi b/lib/matplotlib/colorbar.pyi index 8afb2e317fa9..b64d326ef5b9 100644 --- a/lib/matplotlib/colorbar.pyi +++ b/lib/matplotlib/colorbar.pyi @@ -115,7 +115,6 @@ class Colorbar: def remove(self) -> None: ... def drag_pan(self, button: Any, key: Any, x: float, y: float) -> None: ... - ColorbarBase = Colorbar class BivarColorbar: diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index f308ffc1908d..d3aef0c545ff 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1417,8 +1417,8 @@ def colorbar_bivar( Parameters ---------- mappable - The `matplotlib.colorizer.ColorizingArtist` (i.e., `.AxesImage`, - `.ContourSet`, etc.) described by this bivariate colorbar. + The `matplotlib.colorizer.ColorizingArtist` (i.e., `.AxesImage` + etc.) described by this bivariate colorbar. This argument is mandatory for the `.Figure.colorbar_bivar` method but optional for the`.pyplot.colorbar_bivar` function, which sets the default to the current image. @@ -1504,6 +1504,39 @@ def colorbar_bivar( def colorbar_multivar( self, mappable, *, caxes=None, ax=None, n_major=-1, **kwargs): + """ + Add a bivariate colorbar to a plot. + + Parameters + ---------- + mappable + The `matplotlib.colorizer.ColorizingArtist` (i.e., `.AxesImage` + etc.) described by this multivariate colorbar. + This argument is mandatory for the `.Figure.colorbar_multivar` method + but optional for the`.pyplot.colorbar_multivar` function, which sets + the default to the current image. + + caxes : `~matplotlib.axes.Axes`, optional + Axes into which the colorbar will be drawn. If `None`, then new + Axes are created and the space for it will be stolen from the Axes(s) + specified in *ax*. + + ax : `~matplotlib.axes.Axes` or iterable or `numpy.ndarray` of Axes, optional + The one or more parent Axes from which space for a new colorbar Axes + will be stolen. This parameter is only used if *cax* is not set. + + Defaults to the Axes that contains the mappable used to create the + colorbar. + + Returns + ------- + multivariate_colorbar : `~matplotlib.colorbar.MultivarColorbar` + + Other Parameters + ---------------- + %(_make_multivar_axes_kw_doc)s + + """ if isinstance(mappable, mpl.colorizer.Colorizer): mappable = mcolorizer.ColorizingArtist(mappable) diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_locationing.png b/lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_locationing.png index 7cf5b9be70f5376f027c7cee18c33f0974c07589..a81d0fb1d4ee7bd4ea3fd703ffce1abd9cfa0d13 100644 GIT binary patch literal 2884 zcmcImc{Cg77XN~pOe|qqQ`Hhr)LQ#)XlQE*4Qg*oN>L3(5ld(^Oqq_IP_%s?m73|@80vfzu*1Y#>z|pCI$ll z2%yc+*a86ITtPdOm$R>|wp`&HPIwb%yj{Q*e3*NX7r?mVuU`wmU&CFN4D|{M#s&Cm zD66R{t15eXg{o_4t0{WxY3f|j($UrMRQ6O?Ro7P4RM$8mc@>Yp9;~mT;`gV!azK!` z%E*Dv8cv9BubZC_27p)g%YtC+Cwl>ak{nKd4|7PgyjRkGapw@0pEzLKSK>zhImsaOBRz z{ziV2?&=agoLr7fxA5E4V8UmV(Y+G`$0XobLj#SxU|r!jcR3*AYTm>RMot3|Ck#mL zJitv90z^&!FVD$xTo~A{uC6YMXsNKEriVJI%R{*c1tmW8oKUCBir0V*t;9v-N(*-4 z!-pF3Nyd)b@r$cBSlBn(FSL&{TFroGbi(D3b1qg>=FWK)uu-qOgV>feN`Bp3%Mv9& z7p03c?SIK<<<8x1Qr#$Ld-%XIMK{>-t|(lYi|eoH-R|Bb{!&p<%n;CZdN6W5%j}dr*f+hkXjQY< z7Ma=%Powl|zIiig;h$WYu`6onpR#e6A_h8@r!#x$6KSGA<%^ykINA2DV*7nKo7&x7{VT4o2YPSgRhQ_ui5mZi zrVRej&oC6jg<(^pK&`$hVC*D9dk7>FWeRfrPV)6C5x_u_L`xt)3JUSX)c2~1W9P(} zYP(s9F5I`@vBV%}aFeq9pF|;&W)UyKDZQ~BfiJ?AAytvmEwd|JeMgYd#z@j=g6y2& zv=aXHUBOD%d4?8)=?d&1!|BTW`l#hDUqH zc?QdGS*$(nW9^1g`kGnDE_=cICms+a?U+nb15IM9lcRuD31QcDCdTQ0$RZ^ zQwC1$Gc2t|V*K-GPrK$kf0U09;o-xDh~HL%a;@@+Yhfcy^r4q%>wTZm)PYYkByVep zGRH`hk;TLHm=K;}ANt27??PW3Zpno`?xUoY`{UqI(nqaa3~}dW;gG{TeF{mxFs}zU z-*bGm#*CKA&dnX2nVGTC!fsuR{%H^1Gt2CW#~XGSHTBnaCTSW;x(4^t2plSfcdr|zjM?Bad`aj6 z0ZThvK$Ck11#8{n4(K&gq=}!bB8R);*0sNy^53B}@ApL13)^#nb=);^BGH3YbUKyA zVnt7n`)Cae4W%@r2qNC{)%W{%)ZKZE5UosKBGDB!w?{76^|i{$AuBwxB{(V!q3s^o zZpYiV-Y+aPbZT(1YI>IKP&q+wZq9p{bP++L)Kp3{c9vU;HeIJP@N5Z4su|T5ZFg8@ z(JA08dNhlY;6z!{p)Qr-*FRPZpH$sm{yrJ1>n zX0==MsJ0A=)yvAYEJF-!hrjl4z*CQF={eI zzY3AqV5j$n9mV#bry73oEv`kqI`rzcFbIkVi=CT9PNn?B4JK|0QePAtYL;1zwcTD? z&+Pb+tjThI-QjVdBVcqbNJeD!(Z~2Wpxe2-+q||v&*Tp9{5D39P@q$cg{%ny;`{ys zeCh~rM>f4Hjnyaf^zpCXkP?2DPNR@d<<*hh($Uf@WK61GHXLMC?xJNf@D}f+!j>XgB>IuQXj=eZwbE&-$c{J5>U(ofti|J4!y63XA^{n{zP zeMt208DSRsZ3gL{1qA8&je%)@n)$@RnTbHz4mInDJo4B8rSM&YS)%a&&{#K$GhA|- z^MgZh)eGD8!qA3sEc{h2o*g~QF`kftBsvybLqOv9|0dAct;5x5$g6;A<}}sX9BXBkJC-lK zWT5)l11i)HH2YR5jH}+>{zCcw90n}~GS&eYd|;_$u97ANY~(ntQnDw}c2}1;kf@_QLv{`a!B6S;x z))wU9lS3H1G{v_*#QMO;WR}+3p3-rjr45z+`&24bqpo~BbnKcse`=5r2%I33XOx)s zW1;c14bi~nH@VPG)nm;^NK%*eRli33e}@JCoB}`Hq|_drCCBM}nOOna#Oh4NDYuwE E0OC;eDgXcg literal 19383 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 Ql*s~FnA)0@U3828Cl=XJ-~a#s diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_locationing_constrained.png b/lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_locationing_constrained.png new file mode 100644 index 0000000000000000000000000000000000000000..d412a50f4fcc23f8b183b2be4fa5ac39c11b5613 GIT binary patch literal 2847 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 OM*+^r+_3VrOZ?B5Iqh@+ literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_locationing_gridspec.png b/lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_locationing_gridspec.png new file mode 100644 index 0000000000000000000000000000000000000000..5abdeceae8a9e8a18c12b2af827e4c76ab5b347a GIT binary patch literal 19383 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 B_wWD! literal 1615 zcmeAS@N?(olHy`uVBq!ia0y~yU~~YoKX9-C$wJ+|*+441#5JNMI6tkVJh3R1!8fs_ zASb^hCo@T*EVZaOGe6H*&(KiMKrbb=%+SKXLOVUp&@#!yC^^+cFU82f$jrdlz+6Wm zqokyu*h*hN7p`3|zbIY*qVBQjKr1*4JR*x37}(T7n9(=n#&HG))_I;Tjv*Cu-rn7x z6`d|~{Gqw3Gl!&$Nl6E9xiju-*~U zmNvnX?^s1J`)SP?EHx?L_gMb2OY&!;~P9s`cyRw#X%G zf2|1h(oFrAa;sv}+bfT!GHeodlRWUV#_rbQ^BQB;UOR&cPt(81Jovf$^!4;;ox%e7|zd;Uj0X5v&Z*z@uk(23iUG5p4| zqV)7}zjNl>v*rEl|NRS|zt-$^?Aq~r$S4Fj@VlFA8#yJ*(lDH$jJki z&0p8OW^9o2%X-aO)};*0>&gxilaGJCCj3Uk9VJZ2E()+&N`Be0m?vTF)~_6MdVZuA z1Rb~k?|QweYp%g52B$TXqrAR{zt$`}w)bn*-aOkQX7lXp${IGdTU^v0LqBE%MZYUfPB|L0_EC%qs+ZxhkUv9fyFn2r>mdKI;Vst05}~~4FCWD diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_sharing.png b/lib/matplotlib/tests/baseline_images/test_colorbar/bivar_cbar_sharing.png index 8fe857c2bad9e4ba3f7e8083d0e5fa6c52732151..243a5a944351eb507e8912ee31590f6a3f24ba98 100644 GIT binary patch literal 2870 zcmb`Jdpw(G8^`ZR99ly(4O8@{OGeLurg6z(pKAmy%-Gb_*u>b&I}B}hz!d3w7<15P|G`6MUdCQ%6Z8QS4BE^Hafw19Uo}Ue z0{{8CaZrdaiYwpvgUrPyGM01|00o1M3t~^~Zw6rVb)4-n;>?~xTl7}!(Wlj0{ z`WAFvwOA!rriSjGX?Y?FDT{hX;5NH&0gw3RmaBaWLmiy?BWl~X4XjK|T|bjHS+$s} z_9rn~7lqZ9N+gn^aHx|F0P(@PfN+$4?(L@*J~|rUf))g5eJ<)=XKp4?v~;=wo_EPW$O(}WG;LHdvy?>oE* z+I39+k3L9X!^bd+oWLuPk6+f*eudP~N&Ioh`01SX$v@8jEktY+nYX%O2Je6qGSw8N zH3O0ciX-HA;s1SH6Wphcrnzn38kwATMH*UfwP!_zGZ!Y^!C6~d8yFfgDwmMXD90im zMjDt5nr9|kbT~bEokXKjb30d7!tU2Pw?3tf_VxDObuH~BX4U%>!OL#{iVP`4zY>me zXAai2Jd_=TX&CI!4n&nIFf-(j=RJ<7ck~@l0zdESSXv&Q(wkPRe|;-G$gY@A@bp}c z!VI773tt!GQk57PvPl`TI>p67C9`4n^=|HWdwU-Y^0@=LesgTf8oRtVwRe8jas5@v z$BE=pTej1EXB7zJerPw7PSr?1U28$>X3pCO>sfSkeT`Z#312A1Dz_16B-gX%-&w9l zVHUWQYt`kUHvHbS^gkvGKy5Co|&NOHzL?^B;yTUR&*?cvaIMmAq)>Qr!EoEC??m zOSSHU^}>jbUef8-WVOZTt*@7#x4sdGLoP*r5c!W!DDKWTWcPWvxUP*(r+l>qnIIpp zU2Cq5<;1*-z4Ynlzo3q&cBB*%FBYh~b+tJ9zZrgwEW&{F_I!G+TbJHKFRSTvjqw3v zsnLFeE@6Cm_l?!shzQOv9l;w>gfLO+hjl|)%wT!=p2wR^0wuW=iyILLKdtiv?`jZe z-KEkQN$wI`<4lB}br2%-3nxu14&OJ0LfQv&RBH+Q1{C3|ihJ|qrqv!yyolZlwV(2}d7+X|<)liX6>13b&Cb zb2?JKn|X-TJ$VGeQl5LuwvtGyI$<~HsRgtF-~C%Y-hR*-a%ex~IlKQ;1h|qVD?NvE zR5BHsBQIo4xN!Nq3xn?z{vTXiX@z6_>^E!C>9f*|aJl5&v`7PIIYWr1NSZHK zH96JX-o+O#r`6N*Po`{V8CO<{zOEUb34<*sd~3;gR7`naZDin?$&ln8m*)*+B}UQZ zcrP}Mjs|JY3&RW5O&oHTxg!;x>Pu48h2OPEELCa7dXED~`MK%vv=wa@!=!89ej)t(wq69eWo;cXh=4 zoy{6xvw4Ji(XK8B$%(0x1?SmXC8QFNcmR?rR+WAHd*<xM@iVHx+X0k9VkT9iwU@N*lj0U6K{I2R5y;Yu#3$h%yG}t~`gIqEEwYPfuks z2x03)*M#hglT8-crTPudY*^B(t5kZ1jwvW0z@%FMAU?AU!Td32dc|VR8c0F}Q(;&jfzfb{f{vh?ZZo7>7~taSS4H zR?V}hyQEfXtEViq!_{1iKHesRPOGpyvh*2wP7cuTb8hFB!$rr&(LWxXFS2ICGNx4? zIQ7-aO-)0!2#}l{sWrhBU|sm{CbAf}*Xo20P#o0n+Rn_8)Y%xKUe7sUUHLVO)zn)h zC*y~+SB&L0Vcz+=lFf+fL{;7ZaP4p{3N0Y6@rL5E`@RF_iuxoN(p%@D+;s1NY>@zMF;B( zcLOh9zU1kdm#UVEeo9h~<)sHbgYfcmQed=m-9kV>K-)}l_hJ2|7(kZ&$-=_I zLdw?PG!BRKMXk88g{p3N@q1*!!6VIHmT;h&NN+72t=Mxe0|N_t{Jo9aOz*1^fvRiinMs`sAq7K=#K-NX$t~0z l8HnVm!~b7o_TN$6npG}0t(0f)4wq3Xz@2cgW!qec{Tn(Q|0)0g literal 10809 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 diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/multivar_cbar_locationing.png b/lib/matplotlib/tests/baseline_images/test_colorbar/multivar_cbar_locationing.png new file mode 100644 index 0000000000000000000000000000000000000000..7600a8ab99d0c00313eaa05d8dbe448eee936102 GIT binary patch literal 3668 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 literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/multivar_cbar_locationing_constrained.png b/lib/matplotlib/tests/baseline_images/test_colorbar/multivar_cbar_locationing_constrained.png new file mode 100644 index 0000000000000000000000000000000000000000..57b300920c0f6666ff6b003b8bfb9d5a9c2d2bcd GIT binary patch literal 3532 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 literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/multivar_cbar_sharing.png b/lib/matplotlib/tests/baseline_images/test_colorbar/multivar_cbar_sharing.png new file mode 100644 index 0000000000000000000000000000000000000000..6423085fb9e01654bbd0ae2410cf35b2b46b36bd GIT binary patch literal 3818 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 tSQhvN4?h~tZU`6suimBp!?%57K8{%X<)jgn2)