From 196edf85c7cfc653a6049a8b9e3ae88ab52ab0b7 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 19 Sep 2020 19:49:23 +0200 Subject: [PATCH 1/3] Inherit OffsetBox.{get_offset,get_window_extent}. By making the parameters of OffsetBox.get_offset optional (which matches the fact that there are currently subclasses for which get_offset does *not* take any parameters), and adjusting OffsetBox.get_window_extent to use get_extent (which is typically redefined in subclasses) instead of get_extent_offsets (which is not), we can inherit the implementation of OffsetBox.get_window_extent across most subclasses instead of having to copy-paste it again and again. --- lib/matplotlib/offsetbox.py | 70 ++++++------------------------------- 1 file changed, 11 insertions(+), 59 deletions(-) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 229158066675..e49c0d72c616 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -272,23 +272,15 @@ def offset(width, height, xdescent, ydescent, renderer) \ self._offset = xy self.stale = True - def get_offset(self, width, height, xdescent, ydescent, renderer): + def get_offset(self, *args, **kwargs): """ - Return the offset as a tuple (x, y). - - The extent parameters have to be provided to handle the case where the - offset is dynamically determined by a callable (see - `~.OffsetBox.set_offset`). - - Parameters - ---------- - width, height, xdescent, ydescent - Extent parameters. - renderer : `.RendererBase` subclass + Return the (x, y) offset. + Parameters must be passed if the offset is dynamically determined by a + callable (see `~.OffsetBox.set_offset`), and are forwarded to that + callable. """ - return (self._offset(width, height, xdescent, ydescent, renderer) - if callable(self._offset) + return (self._offset(*args, **kwargs) if callable(self._offset) else self._offset) def set_width(self, width): @@ -347,7 +339,7 @@ def get_extent(self, renderer): def get_window_extent(self, renderer): # docstring inherited - w, h, xd, yd, offsets = self.get_extent_offsets(renderer) + w, h, xd, yd = self.get_extent(renderer) px, py = self.get_offset(w, h, xd, yd, renderer) return mtransforms.Bbox.from_bounds(px - xd, py - yd, w, h) @@ -622,21 +614,9 @@ def set_offset(self, xy): xy : (float, float) The (x, y) coordinates of the offset in display units. """ - self._offset = xy self.offset_transform.clear() self.offset_transform.translate(xy[0], xy[1]) - self.stale = True - - def get_offset(self): - """Return offset of the container.""" - return self._offset - - def get_window_extent(self, renderer): - # docstring inherited - w, h, xd, yd = self.get_extent(renderer) - ox, oy = self.get_offset() # w, h, xd, yd) - - return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h) + super().set_offset(xy) def get_extent(self, renderer): """Return width, height, xdescent, ydescent of box.""" @@ -782,20 +762,9 @@ def set_offset(self, xy): xy : (float, float) The (x, y) coordinates of the offset in display units. """ - self._offset = xy self.offset_transform.clear() self.offset_transform.translate(xy[0], xy[1]) - self.stale = True - - def get_offset(self): - """Return offset of the container.""" - return self._offset - - def get_window_extent(self, renderer): - # docstring inherited - w, h, xd, yd = self.get_extent(renderer) - ox, oy = self.get_offset() - return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h) + super().set_offset(xy) def get_extent(self, renderer): _, h_, d_ = renderer.get_text_width_height_descent( @@ -883,20 +852,9 @@ def set_offset(self, xy): xy : (float, float) The (x, y) coordinates of the offset in display units. """ - self._offset = xy self.offset_transform.clear() self.offset_transform.translate(xy[0], xy[1]) - self.stale = True - - def get_offset(self): - """Return offset of the container.""" - return self._offset - - def get_window_extent(self, renderer): - # docstring inherited - w, h, xd, yd = self.get_extent(renderer) - ox, oy = self.get_offset() # w, h, xd, yd) - return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h) + super().set_offset(xy) def get_extent(self, renderer): # clear the offset transforms @@ -1077,9 +1035,7 @@ def set_bbox_to_anchor(self, bbox, transform=None): def get_window_extent(self, renderer): # docstring inherited self._update_offset_func(renderer) - w, h, xd, yd = self.get_extent(renderer) - ox, oy = self.get_offset(w, h, xd, yd, renderer) - return Bbox.from_bounds(ox - xd, oy - yd, w, h) + return super().get_window_extent(renderer) def _update_offset_func(self, renderer, fontsize=None): """ @@ -1230,10 +1186,6 @@ def set_zoom(self, zoom): def get_zoom(self): return self._zoom - def get_offset(self): - """Return offset of the container.""" - return self._offset - def get_children(self): return [self.image] From a68dfad5a9fe57fb8ecc127a4c1551279aec5aff Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 21 Nov 2021 14:12:38 +0100 Subject: [PATCH 2/3] Inline AnchoredOffsetBox._update_offset_func. It is only called in `get_window_extent` -- and in `draw`, but *that* call is immediately followed by a call to `get_window_extent`, so it is redundant. --- lib/matplotlib/offsetbox.py | 24 ++++++-------------- lib/mpl_toolkits/axes_grid1/inset_locator.py | 15 ++++-------- 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index e49c0d72c616..64ded364c0a3 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -1034,27 +1034,19 @@ def set_bbox_to_anchor(self, bbox, transform=None): def get_window_extent(self, renderer): # docstring inherited - self._update_offset_func(renderer) - return super().get_window_extent(renderer) - - def _update_offset_func(self, renderer, fontsize=None): - """ - Update the offset func which depends on the dpi of the - renderer (because of the padding). - """ - if fontsize is None: - fontsize = renderer.points_to_pixels( - self.prop.get_size_in_points()) + # Update the offset func, which depends on the dpi of the renderer + # (because of the padding). + fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) def _offset(w, h, xd, yd, renderer): bbox = Bbox.from_bounds(0, 0, w, h) - borderpad = self.borderpad * fontsize + pad = self.borderpad * fontsize bbox_to_anchor = self.get_bbox_to_anchor() - x0, y0 = _get_anchored_bbox( - self.loc, bbox, bbox_to_anchor, borderpad) + x0, y0 = _get_anchored_bbox(self.loc, bbox, bbox_to_anchor, pad) return x0 + xd, y0 + yd self.set_offset(_offset) + return super().get_window_extent(renderer) def update_frame(self, bbox, fontsize=None): self.patch.set_bounds(bbox.bounds) @@ -1066,11 +1058,9 @@ def draw(self, renderer): if not self.get_visible(): return - fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) - self._update_offset_func(renderer, fontsize) - # update the location and size of the legend bbox = self.get_window_extent(renderer) + fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) self.update_frame(bbox, fontsize) self.patch.draw(renderer) diff --git a/lib/mpl_toolkits/axes_grid1/inset_locator.py b/lib/mpl_toolkits/axes_grid1/inset_locator.py index 56e3b83573b1..6b9933e353c1 100644 --- a/lib/mpl_toolkits/axes_grid1/inset_locator.py +++ b/lib/mpl_toolkits/axes_grid1/inset_locator.py @@ -70,18 +70,11 @@ def draw(self, renderer): def __call__(self, ax, renderer): self.axes = ax - - fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) - self._update_offset_func(renderer, fontsize) - - width, height, xdescent, ydescent = self.get_extent(renderer) - - px, py = self.get_offset(width, height, 0, 0, renderer) - bbox_canvas = Bbox.from_bounds(px, py, width, height) + bbox = self.get_window_extent(renderer) + px, py = self.get_offset(bbox.width, bbox.height, 0, 0, renderer) + bbox_canvas = Bbox.from_bounds(px, py, bbox.width, bbox.height) tr = ax.figure.transFigure.inverted() - bb = TransformedBbox(bbox_canvas, tr) - - return bb + return TransformedBbox(bbox_canvas, tr) class AnchoredSizeLocator(AnchoredLocatorBase): From 35411485e9513f543ed3c375b039ab879721e1cb Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 13 Feb 2022 16:50:26 +0100 Subject: [PATCH 3/3] Make OffsetBox.set_offset callables nullary. --- lib/matplotlib/legend.py | 8 +++- lib/matplotlib/offsetbox.py | 39 +++++++++++++------- lib/mpl_toolkits/axes_grid1/inset_locator.py | 2 +- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 7aef9ed7e8b3..15f56959bc4f 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -585,8 +585,12 @@ def _get_loc(self): _loc = property(_get_loc, _set_loc) - def _findoffset(self, width, height, xdescent, ydescent, renderer): + def _findoffset(self): """Helper function to locate the legend.""" + renderer = (self.figure._cachedRenderer + or self._legend_box._cached_renderer) + width, height, xdescent, ydescent = self._legend_box.get_extent( + renderer) if self._loc == 0: # "best". x, y = self._find_best_position(width, height, renderer) @@ -883,6 +887,8 @@ def get_window_extent(self, renderer=None): # docstring inherited if renderer is None: renderer = self.figure._cachedRenderer + # May not be cached on the figure, so cache it ourselves. + self._cached_renderer = renderer return self._legend_box.get_window_extent(renderer=renderer) def get_tightbbox(self, renderer): diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 64ded364c0a3..deb624f8be1a 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -263,11 +263,19 @@ def set_offset(self, xy): xy : (float, float) or callable The (x, y) coordinates of the offset in display units. These can either be given explicitly as a tuple (x, y), or by providing a - function that converts the extent into the offset. This function - must have the signature:: + function that dynamically computes an offset (taking the arguments + passed to `.OffsetBox.get_offset`). It is recommended to make such + functions take no arguments. + + Before version 3.6, the callable had to have the signature:: def offset(width, height, xdescent, ydescent, renderer) \ -> (float, float) + + For backwards compatibility, callables with arbitrary signatures + are currently accepted as long as compatible arguments are + passed in calls to `.set_offset`. This should be considered an + implementation detail, and may be deprecated in the future. """ self._offset = xy self.stale = True @@ -276,9 +284,12 @@ def get_offset(self, *args, **kwargs): """ Return the (x, y) offset. - Parameters must be passed if the offset is dynamically determined by a - callable (see `~.OffsetBox.set_offset`), and are forwarded to that - callable. + Parameters are usually not necessary. The only exception can occur + if you have defined a callable to calculate the offset dynamically (see + `~.OffsetBox.set_offset`). It is now recommended that such a + callable does not take parameters. However, for backward-compatibility, + callables with parameters are still supported; these parameters must be + provided to `.get_offset` so that we can pass them on. """ return (self._offset(*args, **kwargs) if callable(self._offset) else self._offset) @@ -340,7 +351,9 @@ def get_extent(self, renderer): def get_window_extent(self, renderer): # docstring inherited w, h, xd, yd = self.get_extent(renderer) - px, py = self.get_offset(w, h, xd, yd, renderer) + # dynamic offset compute callables may need to access the renderer. + self._cached_renderer = renderer + px, py = self.get_offset() return mtransforms.Bbox.from_bounds(px - xd, py - yd, w, h) def draw(self, renderer): @@ -349,7 +362,7 @@ def draw(self, renderer): to the given *renderer*. """ w, h, xdescent, ydescent, offsets = self.get_extent_offsets(renderer) - px, py = self.get_offset(w, h, xdescent, ydescent, renderer) + px, py = self.get_offset() for c, (ox, oy) in zip(self.get_visible_children(), offsets): c.set_offset((px + ox, py + oy)) c.draw(renderer) @@ -530,7 +543,7 @@ def get_extent_offsets(self, renderer): def draw(self, renderer): # docstring inherited w, h, xdescent, ydescent, offsets = self.get_extent_offsets(renderer) - px, py = self.get_offset(w, h, xdescent, ydescent, renderer) + px, py = self.get_offset() for c, (ox, oy) in zip(self.get_visible_children(), offsets): c.set_offset((px + ox, py + oy)) @@ -1036,9 +1049,10 @@ def get_window_extent(self, renderer): # docstring inherited # Update the offset func, which depends on the dpi of the renderer # (because of the padding). + w, h, xd, yd = self.get_extent(renderer) fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) - def _offset(w, h, xd, yd, renderer): + def _offset(*args, **kwargs): # args are ignored; left for backcompat. bbox = Bbox.from_bounds(0, 0, w, h) pad = self.borderpad * fontsize bbox_to_anchor = self.get_bbox_to_anchor() @@ -1064,9 +1078,7 @@ def draw(self, renderer): self.update_frame(bbox, fontsize) self.patch.draw(renderer) - width, height, xdescent, ydescent = self.get_extent(renderer) - - px, py = self.get_offset(width, height, xdescent, ydescent, renderer) + px, py = self.get_offset() self.get_child().set_offset((px, py)) self.get_child().draw(renderer) @@ -1545,8 +1557,7 @@ def __init__(self, ref_artist, offsetbox, use_blit=False): def save_offset(self): offsetbox = self.offsetbox renderer = offsetbox.figure._cachedRenderer - w, h, xd, yd = offsetbox.get_extent(renderer) - offset = offsetbox.get_offset(w, h, xd, yd, renderer) + offset = offsetbox.get_offset() self.offsetbox_x, self.offsetbox_y = offset self.offsetbox.set_offset(offset) diff --git a/lib/mpl_toolkits/axes_grid1/inset_locator.py b/lib/mpl_toolkits/axes_grid1/inset_locator.py index 6b9933e353c1..a67126b37a22 100644 --- a/lib/mpl_toolkits/axes_grid1/inset_locator.py +++ b/lib/mpl_toolkits/axes_grid1/inset_locator.py @@ -71,7 +71,7 @@ def draw(self, renderer): def __call__(self, ax, renderer): self.axes = ax bbox = self.get_window_extent(renderer) - px, py = self.get_offset(bbox.width, bbox.height, 0, 0, renderer) + px, py = self.get_offset() bbox_canvas = Bbox.from_bounds(px, py, bbox.width, bbox.height) tr = ax.figure.transFigure.inverted() return TransformedBbox(bbox_canvas, tr)