Skip to content

Styling Helpers

PlotStyler class that applies styling using the options system.

PlotStyler

Helper class for applying all styling using the options system.

Source code in pyretailscience/plots/styles/styling_helpers.py
class PlotStyler:
    """Helper class for applying all styling using the options system."""

    def __init__(self) -> None:
        """Initialize the PlotStyler."""
        self._style = PlotStyleHelper()

    def apply_base_styling(self, ax: Axes) -> None:
        """Apply base plot styling (spines, grid, background) using options."""
        ax.set_facecolor(self._style.background_color)
        ax.set_axisbelow(True)

        # Configure spines based on options
        ax.spines["top"].set_visible(self._style.show_top_spine)
        ax.spines["right"].set_visible(self._style.show_right_spine)
        ax.spines["bottom"].set_visible(self._style.show_bottom_spine)
        ax.spines["left"].set_visible(self._style.show_left_spine)

        # Configure grid based on options
        ax.grid(which="major", axis="x", color=self._style.grid_color, alpha=self._style.grid_alpha, zorder=1)
        ax.grid(which="major", axis="y", color=self._style.grid_color, alpha=self._style.grid_alpha, zorder=1)

    def apply_title(self, ax: Axes, title: str, pad: int | None = None) -> None:
        """Apply title styling using options."""
        effective_pad = pad if pad is not None else self._style.title_pad

        ax.set_title(
            title,
            fontproperties=get_font_properties(self._style.title_font),
            fontsize=self._style.title_size,
            pad=effective_pad,
        )

    def apply_label(self, ax: Axes, label: str, axis: str, pad: int | None = None) -> None:
        """Apply axis label styling using options."""
        # Use appropriate default padding based on axis
        if pad is None:
            pad = self._style.x_label_pad if axis == "x" else self._style.y_label_pad

        font_props = get_font_properties(self._style.label_font)

        axis_fn = ax.set_xlabel if axis == "x" else ax.set_ylabel
        axis_fn(label, fontproperties=font_props, fontsize=self._style.label_size, labelpad=pad)

    def apply_ticks(self, ax: Axes) -> None:
        """Apply tick styling using options."""
        ax.tick_params(axis="both", which="both", labelsize=self._style.tick_size)

        tick_font_props = get_font_properties(self._style.tick_font)
        for tick in [
            *ax.xaxis.get_major_ticks(),
            *ax.xaxis.get_minor_ticks(),
            *ax.yaxis.get_major_ticks(),
            *ax.yaxis.get_minor_ticks(),
        ]:
            tick.label1.set_fontproperties(tick_font_props)
            tick.label2.set_fontproperties(tick_font_props)

    def apply_source_text(
        self,
        ax: Axes,
        text: str,
        font_size: float | None = None,
        vertical_padding: float = 2,
        is_venn_diagram: bool = False,
        **kwargs: object,
    ) -> Text:
        """Apply source text styling using options.

        Args:
            ax (Axes): The graph to add the source text to.
            text (str): The source text.
            font_size (float, optional): The font size of the source text. If None, uses options default.
            vertical_padding (float, optional): The padding in ems below the x-axis label. Defaults to 2.
            is_venn_diagram (bool, optional): Flag to indicate if the diagram is a Venn diagram.
                If True, `x_norm` and `y_norm` will be set to fixed values. Defaults to False.
            **kwargs: Additional keyword arguments for positioning (x, y coordinates)

        Returns:
            Text: The source text object.
        """
        effective_font_size = font_size or self._style.source_size

        # Calculate position if not provided in kwargs
        if "x" not in kwargs or "y" not in kwargs:
            ax.figure.canvas.draw()

            if is_venn_diagram:
                x_norm, y_norm = 0.01, 0.02
            else:
                # Get y coordinate of the text
                xlabel_box = ax.xaxis.label.get_window_extent(renderer=ax.figure.canvas.get_renderer())

                top_of_label_px = xlabel_box.y0
                padding_px = vertical_padding * effective_font_size
                y_disp = top_of_label_px - padding_px - (xlabel_box.height)

                # Convert display coordinates to normalized figure coordinates
                y_norm = y_disp / ax.figure.bbox.height

                # Get x coordinate of the text
                ylabel_box = ax.yaxis.label.get_window_extent(renderer=ax.figure.canvas.get_renderer())
                title_box = ax.title.get_window_extent(renderer=ax.figure.canvas.get_renderer())
                min_x0 = min(ylabel_box.x0, title_box.x0)
                x_norm = ax.figure.transFigure.inverted().transform((min_x0, 0))[0]

            # Use calculated positions if not provided
            x_pos = kwargs.get("x", x_norm)
            y_pos = kwargs.get("y", y_norm)
        else:
            x_pos = kwargs.get("x", 0.01)
            y_pos = kwargs.get("y", 0.02)

        return ax.figure.text(
            x_pos,
            y_pos,
            text,
            ha="left",
            va="bottom",
            transform=ax.figure.transFigure,
            fontsize=effective_font_size,
            fontproperties=get_font_properties(self._style.source_font),
            color="dimgray",
        )

    def apply_legend(self, ax: Axes, title: str | None = None, outside: bool = False) -> None:
        """Apply legend styling using options."""
        if outside:
            legend = ax.legend(
                bbox_to_anchor=self._style.legend_bbox_to_anchor,
                loc=self._style.legend_loc,
                frameon=False,
            )
        else:
            legend = ax.legend(frameon=False)

        if title:
            legend.set_title(title)
            legend.get_title().set_fontproperties(get_font_properties(self._style.label_font))
            legend.get_title().set_fontsize(self._style.label_size)

        # Apply styling to legend text
        legend_font_props = get_font_properties(self._style.label_font)
        for text in legend.get_texts():
            text.set_fontproperties(legend_font_props)
            text.set_fontsize(self._style.label_size - 1)

__init__()

Initialize the PlotStyler.

Source code in pyretailscience/plots/styles/styling_helpers.py
def __init__(self) -> None:
    """Initialize the PlotStyler."""
    self._style = PlotStyleHelper()

apply_base_styling(ax)

Apply base plot styling (spines, grid, background) using options.

Source code in pyretailscience/plots/styles/styling_helpers.py
def apply_base_styling(self, ax: Axes) -> None:
    """Apply base plot styling (spines, grid, background) using options."""
    ax.set_facecolor(self._style.background_color)
    ax.set_axisbelow(True)

    # Configure spines based on options
    ax.spines["top"].set_visible(self._style.show_top_spine)
    ax.spines["right"].set_visible(self._style.show_right_spine)
    ax.spines["bottom"].set_visible(self._style.show_bottom_spine)
    ax.spines["left"].set_visible(self._style.show_left_spine)

    # Configure grid based on options
    ax.grid(which="major", axis="x", color=self._style.grid_color, alpha=self._style.grid_alpha, zorder=1)
    ax.grid(which="major", axis="y", color=self._style.grid_color, alpha=self._style.grid_alpha, zorder=1)

apply_label(ax, label, axis, pad=None)

Apply axis label styling using options.

Source code in pyretailscience/plots/styles/styling_helpers.py
def apply_label(self, ax: Axes, label: str, axis: str, pad: int | None = None) -> None:
    """Apply axis label styling using options."""
    # Use appropriate default padding based on axis
    if pad is None:
        pad = self._style.x_label_pad if axis == "x" else self._style.y_label_pad

    font_props = get_font_properties(self._style.label_font)

    axis_fn = ax.set_xlabel if axis == "x" else ax.set_ylabel
    axis_fn(label, fontproperties=font_props, fontsize=self._style.label_size, labelpad=pad)

apply_legend(ax, title=None, outside=False)

Apply legend styling using options.

Source code in pyretailscience/plots/styles/styling_helpers.py
def apply_legend(self, ax: Axes, title: str | None = None, outside: bool = False) -> None:
    """Apply legend styling using options."""
    if outside:
        legend = ax.legend(
            bbox_to_anchor=self._style.legend_bbox_to_anchor,
            loc=self._style.legend_loc,
            frameon=False,
        )
    else:
        legend = ax.legend(frameon=False)

    if title:
        legend.set_title(title)
        legend.get_title().set_fontproperties(get_font_properties(self._style.label_font))
        legend.get_title().set_fontsize(self._style.label_size)

    # Apply styling to legend text
    legend_font_props = get_font_properties(self._style.label_font)
    for text in legend.get_texts():
        text.set_fontproperties(legend_font_props)
        text.set_fontsize(self._style.label_size - 1)

apply_source_text(ax, text, font_size=None, vertical_padding=2, is_venn_diagram=False, **kwargs)

Apply source text styling using options.

Parameters:

Name Type Description Default
ax Axes

The graph to add the source text to.

required
text str

The source text.

required
font_size float

The font size of the source text. If None, uses options default.

None
vertical_padding float

The padding in ems below the x-axis label. Defaults to 2.

2
is_venn_diagram bool

Flag to indicate if the diagram is a Venn diagram. If True, x_norm and y_norm will be set to fixed values. Defaults to False.

False
**kwargs object

Additional keyword arguments for positioning (x, y coordinates)

{}

Returns:

Name Type Description
Text Text

The source text object.

Source code in pyretailscience/plots/styles/styling_helpers.py
def apply_source_text(
    self,
    ax: Axes,
    text: str,
    font_size: float | None = None,
    vertical_padding: float = 2,
    is_venn_diagram: bool = False,
    **kwargs: object,
) -> Text:
    """Apply source text styling using options.

    Args:
        ax (Axes): The graph to add the source text to.
        text (str): The source text.
        font_size (float, optional): The font size of the source text. If None, uses options default.
        vertical_padding (float, optional): The padding in ems below the x-axis label. Defaults to 2.
        is_venn_diagram (bool, optional): Flag to indicate if the diagram is a Venn diagram.
            If True, `x_norm` and `y_norm` will be set to fixed values. Defaults to False.
        **kwargs: Additional keyword arguments for positioning (x, y coordinates)

    Returns:
        Text: The source text object.
    """
    effective_font_size = font_size or self._style.source_size

    # Calculate position if not provided in kwargs
    if "x" not in kwargs or "y" not in kwargs:
        ax.figure.canvas.draw()

        if is_venn_diagram:
            x_norm, y_norm = 0.01, 0.02
        else:
            # Get y coordinate of the text
            xlabel_box = ax.xaxis.label.get_window_extent(renderer=ax.figure.canvas.get_renderer())

            top_of_label_px = xlabel_box.y0
            padding_px = vertical_padding * effective_font_size
            y_disp = top_of_label_px - padding_px - (xlabel_box.height)

            # Convert display coordinates to normalized figure coordinates
            y_norm = y_disp / ax.figure.bbox.height

            # Get x coordinate of the text
            ylabel_box = ax.yaxis.label.get_window_extent(renderer=ax.figure.canvas.get_renderer())
            title_box = ax.title.get_window_extent(renderer=ax.figure.canvas.get_renderer())
            min_x0 = min(ylabel_box.x0, title_box.x0)
            x_norm = ax.figure.transFigure.inverted().transform((min_x0, 0))[0]

        # Use calculated positions if not provided
        x_pos = kwargs.get("x", x_norm)
        y_pos = kwargs.get("y", y_norm)
    else:
        x_pos = kwargs.get("x", 0.01)
        y_pos = kwargs.get("y", 0.02)

    return ax.figure.text(
        x_pos,
        y_pos,
        text,
        ha="left",
        va="bottom",
        transform=ax.figure.transFigure,
        fontsize=effective_font_size,
        fontproperties=get_font_properties(self._style.source_font),
        color="dimgray",
    )

apply_ticks(ax)

Apply tick styling using options.

Source code in pyretailscience/plots/styles/styling_helpers.py
def apply_ticks(self, ax: Axes) -> None:
    """Apply tick styling using options."""
    ax.tick_params(axis="both", which="both", labelsize=self._style.tick_size)

    tick_font_props = get_font_properties(self._style.tick_font)
    for tick in [
        *ax.xaxis.get_major_ticks(),
        *ax.xaxis.get_minor_ticks(),
        *ax.yaxis.get_major_ticks(),
        *ax.yaxis.get_minor_ticks(),
    ]:
        tick.label1.set_fontproperties(tick_font_props)
        tick.label2.set_fontproperties(tick_font_props)

apply_title(ax, title, pad=None)

Apply title styling using options.

Source code in pyretailscience/plots/styles/styling_helpers.py
def apply_title(self, ax: Axes, title: str, pad: int | None = None) -> None:
    """Apply title styling using options."""
    effective_pad = pad if pad is not None else self._style.title_pad

    ax.set_title(
        title,
        fontproperties=get_font_properties(self._style.title_font),
        fontsize=self._style.title_size,
        pad=effective_pad,
    )