diff --git a/ultraplot/axes/polar.py b/ultraplot/axes/polar.py index bf62e010c..bbf2eee5b 100644 --- a/ultraplot/axes/polar.py +++ b/ultraplot/axes/polar.py @@ -82,6 +82,13 @@ thetaformatter_kw, rformatter_kw : dict-like, optional The azimuthal and radial label formatter settings. Passed to `~ultraplot.constructor.Formatter`. +xlabel, ylabel : str, optional + The x and y axis labels. Applied with `~matplotlib.axes.Axes.set_xlabel` + and `~matplotlib.axes.Axes.set_ylabel`. +xlabel_kw, ylabel_kw : dict-like, optional + Additional axis label settings applied with `~matplotlib.axes.Axes.set_xlabel` + and `~matplotlib.axes.Axes.set_ylabel`. See also `labelpad`, `labelcolor`, + `labelsize`, and `labelweight`. color : color-spec, default: :rc:`meta.color` Color for the axes edge. Propagates to `labelcolor` unless specified otherwise (similar to :func:`~ultraplot.axes.CartesianAxes.format`). @@ -212,6 +219,23 @@ def _update_locators( else: axis.set_minor_locator(loc) + def _update_labels(self, x, *args, **kwargs): + """ + Apply axis labels via `set_xlabel` / `set_ylabel`. + """ + # NOTE: Critical to test whether arguments are None or else this + # will set isDefault_label to False every time format() is called. + kwargs = rc._get_label_props(**kwargs) + no_args = all(a is None for a in args) + no_kwargs = all(v is None for v in kwargs.values()) + if no_args and no_kwargs: + return + setter = getattr(self, f"set_{x}label") + getter = getattr(self, f"get_{x}label") + if no_args: # otherwise label text is reset! + args = (getter(),) + setter(*args, **kwargs) + @docstring._snippet_manager def format( self, @@ -256,6 +280,10 @@ def format( labelsize=None, labelcolor=None, labelweight=None, + xlabel=None, + ylabel=None, + xlabel_kw=None, + ylabel_kw=None, **kwargs, ): """ @@ -335,6 +363,8 @@ def format( formatter_kw, minorlocator, minorlocator_kw, + label, + label_kw, ) in zip( ("x", "y"), (thetamin, rmin), @@ -349,6 +379,8 @@ def format( (thetaformatter_kw, rformatter_kw), (thetaminorlocator, rminorlocator), (thetaminorlocator_kw, rminorlocator_kw), + (xlabel, ylabel), + (xlabel_kw, ylabel_kw), ): # Axis limits self._update_limits(x, min_=min_, max_=max_, lim=lim) @@ -382,6 +414,16 @@ def format( x, formatter=formatter, formatter_kw=formatter_kw ) + # Axis label + kw = dict( + labelpad=labelpad, + color=labelcolor, + size=labelsize, + weight=labelweight, + ) + kw.update(label_kw or {}) + self._update_labels(x, label, **kw) + # Parent format method super().format(rc_kw=rc_kw, rc_mode=rc_mode, **kwargs) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index 334c61a44..2716a4316 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -3277,11 +3277,15 @@ def format( if skip_axes: # avoid recursion return - # Remove all keywords that are not in the allowed signature parameters + # Collect each class's matching kwargs without popping, then drop the union — + # shared params (e.g. xlabel/ylabel, accepted by both CartesianAxes and + # PolarAxes) need to reach every matching class. kws = { - cls: _pop_params(kwargs, sig) + cls: {k: kwargs[k] for k in sig.parameters if kwargs.get(k) is not None} for cls, sig in paxes.Axes._format_signatures.items() } + for k in {k for cls_kw in kws.values() for k in cls_kw}: + kwargs.pop(k, None) classes = set() # track used dictionaries def _axis_has_share_label_text(ax, axis): @@ -3314,11 +3318,14 @@ def _axis_has_label_text(ax, axis): kw.pop("ylabel", None) ax.format(rc_kw=rc_kw, rc_mode=rc_mode, skip_figure=True, **kw, **kwargs) ax.number = store_old_number - # Warn unused keyword argument(s) + # Warn unused keyword argument(s). Shared params (those in multiple + # signatures) are considered "used" if any matched class consumed them. + used_keys = {k for cls in classes for k in kws[cls]} kw = { key: value for name in kws.keys() - classes for key, value in kws[name].items() + if key not in used_keys } if kw: warnings._warn_ultraplot( diff --git a/ultraplot/tests/test_projections.py b/ultraplot/tests/test_projections.py index a52d11318..1a5daea47 100644 --- a/ultraplot/tests/test_projections.py +++ b/ultraplot/tests/test_projections.py @@ -154,6 +154,16 @@ def test_polar_projections(): return fig +def test_polar_format_labels(): + """ + ax.format(xlabel=..., ylabel=...) must forward to set_xlabel/set_ylabel. + """ + fig, ax = uplt.subplots(proj="polar") + ax.format(xlabel="xlabel", ylabel="ylabel") + assert ax.get_xlabel() == "xlabel" + assert ax.get_ylabel() == "ylabel" + + def test_sharing_axes(): """ Test sharing axes for GeoAxes