diff --git a/doc/changelog.qmd b/doc/changelog.qmd index 41f3308c1..987895680 100644 --- a/doc/changelog.qmd +++ b/doc/changelog.qmd @@ -9,6 +9,8 @@ title: Changelog - Fix expansion of datetime and timedelta scales when using both the lower and upper addition constants. +- In quarto documents make the output `retina` even if the `fig-format` is + `png`. ## v0.14.0 (2024-10-28) diff --git a/plotnine/_utils/ipython.py b/plotnine/_utils/ipython.py index 546791e1d..70b102757 100644 --- a/plotnine/_utils/ipython.py +++ b/plotnine/_utils/ipython.py @@ -39,7 +39,9 @@ def is_inline_backend(): return "matplotlib_inline.backend_inline" in mpl.get_backend() -def get_display_function(format: FigureFormat) -> Callable[[bytes], None]: +def get_display_function( + format: FigureFormat, figure_size_px: tuple[int, int] +) -> Callable[[bytes], None]: """ Return a function that will display the plot image """ @@ -52,14 +54,16 @@ def get_display_function(format: FigureFormat) -> Callable[[bytes], None]: display_svg, ) + w, h = figure_size_px + def png(b: bytes): - display_png(Image(b, format="png")) + display_png(Image(b, format="png", width=w, height=h)) def retina(b: bytes): display_png(Image(b, format="png", retina=True)) def jpeg(b: bytes): - display_jpeg(Image(b, format="jpeg")) + display_jpeg(Image(b, format="jpeg", width=w, height=h)) def svg(b: bytes): display_svg(SVG(b)) diff --git a/plotnine/_utils/quarto.py b/plotnine/_utils/quarto.py new file mode 100644 index 000000000..154335de9 --- /dev/null +++ b/plotnine/_utils/quarto.py @@ -0,0 +1,33 @@ +import os + + +def is_quarto_environment() -> bool: + """ + Return True if running in quarto + """ + return "QUARTO_FIG_WIDTH" in os.environ + + +def set_options_from_quarto(): + """ + Set options from quarto + """ + from plotnine.options import set_option + + dpi = int(os.environ["QUARTO_FIG_DPI"]) + figure_size = ( + float(os.environ["QUARTO_FIG_WIDTH"]), + float(os.environ["QUARTO_FIG_HEIGHT"]), + ) + # quarto verifies the format + # If is retina, it doubles the original dpi and changes the + # format to png. Since we cannot tell whether fig-format is + # png or retina, we assume retina. + figure_format = os.environ["QUARTO_FIG_FORMAT"] + if figure_format == "png": + figure_format = "retina" + dpi = dpi // 2 + + set_option("dpi", dpi) + set_option("figure_size", figure_size) + set_option("figure_format", figure_format) diff --git a/plotnine/ggplot.py b/plotnine/ggplot.py index 99dc88a48..44b36aa42 100755 --- a/plotnine/ggplot.py +++ b/plotnine/ggplot.py @@ -22,6 +22,7 @@ get_ipython, is_inline_backend, ) +from ._utils.quarto import is_quarto_environment from .coords import coord_cartesian from .exceptions import PlotnineError, PlotnineWarning from .facets import facet_null @@ -127,10 +128,8 @@ def __str__(self) -> str: """ Return a wrapped display size (in pixels) of the plot """ - dpi = self.theme.getp("dpi") - width, height = self.theme.getp("figure_size") - W, H = int(width * dpi), int(height * dpi) - return f"" + w, h = self.theme._figure_size_px + return f"" def _ipython_display_(self): """ @@ -148,7 +147,12 @@ def show(self): Users should prefer this method instead of printing or repring the object. """ - self._display() if is_inline_backend() else self.draw(show=True) + if is_inline_backend() or is_quarto_environment(): + # Take charge of the display because we have to make + # adjustments for retina output. + self._display() + else: + self.draw(show=True) def _display(self): """ @@ -172,9 +176,10 @@ def _display(self): self.theme = self.theme.to_retina() save_format = "png" + figure_size_px = self.theme._figure_size_px buf = BytesIO() self.save(buf, format=save_format, verbose=False) - display_func = get_display_function(format) + display_func = get_display_function(format, figure_size_px) display_func(buf.getvalue()) def __deepcopy__(self, memo: dict[Any, Any]) -> ggplot: diff --git a/plotnine/options.py b/plotnine/options.py index 02e88f78d..f96e8fa14 100644 --- a/plotnine/options.py +++ b/plotnine/options.py @@ -1,8 +1,9 @@ from __future__ import annotations -import os from typing import TYPE_CHECKING +from ._utils import quarto + if TYPE_CHECKING: from typing import Any, Literal, Optional, Type @@ -125,23 +126,5 @@ def set_option(name: str, value: Any) -> Any: # Note that, reading the variables and setting them in a context manager # cannot not work since the option values would be set after the original # defaults have been used by the theme. -if "QUARTO_FIG_WIDTH" in os.environ: - - def _set_options_from_quarto(): - """ - Set options from quarto - """ - global dpi, figure_size, figure_format - - dpi = int(os.environ["QUARTO_FIG_DPI"]) - figure_size = ( - float(os.environ["QUARTO_FIG_WIDTH"]), - float(os.environ["QUARTO_FIG_HEIGHT"]), - ) - - # quarto verifies the format - # If is retina, it doubles the original dpi and changes the - # format to png - figure_format = os.environ["QUARTO_FIG_FORMAT"] # pyright: ignore - - _set_options_from_quarto() +if quarto.is_quarto_environment(): + quarto.set_options_from_quarto() diff --git a/plotnine/themes/theme.py b/plotnine/themes/theme.py index 6eb18dd2d..2c8e9c095 100644 --- a/plotnine/themes/theme.py +++ b/plotnine/themes/theme.py @@ -485,6 +485,15 @@ def _smart_title_and_subtitle_ha(self): if kwargs: self += theme(**kwargs) + @property + def _figure_size_px(self) -> tuple[int, int]: + """ + Return the size of the output in pixels + """ + dpi = self.getp("dpi") + width, height = self.getp("figure_size") + return (int(width * dpi), int(height * dpi)) + def theme_get() -> theme: """