diff --git a/doc/_quartodoc.yml b/doc/_quartodoc.yml index 4ac6c526d..6414031a5 100644 --- a/doc/_quartodoc.yml +++ b/doc/_quartodoc.yml @@ -586,6 +586,10 @@ quartodoc: contents: - get_aesthetic_limits + - package: plotnine.session + contents: + - last_plot + - title: Datasets desc: | These datasets ship with the plotnine and you can import them with diff --git a/plotnine/composition/_compose.py b/plotnine/composition/_compose.py index 010c07d8d..816ca5199 100644 --- a/plotnine/composition/_compose.py +++ b/plotnine/composition/_compose.py @@ -18,6 +18,7 @@ from ..composition._plot_layout import plot_layout from ..composition._types import ComposeAddable from ..options import get_option +from ..session import set_last_plot if TYPE_CHECKING: from pathlib import Path @@ -559,6 +560,7 @@ def _draw(cmp): self.theme.apply() figure.set_layout_engine(PlotnineLayoutEngine(self)) + set_last_plot(self) return figure def _draw_plots(self): diff --git a/plotnine/ggplot.py b/plotnine/ggplot.py index 5328c7ec2..f1fa90fe2 100755 --- a/plotnine/ggplot.py +++ b/plotnine/ggplot.py @@ -42,6 +42,7 @@ from .mapping.aes import aes from .options import get_option from .scales.scales import Scales +from .session import set_last_plot from .themes.theme import theme, theme_get if TYPE_CHECKING: @@ -374,6 +375,7 @@ def draw(self, *, show: bool = False) -> Figure: self.theme.apply() figure.set_layout_engine(PlotnineLayoutEngine(self)) + set_last_plot(self) return figure def _setup(self) -> Figure: diff --git a/plotnine/session.py b/plotnine/session.py new file mode 100644 index 000000000..abf5a284d --- /dev/null +++ b/plotnine/session.py @@ -0,0 +1,45 @@ +""" +Session state for plotnine +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from plotnine.composition._compose import Compose + from plotnine.ggplot import ggplot + +__all__ = ("last_plot",) + +LAST_PLOT: ggplot | Compose | None = None + + +def last_plot() -> ggplot | Compose | None: + """ + Retrieve the last plot rendered in this session + + Returns + ------- + ggplot | Compose | None + The last plot that was rendered via `draw()`, `save()`, + or notebook display. Returns `None` if no plot has been + rendered yet. + """ + return LAST_PLOT + + +def set_last_plot(plot: ggplot | Compose) -> None: + """ + Save the last plot rendered in this session + """ + global LAST_PLOT + LAST_PLOT = plot + + +def reset_last_plot() -> None: + """ + Clear the last plot rendered in this session + """ + global LAST_PLOT + LAST_PLOT = None diff --git a/plotnine/themes/theme.py b/plotnine/themes/theme.py index 98ebc9cd7..d32fbb03e 100644 --- a/plotnine/themes/theme.py +++ b/plotnine/themes/theme.py @@ -105,6 +105,7 @@ class theme: # be targeted for theming by the themeables # It is initialised in the setup method. targets: ThemeTargets + _is_retina = False def __init__( self, @@ -463,8 +464,13 @@ def to_retina(self) -> theme: The result is a theme that has double the dpi. """ + if self._is_retina: + return deepcopy(self) + dpi = self.getp("dpi") - return self + theme(dpi=dpi * 2) + self = self + theme(dpi=dpi * 2) + self._is_retina = True + return self def _smart_title_and_subtitle_ha( self, title: str | None, subtitle: str | None diff --git a/tests/test_session.py b/tests/test_session.py new file mode 100644 index 000000000..f18be3a1b --- /dev/null +++ b/tests/test_session.py @@ -0,0 +1,43 @@ +from tempfile import NamedTemporaryFile + +from plotnine import aes, geom_point, ggplot +from plotnine.composition import Compose +from plotnine.data import mtcars +from plotnine.session import last_plot, reset_last_plot + + +def test_last_plot_initially_none(): + reset_last_plot() + assert last_plot() is None + + +def test_last_plot_after_draw(): + reset_last_plot() + p = ggplot(mtcars, aes("wt", "mpg")) + geom_point() + p.draw() + assert last_plot() is not None + assert isinstance(last_plot(), ggplot) + + +def test_last_plot_after_save(tmp_path): + reset_last_plot() + p = ggplot(mtcars, aes("wt", "mpg")) + geom_point() + + with NamedTemporaryFile(suffix=".png") as tmp_file: + p.save(tmp_file.name, verbose=False) + + result = last_plot() + assert result is not None + # save() deepcopies, so last_plot won't be the same object + assert isinstance(result, ggplot) + + +def test_last_plot_tracks_compose(): + reset_last_plot() + p1 = ggplot(mtcars, aes("wt", "mpg")) + geom_point() + p2 = ggplot(mtcars, aes("hp", "mpg")) + geom_point() + compose = p1 | p2 + compose.draw() + result = last_plot() + assert result is not None + assert isinstance(result, Compose)