Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion plotnine/ggplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down
6 changes: 6 additions & 0 deletions plotnine/guides/guide.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 39 additions & 9 deletions plotnine/guides/guide_colorbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
"""
Expand All @@ -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)
Expand Down Expand Up @@ -123,12 +135,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
Expand Down Expand Up @@ -179,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()
Expand Down Expand Up @@ -227,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(
Expand Down Expand Up @@ -272,6 +279,7 @@ def draw(self):
def add_gradient_colorbar(
auxbox: AuxTransformBox,
colors: Sequence[str],
alpha: float | None,
elements: GuideElementsColorbar,
raster: bool = False,
):
Expand Down Expand Up @@ -327,6 +335,7 @@ def add_gradient_colorbar(
shading="gouraud",
cmap=cmap,
array=Z.ravel(),
alpha=alpha,
rasterized=raster,
)
auxbox.add_artist(coll)
Expand All @@ -335,6 +344,7 @@ def add_gradient_colorbar(
def add_segmented_colorbar(
auxbox: AuxTransformBox,
colors: Sequence[str],
alpha: float | None,
elements: GuideElementsColorbar,
):
"""
Expand All @@ -343,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
Expand All @@ -353,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
Expand All @@ -364,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)
Expand Down