Skip to content

Commit cce47dc

Browse files
committed
fix: svgs with colorbars
1 parent 03a5a45 commit cce47dc

File tree

2 files changed

+44
-4
lines changed

2 files changed

+44
-4
lines changed

plotnine/ggplot.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def __init__(
129129
self.watermarks: list[watermark] = []
130130

131131
# build artefacts
132-
self._build_objs = NS()
132+
self._build_objs = NS(meta={})
133133

134134
def __str__(self) -> str:
135135
"""
@@ -595,6 +595,9 @@ def save_helper(
595595
This method has the same arguments as [](`~plotnine.ggplot.save`).
596596
Use it to get access to the figure that will be saved.
597597
"""
598+
if format is None and isinstance(filename, (str, Path)):
599+
format = str(filename).split(".")[-1]
600+
598601
fig_kwargs: Dict[str, Any] = {"format": format, **kwargs}
599602

600603
if limitsize is None:
@@ -644,6 +647,7 @@ def save_helper(
644647
if dpi is not None:
645648
self.theme = self.theme + theme(dpi=dpi)
646649

650+
self._build_objs.meta["figure_format"] = format
647651
figure = self.draw(show=False)
648652
return mpl_save_view(figure, fig_kwargs)
649653

plotnine/guides/guide_colorbar.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from matplotlib.text import Text
2727

2828
from plotnine import theme
29+
from plotnine.guides import guides
2930
from plotnine.scales.scale import scale
3031
from plotnine.typing import Side
3132

@@ -48,7 +49,12 @@ class guide_colorbar(guide):
4849
"""
4950

5051
display: Literal["gradient", "rectangles", "raster"] = "gradient"
51-
"""How to render the colorbar."""
52+
"""
53+
How to render the colorbar
54+
55+
SVG figures will always use "rectangles" to create gradients. This has
56+
better support across applications that render svg images.
57+
"""
5258

5359
alpha: Optional[float] = None
5460
"""
@@ -74,6 +80,12 @@ def __post_init__(self):
7480
if self.nbin is None:
7581
self.nbin = 300 # if self.display == "gradient" else 300
7682

83+
def setup(self, guides: guides):
84+
super().setup(guides)
85+
# See: add_segmented_colorbar
86+
if guides.plot._build_objs.meta["figure_format"] == "svg":
87+
self.display = "rectangles"
88+
7789
def train(self, scale: scale, aesthetic=None):
7890
self.nbin = cast("int", self.nbin)
7991
self.title = cast("str", self.title)
@@ -171,6 +183,7 @@ def draw(self):
171183
nbars = len(self.bar)
172184
elements = self.elements
173185
raster = self.display == "raster"
186+
alpha = self.alpha
174187

175188
colors = self.bar["color"].tolist()
176189
labels = self.key["label"].tolist()
@@ -219,9 +232,9 @@ def draw(self):
219232

220233
# colorbar
221234
if self.display == "rectangles":
222-
add_segmented_colorbar(auxbox, colors, elements)
235+
add_segmented_colorbar(auxbox, colors, alpha, elements)
223236
else:
224-
add_gradient_colorbar(auxbox, colors, elements, raster)
237+
add_gradient_colorbar(auxbox, colors, alpha, elements, raster)
225238

226239
# ticks
227240
visible = slice(
@@ -264,6 +277,7 @@ def draw(self):
264277
def add_gradient_colorbar(
265278
auxbox: AuxTransformBox,
266279
colors: Sequence[str],
280+
alpha: float | None,
267281
elements: GuideElementsColorbar,
268282
raster: bool = False,
269283
):
@@ -319,6 +333,7 @@ def add_gradient_colorbar(
319333
shading="gouraud",
320334
cmap=cmap,
321335
array=Z.ravel(),
336+
alpha=alpha,
322337
rasterized=raster,
323338
)
324339
auxbox.add_artist(coll)
@@ -327,6 +342,7 @@ def add_gradient_colorbar(
327342
def add_segmented_colorbar(
328343
auxbox: AuxTransformBox,
329344
colors: Sequence[str],
345+
alpha: float | None,
330346
elements: GuideElementsColorbar,
331347
):
332348
"""
@@ -335,6 +351,21 @@ def add_segmented_colorbar(
335351
from matplotlib.collections import PolyCollection
336352

337353
nbreak = len(colors)
354+
# Problem:
355+
# 1. Webbrowsers do not properly render SVG with QuadMesh
356+
# colorbars. Also when the QuadMesh is "rasterized",
357+
# the colorbar is misplaced within the SVG (and pdfs!).
358+
# So SVGs cannot use `add_gradient_colobar` at all.
359+
# 2. Webbrowsers do not properly render SVG with PolyCollection
360+
# colorbars when the adjacent rectangles that make up the
361+
# colorbar touch each other precisely. The "bars" appear to
362+
# be separated by lines.
363+
#
364+
# For a wayout, we overlap the bars. Overlapping creates artefacts
365+
# when alpha < 1, but having a gradient + alpha is rare. And, we can
366+
# minimise apparent artefacts by using a large overlap_factor.
367+
# A value of 2 gives the best results in the rare case should alpha < 1.
368+
overlap_factor = 2
338369
if elements.is_vertical:
339370
colorbar_height = elements.key_height
340371
colorbar_width = elements.key_width
@@ -345,6 +376,8 @@ def add_segmented_colorbar(
345376
for i in range(nbreak):
346377
y1 = i * linewidth
347378
y2 = y1 + linewidth
379+
if i > 1:
380+
y1 -= linewidth * overlap_factor
348381
verts.append(((x1, y1), (x1, y2), (x2, y2), (x2, y1)))
349382
else:
350383
colorbar_width = elements.key_height
@@ -356,12 +389,15 @@ def add_segmented_colorbar(
356389
for i in range(nbreak):
357390
x1 = i * linewidth
358391
x2 = x1 + linewidth
392+
if i > 1:
393+
x1 -= linewidth * overlap_factor
359394
verts.append(((x1, y1), (x1, y2), (x2, y2), (x2, y1)))
360395

361396
coll = PolyCollection(
362397
verts,
363398
facecolors=colors,
364399
linewidth=0,
400+
alpha=alpha,
365401
antialiased=False,
366402
)
367403
auxbox.add_artist(coll)

0 commit comments

Comments
 (0)