Source code for plotnine_extra.coords.coord_axes_inside

"""
``coord_axes_inside``: draw the axes inside the panel.

Port of ``ggh4x::coord_axes_inside``. The R version repositions
the axis lines to a chosen origin inside the panel area; the
Python implementation here is a thin subclass of
:class:`coord_cartesian` that records the origin and a small
post-build hook for tweaking the matplotlib axes once the figure
has been drawn.

.. note::

    Because plotnine's tick / spine drawing is done directly
    inside ``facet.set_breaks_and_labels``, this class only
    captures the desired origin. Users should call
    :func:`apply_axes_inside` on the rendered ``ggplot.figure``
    *after* calling ``ggplot.draw()`` to actually move the
    spines. A future plotnine release that exposes a draw hook
    will let us automate this step.
"""

from __future__ import annotations

import functools
from typing import TYPE_CHECKING

from plotnine.coords.coord_cartesian import coord_cartesian

if TYPE_CHECKING:
    from matplotlib.figure import Figure

__all__ = ("coord_axes_inside", "apply_axes_inside")


[docs] class coord_axes_inside(coord_cartesian): """ Cartesian coordinates with axes drawn inside the panel. Parameters ---------- xlim, ylim, expand Same as :class:`coord_cartesian`. xintercept : float, default 0 x-position at which the y axis spine should sit. yintercept : float, default 0 y-position at which the x axis spine should sit. labels_inside : bool, default False If ``True``, tick labels are drawn alongside the relocated spine; otherwise the standard outer labels are kept. Notes ----- Plotnine 0.15/0.16 does not expose a post-draw hook on the coord class. This implementation therefore wraps :meth:`ggplot.draw` via ``__radd__`` so that :func:`apply_axes_inside` is invoked on the resulting matplotlib :class:`~matplotlib.figure.Figure` after plotnine finishes rendering. The standard axis spines are otherwise left alone by ``coord_cartesian``. """ is_linear = True def __init__( self, xlim=None, ylim=None, expand: bool = True, xintercept: float = 0.0, yintercept: float = 0.0, labels_inside: bool = False, ): super().__init__(xlim=xlim, ylim=ylim, expand=expand) self.xintercept = xintercept self.yintercept = yintercept self.labels_inside = labels_inside def __radd__(self, other): # Attach the coord the normal plotnine way, then wrap # the ggplot's ``draw`` method so the spines get moved # once matplotlib finishes rendering. plot = super().__radd__(other) # Avoid wrapping the same ggplot twice (e.g. when the # user chains two coord_axes_inside calls). if getattr(plot.draw, "_axes_inside_wrapped", False): return plot orig_draw = plot.draw xintercept = self.xintercept yintercept = self.yintercept labels_inside = self.labels_inside @functools.wraps(orig_draw) def _wrapped_draw(*args, **kwargs): fig = orig_draw(*args, **kwargs) if fig is not None: apply_axes_inside( fig, xintercept=xintercept, yintercept=yintercept, labels_inside=labels_inside, ) return fig _wrapped_draw._axes_inside_wrapped = True # type: ignore[attr-defined] plot.draw = _wrapped_draw # type: ignore[method-assign] return plot
[docs] def apply_axes_inside( figure: "Figure", xintercept: float = 0.0, yintercept: float = 0.0, labels_inside: bool = False, ) -> "Figure": """ Reposition the axis spines of every ``Axes`` in ``figure`` so that they cross at ``(xintercept, yintercept)``. This is the manual half of :class:`coord_axes_inside`. Call it after ``ggplot.draw()``:: fig = (p + coord_axes_inside(xintercept=5)).draw(show=False) apply_axes_inside(fig, xintercept=5) """ for ax in figure.axes: ax.spines["left"].set_position(("data", xintercept)) ax.spines["bottom"].set_position(("data", yintercept)) ax.spines["right"].set_visible(False) ax.spines["top"].set_visible(False) if labels_inside: ax.xaxis.set_ticks_position("bottom") ax.yaxis.set_ticks_position("left") return figure