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) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy