Source code for plotnine_extra.geoms.geom_half_violin
"""
Half-violin geom for raincloud plots.
Draws only one side of a violin plot, useful for
combining with jittered points and boxplots to create
raincloud plots.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import numpy as np
from plotnine.doctools import document
from plotnine.geoms.geom_violin import geom_violin
if TYPE_CHECKING:
import pandas as pd
from matplotlib.axes import Axes
from plotnine.coords.coord import coord
from plotnine.iapi import panel_view
[docs]
@document
class geom_half_violin(geom_violin):
"""
Half-violin plot showing one side of the density
Draws only the left or right half of a violin,
enabling raincloud plots when combined with
:class:`~plotnine.geom_boxplot` and jittered points.
{usage}
Parameters
----------
{common_parameters}
side : str
Which side to draw: ``"r"`` (right/top) or
``"l"`` (left/bottom). Default ``"r"``.
nudge : float
Amount to shift the half-violin away from center
in data units. Default ``0``.
"""
_aesthetics_doc = """
{aesthetics_table}
"""
DEFAULT_PARAMS = {
**geom_violin.DEFAULT_PARAMS,
"side": "r",
"nudge": 0,
}
[docs]
@staticmethod
def draw_group(
data: pd.DataFrame,
panel_params: panel_view,
coord: coord,
ax: Axes,
params: dict[str, Any],
):
side = params.get("side", "r")
nudge = params.get("nudge", 0)
# The violin data has x, y, and violinwidth
# columns. We modify x to clip to one side.
if "violinwidth" not in data.columns:
# Fall back to parent drawing
return geom_violin.draw_group(
data, panel_params, coord, ax, params
)
data = data.copy()
half_w = data["violinwidth"] / 2
if side == "r":
# Keep only the right half: x stays at
# center to center+width
xmin = data["x"]
xmax = data["x"] + half_w
else:
# Keep only the left half
xmin = data["x"] - half_w
xmax = data["x"]
# Apply nudge
xmin = xmin + nudge
xmax = xmax + nudge
# Build polygon for the half violin
data = coord.transform(data, panel_params)
# Re-derive in transformed coordinates
x_center = data["x"].to_numpy(dtype=float)
y_vals = data["y"].to_numpy(dtype=float)
vw = data["violinwidth"].to_numpy(dtype=float)
# Compute x range of transformed coords
if len(x_center) < 2:
return
# Build the half-violin polygon
half = vw / 2
if side == "r":
x_poly = np.concatenate([x_center, x_center[::-1] + half[::-1]])
y_poly = np.concatenate([y_vals, y_vals[::-1]])
else:
x_poly = np.concatenate(
[
x_center - half,
x_center[::-1],
]
)
y_poly = np.concatenate([y_vals, y_vals[::-1]])
# Apply nudge in transformed space
# (approximate: scale nudge by data range)
if nudge != 0:
x_range = panel_params.x.range[1] - panel_params.x.range[0]
nudge_t = nudge / x_range if x_range > 0 else 0
x_poly = x_poly + nudge_t
color = data["color"].iloc[0]
fill = data["fill"].iloc[0]
alpha = data["alpha"].iloc[0]
size = data["size"].iloc[0]
from matplotlib.patches import Polygon
poly = Polygon(
np.column_stack([x_poly, y_poly]),
closed=True,
facecolor=fill,
edgecolor=color,
alpha=alpha,
linewidth=size,
zorder=params.get("zorder", 1),
)
ax.add_patch(poly)