Skip to content
218 changes: 172 additions & 46 deletions ultraplot/axes/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -6342,6 +6342,7 @@ def _apply_ridgeline(
points=200,
hist=False,
bins="auto",
histtype=None,
fill=True,
alpha=1.0,
linewidth=1.5,
Expand Down Expand Up @@ -6383,6 +6384,10 @@ def _apply_ridgeline(
bins : int or sequence or str, default: 'auto'
Bin specification for histograms. Passed to numpy.histogram.
Only used when hist=True.
histtype : {'fill', 'bar', 'step', 'stepfilled'}, optional
Rendering style for histogram ridgelines. Defaults to ``'fill'``,
which uses a filled ridge curve. ``'bar'`` draws histogram bars.
Only used when hist=True.
fill : bool, default: True
Whether to fill the area under each curve.
alpha : float, default: 1.0
Expand Down Expand Up @@ -6446,6 +6451,14 @@ def _apply_ridgeline(

# Calculate KDE or histogram for each distribution
ridges = []
if hist and histtype is None:
histtype = "fill"
if hist:
allowed = ("fill", "bar", "step", "stepfilled")
if histtype not in allowed:
raise ValueError(
f"Invalid histtype={histtype!r}. Options are {allowed}."
)
for i, dist in enumerate(data):
dist = np.asarray(dist).ravel()
dist = dist[~np.isnan(dist)] # Remove NaNs
Expand All @@ -6465,7 +6478,15 @@ def _apply_ridgeline(
# Extend to bin edges for proper fill
x_extended = np.concatenate([[bin_edges[0]], x, [bin_edges[-1]]])
y_extended = np.concatenate([[0], counts, [0]])
ridges.append((x_extended, y_extended))
ridges.append(
{
"x": x_extended,
"y": y_extended,
"hist": True,
"counts": counts,
"bin_edges": bin_edges,
}
)
except Exception as e:
warnings._warn_ultraplot(
f"Histogram failed for distribution {i}: {e}, skipping"
Expand All @@ -6481,7 +6502,7 @@ def _apply_ridgeline(
x_margin = x_range * 0.1 # 10% margin
x = np.linspace(x_min - x_margin, x_max + x_margin, points)
y = kde(x)
ridges.append((x, y))
ridges.append({"x": x, "y": y, "hist": False})
except Exception as e:
warnings._warn_ultraplot(
f"KDE failed for distribution {i}: {e}, skipping"
Expand Down Expand Up @@ -6524,15 +6545,18 @@ def _apply_ridgeline(
)
else:
# Categorical (evenly-spaced) positioning mode
max_height = max(y.max() for x, y in ridges)
spacing = max(0.0, 1 - overlap)
max_height = max(ridge["y"].max() for ridge in ridges)
spacing = max_height * (1 + overlap)

artists = []
# Base zorder for ridgelines - use a high value to ensure they're on top
base_zorder = kwargs.pop("zorder", 2)
n_ridges = len(ridges)

for i, (x, y) in enumerate(ridges):
for i, ridge in enumerate(ridges):
x = ridge["x"]
y = ridge["y"]
is_hist = ridge.get("hist", False)
if continuous_mode:
# Continuous mode: scale to specified height and position at coordinate
y_max = y.max()
Expand All @@ -6554,68 +6578,170 @@ def _apply_ridgeline(
fill_zorder = base_zorder + (n_ridges - i - 1) * 2
outline_zorder = fill_zorder + 1

if vert:
# Traditional horizontal ridges
if fill:
# Fill without edge
poly = self.fill_between(
x,
offset,
y_plot,
facecolor=colors[i],
if is_hist and histtype == "bar":
counts = ridge["counts"]
bin_edges = ridge["bin_edges"]
if continuous_mode:
y_max = y.max()
scale = (heights[i] / y_max) if y_max > 0 else 1.0
bar_heights = counts * scale
else:
scale = (1.0 / max_height) if max_height > 0 else 1.0
bar_heights = counts * scale
if vert:
poly = self.bar(
bin_edges[:-1],
bar_heights,
width=np.diff(bin_edges),
bottom=offset,
align="edge",
color=colors[i],
alpha=alpha,
edgecolor="none",
edgecolor=edgecolor,
linewidth=linewidth,
label=labels[i],
zorder=fill_zorder,
)
# Draw outline on top (excluding baseline)
self.plot(
x,
y_plot,
color=edgecolor,
linewidth=linewidth,
zorder=outline_zorder,
)
else:
poly = self.plot(
x,
y_plot,
poly = self.barh(
bin_edges[:-1],
bar_heights,
height=np.diff(bin_edges),
left=offset,
align="edge",
color=colors[i],
linewidth=linewidth,
label=labels[i],
zorder=outline_zorder,
)[0]
else:
# Vertical ridges
if fill:
# Fill without edge
poly = self.fill_betweenx(
x,
offset,
y_plot,
facecolor=colors[i],
alpha=alpha,
edgecolor="none",
edgecolor=edgecolor,
linewidth=linewidth,
label=labels[i],
zorder=fill_zorder,
)
# Draw outline on top (excluding baseline)
elif is_hist and histtype in ("step", "stepfilled"):
if vert:
if histtype == "stepfilled":
poly = self.fill_between(
x,
offset,
y_plot,
facecolor=colors[i],
alpha=alpha,
edgecolor="none",
label=labels[i],
step="mid",
zorder=fill_zorder,
)
else:
poly = self.plot(
x,
y_plot,
color=edgecolor,
linewidth=linewidth,
label=labels[i],
drawstyle="steps-mid",
zorder=outline_zorder,
)[0]
self.plot(
y_plot,
x,
y_plot,
color=edgecolor,
linewidth=linewidth,
drawstyle="steps-mid",
zorder=outline_zorder,
)
else:
poly = self.plot(
if histtype == "stepfilled":
poly = self.fill_betweenx(
x,
offset,
y_plot,
facecolor=colors[i],
alpha=alpha,
edgecolor="none",
label=labels[i],
step="mid",
zorder=fill_zorder,
)
else:
poly = self.plot(
y_plot,
x,
color=edgecolor,
linewidth=linewidth,
label=labels[i],
drawstyle="steps-mid",
zorder=outline_zorder,
)[0]
self.plot(
y_plot,
x,
color=colors[i],
color=edgecolor,
linewidth=linewidth,
label=labels[i],
drawstyle="steps-mid",
zorder=outline_zorder,
)[0]
)
else:
if vert:
# Traditional horizontal ridges
if fill:
# Fill without edge
poly = self.fill_between(
x,
offset,
y_plot,
facecolor=colors[i],
alpha=alpha,
edgecolor="none",
label=labels[i],
zorder=fill_zorder,
)
# Draw outline on top (excluding baseline)
self.plot(
x,
y_plot,
color=edgecolor,
linewidth=linewidth,
zorder=outline_zorder,
)
else:
poly = self.plot(
x,
y_plot,
color=colors[i],
linewidth=linewidth,
label=labels[i],
zorder=outline_zorder,
)[0]
else:
# Vertical ridges
if fill:
# Fill without edge
poly = self.fill_betweenx(
x,
offset,
y_plot,
facecolor=colors[i],
alpha=alpha,
edgecolor="none",
label=labels[i],
zorder=fill_zorder,
)
# Draw outline on top (excluding baseline)
self.plot(
y_plot,
x,
color=edgecolor,
linewidth=linewidth,
zorder=outline_zorder,
)
else:
poly = self.plot(
y_plot,
x,
color=colors[i],
linewidth=linewidth,
label=labels[i],
zorder=outline_zorder,
)[0]

artists.append(poly)

Expand Down
20 changes: 20 additions & 0 deletions ultraplot/tests/test_statistical_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,26 @@ def test_ridgeline_histogram_colormap(rng):
return fig


def test_ridgeline_histogram_bar(rng):
"""
Test ridgeline plot with histogram bars.
"""
data = [rng.normal(i, 1, 300) for i in range(4)]
labels = [f"Group {i+1}" for i in range(4)]

fig, ax = uplt.subplots()
artists = ax.ridgeline(
data,
labels=labels,
overlap=0.5,
hist=True,
histtype="bar",
bins=12,
)
assert len(artists) == len(data)
uplt.close(fig)


@pytest.mark.mpl_image_compare
def test_ridgeline_comparison_kde_vs_hist(rng):
"""
Expand Down
Loading