From 66e02137fea4e89cbc8c141294bb6958e1ad78a4 Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Tue, 16 Aug 2022 08:48:32 -0400 Subject: [PATCH 01/27] Fix typo in docs. --- matplotview/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matplotview/__init__.py b/matplotview/__init__.py index 7f5bc07..19c0c37 100644 --- a/matplotview/__init__.py +++ b/matplotview/__init__.py @@ -89,7 +89,7 @@ def stop_viewing(view: Axes, axes_of_viewing: Axes) -> Axes: Parameters ---------- view: Axes - The axes the is currently viewing the `axes_of_viewing`... + The axes that is currently viewing the `axes_of_viewing`... axes_of_viewing: Axes The axes that the view should stop viewing. From f7e1e587f27fe138a93406e4da542784d949a7df Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Tue, 16 Aug 2022 09:11:58 -0400 Subject: [PATCH 02/27] Add test to test stop_viewing... --- matplotview/_docs.py | 3 +-- matplotview/tests/test_view_rendering.py | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/matplotview/_docs.py b/matplotview/_docs.py index ec5a324..acea1f2 100644 --- a/matplotview/_docs.py +++ b/matplotview/_docs.py @@ -1,11 +1,10 @@ import inspect -from inspect import signature def dynamic_doc_string(**kwargs): def convert(func): default_vals = { - k: v.default for k, v in signature(func).parameters.items() + k: v.default for k, v in inspect.signature(func).parameters.items() if(v.default is not inspect.Parameter.empty) } default_vals.update(kwargs) diff --git a/matplotview/tests/test_view_rendering.py b/matplotview/tests/test_view_rendering.py index 0e3ea66..03b9547 100644 --- a/matplotview/tests/test_view_rendering.py +++ b/matplotview/tests/test_view_rendering.py @@ -1,7 +1,7 @@ import numpy as np import matplotlib.pyplot as plt from matplotlib.testing.decorators import check_figures_equal -from matplotview import view, inset_zoom_axes +from matplotview import view, inset_zoom_axes, stop_viewing @check_figures_equal(tol=6) @@ -219,3 +219,24 @@ def test_double_view(fig_test, fig_ref): ax.set_aspect(1) ax.relim() ax.autoscale_view() + + +@check_figures_equal() +def test_stop_viewing(fig_test, fig_ref): + np.random.seed(1) + data = np.random.randint(0, 10, 10) + + # Test case... Create a view and stop it... + ax1_test, ax2_test = fig_test.subplots(1, 2) + + ax1_test.plot(data) + ax1_test.text(0.5, 0.5, "Hello") + + view(ax2_test, ax1_test) + stop_viewing(ax2_test, ax1_test) + + # Reference, just don't plot anything at all in the second axes... + ax1_ref, ax2_ref = fig_ref.subplots(1, 2) + + ax1_ref.plot(data) + ax1_ref.text(0.5, 0.5, "Hello") From 3b754b52a35e12e65e23f1344d75dd234e7661f7 Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Fri, 2 Sep 2022 14:07:34 -0400 Subject: [PATCH 03/27] Remove depreciated doc decorator. --- matplotview/_view_axes.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/matplotview/_view_axes.py b/matplotview/_view_axes.py index 91c5e4b..c9162ea 100644 --- a/matplotview/_view_axes.py +++ b/matplotview/_view_axes.py @@ -3,7 +3,6 @@ from typing import Type, List, Optional, Any, Set, Dict, Union from matplotlib.axes import Axes from matplotlib.transforms import Bbox -import matplotlib.docstring as docstring from matplotview._transform_renderer import _TransformRenderer from matplotlib.artist import Artist from matplotlib.backend_bases import RendererBase @@ -166,7 +165,6 @@ def view_wrapper(axes_class: Type[Axes]) -> Type[Axes]: if(issubclass(axes_class, Axes) and issubclass(axes_class, __ViewType)): return axes_class - @docstring.interpd class View(axes_class, __ViewType): """ An axes which automatically displays elements of another axes. Does not @@ -197,9 +195,7 @@ def __init__( **kwargs Other optional keyword arguments supported by the Axes - constructor this ViewAxes wraps: - - %(Axes:kwdoc)s + constructor this ViewAxes wraps. Returns ------- From 2e590e776719dfd7ee7d71941b6b2ddb2bb6939e Mon Sep 17 00:00:00 2001 From: LGTM Migrator Date: Thu, 10 Nov 2022 10:49:50 +0000 Subject: [PATCH 04/27] Add CodeQL workflow for GitHub code scanning --- .github/workflows/codeql.yml | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..6135e19 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,41 @@ +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: "15 9 * * 3" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ python ] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{ matrix.language }}" From 5b7da5f8a169e10afa1f50a2d88bd3f61559a8e4 Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Thu, 2 Mar 2023 10:06:50 -0500 Subject: [PATCH 05/27] Update and fix tests for 3.7.0. --- matplotview/tests/test_view_obj.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/matplotview/tests/test_view_obj.py b/matplotview/tests/test_view_obj.py index 6210e63..2562188 100644 --- a/matplotview/tests/test_view_obj.py +++ b/matplotview/tests/test_view_obj.py @@ -8,6 +8,9 @@ def test_obj_comparison(): from matplotlib.axes import Subplot, Axes + import matplotlib + + mpl_version = tuple(int(v) for v in matplotlib.__version__.split(".")) view_class1 = view_wrapper(Subplot) view_class2 = view_wrapper(Subplot) @@ -15,7 +18,11 @@ def test_obj_comparison(): assert view_class1 is view_class2 assert view_class1 == view_class2 - assert view_class2 != view_class3 + if(mpl_version >= (3, 7, 0)): + # As of 3.7.0, the subplot class no long exists, and is an alias to the Axes class... + assert view_class2 == view_class3 + else: + assert view_class2 != view_class3 @check_figures_equal(tol=5.6) From 471f3e8aa3037d86408180bdb7f4348bce929d7c Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Wed, 12 Jul 2023 09:01:53 -0400 Subject: [PATCH 06/27] Mitigate to readthedocs v2. --- .readthedocs.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..e88e6c7 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,13 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: docs/conf.py + +python: + install: + - requirements: docs/requirements.txt From b6f03a1e5861d36d526882c5cdcb1127b370c90a Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Tue, 5 Sep 2023 22:20:09 -0400 Subject: [PATCH 07/27] Fix testing to properly support rc releases of matplotlib. Remove depreciated apis from tests. --- matplotview/tests/test_view_obj.py | 6 +++--- matplotview/tests/utils.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/matplotview/tests/test_view_obj.py b/matplotview/tests/test_view_obj.py index 2562188..5eb6067 100644 --- a/matplotview/tests/test_view_obj.py +++ b/matplotview/tests/test_view_obj.py @@ -10,7 +10,7 @@ def test_obj_comparison(): from matplotlib.axes import Subplot, Axes import matplotlib - mpl_version = tuple(int(v) for v in matplotlib.__version__.split(".")) + mpl_version = tuple(int(v) for v in matplotlib.__version__.split(".")[:2]) view_class1 = view_wrapper(Subplot) view_class2 = view_wrapper(Subplot) @@ -18,8 +18,8 @@ def test_obj_comparison(): assert view_class1 is view_class2 assert view_class1 == view_class2 - if(mpl_version >= (3, 7, 0)): - # As of 3.7.0, the subplot class no long exists, and is an alias to the Axes class... + if(mpl_version >= (3, 7)): + # As of 3.7.0, the subplot class no longer exists, and is an alias to the Axes class... assert view_class2 == view_class3 else: assert view_class2 != view_class3 diff --git a/matplotview/tests/utils.py b/matplotview/tests/utils.py index 812eb84..8ca3f56 100644 --- a/matplotview/tests/utils.py +++ b/matplotview/tests/utils.py @@ -4,8 +4,8 @@ def figure_to_image(figure): figure.canvas.draw() - img = np.frombuffer(figure.canvas.tostring_rgb(), dtype=np.uint8) - return img.reshape(figure.canvas.get_width_height()[::-1] + (3,)) + img = np.frombuffer(figure.canvas.buffer_rgba(), dtype=np.uint8) + return img.reshape(figure.canvas.get_width_height()[::-1] + (4,))[..., :3] def matches_post_pickle(figure): From dab9d62cbfdafaee1a02b8dd548e574b75f16394 Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Tue, 5 Sep 2023 22:46:07 -0400 Subject: [PATCH 08/27] Update workflows to test against same python versions as matplotlib. --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index f4bda9b..01d8516 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ["3.7", "3.8", "3.9"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 From 81a363558e9742213175fdddf588a580c948d815 Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Wed, 6 Sep 2023 08:43:14 -0400 Subject: [PATCH 09/27] Update workflows to test against same python versions as matplotlib. --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 01d8516..eed6181 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -61,7 +61,7 @@ jobs: runs-on: windows-latest strategy: matrix: - python-version: [ "3.7", "3.8", "3.9" ] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 From e23934cca6104344ce1238c094b27a65d87a613b Mon Sep 17 00:00:00 2001 From: Isaac Robinson Date: Wed, 6 Sep 2023 08:46:48 -0400 Subject: [PATCH 10/27] Update workflows to test against same python versions as matplotlib. --- .github/workflows/pytest.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index eed6181..83075c8 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 @@ -61,7 +61,7 @@ jobs: runs-on: windows-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 From 6e3bca8b1834f39bb3bfe2d4ebdc98f8373c45b3 Mon Sep 17 00:00:00 2001 From: isaacrobinson2000 Date: Fri, 15 Dec 2023 11:28:41 -0500 Subject: [PATCH 11/27] PEP-8 Compliance, support for latest matplotlib version (3.8) --- matplotview/_docs.py | 4 ++-- matplotview/_transform_renderer.py | 28 ++++++++++++++-------------- matplotview/_view_axes.py | 29 ++++++++++++++--------------- matplotview/tests/test_view_obj.py | 5 +++-- setup.py | 2 +- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/matplotview/_docs.py b/matplotview/_docs.py index acea1f2..5753df5 100644 --- a/matplotview/_docs.py +++ b/matplotview/_docs.py @@ -5,7 +5,7 @@ def dynamic_doc_string(**kwargs): def convert(func): default_vals = { k: v.default for k, v in inspect.signature(func).parameters.items() - if(v.default is not inspect.Parameter.empty) + if (v.default is not inspect.Parameter.empty) } default_vals.update(kwargs) func.__doc__ = func.__doc__.format(**default_vals) @@ -18,6 +18,6 @@ def convert(func): def get_interpolation_list_str(): from matplotlib.image import _interpd_ return ", ".join([ - f"'{k}'" if(i != len(_interpd_) - 1) else f"or '{k}'" + f"'{k}'" if (i != len(_interpd_) - 1) else f"or '{k}'" for i, k in enumerate(_interpd_) ]) diff --git a/matplotview/_transform_renderer.py b/matplotview/_transform_renderer.py index 8c6e747..a61d1ba 100644 --- a/matplotview/_transform_renderer.py +++ b/matplotview/_transform_renderer.py @@ -105,7 +105,7 @@ def _scale_gc(self, gc: GraphicsContextBase) -> GraphicsContextBase: unit_box = transfer_transform.transform_bbox(unit_box) mult_factor = np.sqrt(unit_box.width * unit_box.height) - if(mult_factor == 0 or (not np.isfinite(mult_factor))): + if (mult_factor == 0 or (not np.isfinite(mult_factor))): return new_gc new_gc.set_linewidth(gc.get_linewidth() * mult_factor) @@ -218,18 +218,18 @@ def draw_path( # 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)): + if (not path.intersects_bbox(bbox, True)): return - if(self.__scale_widths): + if (self.__scale_widths): gc = self._scale_gc(gc) # Change the clip to the sub-axes box gc.set_clip_rectangle(bbox) - if(not isinstance(self.__bounding_axes.patch, Rectangle)): + if (not isinstance(self.__bounding_axes.patch, Rectangle)): gc.set_clip_path(TransformedPatchPath(self.__bounding_axes.patch)) - rgbFace = tuple(rgbFace) if(rgbFace is not None) else None + rgbFace = tuple(rgbFace) if (rgbFace is not None) else None self.__renderer.draw_path(gc, path, IdentityTransform(), rgbFace) @@ -244,7 +244,7 @@ def _draw_text_as_path( ismath: bool ): # If the text field is empty, don't even try rendering it... - if((s is None) or (s.strip() == "")): + if ((s is None) or (s.strip() == "")): return # Call the super class instance, which works for all cases except one @@ -264,14 +264,14 @@ def draw_gouraud_triangle( path = Path(points, closed=True) bbox = self._get_axes_display_box() - if(not path.intersects_bbox(bbox, True)): + if (not path.intersects_bbox(bbox, True)): return - if(self.__scale_widths): + if (self.__scale_widths): gc = self._scale_gc(gc) gc.set_clip_rectangle(bbox) - if(not isinstance(self.__bounding_axes.patch, Rectangle)): + if (not isinstance(self.__bounding_axes.patch, Rectangle)): gc.set_clip_path(TransformedPatchPath(self.__bounding_axes.patch)) self.__renderer.draw_gouraud_triangle(gc, path.vertices, colors, @@ -299,7 +299,7 @@ def draw_image( out_box = img_bbox_disp.transformed(shift_data_transform) clipped_out_box = Bbox.intersection(out_box, axes_bbox) - if(clipped_out_box is None): + if (clipped_out_box is None): return # We compute what the dimensions of the final output image within the @@ -307,7 +307,7 @@ def draw_image( 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)): + if ((out_w <= 0) or (out_h <= 0)): return # We can now construct the transform which converts between the @@ -330,16 +330,16 @@ def draw_image( alpha=1) out_arr[:, :, 3] = trans_msk - if(self.__scale_widths): + if (self.__scale_widths): gc = self._scale_gc(gc) gc.set_clip_rectangle(clipped_out_box) - if(not isinstance(self.__bounding_axes.patch, Rectangle)): + if (not isinstance(self.__bounding_axes.patch, Rectangle)): gc.set_clip_path(TransformedPatchPath(self.__bounding_axes.patch)) x, y = clipped_out_box.x0, clipped_out_box.y0 - if(self.option_scale_image()): + 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) diff --git a/matplotview/_view_axes.py b/matplotview/_view_axes.py index c9162ea..f0b47a9 100644 --- a/matplotview/_view_axes.py +++ b/matplotview/_view_axes.py @@ -53,14 +53,14 @@ def draw(self, renderer: RendererBase): # If we are working with a 3D object, swap out it's axes with # this zoom axes (swapping out the 3d transform) and reproject it. - if(hasattr(self._artist, "do_3d_projection")): + if (hasattr(self._artist, "do_3d_projection")): self.do_3d_projection() # Check and see if the passed limiting box and extents of the # artist intersect, if not don't bother drawing this artist. # First 2 checks are a special case where we received a bad clip box. # (those can happen when we try to get the bounds of a map projection) - if( + if ( self._clip_box.width == 0 or self._clip_box.height == 0 or Bbox.intersection(full_extents, self._clip_box) is not None ): @@ -128,7 +128,7 @@ class ViewSpecification: def __post_init__(self): self.image_interpolation = str(self.image_interpolation) - if(self.filter_set is not None): + if (self.filter_set is not None): self.filter_set = set(self.filter_set) self.scale_lines = bool(self.scale_lines) @@ -138,7 +138,6 @@ class __ViewType: PRIVATE: A simple identifier class for identifying view types, a view will inherit from the axes class it is wrapping and this type... """ - ... # Cache classes so grabbing the same type twice leads to actually getting the @@ -162,7 +161,7 @@ def view_wrapper(axes_class: Type[Axes]) -> Type[Axes]: another axes contents... """ # If the passed class is a view, simply return it. - if(issubclass(axes_class, Axes) and issubclass(axes_class, __ViewType)): + if (issubclass(axes_class, Axes) and issubclass(axes_class, __ViewType)): return axes_class class View(axes_class, __ViewType): @@ -228,14 +227,14 @@ def get_children(self) -> List[Artist]: child_list = super().get_children() def filter_check(artist, filter_set): - if(filter_set is None): + if (filter_set is None): return True return ( (artist not in filter_set) and (type(artist) not in filter_set) ) - if(self.__renderer is not None): + if (self.__renderer is not None): for ax, spec in self.view_specifications.items(): mock_renderer = _TransformRenderer( self.__renderer, ax.transData, self.transData, @@ -253,7 +252,7 @@ def filter_check(artist, filter_set): for a in itertools.chain( ax._children, ax.child_axes - ) if(filter_check(a, spec.filter_set)) + ) if (filter_check(a, spec.filter_set)) ]) return child_list @@ -262,7 +261,7 @@ def draw(self, renderer: RendererBase = None): # It is possible to have two axes which are views of each other # therefore we track the number of recursions and stop drawing # at a certain depth - if(self.figure._current_render_depth >= self.__max_render_depth): + if (self.figure._current_render_depth >= self.__max_render_depth): return self.figure._current_render_depth += 1 # Set the renderer, causing get_children to return the view's @@ -281,7 +280,7 @@ def __reduce__(self): cls = type(self) args = tuple( - arg if(arg != cls) else cls.__bases__[0] for arg in args + arg if (arg != cls) else cls.__bases__[0] for arg in args ) return ( @@ -318,7 +317,7 @@ def set_max_render_depth(self, val: int): allow. Zero and negative values are invalid, and will raise a ValueError. """ - if(val <= 0): + if (val <= 0): raise ValueError(f"Render depth must be positive, not {val}.") self.__max_render_depth = val @@ -376,12 +375,12 @@ def from_axes( If the provided axes to convert has an Axes type which does not match the axes class this view type wraps. """ - if(isinstance(axes, cls)): - if(render_depth is not None): + if (isinstance(axes, cls)): + if (render_depth is not None): axes.set_max_render_depth(render_depth) return axes - if(type(axes) != axes_class): + if (type(axes) is not axes_class): raise TypeError( f"Can't convert {type(axes).__name__} to {cls.__name__}" ) @@ -389,7 +388,7 @@ def from_axes( axes.__class__ = cls axes._init_vars( DEFAULT_RENDER_DEPTH - if(render_depth is None) + if (render_depth is None) else render_depth ) return axes diff --git a/matplotview/tests/test_view_obj.py b/matplotview/tests/test_view_obj.py index 5eb6067..c356664 100644 --- a/matplotview/tests/test_view_obj.py +++ b/matplotview/tests/test_view_obj.py @@ -18,8 +18,9 @@ def test_obj_comparison(): assert view_class1 is view_class2 assert view_class1 == view_class2 - if(mpl_version >= (3, 7)): - # As of 3.7.0, the subplot class no longer exists, and is an alias to the Axes class... + if (mpl_version >= (3, 7)): + # As of 3.7.0, the subplot class no longer exists, and is an alias + # to the Axes class... assert view_class2 == view_class3 else: assert view_class2 != view_class3 diff --git a/setup.py b/setup.py index b62ee79..2f0e9c4 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ import setuptools -VERSION = "1.0.0" +VERSION = "1.0.1" with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read() From 011c9cea3a5f4dd9a014bda6794779c4fa590963 Mon Sep 17 00:00:00 2001 From: Isaac Robinson <47544550+isaacrobinson2000@users.noreply.github.com> Date: Fri, 15 Dec 2023 11:38:27 -0500 Subject: [PATCH 12/27] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0a03f53..788bca6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # matplotview -#### A library for creating lightweight views of matplotlib axes. +#### A small library for creating lightweight views of matplotlib axes. matplotview provides a simple interface for creating "views" of matplotlib axes, providing a simple way of displaying overviews and zoomed views of From 523f5c2d3e061fdcd97f923ff97e5c04e4e04078 Mon Sep 17 00:00:00 2001 From: isaacrobinson2000 Date: Sun, 22 Sep 2024 19:44:29 -0400 Subject: [PATCH 13/27] Working with latest matplotlib, scale_lines also now scales markers. --- matplotview/_transform_renderer.py | 70 +++++++++++++++++++++++ matplotview/tests/test_view_rendering.py | 73 ++++++++++++++++++++++++ 2 files changed, 143 insertions(+) diff --git a/matplotview/_transform_renderer.py b/matplotview/_transform_renderer.py index a61d1ba..6732636 100644 --- a/matplotview/_transform_renderer.py +++ b/matplotview/_transform_renderer.py @@ -251,6 +251,76 @@ def _draw_text_as_path( # checked above... (Above case causes error) super()._draw_text_as_path(gc, x, y, s, prop, angle, ismath) + def draw_markers( + self, + gc, + marker_path, + marker_trans, + path, + trans, + rgbFace = None, + ): + # If the markers need to be scaled accurately (such as in log scale), just use the fallback as each will need + # to be scaled separately. + if(self.__scale_widths): + super().draw_markers(gc, marker_path, marker_trans, path, trans, rgbFace) + return + + # Otherwise we transform just the marker offsets (not the marker patch), so they stay the same size. + path = path.deepcopy() + path.vertices = self._get_transfer_transform(trans).transform(path.vertices) + bbox = self._get_axes_display_box() + + # Change the clip to the sub-axes box + gc.set_clip_rectangle(bbox) + if (not isinstance(self.__bounding_axes.patch, Rectangle)): + gc.set_clip_path(TransformedPatchPath(self.__bounding_axes.patch)) + + rgbFace = tuple(rgbFace) if (rgbFace is not None) else None + self.__renderer.draw_markers(gc, marker_path, marker_trans, path, IdentityTransform(), rgbFace) + + def draw_path_collection( + self, + gc, + master_transform, + paths, + all_transforms, + offsets, + offset_trans, + facecolors, + edgecolors, + linewidths, + linestyles, + antialiaseds, + urls, + offset_position, + ): + # If we want accurate scaling for each marker (such as in log scale), just use superclass implementation... + if(self.__scale_widths): + super().draw_path_collection( + gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, + edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position + ) + return + + # Otherwise we transform just the offsets, and pass them to the backend. + print(offsets) + if(np.any(np.isnan(offsets))): + raise ValueError("???") + offsets = self._get_transfer_transform(offset_trans).transform(offsets) + print(offsets) + bbox = self._get_axes_display_box() + + # Change the clip to the sub-axes box + gc.set_clip_rectangle(bbox) + if (not isinstance(self.__bounding_axes.patch, Rectangle)): + gc.set_clip_path(TransformedPatchPath(self.__bounding_axes.patch)) + + self.__renderer.draw_path_collection( + gc, master_transform, paths, all_transforms, offsets, IdentityTransform(), facecolors, + edgecolors, linewidths, linestyles, antialiaseds, urls, None + ) + def draw_gouraud_triangle( self, gc: GraphicsContextBase, diff --git a/matplotview/tests/test_view_rendering.py b/matplotview/tests/test_view_rendering.py index 03b9547..bb9b744 100644 --- a/matplotview/tests/test_view_rendering.py +++ b/matplotview/tests/test_view_rendering.py @@ -240,3 +240,76 @@ def test_stop_viewing(fig_test, fig_ref): ax1_ref.plot(data) ax1_ref.text(0.5, 0.5, "Hello") + + +@check_figures_equal() +def test_log_line(fig_test, fig_ref): + data = [i for i in range(10)] + + # Test case... Create a view and stop it... + ax1_test, ax2_test = fig_test.subplots(1, 2) + + ax1_test.set(xscale="log", yscale="log") + ax1_test.plot(data, "-o") + + view(ax2_test, ax1_test, scale_lines=False) + ax2_test.set_xlim(-1, 10) + ax2_test.set_ylim(-1, 10) + + # Reference, just don't plot anything at all in the second axes... + ax1_ref, ax2_ref = fig_ref.subplots(1, 2) + + ax1_ref.set(xscale="log", yscale="log") + ax1_ref.plot(data, "-o") + ax2_ref.plot(data, "-o") + ax2_ref.set_xlim(-1, 10) + ax2_ref.set_ylim(-1, 10) + + +@check_figures_equal() +def test_log_scatter(fig_test, fig_ref): + data = [i for i in range(1, 11)] + + # Test case... Create a view and stop it... + ax1_test, ax2_test = fig_test.subplots(1, 2) + + ax1_test.set(xscale="log", yscale="log") + ax1_test.scatter(data, data) + + view(ax2_test, ax1_test, scale_lines=False) + ax2_test.set_xlim(-5, 15) + ax2_test.set_ylim(-5, 15) + + # Reference, just don't plot anything at all in the second axes... + ax1_ref, ax2_ref = fig_ref.subplots(1, 2) + + ax1_ref.set(xscale="log", yscale="log") + ax1_ref.scatter(data, data) + ax2_ref.scatter(data, data) + ax2_ref.set_xlim(-5, 15) + ax2_ref.set_ylim(-5, 15) + + +@check_figures_equal() +def test_log_scatter_with_colors(fig_test, fig_ref): + data = [i for i in range(1, 11)] + colors = list("rgbrgbrgbr") + + # Test case... Create a view and stop it... + ax1_test, ax2_test = fig_test.subplots(1, 2) + + ax1_test.set(xscale="log", yscale="log") + ax1_test.scatter(data, data, color=colors) + + view(ax2_test, ax1_test, scale_lines=False) + ax2_test.set_xlim(-5, 15) + ax2_test.set_ylim(-5, 15) + + # Reference, just don't plot anything at all in the second axes... + ax1_ref, ax2_ref = fig_ref.subplots(1, 2) + + ax1_ref.set(xscale="log", yscale="log") + ax1_ref.scatter(data, data, color=colors) + ax2_ref.scatter(data, data, color=colors) + ax2_ref.set_xlim(-5, 15) + ax2_ref.set_ylim(-5, 15) From 9c059cb2aa09c17dcc441da47a6132c912488fbe Mon Sep 17 00:00:00 2001 From: isaacrobinson2000 Date: Sun, 22 Sep 2024 19:54:47 -0400 Subject: [PATCH 14/27] Minor adjustment so we don't have a marker at 0 on the log scale. --- matplotview/tests/test_view_rendering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matplotview/tests/test_view_rendering.py b/matplotview/tests/test_view_rendering.py index bb9b744..79c3eea 100644 --- a/matplotview/tests/test_view_rendering.py +++ b/matplotview/tests/test_view_rendering.py @@ -244,7 +244,7 @@ def test_stop_viewing(fig_test, fig_ref): @check_figures_equal() def test_log_line(fig_test, fig_ref): - data = [i for i in range(10)] + data = [i for i in range(1, 10)] # Test case... Create a view and stop it... ax1_test, ax2_test = fig_test.subplots(1, 2) From c260e6d9ad55ae5614c56ccf04501c04ba1ff92c Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 17 Jul 2025 23:08:39 -0400 Subject: [PATCH 15/27] CI: auto-fix via zizmor May include: - Avoids risky string interpolation. - Prevents checkout premissions from leaking --- .github/workflows/codeql.yml | 2 ++ .github/workflows/pytest.yml | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6135e19..f070eb6 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -25,6 +25,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + with: + persist-credentials: false - name: Initialize CodeQL uses: github/codeql-action/init@v2 diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 83075c8..ffa8201 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -21,6 +21,8 @@ jobs: steps: - uses: actions/checkout@v3 + with: + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: @@ -65,6 +67,8 @@ jobs: steps: - uses: actions/checkout@v3 + with: + persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: From 947dfa04b1bf9156a16b5e9beab309f078c913b6 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 17 Jul 2025 23:24:56 -0400 Subject: [PATCH 16/27] CI: Restrict default permissions Reduces risk of arbitrary code is run by attacker. --- .github/workflows/pytest.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index ffa8201..c211b71 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,5 +1,7 @@ name: Validate Python Code +permissions: + contents: read on: push: From 3a2fcc1ea1c1e381ad66f4d7dc01b4b242bebef9 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 18 Jul 2025 11:31:15 -0400 Subject: [PATCH 17/27] CI: add dependabot config file for GHA --- .github/dependabot.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..fc9f855 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" # Location of your workflow files + schedule: + interval: "weekly" # Options: daily, weekly, monthly From 426821d0cfa8054d4c30d365b91806ae3ec0e0cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:10:55 +0000 Subject: [PATCH 18/27] Bump github/codeql-action from 2 to 3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f070eb6..5e6e053 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,15 +29,15 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} queries: +security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{ matrix.language }}" From 806f86ea1813d5ab209bc6851545e92ba88a694a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:10:57 +0000 Subject: [PATCH 19/27] Bump actions/setup-python from 3 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 3 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v3...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/pytest.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index c211b71..3d10f55 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -26,7 +26,7 @@ jobs: with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -72,7 +72,7 @@ jobs: with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} From 2b02497ab60ed78ab801c785e09d02e57a5034ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:11:00 +0000 Subject: [PATCH 20/27] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- .github/workflows/pytest.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f070eb6..91ae017 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index c211b71..2026133 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -22,7 +22,7 @@ jobs: python-version: ["3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} @@ -68,7 +68,7 @@ jobs: python-version: ["3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} From a8cf1b415b75204f58c636839aa4812c11889053 Mon Sep 17 00:00:00 2001 From: isaacr Date: Fri, 18 Jul 2025 12:52:39 -0600 Subject: [PATCH 21/27] Add artifact upload on failure. --- .github/workflows/pytest.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 685d7b0..e8f7712 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -57,9 +57,18 @@ jobs: flake8 matplotview --count --select=E9,F63,F7,F82 --show-source --statistics flake8 matplotview --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest + id: pytest run: | pytest + - name: Upload images on failure + uses: actions/upload-artifact@v4 + if: ${{ failure() && steps.pytest.conclusion == 'failure' }} + with: + name: test-result-images + retention-days: 1 + path: result_images/ + test-windows: runs-on: windows-latest @@ -82,5 +91,14 @@ jobs: pip install pytest pip install -r requirements.txt - name: Test with pytest + id: pytest run: | - pytest \ No newline at end of file + pytest + + - name: Upload images on failure + uses: actions/upload-artifact@v4 + if: ${{ failure() && steps.pytest.conclusion == 'failure' }} + with: + name: test-result-images + retention-days: 1 + path: result_images/ From 351342b346f12074b26364e403531e2f3e941493 Mon Sep 17 00:00:00 2001 From: isaacr Date: Fri, 18 Jul 2025 13:12:36 -0600 Subject: [PATCH 22/27] Add small tolerance for macos --- .github/workflows/pytest.yml | 4 ++-- matplotview/tests/test_view_rendering.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index e8f7712..f8c31fa 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.11", "3.12", "3.13", "3.13t"] steps: - uses: actions/checkout@v4 @@ -74,7 +74,7 @@ jobs: runs-on: windows-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.11", "3.12", "3.13", "3.13t"] steps: - uses: actions/checkout@v4 diff --git a/matplotview/tests/test_view_rendering.py b/matplotview/tests/test_view_rendering.py index 79c3eea..32dbb14 100644 --- a/matplotview/tests/test_view_rendering.py +++ b/matplotview/tests/test_view_rendering.py @@ -1,3 +1,5 @@ +import sys + import numpy as np import matplotlib.pyplot as plt from matplotlib.testing.decorators import check_figures_equal @@ -241,8 +243,8 @@ def test_stop_viewing(fig_test, fig_ref): ax1_ref.plot(data) ax1_ref.text(0.5, 0.5, "Hello") - -@check_figures_equal() +# On MacOS the results are off by an extremely tiny amount, can't even see in diff. It's close enough... +@check_figures_equal(tol=0 if sys.platform.startswith("darwin") else 0.2) def test_log_line(fig_test, fig_ref): data = [i for i in range(1, 10)] From ad031e681330c3a95592aae205e5b857e0ea1515 Mon Sep 17 00:00:00 2001 From: isaacr Date: Fri, 18 Jul 2025 13:15:23 -0600 Subject: [PATCH 23/27] Add small tolerance for macos --- matplotview/tests/test_view_rendering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matplotview/tests/test_view_rendering.py b/matplotview/tests/test_view_rendering.py index 32dbb14..b348ce8 100644 --- a/matplotview/tests/test_view_rendering.py +++ b/matplotview/tests/test_view_rendering.py @@ -244,7 +244,7 @@ def test_stop_viewing(fig_test, fig_ref): ax1_ref.text(0.5, 0.5, "Hello") # On MacOS the results are off by an extremely tiny amount, can't even see in diff. It's close enough... -@check_figures_equal(tol=0 if sys.platform.startswith("darwin") else 0.2) +@check_figures_equal(tol=0.02) def test_log_line(fig_test, fig_ref): data = [i for i in range(1, 10)] From 3e94fa2b6d970642b022cc16c552ea87c37d0df5 Mon Sep 17 00:00:00 2001 From: isaacr Date: Fri, 18 Jul 2025 13:15:33 -0600 Subject: [PATCH 24/27] Add small tolerance for macos --- matplotview/tests/test_view_rendering.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/matplotview/tests/test_view_rendering.py b/matplotview/tests/test_view_rendering.py index b348ce8..6e3ab02 100644 --- a/matplotview/tests/test_view_rendering.py +++ b/matplotview/tests/test_view_rendering.py @@ -1,5 +1,3 @@ -import sys - import numpy as np import matplotlib.pyplot as plt from matplotlib.testing.decorators import check_figures_equal From 0e560d958d84f07c9172e499c893efce8a68ab56 Mon Sep 17 00:00:00 2001 From: isaacr Date: Fri, 18 Jul 2025 13:16:42 -0600 Subject: [PATCH 25/27] Add small tolerance for macos --- matplotview/tests/test_view_rendering.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/matplotview/tests/test_view_rendering.py b/matplotview/tests/test_view_rendering.py index 6e3ab02..5cafa7c 100644 --- a/matplotview/tests/test_view_rendering.py +++ b/matplotview/tests/test_view_rendering.py @@ -1,3 +1,5 @@ +import sys + import numpy as np import matplotlib.pyplot as plt from matplotlib.testing.decorators import check_figures_equal @@ -242,7 +244,7 @@ def test_stop_viewing(fig_test, fig_ref): ax1_ref.text(0.5, 0.5, "Hello") # On MacOS the results are off by an extremely tiny amount, can't even see in diff. It's close enough... -@check_figures_equal(tol=0.02) +@check_figures_equal(tol=0.02 if sys.platform.startswith("darwin") else 0) def test_log_line(fig_test, fig_ref): data = [i for i in range(1, 10)] From 4ca387b637474f5677f0ab7162cbee9c39000e4f Mon Sep 17 00:00:00 2001 From: isaacr Date: Fri, 18 Jul 2025 13:21:36 -0600 Subject: [PATCH 26/27] Fix some linting issues. --- matplotview/_transform_renderer.py | 8 ++++---- matplotview/tests/test_view_rendering.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/matplotview/_transform_renderer.py b/matplotview/_transform_renderer.py index 6732636..4bfd180 100644 --- a/matplotview/_transform_renderer.py +++ b/matplotview/_transform_renderer.py @@ -258,11 +258,11 @@ def draw_markers( marker_trans, path, trans, - rgbFace = None, + rgbFace=None, ): # If the markers need to be scaled accurately (such as in log scale), just use the fallback as each will need # to be scaled separately. - if(self.__scale_widths): + if (self.__scale_widths): super().draw_markers(gc, marker_path, marker_trans, path, trans, rgbFace) return @@ -296,7 +296,7 @@ def draw_path_collection( offset_position, ): # If we want accurate scaling for each marker (such as in log scale), just use superclass implementation... - if(self.__scale_widths): + if (self.__scale_widths): super().draw_path_collection( gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position @@ -305,7 +305,7 @@ def draw_path_collection( # Otherwise we transform just the offsets, and pass them to the backend. print(offsets) - if(np.any(np.isnan(offsets))): + if (np.any(np.isnan(offsets))): raise ValueError("???") offsets = self._get_transfer_transform(offset_trans).transform(offsets) print(offsets) diff --git a/matplotview/tests/test_view_rendering.py b/matplotview/tests/test_view_rendering.py index 5cafa7c..05d44e1 100644 --- a/matplotview/tests/test_view_rendering.py +++ b/matplotview/tests/test_view_rendering.py @@ -243,6 +243,7 @@ def test_stop_viewing(fig_test, fig_ref): ax1_ref.plot(data) ax1_ref.text(0.5, 0.5, "Hello") + # On MacOS the results are off by an extremely tiny amount, can't even see in diff. It's close enough... @check_figures_equal(tol=0.02 if sys.platform.startswith("darwin") else 0) def test_log_line(fig_test, fig_ref): From 2e833f4875abb2c12ee4a02fc9ff4bd46c71f8eb Mon Sep 17 00:00:00 2001 From: isaacr Date: Fri, 18 Jul 2025 13:48:21 -0600 Subject: [PATCH 27/27] Version bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2f0e9c4..9e53965 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ import setuptools -VERSION = "1.0.1" +VERSION = "1.0.2" with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read() 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