Source code for plotnine.themes.elements

"""
Theme elements used to decorate the graph.
"""
from __future__ import annotations

import typing
from contextlib import suppress
from dataclasses import dataclass

if typing.TYPE_CHECKING:
    from typing import Any, Callable, Literal, Optional, Sequence

    from plotnine.typing import Theme, TupleFloat3, TupleFloat4


class element_base:
    """
    Base class for all theme elements
    """

    properties: dict[str, Any]  # dict of the properties

    def __init__(self):
        self.properties = {"visible": True}

    def __repr__(self) -> str:
        """
        Element representation
        """
        return f"{self.__class__.__name__}({self})"

    def __str__(self) -> str:
        """
        Element as string
        """
        d = self.properties.copy()
        del d["visible"]
        return f"{d}"

    def setup(self, theme: Theme):
        """
        Setup the theme_element before drawing
        """


[docs]class element_line(element_base): """ theme element: line used for backgrounds and borders parameters ---------- color : str | tuple line color colour : str | tuple alias of color linetype : str | tuple line style. if a string, it should be one of *solid*, *dashed*, *dashdot* or *dotted*. you can create interesting dashed patterns using tuples, see :meth:`matplotlib.lines.line2D.set_linestyle`. size : float line thickness kwargs : dict parameters recognised by :class:`matplotlib.lines.line2d`. """ def __init__( self, *, color: Optional[str | TupleFloat3 | TupleFloat4] = None, size: Optional[float] = None, linetype: Optional[str | Sequence[int]] = None, lineend: Optional[Literal["butt", "projecting", "round"]] = None, colour: Optional[str | TupleFloat3 | TupleFloat4] = None, **kwargs: Any, ): super().__init__() self.properties.update(**kwargs) color = color if color else colour if color: self.properties["color"] = color if size: self.properties["linewidth"] = size if linetype: self.properties["linestyle"] = linetype if linetype in ("solid", "-") and lineend: self.properties["solid_capstyle"] = lineend elif linetype and lineend: self.properties["dash_capstyle"] = lineend
[docs]class element_rect(element_base): """ Theme element: Rectangle Used for backgrounds and borders Parameters ---------- fill : str | tuple Rectangle background color color : str | tuple Line color colour : str | tuple Alias of color size : float Line thickness kwargs : dict Parameters recognised by :class:`matplotlib.patches.Rectangle`. In some cases you can use the fancy parameters from :class:`matplotlib.patches.FancyBboxPatch`. """ def __init__( self, fill: Optional[str | TupleFloat3 | TupleFloat4] = None, color: Optional[str | TupleFloat3 | TupleFloat4] = None, size: Optional[float] = None, linetype: Optional[str | Sequence[int]] = None, colour: Optional[str | TupleFloat3 | TupleFloat4] = None, **kwargs: Any, ): super().__init__() self.properties.update(**kwargs) color = color if color else colour if fill: self.properties["facecolor"] = fill if color: self.properties["edgecolor"] = color if size: self.properties["linewidth"] = size if linetype: self.properties["linestyle"] = linetype
[docs]class element_text(element_base): """ Theme element: Text Parameters ---------- family : str Font family. See :meth:`matplotlib.text.Text.set_family` for supported values. style : str in ``['normal', 'italic', 'oblique']`` Font style color : str | tuple Text color weight : str Should be one of *normal*, *bold*, *heavy*, *light*, *ultrabold* or *ultralight*. size : float text size ha : str in ``['center', 'left', 'right']`` Horizontal Alignment. va : str in ``['center' , 'top', 'bottom', 'baseline']`` Vertical alignment. rotation : float Rotation angle in the range [0, 360] linespacing : float Line spacing backgroundcolor : str | tuple Background color margin : dict Margin around the text. The keys are one of ``['t', 'b', 'l', 'r']`` and ``units``. The units are one of ``['pt', 'lines', 'in']``. The *units* default to ``pt`` and the other keys to ``0``. Not all text themeables support margin parameters and other than the ``units``, only some of the other keys may apply. kwargs : dict Parameters recognised by :class:`matplotlib.text.Text` Notes ----- :class:`element_text` will accept parameters that conform to the **ggplot2** *element_text* API, but it is preferable the **Matplotlib** based API described above. """ def __init__( self, family: Optional[str | list[str]] = None, style: Optional[str] = None, weight: Optional[int | str] = None, color: Optional[str | TupleFloat3 | TupleFloat4] = None, size: Optional[float] = None, ha: Optional[Literal["center", "left", "right"]] = None, va: Optional[Literal["center", "top", "bottom", "baseline"]] = None, rotation: Optional[float] = None, linespacing: Optional[float] = None, backgroundcolor: Optional[str | TupleFloat3 | TupleFloat4] = None, margin: Optional[dict[str, Any]] = None, **kwargs: Any, ): # ggplot2 translation with suppress(KeyError): linespacing = kwargs.pop("lineheight") with suppress(KeyError): color = color or kwargs.pop("colour") with suppress(KeyError): _face = kwargs.pop("face") if _face == "plain": style = "normal" elif _face == "italic": style = "italic" elif _face == "bold": weight = "bold" elif _face == "bold.italic": style = "italic" weight = "bold" with suppress(KeyError): ha = self._translate_hjust(kwargs.pop("hjust")) with suppress(KeyError): va = self._translate_vjust(kwargs.pop("vjust")) with suppress(KeyError): rotation = kwargs.pop("angle") super().__init__() self.properties.update(**kwargs) if margin is not None: margin = Margin(self, **margin) # type: ignore # Use the parameters that have been set names = ( "backgroundcolor", "color", "family", "ha", "linespacing", "rotation", "size", "style", "va", "weight", "margin", ) variables = locals() for name in names: if variables[name] is not None: self.properties[name] = variables[name] def setup(self, theme: Theme): """ Setup the theme_element before drawing """ if "margin" in self.properties: self.properties["margin"].theme = theme def _translate_hjust( self, just: float ) -> Literal["left", "right", "center"]: """ Translate ggplot2 justification from [0, 1] to left, right, center. """ if just == 0: return "left" elif just == 1: return "right" else: return "center" def _translate_vjust( self, just: float ) -> Literal["top", "bottom", "center"]: """ Translate ggplot2 justification from [0, 1] to top, bottom, center. """ if just == 0: return "bottom" elif just == 1: return "top" else: return "center"
class element_blank(element_base): """ Theme element: Blank """ def __init__(self): self.properties = {"visible": False} @dataclass class Margin: element: element_base theme: Optional[Theme] = None t: float = 0 b: float = 0 l: float = 0 r: float = 0 units: Literal["pt", "in", "lines", "fig"] = "pt" def __post_init__(self): if self.units in ("pts", "points", "px", "pixels"): self.units = "pt" elif self.units in ("in", "inch", "inches"): self.units = "in" elif self.units in ("line", "lines"): self.units = "lines" def __eq__(self, other: Any) -> bool: core = ("t", "b", "l", "r", "units") if self is other: return True if type(self) is not type(other): return False for attr in core: if getattr(self, attr) != getattr(other, attr): return False s_size = self.element.properties.get("size") o_size = other.element.properties.get("size") return s_size == o_size def get_as( self, loc: Literal["t", "b", "l", "r"], units: Literal["pt", "in", "lines", "fig"] = "pt", ) -> float: """ Return key in given units """ assert self.theme is not None dpi = 72 # TODO: Get the inherited size. We need to consider the # themeables mro size: float = self.element.properties.get("size", 11) from_units = self.units to_units = units W: float H: float W, H = self.theme.themeables.property("figure_size") # inches L = (W * dpi) if loc in "tb" else (H * dpi) # pts functions: dict[str, Callable[[float], float]] = { "fig-in": lambda x: x * L / dpi, "fig-lines": lambda x: x * L / size, "fig-pt": lambda x: x * L, "in-fig": lambda x: x * dpi / L, "in-lines": lambda x: x * dpi / size, "in-pt": lambda x: x * dpi, "lines-fig": lambda x: x * size / L, "lines-in": lambda x: x * size / dpi, "lines-pt": lambda x: x * size, "pt-fig": lambda x: x / L, "pt-in": lambda x: x / dpi, "pt-lines": lambda x: x / size, } value: float = getattr(self, loc) if from_units != to_units: conversion = f"{self.units}-{units}" try: value = functions[conversion](value) except ZeroDivisionError: value = 0 return value