Repository: scipp/plopp

Issue:
Polar slicer plot not keeping xrange upon value change
Hello,

I am trying to use a slicer plot to visualize my data in polar coordinates.  It seems that pp.slicer is not setting the theta limits correctly.  I have attempted to use p.canvas.xrange = 0., 2*np.pi, but this only sets the theta limits on initialization, which is reset once a slider is changed.  I have attempted to inject an update node with no sucess.  Here is a MWE:

'''

fig = plt.figure()
ax = fig.add_subplot(projection='polar')

p = pp.slicer({'Data':_data, 'Fit':_fit},
      keep=['theta'], 
      marker={'Data':'o', 'Fit':''}, 
      linestyle={'Data':'', 'Fit':'-'}, 
      color={'Data':'k', 'Fit':'r'},
          autoscale=True,
          legend=True,
          ax=ax
          )
p.canvas.xrange = 0., 2*np.pi
p
'''
![image](https://github.com/user-attachments/assets/7a62712d-367c-4fe0-8dd8-07c58882e057)

and upon value change:

![image](https://github.com/user-attachments/assets/e17bd301-b1f9-4c47-87c0-64c67c33ee02)


Please advise



Discussion:

Patch:
diff --git a/docs/gallery/polar-plots.ipynb b/docs/gallery/polar-plots.ipynb
index ab5d625..6153b02 100644
--- a/docs/gallery/polar-plots.ipynb
+++ b/docs/gallery/polar-plots.ipynb
@@ -63,7 +63,6 @@
     "\n",
     "# Make the plot and tweak the axes\n",
     "polar1d = pp.plot(da, ax=ax, grid=True)\n",
-    "polar1d.canvas.xrange = 0, 2 * np.pi\n",
     "polar1d"
    ]
   },
diff --git a/src/plopp/backends/matplotlib/canvas.py b/src/plopp/backends/matplotlib/canvas.py
index a57286c..b4471cc 100644
--- a/src/plopp/backends/matplotlib/canvas.py
+++ b/src/plopp/backends/matplotlib/canvas.py
@@ -31,6 +31,25 @@ def _cursor_formatter(x: float, dtype: sc.DType, unit: str) -> str:
     return scalar_to_string(sc.scalar(x, unit=unit))
 
 
+def _maybe_trim_polar_limits(
+    axis_type: str, limits: tuple[float, float]
+) -> tuple[float, float]:
+    """
+    If the axes are polar, trim the limits of the polar plot to be within the range
+    [0, 2π].
+
+    Parameters
+    ----------
+    axis_type:
+        The type of the axis. If this is not 'polar', the limits are returned as is.
+    limits:
+        The limits of the axis.
+    """
+    if axis_type != 'polar':
+        return limits
+    return tuple(np.clip(limits, 0, 2 * np.pi))
+
+
 class Canvas:
     """
     Matplotlib-based canvas used to render 2D graphics.
@@ -334,7 +353,9 @@ class Canvas:
 
     @xmin.setter
     def xmin(self, value: float):
-        self.ax.set_xlim(value, self.xmax)
+        self.ax.set_xlim(
+            _maybe_trim_polar_limits(axis_type=self.ax.name, limits=(value, self.xmax))
+        )
 
     @property
     def xmax(self) -> float:
@@ -345,7 +366,9 @@ class Canvas:
 
     @xmax.setter
     def xmax(self, value: float):
-        self.ax.set_xlim(self.xmin, value)
+        self.ax.set_xlim(
+            _maybe_trim_polar_limits(axis_type=self.ax.name, limits=(self.xmin, value))
+        )
 
     @property
     def xrange(self) -> tuple[float, float]:
@@ -356,7 +379,7 @@ class Canvas:
 
     @xrange.setter
     def xrange(self, value: tuple[float, float]):
-        self.ax.set_xlim(value)
+        self.ax.set_xlim(_maybe_trim_polar_limits(axis_type=self.ax.name, limits=value))
 
     @property
     def ymin(self) -> float:


Test patch:
diff --git a/tests/backends/matplotlib/mpl_plot_test.py b/tests/backends/matplotlib/mpl_plot_test.py
index ddce55a..423fe13 100644
--- a/tests/backends/matplotlib/mpl_plot_test.py
+++ b/tests/backends/matplotlib/mpl_plot_test.py
@@ -150,3 +150,40 @@ def test_with_strings_as_bin_edges_other_coord_is_bin_centers_2d():
     )
     fig = da.plot()
     assert [t.get_text() for t in fig.canvas.ax.get_xticklabels()] == strings
+
+
+def test_polar_axes_limits_with_padding_are_clipped():
+    # Make some spiral data
+    N = 50
+    r = sc.linspace('theta', 0, 10, N, unit='m')
+    theta = sc.linspace('theta', 0, 0.995 * 2 * np.pi, N, unit='rad')
+    da = sc.DataArray(data=r, coords={'theta': theta})
+
+    _, ax = plt.subplots(subplot_kw={'projection': 'polar'})
+    fig = pp.plot(da, ax=ax)
+    assert fig.canvas.xrange == (0, 2 * np.pi)
+
+
+def test_polar_axes_limits_more_than_two_pi_are_clipped():
+    # Make some spiral data
+    N = 50
+    r = sc.linspace('theta', 0, 10, N, unit='m')
+    theta = sc.linspace('theta', 0, 3.5 * 2 * np.pi, N, unit='rad')
+    da = sc.DataArray(data=r, coords={'theta': theta})
+
+    _, ax = plt.subplots(subplot_kw={'projection': 'polar'})
+    fig = pp.plot(da, ax=ax)
+    assert fig.canvas.xrange == (0, 2 * np.pi)
+
+
+def test_polar_axes_limits_small_range_fits_to_data():
+    # Make some spiral data
+    N = 50
+    r = sc.linspace('theta', 0, 10, N, unit='m')
+    theta = sc.linspace('theta', 0.25 * 2 * np.pi, 0.75 * 2 * np.pi, N, unit='rad')
+    da = sc.DataArray(data=r, coords={'theta': theta})
+
+    _, ax = plt.subplots(subplot_kw={'projection': 'polar'})
+    fig = pp.plot(da, ax=ax)
+    assert fig.canvas.xmin > 0.0
+    assert fig.canvas.xmax < 2 * np.pi