From 67c251503b2a0f8d10a6cb7abff42943aaaf087b Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Sat, 17 Jan 2026 04:42:02 +0300 Subject: [PATCH 1/2] Create guide.merge method on the base class --- plotnine/guides/guide.py | 6 ++++++ plotnine/guides/guide_colorbar.py | 6 ------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plotnine/guides/guide.py b/plotnine/guides/guide.py index 44134f3aa..418aa6b29 100644 --- a/plotnine/guides/guide.py +++ b/plotnine/guides/guide.py @@ -156,6 +156,12 @@ def train( Returns guide if training is successful """ + def merge(self, other: Self) -> Self: + """ + Merge with another guide + """ + return self + def draw(self) -> PackerBase: """ Draw guide diff --git a/plotnine/guides/guide_colorbar.py b/plotnine/guides/guide_colorbar.py index d3b1bd5af..03d19e1df 100644 --- a/plotnine/guides/guide_colorbar.py +++ b/plotnine/guides/guide_colorbar.py @@ -123,12 +123,6 @@ def train(self, scale: scale, aesthetic=None): self.hash = hashlib.sha256(info.encode("utf-8")).hexdigest() return self - def merge(self, other): - """ - Simply discards the other guide - """ - return self - def create_geoms(self): """ Return self if colorbar will be drawn and None if not From e09260817f48a1299e972f75f6e4a69d84f6bf75 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Sat, 17 Jan 2026 04:42:45 +0300 Subject: [PATCH 2/2] fix: svgs with colorbars --- plotnine/ggplot.py | 6 ++++- plotnine/guides/guide_colorbar.py | 42 ++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/plotnine/ggplot.py b/plotnine/ggplot.py index 6fd225e03..3130e41d6 100755 --- a/plotnine/ggplot.py +++ b/plotnine/ggplot.py @@ -129,7 +129,7 @@ def __init__( self.watermarks: list[watermark] = [] # build artefacts - self._build_objs = NS() + self._build_objs = NS(meta={}) def __str__(self) -> str: """ @@ -595,6 +595,9 @@ def save_helper( This method has the same arguments as [](`~plotnine.ggplot.save`). Use it to get access to the figure that will be saved. """ + if format is None and isinstance(filename, (str, Path)): + format = str(filename).split(".")[-1] + fig_kwargs: Dict[str, Any] = {"format": format, **kwargs} if limitsize is None: @@ -644,6 +647,7 @@ def save_helper( if dpi is not None: self.theme = self.theme + theme(dpi=dpi) + self._build_objs.meta["figure_format"] = format figure = self.draw(show=False) return mpl_save_view(figure, fig_kwargs) diff --git a/plotnine/guides/guide_colorbar.py b/plotnine/guides/guide_colorbar.py index 03d19e1df..29a5234d7 100644 --- a/plotnine/guides/guide_colorbar.py +++ b/plotnine/guides/guide_colorbar.py @@ -28,6 +28,7 @@ from matplotlib.text import Text from plotnine import theme + from plotnine.guides import guides from plotnine.scales.scale import scale from plotnine.typing import Side @@ -50,7 +51,12 @@ class guide_colorbar(guide): """ display: Literal["gradient", "rectangles", "raster"] = "gradient" - """How to render the colorbar.""" + """ + How to render the colorbar + + SVG figures will always use "rectangles" to create gradients. This has + better support across applications that render svg images. + """ alpha: Optional[float] = None """ @@ -76,6 +82,12 @@ def __post_init__(self): if self.nbin is None: self.nbin = 300 # if self.display == "gradient" else 300 + def setup(self, guides: guides): + super().setup(guides) + # See: add_segmented_colorbar + if guides.plot._build_objs.meta.get("figure_format") == "svg": + self.display = "rectangles" + def train(self, scale: scale, aesthetic=None): self.nbin = cast("int", self.nbin) self.title = cast("str", self.title) @@ -173,6 +185,7 @@ def draw(self): nbars = len(self.bar) elements = self.elements raster = self.display == "raster" + alpha = self.alpha colors = self.bar["color"].tolist() labels = self.key["label"].tolist() @@ -221,9 +234,9 @@ def draw(self): # colorbar if self.display == "rectangles": - add_segmented_colorbar(auxbox, colors, elements) + add_segmented_colorbar(auxbox, colors, alpha, elements) else: - add_gradient_colorbar(auxbox, colors, elements, raster) + add_gradient_colorbar(auxbox, colors, alpha, elements, raster) # ticks visible = slice( @@ -266,6 +279,7 @@ def draw(self): def add_gradient_colorbar( auxbox: AuxTransformBox, colors: Sequence[str], + alpha: float | None, elements: GuideElementsColorbar, raster: bool = False, ): @@ -321,6 +335,7 @@ def add_gradient_colorbar( shading="gouraud", cmap=cmap, array=Z.ravel(), + alpha=alpha, rasterized=raster, ) auxbox.add_artist(coll) @@ -329,6 +344,7 @@ def add_gradient_colorbar( def add_segmented_colorbar( auxbox: AuxTransformBox, colors: Sequence[str], + alpha: float | None, elements: GuideElementsColorbar, ): """ @@ -337,6 +353,21 @@ def add_segmented_colorbar( from matplotlib.collections import PolyCollection nbreak = len(colors) + # Problem: + # 1. Webbrowsers do not properly render SVG with QuadMesh + # colorbars. Also when the QuadMesh is "rasterized", + # the colorbar is misplaced within the SVG (and pdfs!). + # So SVGs cannot use `add_gradient_colobar` at all. + # 2. Webbrowsers do not properly render SVG with PolyCollection + # colorbars when the adjacent rectangles that make up the + # colorbar touch each other precisely. The "bars" appear to + # be separated by lines. + # + # For a wayout, we overlap the bars. Overlapping creates artefacts + # when alpha < 1, but having a gradient + alpha is rare. And, we can + # minimise apparent artefacts by using a large overlap_factor. + # A value of 2 gives the best results in the rare case should alpha < 1. + overlap_factor = 2 if elements.is_vertical: colorbar_height = elements.key_height colorbar_width = elements.key_width @@ -347,6 +378,8 @@ def add_segmented_colorbar( for i in range(nbreak): y1 = i * linewidth y2 = y1 + linewidth + if i > 1: + y1 -= linewidth * overlap_factor verts.append(((x1, y1), (x1, y2), (x2, y2), (x2, y1))) else: colorbar_width = elements.key_height @@ -358,12 +391,15 @@ def add_segmented_colorbar( for i in range(nbreak): x1 = i * linewidth x2 = x1 + linewidth + if i > 1: + x1 -= linewidth * overlap_factor verts.append(((x1, y1), (x1, y2), (x2, y2), (x2, y1))) coll = PolyCollection( verts, facecolors=colors, linewidth=0, + alpha=alpha, antialiased=False, ) auxbox.add_artist(coll)