diff --git a/ultraplot/axes/geo.py b/ultraplot/axes/geo.py index 786fbff54..c6bd8eb5c 100644 --- a/ultraplot/axes/geo.py +++ b/ultraplot/axes/geo.py @@ -898,6 +898,8 @@ def _pad_ticks(ticks: np.ndarray, vmin: float, vmax: float) -> np.ndarray: # giant lists of 10,000 gridline locations. if len(ticks) == 0: return ticks + if len(ticks) == 1: + return ticks range_ = np.max(ticks) - np.min(ticks) vmin = max(vmin, ticks[0] - range_) vmax = min(vmax, ticks[-1] + range_) @@ -1595,6 +1597,46 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self._edge_lat_labels: list[mtext.Text] = [] super().__init__(*args, **kwargs) + def _sync_shared_tick_state( + self, + which: str, + *, + copy_major_locator: bool = False, + copy_minor_locator: bool = False, + copy_major_formatter: bool = False, + ) -> None: + """ + Copy explicit tick-state changes from this axis to shared GeoAxes siblings. + """ + if which not in {"x", "y"}: + raise ValueError(f"Invalid axis: {which!r}") + if not any((copy_major_locator, copy_minor_locator, copy_major_formatter)): + return + if which == "x": + if self.figure._sharex < 2: + return + this_axis = self._lonaxis + siblings = list(self._shared_axes["x"].get_siblings(self)) + else: + if self.figure._sharey < 2: + return + this_axis = self._lataxis + siblings = list(self._shared_axes["y"].get_siblings(self)) + for sibling in siblings: + if sibling is self or not isinstance(sibling, GeoAxes): + continue + sibling_axis = sibling._lataxis if which == "y" else sibling._lonaxis + if copy_major_locator: + sibling_axis.set_major_locator(this_axis.get_major_locator()) + if copy_minor_locator: + sibling_axis.set_minor_locator(this_axis.get_minor_locator()) + if copy_major_formatter: + sibling_axis.set_major_formatter(this_axis.get_major_formatter()) + if copy_major_locator or copy_major_formatter: + sibling._update_major_gridlines() + if copy_minor_locator: + sibling._update_minor_gridlines() + @docstring._snippet_manager def hawkeye( self, @@ -3087,6 +3129,26 @@ def format( labelpad=labelpad, nsteps=nsteps, ) + self._sync_shared_tick_state( + "x", + copy_major_locator=_not_none(lonlocator=lonlocator, lonlines=lonlines) + is not None, + copy_minor_locator=_not_none( + lonminorlocator=lonminorlocator, lonminorlines=lonminorlines + ) + is not None, + copy_major_formatter=lonformatter is not None, + ) + self._sync_shared_tick_state( + "y", + copy_major_locator=_not_none(latlocator=latlocator, latlines=latlines) + is not None, + copy_minor_locator=_not_none( + latminorlocator=latminorlocator, latminorlines=latminorlines + ) + is not None, + copy_major_formatter=latformatter is not None, + ) self._format_apply_ticklen( lonlim=lonlim, latlim=latlim, diff --git a/ultraplot/tests/test_geographic.py b/ultraplot/tests/test_geographic.py index 729f168a7..699800566 100644 --- a/ultraplot/tests/test_geographic.py +++ b/ultraplot/tests/test_geographic.py @@ -3,6 +3,7 @@ import numpy as np import pytest +from matplotlib import ticker as mticker import ultraplot as uplt @@ -795,6 +796,70 @@ def test_copy_locator_props(): assert getattr(g1, prop) == getattr(g2, prop) +def test_format_shared_ticks_sync(): + pytest.importorskip("cartopy") + fig, ax = uplt.subplots(ncols=2, proj="cyl", share="all") + ax.format(lonlim=(100, 105), latlim=(30, 35), labels=True) + + before_lon = ax[0]._get_lonticklocs() + before_lat = ax[0]._get_latticklocs() + + ax[1].format(lonlines=2, latlines=1) + + after_left_lon = ax[0]._get_lonticklocs() + after_left_lat = ax[0]._get_latticklocs() + after_right_lon = ax[1]._get_lonticklocs() + after_right_lat = ax[1]._get_latticklocs() + left_gridliner = ax[0]._gridliner_adapters["major"].gridliner + + assert np.asarray(after_left_lon).shape != np.asarray( + before_lon + ).shape or not np.allclose( + after_left_lon, + before_lon, + ) + assert np.asarray(after_left_lat).shape != np.asarray( + before_lat + ).shape or not np.allclose( + after_left_lat, + before_lat, + ) + assert np.allclose(after_left_lon, after_right_lon) + assert np.allclose(after_left_lat, after_right_lat) + assert np.allclose(left_gridliner.xlocator.tick_values(100, 105), after_left_lon) + assert np.allclose(left_gridliner.ylocator.tick_values(30, 35), after_left_lat) + + ax[1].format(lonminorlines=0.5, latminorlines=0.5) + assert np.allclose( + ax[0]._lonaxis.get_minorticklocs(), ax[1]._lonaxis.get_minorticklocs() + ) + assert np.allclose( + ax[0]._lataxis.get_minorticklocs(), ax[1]._lataxis.get_minorticklocs() + ) + + formatter = mticker.FormatStrFormatter("%.1f") + ax[1].format(lonformatter=formatter, latformatter=formatter) + lonformatter = ax[1]._lonaxis.get_major_formatter() + latformatter = ax[1]._lataxis.get_major_formatter() + assert ax[0]._lonaxis.get_major_formatter() is lonformatter + assert ax[0]._lataxis.get_major_formatter() is latformatter + assert left_gridliner.xformatter is lonformatter + assert left_gridliner.yformatter is latformatter + uplt.close(fig) + + +def test_sync_shared_tick_state_guards(): + pytest.importorskip("cartopy") + fig, ax = uplt.subplots(ncols=2, proj="cyl") + + ax[0]._sync_shared_tick_state("x") + ax[0]._sync_shared_tick_state("x", copy_major_locator=True) + ax[0]._sync_shared_tick_state("y", copy_major_locator=True) + with pytest.raises(ValueError, match="Invalid axis"): + ax[0]._sync_shared_tick_state("z", copy_major_locator=True) + uplt.close(fig) + + def test_turn_off_tick_labels_basemap(): """ Check if we can toggle the labels off for GeoAxes