diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 8ad5753d8..8c44c92be 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -633,6 +633,10 @@ titlefontsize, titlefontweight, titlefontcolor : optional The font size, weight, and color for the legend title. Font size is interpreted by `~ultraplot.utils.units`. The default size is `fontsize`. +title_kw : dict-like, optional + Additional properties passed to the legend title text object, e.g. + ``title_kw={'style': 'italic'}``. This can be used to set any + `~matplotlib.text.Text` property on the legend title. borderpad, borderaxespad, handlelength, handleheight, handletextpad, labelspacing, columnspacing : unit-spec, optional Various matplotlib `~matplotlib.axes.Axes.legend` spacing arguments. %(units.em)s @@ -1231,6 +1235,7 @@ def _add_legend( titlefontsize=None, titlefontweight=None, titlefontcolor=None, + title_kw=None, handle_kw=None, handler_map=None, span: Optional[Union[int, Tuple[int, int]]] = None, @@ -1263,6 +1268,7 @@ def _add_legend( titlefontsize=titlefontsize, titlefontweight=titlefontweight, titlefontcolor=titlefontcolor, + title_kw=title_kw, handle_kw=handle_kw, handler_map=handler_map, span=span, diff --git a/ultraplot/legend.py b/ultraplot/legend.py index c8c5c579d..db71338f3 100644 --- a/ultraplot/legend.py +++ b/ultraplot/legend.py @@ -1318,6 +1318,7 @@ class _LegendInputs: titlefontsize: float titlefontweight: Any titlefontcolor: Any + title_kw: Any handle_kw: Any handler_map: Any span: Optional[Union[int, Tuple[int, int]]] @@ -1794,6 +1795,7 @@ def _resolve_inputs( titlefontsize=None, titlefontweight=None, titlefontcolor=None, + title_kw=None, handle_kw=None, handler_map=None, span: Optional[Union[int, Tuple[int, int]]] = None, @@ -1844,6 +1846,7 @@ def _resolve_inputs( titlefontsize=titlefontsize, titlefontweight=titlefontweight, titlefontcolor=titlefontcolor, + title_kw=title_kw, handle_kw=handle_kw, handler_map=handler_map, span=span, @@ -1896,6 +1899,9 @@ def _resolve_style_kwargs( lax, fontcolor, fontweight, + titlefontweight, + titlefontcolor, + title_kw, handle_kw, kwargs, ): @@ -1908,10 +1914,16 @@ def _resolve_style_kwargs( kw_text["color"] = fontcolor if fontweight is not None: kw_text["weight"] = fontweight + kw_title = {} + if titlefontweight is not None: + kw_title["weight"] = titlefontweight + if titlefontcolor is not None: + kw_title["color"] = titlefontcolor + kw_title.update(title_kw or {}) kw_handle = _pop_props(kwargs, "line") kw_handle.setdefault("solid_capstyle", "butt") kw_handle.update(handle_kw or {}) - return kw_frame, kw_text, kw_handle, kwargs + return kw_frame, kw_text, kw_title, kw_handle, kwargs def _build_legends( self, @@ -1959,12 +1971,14 @@ def _build_legends( lax.add_artist(obj) return objs - def _apply_handle_styles(self, objs, *, kw_text, kw_handle): + def _apply_handle_styles(self, objs, *, kw_text, kw_title, kw_handle): """ Apply per-handle styling overrides to legend artists. """ for obj in objs: obj.set_clip_on(False) + if kw_title: + obj.get_title().update(kw_title) box = getattr(obj, "_legend_handle_box", None) for child in guides._iter_children(box): if isinstance(child, mtext.Text): @@ -2015,6 +2029,7 @@ def add( titlefontsize=None, titlefontweight=None, titlefontcolor=None, + title_kw=None, handle_kw=None, handler_map=None, span: Optional[Union[int, Tuple[int, int]]] = None, @@ -2050,6 +2065,7 @@ def add( titlefontsize=titlefontsize, titlefontweight=titlefontweight, titlefontcolor=titlefontcolor, + title_kw=title_kw, handle_kw=handle_kw, handler_map=handler_map, span=span, @@ -2062,10 +2078,13 @@ def add( lax, kwargs = self._resolve_axes_layout(inputs) - kw_frame, kw_text, kw_handle, kwargs = self._resolve_style_kwargs( + kw_frame, kw_text, kw_title, kw_handle, kwargs = self._resolve_style_kwargs( lax=lax, fontcolor=inputs.fontcolor, fontweight=inputs.fontweight, + titlefontweight=inputs.titlefontweight, + titlefontcolor=inputs.titlefontcolor, + title_kw=inputs.title_kw, handle_kw=inputs.handle_kw, kwargs=kwargs, ) @@ -2079,5 +2098,7 @@ def add( kwargs=kwargs, ) - self._apply_handle_styles(objs, kw_text=kw_text, kw_handle=kw_handle) + self._apply_handle_styles( + objs, kw_text=kw_text, kw_title=kw_title, kw_handle=kw_handle + ) return self._finalize(objs, loc=inputs.loc, align=inputs.align) diff --git a/ultraplot/tests/test_legend.py b/ultraplot/tests/test_legend.py index a8ebb4455..8db7c8ac3 100644 --- a/ultraplot/tests/test_legend.py +++ b/ultraplot/tests/test_legend.py @@ -1437,3 +1437,50 @@ def legend(self, *args, **kwargs): leg = fig.legend(ref=proxy, loc="upper left", rows=1) assert leg is not None + + +def test_legend_title_kw(): + """ + Test that title_kw passes arbitrary Text properties to the legend title. + """ + fig, ax = uplt.subplots() + ax.plot([0, 1], label="data") + leg = ax.legend(title="My Legend", title_kw={"style": "italic", "color": "red"}) + title = leg.get_title() + assert title.get_style() == "italic" + assert title.get_color() == "red" + + +def test_legend_titlefontweight(): + """ + Test that titlefontweight is applied to the legend title text object. + """ + fig, ax = uplt.subplots() + ax.plot([0, 1], label="data") + leg = ax.legend(title="My Legend", titlefontweight="bold") + assert leg.get_title().get_fontweight() == "bold" + + +def test_legend_titlefontcolor(): + """ + Test that titlefontcolor is applied to the legend title text object. + """ + fig, ax = uplt.subplots() + ax.plot([0, 1], label="data") + leg = ax.legend(title="My Legend", titlefontcolor="blue") + assert leg.get_title().get_color() == "blue" + + +def test_legend_title_kw_overrides_titlefont(): + """ + Test that title_kw values override titlefontweight/titlefontcolor when both + are specified, since title_kw is merged last. + """ + fig, ax = uplt.subplots() + ax.plot([0, 1], label="data") + leg = ax.legend( + title="My Legend", + titlefontcolor="blue", + title_kw={"color": "green"}, + ) + assert leg.get_title().get_color() == "green"