From c5b493e9f769a87b00602efc5f4a9b9a7713733d Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Sat, 25 Dec 2021 03:07:23 -0700 Subject: [PATCH 01/15] Add zoom axes code. --- lib/matplotlib/axes/__init__.py | 2 + lib/matplotlib/axes/_zoom_axes.py | 261 ++++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 lib/matplotlib/axes/_zoom_axes.py diff --git a/lib/matplotlib/axes/__init__.py b/lib/matplotlib/axes/__init__.py index 4dd998c0d43d..b4daddb2f2f9 100644 --- a/lib/matplotlib/axes/__init__.py +++ b/lib/matplotlib/axes/__init__.py @@ -1,2 +1,4 @@ from ._subplots import * from ._axes import * +from ._zoom_axes import * + diff --git a/lib/matplotlib/axes/_zoom_axes.py b/lib/matplotlib/axes/_zoom_axes.py new file mode 100644 index 000000000000..03e95cc49a4c --- /dev/null +++ b/lib/matplotlib/axes/_zoom_axes.py @@ -0,0 +1,261 @@ +from matplotlib.path import Path +from matplotlib.axes import Axes +from matplotlib.axes._axes import _TransformedBoundsLocator +from matplotlib.transforms import Bbox, Transform, IdentityTransform, Affine2D +from matplotlib.backend_bases import RendererBase +import matplotlib._image as _image +import numpy as np + + +class _TransformRenderer(RendererBase): + """ + A matplotlib renderer which performs transforms to change the final location of plotted + elements, and then defers drawing work to the original renderer. Used to produce zooming effects... + """ + def __init__(self, base_renderer: RendererBase, mock_transform: Transform, transform: Transform, + bounding_axes: Axes): + """ + Constructs a new TransformRender. + + :param base_renderer: The renderer to use for finally drawing objects. + :param mock_transform: The transform or coordinate space which all passed paths/triangles/images will be + converted to before being placed back into display coordinates by the main transform. + For example if the parent axes transData is passed, all objects will be converted to + the parent axes data coordinate space before being transformed via the main transform + back into coordinate space. + :param transform: The main transform to be used for plotting all objects once converted into the mock_transform + coordinate space. Typically this is the child axes data coordinate space (transData). + :param bounding_axes: The axes to plot everything within. Everything outside of this axes will be clipped. + """ + super().__init__() + self.__renderer = base_renderer + self.__mock_trans = mock_transform + self.__core_trans = transform + self.__bounding_axes = bounding_axes + + def _get_axes_display_box(self) -> Bbox: + """ + Private method, get the bounding box of the child axes in display coordinates. + """ + return self.__bounding_axes.patch.get_bbox().transformed(self.__bounding_axes.transAxes) + + def _get_transfer_transform(self, orig_transform): + """ + Private method, returns the transform which translates and scales coordinates as if they were originally + plotted on the child axes instead of the parent axes. + + :param orig_transform: The transform that was going to be originally used by the object/path/text/image. + + :return: A matplotlib transform which goes from original point data -> display coordinates if the data was + originally plotted on the child axes instead of the parent axes. + """ + # We apply the original transform to go to display coordinates, then apply the parent data transform inverted + # to go to the parent axes coordinate space (data space), then apply the child axes data transform to + # go back into display space, but as if we originally plotted the artist on the child axes.... + return orig_transform + self.__mock_trans.inverted() + self.__core_trans + + # We copy all of the properties of the renderer we are mocking, so that artists plot themselves as if they were + # placed on the original renderer. + @property + def height(self): + return self.__renderer.get_canvas_width_height()[1] + + @property + def width(self): + return self.__renderer.get_canvas_width_height()[0] + + def get_text_width_height_descent(self, s, prop, ismath): + return self.__renderer.get_text_width_height_descent(s, prop, ismath) + + def get_canvas_width_height(self): + return self.__renderer.get_canvas_width_height() + + def get_texmanager(self): + return self.__renderer.get_texmanager() + + def get_image_magnification(self): + return self.__renderer.get_image_magnification() + + def _get_text_path_transform(self, x, y, s, prop, angle, ismath): + return self.__renderer._get_text_path_transform(x, y, s, prop, angle, ismath) + + def option_scale_image(self): + return False + + def points_to_pixels(self, points): + return self.__renderer.points_to_pixels(points) + + def flipy(self): + return self.__renderer.flipy() + + # Actual drawing methods below: + + def draw_path(self, gc, path: Path, transform: Transform, rgbFace=None): + # Convert the path to display coordinates, but if it was originally drawn on the child axes. + path = path.deepcopy() + path.vertices = self._get_transfer_transform(transform).transform(path.vertices) + bbox = self._get_axes_display_box() + + # We check if the path intersects the axes box at all, if not don't waste time drawing it. + if(not path.intersects_bbox(bbox, True)): + return + + # Change the clip to the sub-axes box + gc.set_clip_rectangle(bbox) + + self.__renderer.draw_path(gc, path, IdentityTransform(), rgbFace) + + def _draw_text_as_path(self, gc, x, y, s: str, prop, angle, ismath): + # If the text field is empty, don't even try rendering it... + if((s is None) or (s.strip() == "")): + return + # Call the super class instance, which works for all cases except one checked above... (Above case causes error) + super()._draw_text_as_path(gc, x, y, s, prop, angle, ismath) + + def draw_gouraud_triangle(self, gc, points, colors, transform): + # Pretty much identical to draw_path, transform the points and adjust clip to the child axes bounding box. + points = self._get_transfer_transform(transform).transform(points) + path = Path(points, closed=True) + bbox = self._get_axes_display_box() + + if(not path.intersects_bbox(bbox, True)): + return + + gc.set_clip_rectangle(bbox) + + self.__renderer.draw_gouraud_triangle(gc, path.vertices, colors, IdentityTransform()) + + # Images prove to be especially messy to deal with... + def draw_image(self, gc, x, y, im, transform=None): + mag = self.get_image_magnification() + shift_data_transform = self._get_transfer_transform(IdentityTransform()) + axes_bbox = self._get_axes_display_box() + # Compute the image bounding box in display coordinates.... Image arrives pre-magnified. + img_bbox_disp = Bbox.from_bounds(x, y, im.shape[1], im.shape[0]) + # Now compute the output location, clipping it with the final axes patch. + out_box = img_bbox_disp.transformed(shift_data_transform) + clipped_out_box = Bbox.intersection(out_box, axes_bbox) + + if(clipped_out_box is None): + return + + # We compute what the dimensions of the final output image within the sub-axes are going to be. + x, y, out_w, out_h = clipped_out_box.bounds + out_w, out_h = int(np.ceil(out_w * mag)), int(np.ceil(out_h * mag)) + + if((out_w <= 0) or (out_h <= 0)): + return + + # We can now construct the transform which converts between the original image (a 2D numpy array which starts + # at the origin) to the final zoomed image (also a 2D numpy array which starts at the origin). + img_trans = ( + Affine2D().scale(1/mag, 1/mag).translate(img_bbox_disp.x0, img_bbox_disp.y0) + + shift_data_transform + + Affine2D().translate(-clipped_out_box.x0, -clipped_out_box.y0).scale(mag, mag) + ) + + # We resize and zoom the original image onto the out_arr. + out_arr = np.zeros((out_h, out_w, im.shape[2]), dtype=im.dtype) + trans_msk = np.zeros((out_h, out_w), dtype=im.dtype) + + _image.resample(im, out_arr, img_trans, _image.NEAREST, alpha=1) + _image.resample(im[:, :, 3], trans_msk, img_trans, _image.NEAREST, alpha=1) + out_arr[:, :, 3] = trans_msk + + gc.set_clip_rectangle(clipped_out_box) + + x, y = clipped_out_box.x0, clipped_out_box.y0 + + if(self.option_scale_image()): + self.__renderer.draw_image(gc, x, y, out_arr, None) + else: + self.__renderer.draw_image(gc, x, y, out_arr) + + +class ZoomViewAxes(Axes): + """ + A zoom axes which automatically displays all of the elements it is currently zoomed in on. Does not require + Artists to be plotted twice. + """ + MAX_RENDER_DEPTH = 1 # The number of allowed recursions in the draw method. + # Allows for zoom axes to zoom in on zoom axes + + def __init__(self, axes_of_zoom: Axes, rect: Bbox, transform = None, zorder = 5, **kwargs): + """ + Construct a new zoom axes. + + :param axes_of_zoom: The axes to zoom in on, which this axes will be nested inside. + :param rect: The bounding box to place this axes in, within the parent axes. + :param transform: The transform to use when placing this axes in the parent axes. Defaults to + 'axes_of_zoom.transData'. + :param zorder: An integer, the z-order of the axes. Defaults to 5. + :param kwargs: Any other keyword arguments which the Axes class accepts. + """ + if(transform is None): + transform = axes_of_zoom.transData + + inset_loc = _TransformedBoundsLocator(rect.bounds, transform) + bb = inset_loc(axes_of_zoom, None) + + super().__init__(axes_of_zoom.figure, bb.bounds, zorder=zorder, **kwargs) + + self.__zoom_axes = axes_of_zoom + self.set_axes_locator(inset_loc) + + self._render_depth = 0 + + axes_of_zoom.add_child_axes(self) + + def draw(self, renderer=None): + if(self._render_depth >= self.MAX_RENDER_DEPTH): + return + self._render_depth += 1 + + super().draw(renderer) + + if(not self.get_visible()): + return + + axes_children = [ + *self.__zoom_axes.collections, + *self.__zoom_axes.patches, + *self.__zoom_axes.lines, + *self.__zoom_axes.texts, + *self.__zoom_axes.artists, + *self.__zoom_axes.images, + *self.__zoom_axes.child_axes + ] + + img_boxes = [] + # We need to temporarily disable the clip boxes of all of the images, in order to allow us to continue + # rendering them it even if it is outside of the parent axes (they might still be visible in this zoom axes). + for img in self.__zoom_axes.images: + img_boxes.append(img.get_clip_box()) + img.set_clip_box(img.get_window_extent(renderer)) + + # Sort all rendered item by their z-order so the render in layers correctly... + axes_children.sort(key=lambda obj: obj.get_zorder()) + + # Construct mock renderer and draw all artists to it. + mock_renderer = _TransformRenderer(renderer, self.__zoom_axes.transData, self.transData, self) + x1, x2 = self.get_xlim() + y1, y2 = self.get_ylim() + axes_box = Bbox.from_extents(x1, y1, x2, y2).transformed(self.__zoom_axes.transData) + + for artist in axes_children: + # If the artist is this or it does not land in the area we are drawing artists from, do not draw it, + # otherwise go ahead. + if((artist is not self) and (Bbox.intersection(artist.get_window_extent(renderer), axes_box) is not None)): + artist.draw(mock_renderer) + + # Reset all of the image clip boxes... + for img, box in zip(self.__zoom_axes.images, img_boxes): + img.set_clip_box(box) + + # We need to redraw the splines if enabled, as we have finally drawn everything... This avoids other objects + # being drawn over the splines + if(self.axison and self._frameon): + for spine in self.spines.values(): + spine.draw(renderer) + + self._render_depth -= 1 \ No newline at end of file From 416833959893a491e3ebf2261ff03f7560901953 Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Sat, 25 Dec 2021 11:42:36 -0700 Subject: [PATCH 02/15] Disable clipping temporarily for all artists in zoom axes. --- lib/matplotlib/axes/_zoom_axes.py | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/matplotlib/axes/_zoom_axes.py b/lib/matplotlib/axes/_zoom_axes.py index 03e95cc49a4c..2bef28b0992e 100644 --- a/lib/matplotlib/axes/_zoom_axes.py +++ b/lib/matplotlib/axes/_zoom_axes.py @@ -10,7 +10,7 @@ class _TransformRenderer(RendererBase): """ A matplotlib renderer which performs transforms to change the final location of plotted - elements, and then defers drawing work to the original renderer. Used to produce zooming effects... + elements, and then defers drawing work to the original renderer. """ def __init__(self, base_renderer: RendererBase, mock_transform: Transform, transform: Transform, bounding_axes: Axes): @@ -171,7 +171,6 @@ def draw_image(self, gc, x, y, im, transform=None): else: self.__renderer.draw_image(gc, x, y, out_arr) - class ZoomViewAxes(Axes): """ A zoom axes which automatically displays all of the elements it is currently zoomed in on. Does not require @@ -184,11 +183,12 @@ def __init__(self, axes_of_zoom: Axes, rect: Bbox, transform = None, zorder = 5, """ Construct a new zoom axes. - :param axes_of_zoom: The axes to zoom in on, which this axes will be nested inside. + :param axes_of_zoom: The axes to zoom in on which this axes will be nested inside. :param rect: The bounding box to place this axes in, within the parent axes. :param transform: The transform to use when placing this axes in the parent axes. Defaults to 'axes_of_zoom.transData'. - :param zorder: An integer, the z-order of the axes. Defaults to 5. + :param zorder: An integer, the z-order of the axes. Defaults to 5, which means it is drawn on top of most + object in the plot. :param kwargs: Any other keyword arguments which the Axes class accepts. """ if(transform is None): @@ -226,16 +226,16 @@ def draw(self, renderer=None): *self.__zoom_axes.child_axes ] - img_boxes = [] - # We need to temporarily disable the clip boxes of all of the images, in order to allow us to continue - # rendering them it even if it is outside of the parent axes (they might still be visible in this zoom axes). - for img in self.__zoom_axes.images: - img_boxes.append(img.get_clip_box()) - img.set_clip_box(img.get_window_extent(renderer)) - # Sort all rendered item by their z-order so the render in layers correctly... axes_children.sort(key=lambda obj: obj.get_zorder()) + artist_boxes = [] + # We need to temporarily disable the clip boxes of all of the artists, in order to allow us to continue + # rendering them it even if it is outside of the parent axes (they might still be visible in this zoom axes). + for a in axes_children: + artist_boxes.append(a.get_clip_box()) + a.set_clip_box(a.get_window_extent(renderer)) + # Construct mock renderer and draw all artists to it. mock_renderer = _TransformRenderer(renderer, self.__zoom_axes.transData, self.transData, self) x1, x2 = self.get_xlim() @@ -244,13 +244,13 @@ def draw(self, renderer=None): for artist in axes_children: # If the artist is this or it does not land in the area we are drawing artists from, do not draw it, - # otherwise go ahead. + # otherwise go ahead. Done to improve performance... if((artist is not self) and (Bbox.intersection(artist.get_window_extent(renderer), axes_box) is not None)): artist.draw(mock_renderer) - # Reset all of the image clip boxes... - for img, box in zip(self.__zoom_axes.images, img_boxes): - img.set_clip_box(box) + # Reset all of the artist clip boxes... + for a, box in zip(axes_children, artist_boxes): + a.set_clip_box(box) # We need to redraw the splines if enabled, as we have finally drawn everything... This avoids other objects # being drawn over the splines @@ -258,4 +258,4 @@ def draw(self, renderer=None): for spine in self.spines.values(): spine.draw(renderer) - self._render_depth -= 1 \ No newline at end of file + self._render_depth -= 1 From 5d0985f361a40ed371a8c675e521e81c66139d7a Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Mon, 27 Dec 2021 23:44:21 -0700 Subject: [PATCH 03/15] Add method to Axes for creating a zoomed inset axes. --- lib/matplotlib/axes/__init__.py | 2 - lib/matplotlib/axes/_axes.py | 34 +++++++++++ lib/matplotlib/axes/_zoom_axes.py | 95 ++++++++++++++++++++++--------- 3 files changed, 103 insertions(+), 28 deletions(-) diff --git a/lib/matplotlib/axes/__init__.py b/lib/matplotlib/axes/__init__.py index b4daddb2f2f9..4dd998c0d43d 100644 --- a/lib/matplotlib/axes/__init__.py +++ b/lib/matplotlib/axes/__init__.py @@ -1,4 +1,2 @@ from ._subplots import * from ._axes import * -from ._zoom_axes import * - diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index b8a7c3b2c6bb..14b95255e47b 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -367,6 +367,40 @@ def inset_axes(self, bounds, *, transform=None, zorder=5, **kwargs): return inset_ax + def inset_zoom_axes(self, bounds, *, transform=None, zorder=5, **kwargs): + """ + Add a child inset Axes to this existing Axes, which automatically plots artists contained within the parent + Axes. + + Parameters + ---------- + bounds : [x0, y0, width, height] + Lower-left corner of inset Axes, and its width and height. + + transform : `.Transform` + Defaults to `ax.transAxes`, i.e. the units of *rect* are in + Axes-relative coordinates. + + zorder : number + Defaults to 5 (same as `.Axes.legend`). Adjust higher or lower + to change whether it is above or below data plotted on the + parent Axes. + + **kwargs + Other keyword arguments are passed on to the child `.Axes`. + + Returns + ------- + ax + The created `~.axes.Axes` instance. + + Examples + -------- + See `~.axes.Axes.inset_zoom` method for examples. + """ + from ._zoom_axes import ZoomViewAxes + return ZoomViewAxes(self, mtransforms.Bbox.from_bounds(*bounds), transform, zorder, **kwargs) + @docstring.dedent_interpd def indicate_inset(self, bounds, inset_ax=None, *, transform=None, facecolor='none', edgecolor='0.5', alpha=0.5, diff --git a/lib/matplotlib/axes/_zoom_axes.py b/lib/matplotlib/axes/_zoom_axes.py index 2bef28b0992e..78a50ddbdb72 100644 --- a/lib/matplotlib/axes/_zoom_axes.py +++ b/lib/matplotlib/axes/_zoom_axes.py @@ -1,9 +1,12 @@ +from typing import Optional + from matplotlib.path import Path from matplotlib.axes import Axes from matplotlib.axes._axes import _TransformedBoundsLocator from matplotlib.transforms import Bbox, Transform, IdentityTransform, Affine2D from matplotlib.backend_bases import RendererBase import matplotlib._image as _image +import matplotlib.docstring as docstring import numpy as np @@ -17,15 +20,29 @@ def __init__(self, base_renderer: RendererBase, mock_transform: Transform, trans """ Constructs a new TransformRender. - :param base_renderer: The renderer to use for finally drawing objects. - :param mock_transform: The transform or coordinate space which all passed paths/triangles/images will be - converted to before being placed back into display coordinates by the main transform. - For example if the parent axes transData is passed, all objects will be converted to - the parent axes data coordinate space before being transformed via the main transform - back into coordinate space. - :param transform: The main transform to be used for plotting all objects once converted into the mock_transform - coordinate space. Typically this is the child axes data coordinate space (transData). - :param bounding_axes: The axes to plot everything within. Everything outside of this axes will be clipped. + Parameters + ---------- + base_renderer: `~matplotlib.backend_bases.RenderBase` + The renderer to use for drawing objects after applying transforms. + + mock_transform: `~matplotlib.transforms.Transform` + The transform or coordinate space which all passed paths/triangles/images will be + converted to before being placed back into display coordinates by the main transform. + For example if the parent axes transData is passed, all objects will be converted to + the parent axes data coordinate space before being transformed via the main transform + back into coordinate space. + + transform: `~matplotlib.transforms.Transform` + The main transform to be used for plotting all objects once converted into the mock_transform + coordinate space. Typically this is the child axes data coordinate space (transData). + + bounding_axes: `~matplotlib.axes.Axes` + The axes to plot everything within. Everything outside of this axes will be clipped. + + Returns + ------- + `~._zoom_axes._TransformRenderer` + The new transform renderer. """ super().__init__() self.__renderer = base_renderer @@ -39,15 +56,21 @@ def _get_axes_display_box(self) -> Bbox: """ return self.__bounding_axes.patch.get_bbox().transformed(self.__bounding_axes.transAxes) - def _get_transfer_transform(self, orig_transform): + def _get_transfer_transform(self, orig_transform: Transform) -> Transform: """ Private method, returns the transform which translates and scales coordinates as if they were originally plotted on the child axes instead of the parent axes. - :param orig_transform: The transform that was going to be originally used by the object/path/text/image. + Parameters + ---------- + orig_transform: `~matplotlib.transforms.Transform` + The transform that was going to be originally used by the object/path/text/image. - :return: A matplotlib transform which goes from original point data -> display coordinates if the data was - originally plotted on the child axes instead of the parent axes. + Returns + ------- + `~matplotlib.transforms.Transform` + A matplotlib transform which goes from original point data -> display coordinates if the data was + originally plotted on the child axes instead of the parent axes. """ # We apply the original transform to go to display coordinates, then apply the parent data transform inverted # to go to the parent axes coordinate space (data space), then apply the child axes data transform to @@ -171,28 +194,48 @@ def draw_image(self, gc, x, y, im, transform=None): else: self.__renderer.draw_image(gc, x, y, out_arr) + +@docstring.interpd class ZoomViewAxes(Axes): """ - A zoom axes which automatically displays all of the elements it is currently zoomed in on. Does not require - Artists to be plotted twice. + An inset axes which automatically displays elements of the parent axes it is currently placed inside. + Does not require Artists to be plotted twice. """ MAX_RENDER_DEPTH = 1 # The number of allowed recursions in the draw method. # Allows for zoom axes to zoom in on zoom axes - def __init__(self, axes_of_zoom: Axes, rect: Bbox, transform = None, zorder = 5, **kwargs): + def __init__(self, axes_of_zoom: Axes, rect: Bbox, transform: Optional[Transform] = None, + zorder: int = 5, **kwargs): """ - Construct a new zoom axes. - - :param axes_of_zoom: The axes to zoom in on which this axes will be nested inside. - :param rect: The bounding box to place this axes in, within the parent axes. - :param transform: The transform to use when placing this axes in the parent axes. Defaults to - 'axes_of_zoom.transData'. - :param zorder: An integer, the z-order of the axes. Defaults to 5, which means it is drawn on top of most - object in the plot. - :param kwargs: Any other keyword arguments which the Axes class accepts. + Construct a new zoomed inset axes. + + Parameters + ---------- + axes_of_zoom: `~.axes.Axes` + The axes to zoom in on which this axes will be nested inside. + + rect: `~matplotlib.transforms.Bbox` + The bounding box to place this axes in, within the parent axes. + + transform: `~matplotlib.transforms.Transform` or None + The transform to use when placing this axes in the parent axes. Defaults to + 'axes_of_zoom.transAxes'. + + zorder: int + An integer, the z-order of the axes. Defaults to 5. + + **kwargs + Other optional keyword arguments: + + %(Axes:kwdoc)s + + Returns + ------- + `~.axes.ZoomViewAxes` + The new zoom view axes instance... """ if(transform is None): - transform = axes_of_zoom.transData + transform = axes_of_zoom.transAxes inset_loc = _TransformedBoundsLocator(rect.bounds, transform) bb = inset_loc(axes_of_zoom, None) From b54ea9fce110cd95cf9a6762a1f25e9ab05e1123 Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Tue, 28 Dec 2021 00:25:35 -0700 Subject: [PATCH 04/15] Add new method to docs. --- doc/api/axes_api.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/api/axes_api.rst b/doc/api/axes_api.rst index 2e94fa5f9d65..59e4b87e9f69 100644 --- a/doc/api/axes_api.rst +++ b/doc/api/axes_api.rst @@ -194,6 +194,7 @@ Text and annotations Axes.table Axes.arrow Axes.inset_axes + Axes.inset_zoom_axes Axes.indicate_inset Axes.indicate_inset_zoom Axes.secondary_xaxis From 244b3954acffc66f5774ddfb13843b65ab16543f Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Tue, 28 Dec 2021 01:03:38 -0700 Subject: [PATCH 05/15] Code adjusted to follow 80-character limit... --- lib/matplotlib/axes/_axes.py | 7 +- lib/matplotlib/axes/_zoom_axes.py | 177 +++++++++++++++++++----------- 2 files changed, 119 insertions(+), 65 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 14b95255e47b..7e65a792e2ab 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -369,8 +369,8 @@ def inset_axes(self, bounds, *, transform=None, zorder=5, **kwargs): def inset_zoom_axes(self, bounds, *, transform=None, zorder=5, **kwargs): """ - Add a child inset Axes to this existing Axes, which automatically plots artists contained within the parent - Axes. + Add a child inset Axes to this existing Axes, which automatically plots + artists contained within the parent Axes. Parameters ---------- @@ -399,7 +399,8 @@ def inset_zoom_axes(self, bounds, *, transform=None, zorder=5, **kwargs): See `~.axes.Axes.inset_zoom` method for examples. """ from ._zoom_axes import ZoomViewAxes - return ZoomViewAxes(self, mtransforms.Bbox.from_bounds(*bounds), transform, zorder, **kwargs) + return ZoomViewAxes(self, mtransforms.Bbox.from_bounds(*bounds), + transform, zorder, **kwargs) @docstring.dedent_interpd def indicate_inset(self, bounds, inset_ax=None, *, transform=None, diff --git a/lib/matplotlib/axes/_zoom_axes.py b/lib/matplotlib/axes/_zoom_axes.py index 78a50ddbdb72..f4c04fe5bb1f 100644 --- a/lib/matplotlib/axes/_zoom_axes.py +++ b/lib/matplotlib/axes/_zoom_axes.py @@ -12,11 +12,17 @@ class _TransformRenderer(RendererBase): """ - A matplotlib renderer which performs transforms to change the final location of plotted - elements, and then defers drawing work to the original renderer. + A matplotlib renderer which performs transforms to change the final + location of plotted elements, and then defers drawing work to the + original renderer. """ - def __init__(self, base_renderer: RendererBase, mock_transform: Transform, transform: Transform, - bounding_axes: Axes): + def __init__( + self, + base_renderer: RendererBase, + mock_transform: Transform, + transform: Transform, + bounding_axes: Axes + ): """ Constructs a new TransformRender. @@ -26,18 +32,21 @@ def __init__(self, base_renderer: RendererBase, mock_transform: Transform, trans The renderer to use for drawing objects after applying transforms. mock_transform: `~matplotlib.transforms.Transform` - The transform or coordinate space which all passed paths/triangles/images will be - converted to before being placed back into display coordinates by the main transform. - For example if the parent axes transData is passed, all objects will be converted to - the parent axes data coordinate space before being transformed via the main transform - back into coordinate space. + The transform or coordinate space which all passed + paths/triangles/images will be converted to before being placed + back into display coordinates by the main transform. For example + if the parent axes transData is passed, all objects will be + converted to the parent axes data coordinate space before being + transformed via the main transform back into coordinate space. transform: `~matplotlib.transforms.Transform` - The main transform to be used for plotting all objects once converted into the mock_transform - coordinate space. Typically this is the child axes data coordinate space (transData). + The main transform to be used for plotting all objects once + converted into the mock_transform coordinate space. Typically this + is the child axes data coordinate space (transData). bounding_axes: `~matplotlib.axes.Axes` - The axes to plot everything within. Everything outside of this axes will be clipped. + The axes to plot everything within. Everything outside of this + axes will be clipped. Returns ------- @@ -52,33 +61,43 @@ def __init__(self, base_renderer: RendererBase, mock_transform: Transform, trans def _get_axes_display_box(self) -> Bbox: """ - Private method, get the bounding box of the child axes in display coordinates. + Private method, get the bounding box of the child axes in display + coordinates. """ - return self.__bounding_axes.patch.get_bbox().transformed(self.__bounding_axes.transAxes) + return self.__bounding_axes.patch.get_bbox().transformed( + self.__bounding_axes.transAxes + ) def _get_transfer_transform(self, orig_transform: Transform) -> Transform: """ - Private method, returns the transform which translates and scales coordinates as if they were originally - plotted on the child axes instead of the parent axes. + Private method, returns the transform which translates and scales + coordinates as if they were originally plotted on the child axes + instead of the parent axes. Parameters ---------- orig_transform: `~matplotlib.transforms.Transform` - The transform that was going to be originally used by the object/path/text/image. + The transform that was going to be originally used by the + object/path/text/image. Returns ------- `~matplotlib.transforms.Transform` - A matplotlib transform which goes from original point data -> display coordinates if the data was - originally plotted on the child axes instead of the parent axes. + A matplotlib transform which goes from original point data -> + display coordinates if the data was originally plotted on the + child axes instead of the parent axes. """ - # We apply the original transform to go to display coordinates, then apply the parent data transform inverted - # to go to the parent axes coordinate space (data space), then apply the child axes data transform to - # go back into display space, but as if we originally plotted the artist on the child axes.... - return orig_transform + self.__mock_trans.inverted() + self.__core_trans + # We apply the original transform to go to display coordinates, then + # apply the parent data transform inverted to go to the parent axes + # coordinate space (data space), then apply the child axes data + # transform to go back into display space, but as if we originally + # plotted the artist on the child axes.... + return ( + orig_transform + self.__mock_trans.inverted() + self.__core_trans + ) - # We copy all of the properties of the renderer we are mocking, so that artists plot themselves as if they were - # placed on the original renderer. + # We copy all of the properties of the renderer we are mocking, so that + # artists plot themselves as if they were placed on the original renderer. @property def height(self): return self.__renderer.get_canvas_width_height()[1] @@ -100,7 +119,8 @@ def get_image_magnification(self): return self.__renderer.get_image_magnification() def _get_text_path_transform(self, x, y, s, prop, angle, ismath): - return self.__renderer._get_text_path_transform(x, y, s, prop, angle, ismath) + return self.__renderer._get_text_path_transform(x, y, s, prop, angle, + ismath) def option_scale_image(self): return False @@ -112,14 +132,17 @@ def flipy(self): return self.__renderer.flipy() # Actual drawing methods below: - def draw_path(self, gc, path: Path, transform: Transform, rgbFace=None): - # Convert the path to display coordinates, but if it was originally drawn on the child axes. + # Convert the path to display coordinates, but if it was originally + # drawn on the child axes. path = path.deepcopy() - path.vertices = self._get_transfer_transform(transform).transform(path.vertices) + path.vertices = self._get_transfer_transform(transform).transform( + path.vertices + ) bbox = self._get_axes_display_box() - # We check if the path intersects the axes box at all, if not don't waste time drawing it. + # We check if the path intersects the axes box at all, if not don't + # waste time drawing it. if(not path.intersects_bbox(bbox, True)): return @@ -132,11 +155,13 @@ def _draw_text_as_path(self, gc, x, y, s: str, prop, angle, ismath): # If the text field is empty, don't even try rendering it... if((s is None) or (s.strip() == "")): return - # Call the super class instance, which works for all cases except one checked above... (Above case causes error) + # Call the super class instance, which works for all cases except one + # checked above... (Above case causes error) super()._draw_text_as_path(gc, x, y, s, prop, angle, ismath) def draw_gouraud_triangle(self, gc, points, colors, transform): - # Pretty much identical to draw_path, transform the points and adjust clip to the child axes bounding box. + # Pretty much identical to draw_path, transform the points and adjust + # clip to the child axes bounding box. points = self._get_transfer_transform(transform).transform(points) path = Path(points, closed=True) bbox = self._get_axes_display_box() @@ -146,35 +171,44 @@ def draw_gouraud_triangle(self, gc, points, colors, transform): gc.set_clip_rectangle(bbox) - self.__renderer.draw_gouraud_triangle(gc, path.vertices, colors, IdentityTransform()) + self.__renderer.draw_gouraud_triangle(gc, path.vertices, colors, + IdentityTransform()) # Images prove to be especially messy to deal with... def draw_image(self, gc, x, y, im, transform=None): mag = self.get_image_magnification() - shift_data_transform = self._get_transfer_transform(IdentityTransform()) + shift_data_transform = self._get_transfer_transform( + IdentityTransform() + ) axes_bbox = self._get_axes_display_box() - # Compute the image bounding box in display coordinates.... Image arrives pre-magnified. + # Compute the image bounding box in display coordinates.... + # Image arrives pre-magnified. img_bbox_disp = Bbox.from_bounds(x, y, im.shape[1], im.shape[0]) - # Now compute the output location, clipping it with the final axes patch. + # Now compute the output location, clipping it with the final axes + # patch. out_box = img_bbox_disp.transformed(shift_data_transform) clipped_out_box = Bbox.intersection(out_box, axes_bbox) if(clipped_out_box is None): return - # We compute what the dimensions of the final output image within the sub-axes are going to be. + # We compute what the dimensions of the final output image within the + # sub-axes are going to be. x, y, out_w, out_h = clipped_out_box.bounds out_w, out_h = int(np.ceil(out_w * mag)), int(np.ceil(out_h * mag)) if((out_w <= 0) or (out_h <= 0)): return - # We can now construct the transform which converts between the original image (a 2D numpy array which starts - # at the origin) to the final zoomed image (also a 2D numpy array which starts at the origin). + # We can now construct the transform which converts between the + # original image (a 2D numpy array which starts at the origin) to the + # final zoomed image. img_trans = ( - Affine2D().scale(1/mag, 1/mag).translate(img_bbox_disp.x0, img_bbox_disp.y0) + Affine2D().scale(1/mag, 1/mag) + .translate(img_bbox_disp.x0, img_bbox_disp.y0) + shift_data_transform - + Affine2D().translate(-clipped_out_box.x0, -clipped_out_box.y0).scale(mag, mag) + + Affine2D().translate(-clipped_out_box.x0, -clipped_out_box.y0) + .scale(mag, mag) ) # We resize and zoom the original image onto the out_arr. @@ -182,7 +216,8 @@ def draw_image(self, gc, x, y, im, transform=None): trans_msk = np.zeros((out_h, out_w), dtype=im.dtype) _image.resample(im, out_arr, img_trans, _image.NEAREST, alpha=1) - _image.resample(im[:, :, 3], trans_msk, img_trans, _image.NEAREST, alpha=1) + _image.resample(im[:, :, 3], trans_msk, img_trans, _image.NEAREST, + alpha=1) out_arr[:, :, 3] = trans_msk gc.set_clip_rectangle(clipped_out_box) @@ -198,14 +233,19 @@ def draw_image(self, gc, x, y, im, transform=None): @docstring.interpd class ZoomViewAxes(Axes): """ - An inset axes which automatically displays elements of the parent axes it is currently placed inside. - Does not require Artists to be plotted twice. + An inset axes which automatically displays elements of the parent axes it + is currently placed inside. Does not require Artists to be plotted twice. """ - MAX_RENDER_DEPTH = 1 # The number of allowed recursions in the draw method. - # Allows for zoom axes to zoom in on zoom axes - - def __init__(self, axes_of_zoom: Axes, rect: Bbox, transform: Optional[Transform] = None, - zorder: int = 5, **kwargs): + MAX_RENDER_DEPTH = 1 # The number of allowed recursions in the draw method + + def __init__( + self, + axes_of_zoom: Axes, + rect: Bbox, + transform: Optional[Transform] = None, + zorder: int = 5, + **kwargs + ): """ Construct a new zoomed inset axes. @@ -218,8 +258,8 @@ def __init__(self, axes_of_zoom: Axes, rect: Bbox, transform: Optional[Transform The bounding box to place this axes in, within the parent axes. transform: `~matplotlib.transforms.Transform` or None - The transform to use when placing this axes in the parent axes. Defaults to - 'axes_of_zoom.transAxes'. + The transform to use when placing this axes in the parent axes. + Defaults to 'axes_of_zoom.transAxes'. zorder: int An integer, the z-order of the axes. Defaults to 5. @@ -240,7 +280,8 @@ def __init__(self, axes_of_zoom: Axes, rect: Bbox, transform: Optional[Transform inset_loc = _TransformedBoundsLocator(rect.bounds, transform) bb = inset_loc(axes_of_zoom, None) - super().__init__(axes_of_zoom.figure, bb.bounds, zorder=zorder, **kwargs) + super().__init__(axes_of_zoom.figure, bb.bounds, zorder=zorder, + **kwargs) self.__zoom_axes = axes_of_zoom self.set_axes_locator(inset_loc) @@ -269,34 +310,46 @@ def draw(self, renderer=None): *self.__zoom_axes.child_axes ] - # Sort all rendered item by their z-order so the render in layers correctly... + # Sort all rendered item by their z-order so the render in layers + # correctly... axes_children.sort(key=lambda obj: obj.get_zorder()) artist_boxes = [] - # We need to temporarily disable the clip boxes of all of the artists, in order to allow us to continue - # rendering them it even if it is outside of the parent axes (they might still be visible in this zoom axes). + # We need to temporarily disable the clip boxes of all of the artists, + # in order to allow us to continue rendering them it even if it is + # outside of the parent axes (they might still be visible in this + # zoom axes). for a in axes_children: artist_boxes.append(a.get_clip_box()) a.set_clip_box(a.get_window_extent(renderer)) # Construct mock renderer and draw all artists to it. - mock_renderer = _TransformRenderer(renderer, self.__zoom_axes.transData, self.transData, self) + mock_renderer = _TransformRenderer( + renderer, self.__zoom_axes.transData, self.transData, self + ) x1, x2 = self.get_xlim() y1, y2 = self.get_ylim() - axes_box = Bbox.from_extents(x1, y1, x2, y2).transformed(self.__zoom_axes.transData) + axes_box = Bbox.from_extents(x1, y1, x2, y2).transformed( + self.__zoom_axes.transData + ) for artist in axes_children: - # If the artist is this or it does not land in the area we are drawing artists from, do not draw it, - # otherwise go ahead. Done to improve performance... - if((artist is not self) and (Bbox.intersection(artist.get_window_extent(renderer), axes_box) is not None)): + if( + (artist is not self) + and ( + Bbox.intersection( + artist.get_window_extent(renderer), axes_box + ) is not None + ) + ): artist.draw(mock_renderer) # Reset all of the artist clip boxes... for a, box in zip(axes_children, artist_boxes): a.set_clip_box(box) - # We need to redraw the splines if enabled, as we have finally drawn everything... This avoids other objects - # being drawn over the splines + # We need to redraw the splines if enabled, as we have finally drawn + # everything... This avoids other objects being drawn over the splines if(self.axison and self._frameon): for spine in self.spines.values(): spine.draw(renderer) From dd47418eb97d355033422df5b18467763f484c02 Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Tue, 28 Dec 2021 01:34:28 -0700 Subject: [PATCH 06/15] Adjust zoom example to use the new method. --- examples/subplots_axes_and_figures/zoom_inset_axes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/subplots_axes_and_figures/zoom_inset_axes.py b/examples/subplots_axes_and_figures/zoom_inset_axes.py index 85f3f78ec6b4..9043d33aaa51 100644 --- a/examples/subplots_axes_and_figures/zoom_inset_axes.py +++ b/examples/subplots_axes_and_figures/zoom_inset_axes.py @@ -27,8 +27,7 @@ def get_demo_image(): ax.imshow(Z2, extent=extent, origin="lower") # inset axes.... -axins = ax.inset_axes([0.5, 0.5, 0.47, 0.47]) -axins.imshow(Z2, extent=extent, origin="lower") +axins = ax.inset_zoom_axes([0.5, 0.5, 0.47, 0.47]) # sub region of the original image x1, x2, y1, y2 = -1.5, -0.9, -2.5, -1.9 axins.set_xlim(x1, x2) @@ -47,6 +46,6 @@ def get_demo_image(): # The use of the following functions, methods, classes and modules is shown # in this example: # -# - `matplotlib.axes.Axes.inset_axes` +# - `matplotlib.axes.Axes.inset_zoom_axes` # - `matplotlib.axes.Axes.indicate_inset_zoom` # - `matplotlib.axes.Axes.imshow` From e45d8639ee8d19a6372d5e1836a29f27c17ab0c7 Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Tue, 28 Dec 2021 03:01:01 -0700 Subject: [PATCH 07/15] Fixes for the example. --- examples/subplots_axes_and_figures/zoom_inset_axes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/subplots_axes_and_figures/zoom_inset_axes.py b/examples/subplots_axes_and_figures/zoom_inset_axes.py index 9043d33aaa51..6f0eccb714ed 100644 --- a/examples/subplots_axes_and_figures/zoom_inset_axes.py +++ b/examples/subplots_axes_and_figures/zoom_inset_axes.py @@ -24,7 +24,7 @@ def get_demo_image(): ny, nx = Z.shape Z2[30:30+ny, 30:30+nx] = Z -ax.imshow(Z2, extent=extent, origin="lower") +ax.imshow(Z2, extent=extent, interpolation='nearest', origin="lower") # inset axes.... axins = ax.inset_zoom_axes([0.5, 0.5, 0.47, 0.47]) From dc84dd9e62fe7c3fc20324d50b54d273ba086c9c Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Tue, 28 Dec 2021 17:15:31 -0700 Subject: [PATCH 08/15] Add support for different interpolation modes. --- lib/matplotlib/axes/_axes.py | 13 ++++++++--- lib/matplotlib/axes/_zoom_axes.py | 36 +++++++++++++++++++++++++------ 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 7e65a792e2ab..1b52fcfbd317 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -30,6 +30,7 @@ import matplotlib.transforms as mtransforms import matplotlib.tri as mtri import matplotlib.units as munits +import matplotlib.image as mimage from matplotlib import _api, _preprocess_data, rcParams from matplotlib.axes._base import ( _AxesBase, _TransformedBoundsLocator, _process_plot_format) @@ -367,8 +368,9 @@ def inset_axes(self, bounds, *, transform=None, zorder=5, **kwargs): return inset_ax - def inset_zoom_axes(self, bounds, *, transform=None, zorder=5, **kwargs): - """ + def inset_zoom_axes(self, bounds, *, transform=None, zorder=5, + image_interpolation="nearest", **kwargs): + f""" Add a child inset Axes to this existing Axes, which automatically plots artists contained within the parent Axes. @@ -386,6 +388,11 @@ def inset_zoom_axes(self, bounds, *, transform=None, zorder=5, **kwargs): to change whether it is above or below data plotted on the parent Axes. + image_interpolation: string + Supported options are: {set(mimage._interpd_)} + The default value is 'nearest'. This determines the interpolation + used when attempting to render a zoomed version of an image. + **kwargs Other keyword arguments are passed on to the child `.Axes`. @@ -400,7 +407,7 @@ def inset_zoom_axes(self, bounds, *, transform=None, zorder=5, **kwargs): """ from ._zoom_axes import ZoomViewAxes return ZoomViewAxes(self, mtransforms.Bbox.from_bounds(*bounds), - transform, zorder, **kwargs) + transform, zorder, image_interpolation, **kwargs) @docstring.dedent_interpd def indicate_inset(self, bounds, inset_ax=None, *, transform=None, diff --git a/lib/matplotlib/axes/_zoom_axes.py b/lib/matplotlib/axes/_zoom_axes.py index f4c04fe5bb1f..82538437c4db 100644 --- a/lib/matplotlib/axes/_zoom_axes.py +++ b/lib/matplotlib/axes/_zoom_axes.py @@ -8,7 +8,7 @@ import matplotlib._image as _image import matplotlib.docstring as docstring import numpy as np - +from matplotlib.image import _interpd_ class _TransformRenderer(RendererBase): """ @@ -16,14 +16,16 @@ class _TransformRenderer(RendererBase): location of plotted elements, and then defers drawing work to the original renderer. """ + def __init__( self, base_renderer: RendererBase, mock_transform: Transform, transform: Transform, - bounding_axes: Axes + bounding_axes: Axes, + image_interpolation: str = "nearest" ): - """ + f""" Constructs a new TransformRender. Parameters @@ -47,6 +49,11 @@ def __init__( bounding_axes: `~matplotlib.axes.Axes` The axes to plot everything within. Everything outside of this axes will be clipped. + + image_interpolation: string + Supported options are: {set(_interpd_)} + The default value is 'nearest'. This determines the interpolation + used when attempting to render a zoomed version of an image. Returns ------- @@ -59,6 +66,13 @@ def __init__( self.__core_trans = transform self.__bounding_axes = bounding_axes + try: + self.__img_inter = _interpd_[image_interpolation.lower()] + except KeyError: + raise ValueError( + f"Invalid Interpolation Mode: {image_interpolation}" + ) + def _get_axes_display_box(self) -> Bbox: """ Private method, get the bounding box of the child axes in display @@ -215,8 +229,8 @@ def draw_image(self, gc, x, y, im, transform=None): out_arr = np.zeros((out_h, out_w, im.shape[2]), dtype=im.dtype) trans_msk = np.zeros((out_h, out_w), dtype=im.dtype) - _image.resample(im, out_arr, img_trans, _image.NEAREST, alpha=1) - _image.resample(im[:, :, 3], trans_msk, img_trans, _image.NEAREST, + _image.resample(im, out_arr, img_trans, self.__img_inter, alpha=1) + _image.resample(im[:, :, 3], trans_msk, img_trans, self.__img_inter, alpha=1) out_arr[:, :, 3] = trans_msk @@ -244,9 +258,10 @@ def __init__( rect: Bbox, transform: Optional[Transform] = None, zorder: int = 5, + image_interpolation: str = "nearest", **kwargs ): - """ + f""" Construct a new zoomed inset axes. Parameters @@ -264,6 +279,11 @@ def __init__( zorder: int An integer, the z-order of the axes. Defaults to 5. + image_interpolation: string + Supported options are: {set(_interpd_)} + The default value is 'nearest'. This determines the interpolation + used when attempting to render a zoomed version of an image. + **kwargs Other optional keyword arguments: @@ -284,6 +304,7 @@ def __init__( **kwargs) self.__zoom_axes = axes_of_zoom + self.__image_interpolation = image_interpolation self.set_axes_locator(inset_loc) self._render_depth = 0 @@ -325,7 +346,8 @@ def draw(self, renderer=None): # Construct mock renderer and draw all artists to it. mock_renderer = _TransformRenderer( - renderer, self.__zoom_axes.transData, self.transData, self + renderer, self.__zoom_axes.transData, self.transData, self, + self.__image_interpolation ) x1, x2 = self.get_xlim() y1, y2 = self.get_ylim() From 22d8fbcbc4f0f8e1198eb7c5e7db2214ac8258c4 Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Tue, 28 Dec 2021 17:23:20 -0700 Subject: [PATCH 09/15] Fix docstrings... --- lib/matplotlib/axes/_axes.py | 11 +++++++---- lib/matplotlib/axes/_zoom_axes.py | 22 ++++++++++++++-------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 1b52fcfbd317..e81820ee4083 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -370,7 +370,7 @@ def inset_axes(self, bounds, *, transform=None, zorder=5, **kwargs): def inset_zoom_axes(self, bounds, *, transform=None, zorder=5, image_interpolation="nearest", **kwargs): - f""" + """ Add a child inset Axes to this existing Axes, which automatically plots artists contained within the parent Axes. @@ -389,9 +389,12 @@ def inset_zoom_axes(self, bounds, *, transform=None, zorder=5, parent Axes. image_interpolation: string - Supported options are: {set(mimage._interpd_)} - The default value is 'nearest'. This determines the interpolation - used when attempting to render a zoomed version of an image. + Supported options are 'antialiased', 'nearest', 'bilinear', + 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', + 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', + 'sinc', 'lanczos', or 'none'. The default value is 'nearest'. This + determines the interpolation used when attempting to render a + zoomed version of an image. **kwargs Other keyword arguments are passed on to the child `.Axes`. diff --git a/lib/matplotlib/axes/_zoom_axes.py b/lib/matplotlib/axes/_zoom_axes.py index 82538437c4db..11ae1653e60a 100644 --- a/lib/matplotlib/axes/_zoom_axes.py +++ b/lib/matplotlib/axes/_zoom_axes.py @@ -25,7 +25,7 @@ def __init__( bounding_axes: Axes, image_interpolation: str = "nearest" ): - f""" + """ Constructs a new TransformRender. Parameters @@ -51,9 +51,12 @@ def __init__( axes will be clipped. image_interpolation: string - Supported options are: {set(_interpd_)} - The default value is 'nearest'. This determines the interpolation - used when attempting to render a zoomed version of an image. + Supported options are 'antialiased', 'nearest', 'bilinear', + 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', + 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', + 'sinc', 'lanczos', or 'none'. The default value is 'nearest'. This + determines the interpolation used when attempting to render a + zoomed version of an image. Returns ------- @@ -261,7 +264,7 @@ def __init__( image_interpolation: str = "nearest", **kwargs ): - f""" + """ Construct a new zoomed inset axes. Parameters @@ -280,9 +283,12 @@ def __init__( An integer, the z-order of the axes. Defaults to 5. image_interpolation: string - Supported options are: {set(_interpd_)} - The default value is 'nearest'. This determines the interpolation - used when attempting to render a zoomed version of an image. + Supported options are 'antialiased', 'nearest', 'bilinear', + 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', + 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', + 'sinc', 'lanczos', or 'none'. The default value is 'nearest'. This + determines the interpolation used when attempting to render a + zoomed version of an image. **kwargs Other optional keyword arguments: From 9fef3b0aefac654aed57d560d53efcdbae521f90 Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Tue, 28 Dec 2021 22:22:49 -0700 Subject: [PATCH 10/15] Add description of new feature... --- .../next_whats_new/autoplot_inset_axes.rst | 25 +++++++++++++++++++ lib/matplotlib/axes/_axes.py | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 doc/users/next_whats_new/autoplot_inset_axes.rst diff --git a/doc/users/next_whats_new/autoplot_inset_axes.rst b/doc/users/next_whats_new/autoplot_inset_axes.rst new file mode 100644 index 000000000000..9632b8949358 --- /dev/null +++ b/doc/users/next_whats_new/autoplot_inset_axes.rst @@ -0,0 +1,25 @@ +Addition of an inset Axes with automatic zoom plotting +------------------------------------------------------ + +It is now possible to create an inset axes that is a zoom-in on a region in +the parent axes without needing to replot all items a second time, using the +`~matplotlib.axes.Axes.inset_zoom_axes` method of the +`~matplotlib.axes.Axes` class. Arguments for this method are backwards +compatible with the `~matplotlib.axes.Axes.inset_axes` method. + +.. plot:: + :include-source: true + + import matplotlib.pyplot as plt + import numpy as np + np.random.seed(1) + fig = plt.figure() + ax = fig.gca() + ax.plot([i for i in range(10)], "r") + ax.text(3, 2.5, "Hello World!", ha="center") + ax.imshow(np.random.rand(30, 30), origin="lower", cmap="Blues", alpha=0.5) + axins = ax.inset_zoom_axes([0.5, 0.5, 0.48, 0.48]) + axins.set_xlim(1, 5) + axins.set_ylim(1, 5) + ax.indicate_inset_zoom(axins, edgecolor="black") + plt.show() diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index e81820ee4083..e7f6b2c9c1b9 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -406,7 +406,7 @@ def inset_zoom_axes(self, bounds, *, transform=None, zorder=5, Examples -------- - See `~.axes.Axes.inset_zoom` method for examples. + See `Axes.inset_axes` method for examples. """ from ._zoom_axes import ZoomViewAxes return ZoomViewAxes(self, mtransforms.Bbox.from_bounds(*bounds), From c03e8695df63542f963d3facd944aeca48234680 Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Tue, 28 Dec 2021 23:16:17 -0700 Subject: [PATCH 11/15] Make flake8 compatible. --- lib/matplotlib/axes/_axes.py | 1 - lib/matplotlib/axes/_zoom_axes.py | 13 +++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index e7f6b2c9c1b9..d4ec47e021b7 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -30,7 +30,6 @@ import matplotlib.transforms as mtransforms import matplotlib.tri as mtri import matplotlib.units as munits -import matplotlib.image as mimage from matplotlib import _api, _preprocess_data, rcParams from matplotlib.axes._base import ( _AxesBase, _TransformedBoundsLocator, _process_plot_format) diff --git a/lib/matplotlib/axes/_zoom_axes.py b/lib/matplotlib/axes/_zoom_axes.py index 11ae1653e60a..1e2c4cc3f3df 100644 --- a/lib/matplotlib/axes/_zoom_axes.py +++ b/lib/matplotlib/axes/_zoom_axes.py @@ -10,6 +10,7 @@ import numpy as np from matplotlib.image import _interpd_ + class _TransformRenderer(RendererBase): """ A matplotlib renderer which performs transforms to change the final @@ -49,7 +50,7 @@ def __init__( bounding_axes: `~matplotlib.axes.Axes` The axes to plot everything within. Everything outside of this axes will be clipped. - + image_interpolation: string Supported options are 'antialiased', 'nearest', 'bilinear', 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', @@ -283,11 +284,11 @@ def __init__( An integer, the z-order of the axes. Defaults to 5. image_interpolation: string - Supported options are 'antialiased', 'nearest', 'bilinear', - 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', - 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', - 'sinc', 'lanczos', or 'none'. The default value is 'nearest'. This - determines the interpolation used when attempting to render a + Supported options are 'antialiased', 'nearest', 'bilinear', + 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', + 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', + 'sinc', 'lanczos', or 'none'. The default value is 'nearest'. This + determines the interpolation used when attempting to render a zoomed version of an image. **kwargs From e8b5e39c4d266e27154aa3d6b043b29f92fbbdbc Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Wed, 29 Dec 2021 10:29:55 -0700 Subject: [PATCH 12/15] Add unit testing... --- lib/matplotlib/tests/test_axes.py | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 5da2df1455db..70654b2ac2cc 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6415,6 +6415,40 @@ def test_zoom_inset(): axin1.get_position().get_points(), xx, rtol=1e-4) +# Tolerance needed because the way the auto-zoom axes handles images is +# entirely different, leading to a slightly different result. +@check_figures_equal(tol=3) +def test_auto_zoom_inset(fig_test, fig_ref): + np.random.seed(1) + im_data = np.random.rand(30, 30) + + # Test Case... + ax_test = fig_test.gca() + ax_test.plot([i for i in range(10)], "r") + ax_test.add_patch(plt.Circle((3, 3), 1, ec="black", fc="blue")) + ax_test.imshow(im_data, origin="lower", cmap="Blues", alpha=0.5, + interpolation="nearest") + axins_test = ax_test.inset_zoom_axes([0.5, 0.5, 0.48, 0.48]) + axins_test.set_xlim(1, 5) + axins_test.set_ylim(1, 5) + ax_test.indicate_inset_zoom(axins_test, edgecolor="black") + + # Reference + ax_ref = fig_ref.gca() + ax_ref.plot([i for i in range(10)], "r") + ax_ref.add_patch(plt.Circle((3, 3), 1, ec="black", fc="blue")) + ax_ref.imshow(im_data, origin="lower", cmap="Blues", alpha=0.5, + interpolation="nearest") + axins_ref = ax_ref.inset_axes([0.5, 0.5, 0.48, 0.48]) + axins_ref.set_xlim(1, 5) + axins_ref.set_ylim(1, 5) + axins_ref.plot([i for i in range(10)], "r") + axins_ref.add_patch(plt.Circle((3, 3), 1, ec="black", fc="blue")) + axins_ref.imshow(im_data, origin="lower", cmap="Blues", alpha=0.5, + interpolation="nearest") + ax_ref.indicate_inset_zoom(axins_ref, edgecolor="black") + + @pytest.mark.parametrize('x_inverted', [False, True]) @pytest.mark.parametrize('y_inverted', [False, True]) def test_indicate_inset_inverted(x_inverted, y_inverted): From 0f2fe46d69765e026794e91674542ee3d250b596 Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Wed, 29 Dec 2021 17:25:36 -0700 Subject: [PATCH 13/15] Scale line widths for things seen through the ViewAxes --- .../next_whats_new/autoplot_inset_axes.rst | 2 +- lib/matplotlib/axes/_axes.py | 15 +- lib/matplotlib/axes/_zoom_axes.py | 131 ++++++++++++------ lib/matplotlib/tests/test_axes.py | 1 + 4 files changed, 101 insertions(+), 48 deletions(-) diff --git a/doc/users/next_whats_new/autoplot_inset_axes.rst b/doc/users/next_whats_new/autoplot_inset_axes.rst index 9632b8949358..433b85e9366b 100644 --- a/doc/users/next_whats_new/autoplot_inset_axes.rst +++ b/doc/users/next_whats_new/autoplot_inset_axes.rst @@ -15,7 +15,7 @@ compatible with the `~matplotlib.axes.Axes.inset_axes` method. np.random.seed(1) fig = plt.figure() ax = fig.gca() - ax.plot([i for i in range(10)], "r") + ax.plot([i for i in range(10)], "r-o") ax.text(3, 2.5, "Hello World!", ha="center") ax.imshow(np.random.rand(30, 30), origin="lower", cmap="Blues", alpha=0.5) axins = ax.inset_zoom_axes([0.5, 0.5, 0.48, 0.48]) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index d4ec47e021b7..1ef19888bda9 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -407,9 +407,18 @@ def inset_zoom_axes(self, bounds, *, transform=None, zorder=5, -------- See `Axes.inset_axes` method for examples. """ - from ._zoom_axes import ZoomViewAxes - return ZoomViewAxes(self, mtransforms.Bbox.from_bounds(*bounds), - transform, zorder, image_interpolation, **kwargs) + if(transform is None): + transform = self.transAxes + + inset_loc = _TransformedBoundsLocator(bounds, transform) + bb = inset_loc(self, None) + + from ._zoom_axes import ViewAxes + axin = ViewAxes(self, bb.bounds, zorder, image_interpolation, **kwargs) + axin.set_axes_locator(inset_loc) + self.add_child_axes(axin) + + return axin @docstring.dedent_interpd def indicate_inset(self, bounds, inset_ax=None, *, transform=None, diff --git a/lib/matplotlib/axes/_zoom_axes.py b/lib/matplotlib/axes/_zoom_axes.py index 1e2c4cc3f3df..1299d50daecd 100644 --- a/lib/matplotlib/axes/_zoom_axes.py +++ b/lib/matplotlib/axes/_zoom_axes.py @@ -1,10 +1,7 @@ -from typing import Optional - from matplotlib.path import Path from matplotlib.axes import Axes -from matplotlib.axes._axes import _TransformedBoundsLocator from matplotlib.transforms import Bbox, Transform, IdentityTransform, Affine2D -from matplotlib.backend_bases import RendererBase +from matplotlib.backend_bases import RendererBase, GraphicsContextBase import matplotlib._image as _image import matplotlib.docstring as docstring import numpy as np @@ -24,7 +21,8 @@ def __init__( mock_transform: Transform, transform: Transform, bounding_axes: Axes, - image_interpolation: str = "nearest" + image_interpolation: str = "nearest", + scale_linewidths: bool = True ): """ Constructs a new TransformRender. @@ -59,6 +57,10 @@ def __init__( determines the interpolation used when attempting to render a zoomed version of an image. + scale_linewidths: bool, default is True + Specifies if line widths should be scaled, in addition to the + paths themselves. + Returns ------- `~._zoom_axes._TransformRenderer` @@ -69,6 +71,7 @@ def __init__( self.__mock_trans = mock_transform self.__core_trans = transform self.__bounding_axes = bounding_axes + self.__scale_widths = scale_linewidths try: self.__img_inter = _interpd_[image_interpolation.lower()] @@ -77,6 +80,23 @@ def __init__( f"Invalid Interpolation Mode: {image_interpolation}" ) + def _scale_gc( + self, + gc: GraphicsContextBase + ) -> GraphicsContextBase: + transfer_transform = self._get_transfer_transform(IdentityTransform()) + new_gc = self.__renderer.new_gc() + new_gc.copy_properties(gc) + + unit_box = Bbox.from_bounds(0, 0, 1, 1) + unit_box = transfer_transform.transform_bbox(unit_box) + mult_factor = np.sqrt(unit_box.width * unit_box.height) + + new_gc.set_linewidth(gc.get_linewidth() * mult_factor) + new_gc._hatch_linewidth = gc.get_hatch_linewidth() * mult_factor + + return new_gc + def _get_axes_display_box(self) -> Bbox: """ Private method, get the bounding box of the child axes in display @@ -149,6 +169,9 @@ def points_to_pixels(self, points): def flipy(self): return self.__renderer.flipy() + def new_gc(self): + return self.__renderer.new_gc() + # Actual drawing methods below: def draw_path(self, gc, path: Path, transform: Transform, rgbFace=None): # Convert the path to display coordinates, but if it was originally @@ -164,6 +187,9 @@ def draw_path(self, gc, path: Path, transform: Transform, rgbFace=None): if(not path.intersects_bbox(bbox, True)): return + if(self.__scale_widths): + gc = self._scale_gc(gc) + # Change the clip to the sub-axes box gc.set_clip_rectangle(bbox) @@ -173,6 +199,7 @@ def _draw_text_as_path(self, gc, x, y, s: str, prop, angle, ismath): # If the text field is empty, don't even try rendering it... if((s is None) or (s.strip() == "")): return + # Call the super class instance, which works for all cases except one # checked above... (Above case causes error) super()._draw_text_as_path(gc, x, y, s, prop, angle, ismath) @@ -187,6 +214,9 @@ def draw_gouraud_triangle(self, gc, points, colors, transform): if(not path.intersects_bbox(bbox, True)): return + if(self.__scale_widths): + gc = self._scale_gc(gc) + gc.set_clip_rectangle(bbox) self.__renderer.draw_gouraud_triangle(gc, path.vertices, colors, @@ -238,6 +268,9 @@ def draw_image(self, gc, x, y, im, transform=None): alpha=1) out_arr[:, :, 3] = trans_msk + if(self.__scale_widths): + gc = self._scale_gc(gc) + gc.set_clip_rectangle(clipped_out_box) x, y = clipped_out_box.x0, clipped_out_box.y0 @@ -249,36 +282,31 @@ def draw_image(self, gc, x, y, im, transform=None): @docstring.interpd -class ZoomViewAxes(Axes): +class ViewAxes(Axes): """ - An inset axes which automatically displays elements of the parent axes it - is currently placed inside. Does not require Artists to be plotted twice. + An axes which automatically displays elements of another axes. Does not + require Artists to be plotted twice. """ MAX_RENDER_DEPTH = 1 # The number of allowed recursions in the draw method def __init__( self, - axes_of_zoom: Axes, - rect: Bbox, - transform: Optional[Transform] = None, - zorder: int = 5, - image_interpolation: str = "nearest", + axes_to_view, + rect, + zorder=5, + image_interpolation="nearest", **kwargs ): """ - Construct a new zoomed inset axes. + Construct a new view axes. Parameters ---------- - axes_of_zoom: `~.axes.Axes` + axes_to_view: `~.axes.Axes` The axes to zoom in on which this axes will be nested inside. - rect: `~matplotlib.transforms.Bbox` - The bounding box to place this axes in, within the parent axes. - - transform: `~matplotlib.transforms.Transform` or None - The transform to use when placing this axes in the parent axes. - Defaults to 'axes_of_zoom.transAxes'. + rect: [left, bottom, width, height] + The Axes is built in the rectangle *rect*. zorder: int An integer, the z-order of the axes. Defaults to 5. @@ -289,7 +317,7 @@ def __init__( 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', or 'none'. The default value is 'nearest'. This determines the interpolation used when attempting to render a - zoomed version of an image. + view of an image. **kwargs Other optional keyword arguments: @@ -301,22 +329,13 @@ def __init__( `~.axes.ZoomViewAxes` The new zoom view axes instance... """ - if(transform is None): - transform = axes_of_zoom.transAxes - - inset_loc = _TransformedBoundsLocator(rect.bounds, transform) - bb = inset_loc(axes_of_zoom, None) - - super().__init__(axes_of_zoom.figure, bb.bounds, zorder=zorder, + super().__init__(axes_to_view.figure, rect, zorder=zorder, **kwargs) - self.__zoom_axes = axes_of_zoom + self.__view_axes = axes_to_view self.__image_interpolation = image_interpolation - self.set_axes_locator(inset_loc) - self._render_depth = 0 - - axes_of_zoom.add_child_axes(self) + self.__scale_lines = True def draw(self, renderer=None): if(self._render_depth >= self.MAX_RENDER_DEPTH): @@ -329,13 +348,13 @@ def draw(self, renderer=None): return axes_children = [ - *self.__zoom_axes.collections, - *self.__zoom_axes.patches, - *self.__zoom_axes.lines, - *self.__zoom_axes.texts, - *self.__zoom_axes.artists, - *self.__zoom_axes.images, - *self.__zoom_axes.child_axes + *self.__view_axes.collections, + *self.__view_axes.patches, + *self.__view_axes.lines, + *self.__view_axes.texts, + *self.__view_axes.artists, + *self.__view_axes.images, + *self.__view_axes.child_axes ] # Sort all rendered item by their z-order so the render in layers @@ -353,13 +372,13 @@ def draw(self, renderer=None): # Construct mock renderer and draw all artists to it. mock_renderer = _TransformRenderer( - renderer, self.__zoom_axes.transData, self.transData, self, - self.__image_interpolation + renderer, self.__view_axes.transData, self.transData, self, + self.__image_interpolation, self.__scale_lines ) x1, x2 = self.get_xlim() y1, y2 = self.get_ylim() axes_box = Bbox.from_extents(x1, y1, x2, y2).transformed( - self.__zoom_axes.transData + self.__view_axes.transData ) for artist in axes_children: @@ -384,3 +403,27 @@ def draw(self, renderer=None): spine.draw(renderer) self._render_depth -= 1 + + def get_linescaling(self) -> bool: + """ + Get if line width scaling is enabled. + + Returns + ------- + bool + If line width scaling is enabled returns True, otherwise False. + """ + return self.__scale_lines + + def set_linescaling(self, value: bool): + """ + Set whether line widths should be scaled when rendering a view of an + axes. + + Parameters + ---------- + value: bool + If true, scale line widths in the view to match zoom level. + Otherwise don't. + """ + self.__scale_lines = value diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 70654b2ac2cc..425912185f97 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6429,6 +6429,7 @@ def test_auto_zoom_inset(fig_test, fig_ref): ax_test.imshow(im_data, origin="lower", cmap="Blues", alpha=0.5, interpolation="nearest") axins_test = ax_test.inset_zoom_axes([0.5, 0.5, 0.48, 0.48]) + axins_test.set_linescaling(False) axins_test.set_xlim(1, 5) axins_test.set_ylim(1, 5) ax_test.indicate_inset_zoom(axins_test, edgecolor="black") From 0371cabaad961153ad0e04bec3afb5ee47c2f490 Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Fri, 31 Dec 2021 23:52:52 -0700 Subject: [PATCH 14/15] Remove type hints to match the rest of the codebase. --- lib/matplotlib/axes/_zoom_axes.py | 33 ++++++++++++++----------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/lib/matplotlib/axes/_zoom_axes.py b/lib/matplotlib/axes/_zoom_axes.py index 1299d50daecd..04f9d8534c8a 100644 --- a/lib/matplotlib/axes/_zoom_axes.py +++ b/lib/matplotlib/axes/_zoom_axes.py @@ -1,7 +1,7 @@ from matplotlib.path import Path from matplotlib.axes import Axes -from matplotlib.transforms import Bbox, Transform, IdentityTransform, Affine2D -from matplotlib.backend_bases import RendererBase, GraphicsContextBase +from matplotlib.transforms import Bbox, IdentityTransform, Affine2D +from matplotlib.backend_bases import RendererBase import matplotlib._image as _image import matplotlib.docstring as docstring import numpy as np @@ -17,12 +17,12 @@ class _TransformRenderer(RendererBase): def __init__( self, - base_renderer: RendererBase, - mock_transform: Transform, - transform: Transform, - bounding_axes: Axes, - image_interpolation: str = "nearest", - scale_linewidths: bool = True + base_renderer, + mock_transform, + transform, + bounding_axes, + image_interpolation="nearest", + scale_linewidths=True ): """ Constructs a new TransformRender. @@ -80,10 +80,7 @@ def __init__( f"Invalid Interpolation Mode: {image_interpolation}" ) - def _scale_gc( - self, - gc: GraphicsContextBase - ) -> GraphicsContextBase: + def _scale_gc(self, gc): transfer_transform = self._get_transfer_transform(IdentityTransform()) new_gc = self.__renderer.new_gc() new_gc.copy_properties(gc) @@ -97,7 +94,7 @@ def _scale_gc( return new_gc - def _get_axes_display_box(self) -> Bbox: + def _get_axes_display_box(self): """ Private method, get the bounding box of the child axes in display coordinates. @@ -106,7 +103,7 @@ def _get_axes_display_box(self) -> Bbox: self.__bounding_axes.transAxes ) - def _get_transfer_transform(self, orig_transform: Transform) -> Transform: + def _get_transfer_transform(self, orig_transform): """ Private method, returns the transform which translates and scales coordinates as if they were originally plotted on the child axes @@ -173,7 +170,7 @@ def new_gc(self): return self.__renderer.new_gc() # Actual drawing methods below: - def draw_path(self, gc, path: Path, transform: Transform, rgbFace=None): + def draw_path(self, gc, path, transform, rgbFace=None): # Convert the path to display coordinates, but if it was originally # drawn on the child axes. path = path.deepcopy() @@ -195,7 +192,7 @@ def draw_path(self, gc, path: Path, transform: Transform, rgbFace=None): self.__renderer.draw_path(gc, path, IdentityTransform(), rgbFace) - def _draw_text_as_path(self, gc, x, y, s: str, prop, angle, ismath): + def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath): # If the text field is empty, don't even try rendering it... if((s is None) or (s.strip() == "")): return @@ -404,7 +401,7 @@ def draw(self, renderer=None): self._render_depth -= 1 - def get_linescaling(self) -> bool: + def get_linescaling(self): """ Get if line width scaling is enabled. @@ -415,7 +412,7 @@ def get_linescaling(self) -> bool: """ return self.__scale_lines - def set_linescaling(self, value: bool): + def set_linescaling(self, value): """ Set whether line widths should be scaled when rendering a view of an axes. From 6ece088cc9b1d26887effb46fc7f43656607b08c Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Sat, 1 Jan 2022 09:08:36 -0700 Subject: [PATCH 15/15] Fix typos... --- lib/matplotlib/axes/_zoom_axes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axes/_zoom_axes.py b/lib/matplotlib/axes/_zoom_axes.py index 04f9d8534c8a..a654ac452705 100644 --- a/lib/matplotlib/axes/_zoom_axes.py +++ b/lib/matplotlib/axes/_zoom_axes.py @@ -354,7 +354,7 @@ def draw(self, renderer=None): *self.__view_axes.child_axes ] - # Sort all rendered item by their z-order so the render in layers + # Sort all rendered items by their z-order so they render in layers # correctly... axes_children.sort(key=lambda obj: obj.get_zorder()) @@ -394,7 +394,7 @@ def draw(self, renderer=None): a.set_clip_box(box) # We need to redraw the splines if enabled, as we have finally drawn - # everything... This avoids other objects being drawn over the splines + # everything... This avoids other objects being drawn over the splines. if(self.axison and self._frameon): for spine in self.spines.values(): spine.draw(renderer)