Skip to content

Commit e092608

Browse files
committed
fix: svgs with colorbars
1 parent 67c2515 commit e092608

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
@@ -28,6 +28,7 @@
2828
from matplotlib.text import Text
2929

3030
from plotnine import theme
31+
from plotnine.guides import guides
3132
from plotnine.scales.scale import scale
3233
from plotnine.typing import Side
3334

@@ -50,7 +51,12 @@ class guide_colorbar(guide):
5051
"""
5152

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

5561
alpha: Optional[float] = None
5662
"""
@@ -76,6 +82,12 @@ def __post_init__(self):
7682
if self.nbin is None:
7783
self.nbin = 300 # if self.display == "gradient" else 300
7884

85+
def setup(self, guides: guides):
86+
super().setup(guides)
87+
# See: add_segmented_colorbar
88+
if guides.plot._build_objs.meta.get("figure_format") == "svg":
89+
self.display = "rectangles"
90+
7991
def train(self, scale: scale, aesthetic=None):
8092
self.nbin = cast("int", self.nbin)
8193
self.title = cast("str", self.title)
@@ -173,6 +185,7 @@ def draw(self):
173185
nbars = len(self.bar)
174186
elements = self.elements
175187
raster = self.display == "raster"
188+
alpha = self.alpha
176189

177190
colors = self.bar["color"].tolist()
178191
labels = self.key["label"].tolist()
@@ -221,9 +234,9 @@ def draw(self):
221234

222235
# colorbar
223236
if self.display == "rectangles":
224-
add_segmented_colorbar(auxbox, colors, elements)
237+
add_segmented_colorbar(auxbox, colors, alpha, elements)
225238
else:
226-
add_gradient_colorbar(auxbox, colors, elements, raster)
239+
add_gradient_colorbar(auxbox, colors, alpha, elements, raster)
227240

228241
# ticks
229242
visible = slice(
@@ -266,6 +279,7 @@ def draw(self):
266279
def add_gradient_colorbar(
267280
auxbox: AuxTransformBox,
268281
colors: Sequence[str],
282+
alpha: float | None,
269283
elements: GuideElementsColorbar,
270284
raster: bool = False,
271285
):
@@ -321,6 +335,7 @@ def add_gradient_colorbar(
321335
shading="gouraud",
322336
cmap=cmap,
323337
array=Z.ravel(),
338+
alpha=alpha,
324339
rasterized=raster,
325340
)
326341
auxbox.add_artist(coll)
@@ -329,6 +344,7 @@ def add_gradient_colorbar(
329344
def add_segmented_colorbar(
330345
auxbox: AuxTransformBox,
331346
colors: Sequence[str],
347+
alpha: float | None,
332348
elements: GuideElementsColorbar,
333349
):
334350
"""
@@ -337,6 +353,21 @@ def add_segmented_colorbar(
337353
from matplotlib.collections import PolyCollection
338354

339355
nbreak = len(colors)
356+
# Problem:
357+
# 1. Webbrowsers do not properly render SVG with QuadMesh
358+
# colorbars. Also when the QuadMesh is "rasterized",
359+
# the colorbar is misplaced within the SVG (and pdfs!).
360+
# So SVGs cannot use `add_gradient_colobar` at all.
361+
# 2. Webbrowsers do not properly render SVG with PolyCollection
362+
# colorbars when the adjacent rectangles that make up the
363+
# colorbar touch each other precisely. The "bars" appear to
364+
# be separated by lines.
365+
#
366+
# For a wayout, we overlap the bars. Overlapping creates artefacts
367+
# when alpha < 1, but having a gradient + alpha is rare. And, we can
368+
# minimise apparent artefacts by using a large overlap_factor.
369+
# A value of 2 gives the best results in the rare case should alpha < 1.
370+
overlap_factor = 2
340371
if elements.is_vertical:
341372
colorbar_height = elements.key_height
342373
colorbar_width = elements.key_width
@@ -347,6 +378,8 @@ def add_segmented_colorbar(
347378
for i in range(nbreak):
348379
y1 = i * linewidth
349380
y2 = y1 + linewidth
381+
if i > 1:
382+
y1 -= linewidth * overlap_factor
350383
verts.append(((x1, y1), (x1, y2), (x2, y2), (x2, y1)))
351384
else:
352385
colorbar_width = elements.key_height
@@ -358,12 +391,15 @@ def add_segmented_colorbar(
358391
for i in range(nbreak):
359392
x1 = i * linewidth
360393
x2 = x1 + linewidth
394+
if i > 1:
395+
x1 -= linewidth * overlap_factor
361396
verts.append(((x1, y1), (x1, y2), (x2, y2), (x2, y1)))
362397

363398
coll = PolyCollection(
364399
verts,
365400
facecolors=colors,
366401
linewidth=0,
402+
alpha=alpha,
367403
antialiased=False,
368404
)
369405
auxbox.add_artist(coll)

0 commit comments

Comments
 (0)