From 2a4d7c116812520f8612ddea96216b930d614c94 Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Wed, 18 Feb 2026 21:12:39 +1000 Subject: [PATCH 1/3] Update build-states to new test-map.yml (#590) --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index d9526253e..42854d464 100644 --- a/README.rst +++ b/README.rst @@ -140,8 +140,8 @@ If you use UltraPlot in your research, please cite it using the following BibTeX :target: https://pepy.tech/project/ultraplot :alt: Downloads -.. |build-status| image:: https://github.com/ultraplot/ultraplot/actions/workflows/build-ultraplot.yml/badge.svg - :target: https://github.com/ultraplot/ultraplot/actions/workflows/build-ultraplot.yml +.. |build-status| image:: https://github.com/ultraplot/ultraplot/actions/workflows/test-map.yml/badge.svg + :target: https://github.com/ultraplot/ultraplot/actions/workflows/test-map.yml :alt: Build Status .. |coverage| image:: https://codecov.io/gh/Ultraplot/ultraplot/graph/badge.svg?token=C6ZB7Q9II4&style=flat&color=53C334 From 9e4bccfd589ae166f51bdd27d0775d08bd324ca5 Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Sun, 22 Feb 2026 20:21:27 +1000 Subject: [PATCH 2/3] Preserve figure dpi in draw_without_rendering (#591) --- ultraplot/figure.py | 12 ++++++++++++ ultraplot/tests/test_figure.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 784cbf5f4..4edab717d 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -898,6 +898,18 @@ def draw(self, renderer): self._apply_share_label_groups() super().draw(renderer) + @override + def draw_without_rendering(self): + """ + Draw without output while preserving figure dpi state. + """ + dpi = self.dpi + try: + return super().draw_without_rendering() + finally: + if self.dpi != dpi: + mfigure.Figure.set_dpi(self, dpi) + def _is_auto_share_mode(self, which: str) -> bool: """Return whether a given axis uses auto-share mode.""" if which not in ("x", "y"): diff --git a/ultraplot/tests/test_figure.py b/ultraplot/tests/test_figure.py index e3845d2a1..066f3dd2a 100644 --- a/ultraplot/tests/test_figure.py +++ b/ultraplot/tests/test_figure.py @@ -78,6 +78,21 @@ def test_get_renderer_basic(): assert hasattr(renderer, "draw_path") +def test_draw_without_rendering_preserves_dpi(): + """ + draw_without_rendering should not mutate figure dpi/bbox. + """ + fig, ax = uplt.subplots(figsize=(4, 3), dpi=101) + dpi_before = fig.dpi + bbox_before = np.array([fig.bbox.width, fig.bbox.height]) + + fig.draw_without_rendering() + + assert np.isclose(fig.dpi, dpi_before) + assert np.allclose([fig.bbox.width, fig.bbox.height], bbox_before) + uplt.close(fig) + + def test_figure_sharing_toggle(): """ Check if axis sharing and unsharing works From 153df0d9264f83c8524aa670a3131859abe06d24 Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Wed, 25 Feb 2026 12:05:05 +1000 Subject: [PATCH 3/3] Fix cartopy tri default transform for Triangulation inputs (#595) --- ultraplot/internals/inputs.py | 49 +++++++++++++++++++++--------- ultraplot/tests/test_geographic.py | 17 ++++++++--- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/ultraplot/internals/inputs.py b/ultraplot/internals/inputs.py index c606e7949..0f8ac4e46 100644 --- a/ultraplot/internals/inputs.py +++ b/ultraplot/internals/inputs.py @@ -16,6 +16,10 @@ from cartopy.crs import PlateCarree except ModuleNotFoundError: PlateCarree = object +try: + from matplotlib.tri import Triangulation +except ModuleNotFoundError: + Triangulation = object # Constants @@ -300,8 +304,16 @@ def triangulation_wrapper(self, *args, **kwargs): # Manually set the name to the original function's name triangulation_wrapper.__name__ = func.__name__ + def _tri_cartopy_default(args, kwargs): + # If the first parsed argument is already a Triangulation then it may + # be in projected coordinates, so skip implicit PlateCarree defaults. + return not (args and isinstance(args[0], Triangulation)) + final_wrapper = _preprocess_or_redirect( - *keys, keywords=keywords, allow_extra=allow_extra + *keys, + keywords=keywords, + allow_extra=allow_extra, + cartopy_default_transform=_tri_cartopy_default, )(triangulation_wrapper) # Finally make sure all other metadata is correct @@ -311,7 +323,9 @@ def triangulation_wrapper(self, *args, **kwargs): return _decorator -def _preprocess_or_redirect(*keys, keywords=None, allow_extra=True): +def _preprocess_or_redirect( + *keys, keywords=None, allow_extra=True, cartopy_default_transform=True +): """ Redirect internal plotting calls to native matplotlib methods. Also convert keyword args to positional and pass arguments through 'data' dictionary. @@ -335,18 +349,6 @@ def _preprocess_or_redirect(self, *args, **kwargs): func_native = getattr(super(PlotAxes, self), name) return func_native(*args, **kwargs) else: - # Impose default coordinate system - from ..constructor import Proj - - if self._name == "basemap" and name in BASEMAP_FUNCS: - if kwargs.get("latlon", None) is None: - kwargs["latlon"] = True - if self._name == "cartopy" and name in CARTOPY_FUNCS: - if kwargs.get("transform", None) is None: - kwargs["transform"] = PlateCarree() - else: - kwargs["transform"] = Proj(kwargs["transform"]) - # Process data args # NOTE: Raises error if there are more args than keys args, kwargs = _kwargs_to_args( @@ -358,6 +360,25 @@ def _preprocess_or_redirect(self, *args, **kwargs): for key in set(keywords) & set(kwargs): kwargs[key] = _from_data(data, kwargs[key]) + # Impose default coordinate system using parsed inputs. This keeps + # behavior consistent across positional/keyword/data pathways. + from ..constructor import Proj + + if self._name == "basemap" and name in BASEMAP_FUNCS: + if kwargs.get("latlon", None) is None: + kwargs["latlon"] = True + if self._name == "cartopy" and name in CARTOPY_FUNCS: + if kwargs.get("transform", None) is None: + use_default_transform = cartopy_default_transform + if callable(use_default_transform): + use_default_transform = bool( + use_default_transform(args, kwargs) + ) + if use_default_transform: + kwargs["transform"] = PlateCarree() + else: + kwargs["transform"] = Proj(kwargs["transform"]) + # Auto-setup matplotlib with the input unit registry _load_objects() for arg in args: diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index 63b64d144..7d363f9d9 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -931,10 +931,10 @@ def test_rasterize_feature(): def test_check_tricontourf(): """ - Ensure that tricontour functions are getting - the transform for GeoAxes. + Ensure transform defaults are applied only when appropriate for tri-plots. """ import cartopy.crs as ccrs + from matplotlib.tri import Triangulation lon0 = 90 lon = np.linspace(-180, 180, 10) @@ -947,6 +947,7 @@ def test_check_tricontourf(): data[mask_box] = 1.5 lon, lat, data = map(np.ravel, (lon2d, lat2d, data)) + triangulation = Triangulation(lon, lat) fig, ax = uplt.subplots(proj="cyl", proj_kw={"lon0": lon0}) original_func = ax[0]._call_native @@ -956,10 +957,18 @@ def test_check_tricontourf(): autospec=True, side_effect=original_func, ) as mocked: - for func in "tricontour tricontourf".split(): - getattr(ax[0], func)(lon, lat, data) + ax[0].tricontourf(lon, lat, data) assert "transform" in mocked.call_args.kwargs assert isinstance(mocked.call_args.kwargs["transform"], ccrs.PlateCarree) + + with mock.patch.object( + ax[0], + "_call_native", + autospec=True, + side_effect=original_func, + ) as mocked: + ax[0].tricontourf(triangulation, data) + assert "transform" not in mocked.call_args.kwargs uplt.close(fig)