diff --git a/plotnine/_mpl/layout_manager/_plot_layout_items.py b/plotnine/_mpl/layout_manager/_plot_layout_items.py index df42b2c32..6ba882f9b 100644 --- a/plotnine/_mpl/layout_manager/_plot_layout_items.py +++ b/plotnine/_mpl/layout_manager/_plot_layout_items.py @@ -1,18 +1,19 @@ from __future__ import annotations +from dataclasses import dataclass from itertools import chain from typing import TYPE_CHECKING from matplotlib.text import Text -from plotnine._mpl.patches import StripTextPatch -from plotnine._utils import side_artists +from plotnine._utils import ha_as_float, side_artists, va_as_float from plotnine.composition._compose import Compose from plotnine.exceptions import PlotnineError from ..utils import ( ArtistGeometry, TextJustifier, + bbox_in_axes_space, get_subplotspecs, rel_position, resize_footer_background, @@ -32,7 +33,7 @@ from matplotlib.figure import Figure from matplotlib.lines import Line2D from matplotlib.patches import Rectangle - from matplotlib.transforms import Transform + from matplotlib.transforms import Bbox, Transform from plotnine import ggplot from plotnine._mpl.offsetbox import FlexibleAnchoredOffsetbox @@ -67,6 +68,31 @@ ) +@dataclass +class StripSizing: + """ + Theme inputs that fix a strip background's size and offset + """ + + margin: Margin + """Strip text margin with the units in lines""" + + strip_align: float + """How far the background is offset from the panel edge""" + + bg_x: float + """Left of the strip background in transAxes""" + + bg_y: float + """Bottom of the strip background in transAxes""" + + bg_width: float + """Width of the strip background in transAxes (top strips)""" + + bg_height: float + """Height of the strip background in transAxes (right strips)""" + + class PlotLayoutItems: """ Objects required to compute the layout @@ -193,6 +219,66 @@ def axis_ticks_y(self, ax: Axes) -> Iterator[Tick]: return chain(major, minor) + def _strip_sizing(self, position: StripPosition) -> StripSizing: + """ + Theme inputs that fix one strip's background size and offset + + The keys read depend on the side the strip sits on. + """ + theme = self.plot.theme + if position == "top": + return StripSizing( + margin=theme.getp(("strip_text_x", "margin")).to("lines"), + strip_align=theme.getp("strip_align_x"), + bg_x=theme.getp(("strip_text_x", "x"), 0), + bg_y=1, + bg_width=theme.getp(("strip_background_x", "width"), 1), + bg_height=0, + ) + else: + return StripSizing( + margin=theme.getp(("strip_text_y", "margin")).to("lines"), + strip_align=theme.getp("strip_align_y"), + bg_x=1, + bg_y=theme.getp(("strip_text_y", "y"), 0), + bg_width=0, + bg_height=theme.getp(("strip_background_y", "height"), 1), + ) + + def strip_patch_bbox( + self, strip_text: StripText, scale: float = 1 + ) -> Bbox: + """ + Figure-space bounding box of one strip's background patch + + The breadth (height for top strips, width for right strips) is + scaled by `scale` so the layout manager can equalise strips in + the same group. + """ + from matplotlib.transforms import Bbox + + sizing = self._strip_sizing(strip_text.position) + m = sizing.margin + text_bbox = self.geometry.bbox(strip_text) + ax_bbox = self.geometry.bbox(strip_text.ax) + W, H = self.plot.figure.bbox.width, self.plot.figure.bbox.height + line_height = strip_text._line_height(self.geometry.renderer) + + x0 = rel_position(sizing.bg_x, 0, ax_bbox.x0, ax_bbox.x1) + y0 = rel_position(sizing.bg_y, 0, ax_bbox.y0, ax_bbox.y1) + + if strip_text.position == "top": + margins = (m.b + m.t) * line_height / H + width = ax_bbox.width * sizing.bg_width + height = (text_bbox.height + margins) * scale + y0 += height * sizing.strip_align + else: + margins = (m.l + m.r) * line_height / W + height = ax_bbox.height * sizing.bg_height + width = (text_bbox.width + margins) * scale + x0 += width * sizing.strip_align + return Bbox.from_bounds(x0, y0, width, height) + def strip_text_x_extra_height(self, position: StripPosition) -> float: """ Height taken up by the top strips that is outside the panels @@ -200,50 +286,42 @@ def strip_text_x_extra_height(self, position: StripPosition) -> float: if not self.strip_text_x: return 0 - artists = [ - st.patch if st.patch.get_visible() else st - for st in self.strip_text_x - if st.patch.position == position - ] - heights = [] + for st in self.strip_text_x: + if st.position != position: + continue + strip_align = self._strip_sizing(st.position).strip_align + if st.patch.get_visible(): + # The patch bounds are not yet set, so derive its natural + # height from the sizing inputs. + h = self.strip_patch_bbox(st).height + else: + h = self.geometry.height(st) + heights.append(max(h + h * strip_align, 0)) - for a in artists: - info = ( - a.text.draw_info - if isinstance(a, StripTextPatch) - else a.draw_info - ) - h = self.geometry.height(a) - heights.append(max(h + h * info.strip_align, 0)) - - return max(heights) + return max(heights) if heights else 0 def strip_text_y_extra_width(self, position: StripPosition) -> float: """ - Width taken up by the top strips that is outside the panels + Width taken up by the right strips that is outside the panels """ if not self.strip_text_y: return 0 - artists = [ - st.patch if st.patch.get_visible() else st - for st in self.strip_text_y - if st.patch.position == position - ] - widths = [] + for st in self.strip_text_y: + if st.position != position: + continue + strip_align = self._strip_sizing(st.position).strip_align + if st.patch.get_visible(): + # The patch bounds are not yet set, so derive its natural + # width from the sizing inputs. + w = self.strip_patch_bbox(st).width + else: + w = self.geometry.width(st) + widths.append(max(w + w * strip_align, 0)) - for a in artists: - info = ( - a.text.draw_info - if isinstance(a, StripTextPatch) - else a.draw_info - ) - w = self.geometry.width(a) - widths.append(max(w + w * info.strip_align, 0)) - - return max(widths) + return max(widths) if widths else 0 def axis_ticks_x_max_height_at( self, location: AxesLocation, side: str @@ -448,8 +526,7 @@ def _move_artists(self, spaces: PlotSideSpaces): self._adjust_axis_text_x(justify) self._adjust_axis_text_y(justify) - self._strip_text_x_background_equal_heights() - self._strip_text_y_background_equal_widths() + self._place_strip_backgrounds() def _adjust_axis_text_x(self, justify: TextJustifier): """ @@ -553,37 +630,113 @@ def to_horizontal_axis_dimensions(value: float, ax: Axes) -> float: ) justify.horizontally(text, ha, low, high, width=width) - def _strip_text_x_background_equal_heights(self): + def _strip_breadth_scales( + self, group: list[StripText], breadth: Literal["height", "width"] + ) -> list[float]: """ - Make the strip_text_x_backgrounds have equal heights + Per-strip factor that equalises the breadth across a group - The smaller heights are expanded to match the largest height + Each strip's natural breadth is grown to match the largest in + the group, so the backgrounds share a common height (top strips) + or width (right strips). """ - if not self.strip_text_x: - return - - heights = [ - self.geometry.bbox(t.patch).height for t in self.strip_text_x - ] - max_height = max(heights) - relative_heights = [max_height / h for h in heights] - for text, scale in zip(self.strip_text_x, relative_heights): - text.patch.expand = scale + natural = [getattr(self.strip_patch_bbox(st), breadth) for st in group] + largest = max(natural) + return [largest / b for b in natural] - def _strip_text_y_background_equal_widths(self): + def _place_strip_backgrounds(self): """ - Make the strip_text_y_backgrounds have equal widths - - The smaller widths are expanded to match the largest width + Fix each strip background at its final bounds and place its text """ - if not self.strip_text_y: - return + groups: tuple[ + tuple[list[StripText], Literal["height", "width"]], ... + ] = ( + (self.strip_text_x or [], "height"), + (self.strip_text_y or [], "width"), + ) + for group, breadth in groups: + if not group: + continue + scales = self._strip_breadth_scales(group, breadth) + for st, scale in zip(group, scales): + bbox = self.strip_patch_bbox(st, scale) + st.patch.set_bounds(bbox.bounds) + st.patch.set_transform(self.plot.figure.transFigure) + self._place_strip_text(st) + + def _place_strip_text(self, st: StripText): + """ + Justify the strip text within its final background bounds + """ + theme = self.plot.theme + position = st.position + ax = st.ax + renderer = self.geometry.renderer + sizing = self._strip_sizing(position) + m = sizing.margin + + patch_bbox = bbox_in_axes_space(st.patch, ax, renderer) + text_bbox = bbox_in_axes_space(st, ax, renderer) + + if position == "top": + ha = theme.getp(("strip_text_x", "ha"), "center") + va = theme.getp(("strip_text_x", "va"), "center") + rel_x, rel_y = ha_as_float(ha), va_as_float(va) + + # line_height and margins in axes space + line_height = st._line_height(renderer) / ax.bbox.height + + x = ( + # Justify horizontally within the strip_background + rel_position( + rel_x, + text_bbox.width + (line_height * (m.l + m.r)), + patch_bbox.x0, + patch_bbox.x1, + ) + + (m.l * line_height) + + text_bbox.width / 2 + ) + # Setting the y position based on the bounding box is wrong + y = ( + rel_position( + rel_y, + text_bbox.height, + patch_bbox.y0 + m.b * line_height, + patch_bbox.y1 - m.t * line_height, + ) + + text_bbox.height / 2 + ) + else: # "right" + ha = theme.getp(("strip_text_y", "ha"), "center") + va = theme.getp(("strip_text_y", "va"), "center") + rel_x, rel_y = ha_as_float(ha), va_as_float(va) + + # line_height in axes space + line_height = st._line_height(renderer) / ax.bbox.width + + x = ( + rel_position( + rel_x, + text_bbox.width, + patch_bbox.x0 + m.l * line_height, + patch_bbox.x1 - m.r * line_height, + ) + + text_bbox.width / 2 + ) + y = ( + # Justify vertically within the strip_background + rel_position( + rel_y, + text_bbox.height + ((m.b + m.t) * line_height), + patch_bbox.y0, + patch_bbox.y1, + ) + + (m.b * line_height) + + text_bbox.height / 2 + ) - widths = [self.geometry.bbox(t.patch).width for t in self.strip_text_y] - max_width = max(widths) - relative_widths = [max_width / w for w in widths] - for text, scale in zip(self.strip_text_y, relative_widths): - text.patch.expand = scale + st.set_position((x, y)) def _text_is_visible(text: Text) -> bool: diff --git a/plotnine/_mpl/patches.py b/plotnine/_mpl/patches.py index 9367da7f1..f09fb3e37 100644 --- a/plotnine/_mpl/patches.py +++ b/plotnine/_mpl/patches.py @@ -1,112 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - from matplotlib import artist -from matplotlib.patches import FancyBboxPatch, Rectangle -from matplotlib.transforms import Bbox - -from plotnine._mpl.utils import rel_position - -if TYPE_CHECKING: - from plotnine.typing import StripPosition - - from .text import StripText - - -# We subclass because we want to learn the size and location -# of the box when the layout manager is running. -# With MPLs default text & boxpatch, the patch gets its -# dimension information at draw time. - - -class StripTextPatch(FancyBboxPatch): - """ - Strip text background box - """ - - text: StripText - """ - The text artists that is wrapped by this box - """ - - position: StripPosition - """ - The position of the strip_text associated with this patch - """ - - expand: float = 1 - """ - Factor by which to expand the thickness of this patch. - - This value is used by the layout manager to increase the breadth - of the narrower strip_backgrounds. - """ - - def __init__(self, text: StripText): - super().__init__( - # The position, width and height are determine in - # .get_window_extent. - (0, 0), - width=1, - height=1, - boxstyle="square, pad=0", - clip_on=False, - zorder=2.2, - ) - - self.text = text - self.position = text.draw_info.position - - def get_window_extent(self, renderer=None): - """ - Location & dimensions of the box in display coordinates - """ - info = self.text.draw_info - m = info.margin - - # bboxes in display space - text_bbox = self.text.get_window_extent(renderer) - ax_bbox = info.ax.bbox.frozen() - - # line height in display space - line_height = self.text._line_height(renderer) - - # Convert the bottom left coordinates of the patch from - # transAxes to display space. We are not justifying the patch - # within the axes so we use 0 for the lengths, this gives us - # a patch that starts at the edge of the axes and not one that - # ends at the edge - x0 = rel_position(info.bg_x, 0, ax_bbox.x0, ax_bbox.x1) - y0 = rel_position(info.bg_y, 0, ax_bbox.y0, ax_bbox.y1) - - # info.bg_width and info.bg_height are in axes space - # so they are a scaling factor - if info.position == "top": - width = ax_bbox.width * info.bg_width - height = text_bbox.height + ((m.b + m.t) * line_height) - height *= self.expand - y0 += height * info.strip_align - else: - height = ax_bbox.height * info.bg_height - width = text_bbox.width + ((m.l + m.r) * line_height) - width *= self.expand - x0 += width * info.strip_align - - return Bbox.from_bounds(x0, y0, width, height) - - @artist.allow_rasterization - def draw(self, renderer): - """ - Draw patch - """ - # The geometry of the patch is determined by its rectangular bounds, - # this is also its "window_extent". As the extent value is in - # display units, we don't need a transform. - bbox = self.get_window_extent(renderer) - self.set_bounds(bbox.bounds) - self.set_transform(None) - return super().draw(renderer) +from matplotlib.patches import Rectangle class InsideStrokedRectangle(Rectangle): diff --git a/plotnine/_mpl/text.py b/plotnine/_mpl/text.py index 31ae730b4..89626757b 100644 --- a/plotnine/_mpl/text.py +++ b/plotnine/_mpl/text.py @@ -3,18 +3,13 @@ from functools import lru_cache from typing import TYPE_CHECKING -from matplotlib import artist +from matplotlib.patches import FancyBboxPatch from matplotlib.text import Text -from plotnine._utils import ha_as_float, va_as_float - -from .patches import StripTextPatch -from .utils import bbox_in_axes_space, rel_position - if TYPE_CHECKING: - from matplotlib.backend_bases import RendererBase + from matplotlib.axes import Axes - from plotnine.iapi import strip_draw_info + from plotnine.typing import StripPosition class StripText(Text): @@ -22,28 +17,43 @@ class StripText(Text): Strip Text """ - draw_info: strip_draw_info - patch: StripTextPatch - - def __init__(self, info: strip_draw_info): + patch: FancyBboxPatch + position: StripPosition + + def __init__( + self, + ax: Axes, + position: StripPosition, + label: str, + ): + self.position = position + is_oneline = len(label.split("\n")) == 1 kwargs = { - "rotation": info.rotation, - "transform": info.ax.transAxes, + "transform": ax.transAxes, "clip_on": False, "zorder": 3.3, - # Since the text can be rotated, it is simpler to anchor it at - # the center, align it, then do the rotation. Vertically, - # center_baseline places the text in the visual center, but - # only if it is one line. For multiline text, we are better - # off with plain center. + # The strip_text themeable rotates the text. We anchor it at + # the center so that alignment holds under any rotation it + # applies. Vertically, center_baseline places the text in the + # visual center, but only if it is one line. For multiline + # text, we are better off with plain center. "ha": "center", - "va": "center_baseline" if info.is_oneline else "center", + "va": "center_baseline" if is_oneline else "center", "rotation_mode": "anchor", } - super().__init__(0, 0, info.label, **kwargs) - self.draw_info = info - self.patch = StripTextPatch(self) + super().__init__(0, 0, label, **kwargs) + self.ax = ax + self.patch = FancyBboxPatch( + (0, 0), + width=1, + height=1, + boxstyle="square, pad=0", + clip_on=False, + zorder=2.2, + ) + # The layout manager groups patches by the side they sit on + self.patch.position = position # pyright: ignore[reportAttributeAccessIssue] # TODO: This should really be part of the unit conversions in the # margin class. @@ -56,20 +66,21 @@ def _line_height(self, renderer) -> float: parts: list[tuple[str, tuple[float, float], float, float]] try: - # matplotlib.Text._get_layout is a private API and we cannot - # tell how using it may fail in the future. + # matplotlib.Text._get_layout is a private API. If its name or + # return shape changes we fall back to an estimate. _, parts, _ = self._get_layout(renderer) # pyright: ignore[reportAttributeAccessIssue] - except Exception: + except (AttributeError, ValueError, TypeError): from warnings import warn from plotnine.exceptions import PlotnineWarning # The canvas height is nearly always bigger than the stated # fontsize. 1.36 is a good multiplication factor obtained by - # some rough exploration + # some rough exploration. A non-numeric size (e.g. "large") + # falls back to a fixed estimate. f = 1.36 size = self.get_fontsize() - height = round(size * f) if isinstance(size, int) else 14 + height = size * f if isinstance(size, (int, float)) else 14 warn( f"Could not calculate line height for {self.get_text()}. " "Using an estimate, please let us know about this at " @@ -82,113 +93,3 @@ def _line_height(self, renderer) -> float: height = max([p[1][1] for p in parts]) return height - - def _set_position(self, renderer): - """ - Set the postion of the text within the strip_background - """ - # We have to two premises that depend on each other: - # - # 1. The breadth of the strip_background grows to accomodate - # the strip_text. - # 2. The strip_text is justified within the strip_background. - # - # From these we note that the strip_background does not need the - # position of the strip_text, but it needs its size. Therefore - # we implement StripTextPatch.get_window_extent can use - # StripText.get_window_extent, peeking only at the size. - # - # And we implement StripText._set_position_* to use - # StripTextPatch.get_window_extent and make the calculations in - # both methods independent. - if self.draw_info.position == "top": - self._set_position_top(renderer) - else: # "right" - self._set_position_right(renderer) - - def _set_position_top(self, renderer): - """ - Set position of the text within the top strip_background - """ - info = self.draw_info - ha, va, ax, m = info.ha, info.va, info.ax, info.margin - - rel_x, rel_y = ha_as_float(ha), va_as_float(va) - patch_bbox = bbox_in_axes_space(self.patch, ax, renderer) - text_bbox = bbox_in_axes_space(self, ax, renderer) - - # line_height and margins in axes space - line_height = self._line_height(renderer) / ax.bbox.height - - x = ( - # Justify horizontally within the strip_background - rel_position( - rel_x, - text_bbox.width + (line_height * (m.l + m.r)), - patch_bbox.x0, - patch_bbox.x1, - ) - + (m.l * line_height) - + text_bbox.width / 2 - ) - # Setting the y position based on the bounding box is wrong - y = ( - rel_position( - rel_y, - text_bbox.height, - patch_bbox.y0 + m.b * line_height, - patch_bbox.y1 - m.t * line_height, - ) - + text_bbox.height / 2 - ) - self.set_position((x, y)) - - def _set_position_right(self, renderer): - """ - Set position of the text within the bottom strip_background - """ - info = self.draw_info - ha, va, ax, m = info.ha, info.va, info.ax, info.margin - - # bboxes in axes space - patch_bbox = bbox_in_axes_space(self.patch, ax, renderer) - text_bbox = bbox_in_axes_space(self, ax, renderer) - - # line_height in axes space - line_height = self._line_height(renderer) / ax.bbox.width - - rel_x, rel_y = ha_as_float(ha), va_as_float(va) - - x = ( - rel_position( - rel_x, - text_bbox.width, - patch_bbox.x0 + m.l * line_height, - patch_bbox.x1 - m.r * line_height, - ) - + text_bbox.width / 2 - ) - y = ( - # Justify vertically within the strip_background - rel_position( - rel_y, - text_bbox.height + ((m.b + m.t) * line_height), - patch_bbox.y0, - patch_bbox.y1, - ) - + (m.b * line_height) - + text_bbox.height / 2 - ) - self.set_position((x, y)) - - @artist.allow_rasterization - def draw(self, renderer: RendererBase): - """ - Draw text along with the patch - """ - if not self.get_visible(): - return - - self._set_position(renderer) - self.patch.draw(renderer) - return super().draw(renderer) diff --git a/plotnine/facets/strips.py b/plotnine/facets/strips.py index 075912bfb..facf1cd65 100644 --- a/plotnine/facets/strips.py +++ b/plotnine/facets/strips.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, List -from ..iapi import strip_draw_info, strip_label_details +from ..iapi import strip_label_details if TYPE_CHECKING: from typing import Sequence @@ -50,70 +50,6 @@ def __init__( label_info = strip_label_details.make(layout_info, vars, position) self.label_info = facet.labeller(label_info) - def get_draw_info(self) -> strip_draw_info: - """ - Get information required to draw strips - - Returns - ------- - out : - A structure with all the coordinates (x, y) required - to draw the strip text and the background box - (box_x, box_y, box_width, box_height). - """ - theme = self.theme - position = self.position - - if position == "top": - # The x & y values are just starting locations - # The final location is determined by the layout manager. - bg_y = 1 - ha = theme.getp(("strip_text_x", "ha"), "center") - va = theme.getp(("strip_text_x", "va"), "center") - rotation = theme.getp(("strip_text_x", "rotation")) - bg_height = 0 # Determined by the text size - margin = theme.getp(("strip_text_x", "margin")).to("lines") - strip_align = theme.getp("strip_align_x") - - # x & width properties of the background slide and - # shrink the strip horizontally. - bg_x = theme.getp(("strip_text_x", "x"), 0) - bg_width = theme.getp(("strip_background_x", "width"), 1) - - elif position == "right": - # The x & y values are just starting locations - # The final location is determined by the layout manager. - bg_x = 1 - ha = theme.getp(("strip_text_y", "ha"), "center") - va = theme.getp(("strip_text_y", "va"), "center") - rotation = theme.getp(("strip_text_y", "rotation")) - bg_width = 0 # Determine by the text height - margin = theme.getp(("strip_text_y", "margin")).to("lines") - strip_align = theme.getp("strip_align_y") - - # y & height properties of the background slide and - # shrink the strip vertically. - bg_y = theme.getp(("strip_text_y", "y"), 0) - bg_height = theme.getp(("strip_background_y", "height"), 1) - else: - raise ValueError(f"Unknown position for strip text: {position!r}") - - return strip_draw_info( - bg_x=bg_x, - bg_y=bg_y, - ha=ha, - va=va, - bg_width=bg_width, - bg_height=bg_height, - margin=margin, - strip_align=strip_align, - position=position, - label=self.label_info.text(), - ax=self.ax, - rotation=rotation, - layout=self.layout_info, - ) - def draw(self): """ Create a background patch and put a label on it @@ -122,14 +58,19 @@ def draw(self): from .._mpl.text import StripText targets = self.theme.targets - draw_info = self.get_draw_info() + position = self.position + + if position not in ("top", "right"): + raise ValueError(f"Unknown position for strip text: {position!r}") - text = StripText(draw_info) + text = StripText(self.ax, position, self.label_info.text()) rect = text.patch - self.facet.plot.figure.add_artist(text) + figure = self.facet.plot.figure + figure.add_artist(rect) + figure.add_artist(text) - if draw_info.position == "right": + if position == "right": targets.strip_background_y.append(rect) targets.strip_text_y.append(text) else: diff --git a/plotnine/iapi.py b/plotnine/iapi.py index d5a8cb101..a7a1d7c22 100644 --- a/plotnine/iapi.py +++ b/plotnine/iapi.py @@ -10,17 +10,14 @@ import itertools from copy import copy from dataclasses import dataclass, field, fields -from functools import cached_property from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any, Iterator, Literal, Optional, Sequence - from matplotlib.axes import Axes from matplotlib.figure import Figure from plotnine.scales.scale import scale - from plotnine.themes.elements.margin import margin from plotnine.typing import ( CoordRange, FloatArrayLike, @@ -28,7 +25,6 @@ ScaledAestheticsName, Side, StripPosition, - VerticalJustification, VerticalTextJustification, ) @@ -239,48 +235,6 @@ def is_bottom(self) -> bool: return self.row == self.nrow -@dataclass -class strip_draw_info: - """ - Information required to draw strips - """ - - bg_x: float - """Left of the strip background in transAxes""" - - bg_y: float - """Bottom of the strip background in transAxes""" - - ha: HorizontalJustification | float - """Horizontal justification of strip text within the background""" - - va: VerticalJustification | float - """Vertical justification of strip text within the background""" - - bg_width: float - """Width of the strip background in transAxes""" - - bg_height: float - """Height of the strip background in transAxes""" - - margin: margin - """Strip text margin with the units in lines""" - - strip_align: float - position: StripPosition - label: str - ax: Axes - rotation: float - layout: layout_details - - @cached_property - def is_oneline(self) -> bool: - """ - Whether the strip text is a single line - """ - return len(self.label.split("\n")) == 1 - - @dataclass class strip_label_details: """ diff --git a/plotnine/themes/targets.py b/plotnine/themes/targets.py index d8410e1c0..f12de7c97 100644 --- a/plotnine/themes/targets.py +++ b/plotnine/themes/targets.py @@ -8,11 +8,10 @@ from matplotlib.collections import LineCollection from matplotlib.lines import Line2D - from matplotlib.patches import Rectangle + from matplotlib.patches import FancyBboxPatch, Rectangle from matplotlib.text import Text from plotnine._mpl.offsetbox import ColoredDrawingArea - from plotnine._mpl.patches import StripTextPatch from plotnine._mpl.text import StripText from plotnine.iapi import legend_artists @@ -48,7 +47,7 @@ class ThemeTargets: plot_background: Optional[Rectangle] = None plot_footer_background: Optional[Rectangle] = None plot_footer_line: Optional[Line2D] = None - strip_background_x: list[StripTextPatch] = field(default_factory=list) - strip_background_y: list[StripTextPatch] = field(default_factory=list) + strip_background_x: list[FancyBboxPatch] = field(default_factory=list) + strip_background_y: list[FancyBboxPatch] = field(default_factory=list) strip_text_x: list[StripText] = field(default_factory=list) strip_text_y: list[StripText] = field(default_factory=list)