Skip to content

Commit f082458

Browse files
Feature: Add histtype option for ridgeline histograms (#557)
* Add histtype option for ridgeline histograms * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Refresh baseline cache key for hash-seed-stable compares (cherry picked from commit 1ff58be) --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent bd9ff0b commit f082458

2 files changed

Lines changed: 192 additions & 46 deletions

File tree

ultraplot/axes/plot.py

Lines changed: 172 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6449,6 +6449,7 @@ def _apply_ridgeline(
64496449
points=200,
64506450
hist=False,
64516451
bins="auto",
6452+
histtype=None,
64526453
fill=True,
64536454
alpha=1.0,
64546455
linewidth=1.5,
@@ -6490,6 +6491,10 @@ def _apply_ridgeline(
64906491
bins : int or sequence or str, default: 'auto'
64916492
Bin specification for histograms. Passed to numpy.histogram.
64926493
Only used when hist=True.
6494+
histtype : {'fill', 'bar', 'step', 'stepfilled'}, optional
6495+
Rendering style for histogram ridgelines. Defaults to ``'fill'``,
6496+
which uses a filled ridge curve. ``'bar'`` draws histogram bars.
6497+
Only used when hist=True.
64936498
fill : bool, default: True
64946499
Whether to fill the area under each curve.
64956500
alpha : float, default: 1.0
@@ -6553,6 +6558,14 @@ def _apply_ridgeline(
65536558

65546559
# Calculate KDE or histogram for each distribution
65556560
ridges = []
6561+
if hist and histtype is None:
6562+
histtype = "fill"
6563+
if hist:
6564+
allowed = ("fill", "bar", "step", "stepfilled")
6565+
if histtype not in allowed:
6566+
raise ValueError(
6567+
f"Invalid histtype={histtype!r}. Options are {allowed}."
6568+
)
65566569
for i, dist in enumerate(data):
65576570
dist = np.asarray(dist).ravel()
65586571
dist = dist[~np.isnan(dist)] # Remove NaNs
@@ -6572,7 +6585,15 @@ def _apply_ridgeline(
65726585
# Extend to bin edges for proper fill
65736586
x_extended = np.concatenate([[bin_edges[0]], x, [bin_edges[-1]]])
65746587
y_extended = np.concatenate([[0], counts, [0]])
6575-
ridges.append((x_extended, y_extended))
6588+
ridges.append(
6589+
{
6590+
"x": x_extended,
6591+
"y": y_extended,
6592+
"hist": True,
6593+
"counts": counts,
6594+
"bin_edges": bin_edges,
6595+
}
6596+
)
65766597
except Exception as e:
65776598
warnings._warn_ultraplot(
65786599
f"Histogram failed for distribution {i}: {e}, skipping"
@@ -6588,7 +6609,7 @@ def _apply_ridgeline(
65886609
x_margin = x_range * 0.1 # 10% margin
65896610
x = np.linspace(x_min - x_margin, x_max + x_margin, points)
65906611
y = kde(x)
6591-
ridges.append((x, y))
6612+
ridges.append({"x": x, "y": y, "hist": False})
65926613
except Exception as e:
65936614
warnings._warn_ultraplot(
65946615
f"KDE failed for distribution {i}: {e}, skipping"
@@ -6631,15 +6652,18 @@ def _apply_ridgeline(
66316652
)
66326653
else:
66336654
# Categorical (evenly-spaced) positioning mode
6634-
max_height = max(y.max() for x, y in ridges)
6635-
spacing = max(0.0, 1 - overlap)
6655+
max_height = max(ridge["y"].max() for ridge in ridges)
6656+
spacing = max_height * (1 + overlap)
66366657

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

6642-
for i, (x, y) in enumerate(ridges):
6663+
for i, ridge in enumerate(ridges):
6664+
x = ridge["x"]
6665+
y = ridge["y"]
6666+
is_hist = ridge.get("hist", False)
66436667
if continuous_mode:
66446668
# Continuous mode: scale to specified height and position at coordinate
66456669
y_max = y.max()
@@ -6661,68 +6685,170 @@ def _apply_ridgeline(
66616685
fill_zorder = base_zorder + (n_ridges - i - 1) * 2
66626686
outline_zorder = fill_zorder + 1
66636687

6664-
if vert:
6665-
# Traditional horizontal ridges
6666-
if fill:
6667-
# Fill without edge
6668-
poly = self.fill_between(
6669-
x,
6670-
offset,
6671-
y_plot,
6672-
facecolor=colors[i],
6688+
if is_hist and histtype == "bar":
6689+
counts = ridge["counts"]
6690+
bin_edges = ridge["bin_edges"]
6691+
if continuous_mode:
6692+
y_max = y.max()
6693+
scale = (heights[i] / y_max) if y_max > 0 else 1.0
6694+
bar_heights = counts * scale
6695+
else:
6696+
scale = (1.0 / max_height) if max_height > 0 else 1.0
6697+
bar_heights = counts * scale
6698+
if vert:
6699+
poly = self.bar(
6700+
bin_edges[:-1],
6701+
bar_heights,
6702+
width=np.diff(bin_edges),
6703+
bottom=offset,
6704+
align="edge",
6705+
color=colors[i],
66736706
alpha=alpha,
6674-
edgecolor="none",
6707+
edgecolor=edgecolor,
6708+
linewidth=linewidth,
66756709
label=labels[i],
66766710
zorder=fill_zorder,
66776711
)
6678-
# Draw outline on top (excluding baseline)
6679-
self.plot(
6680-
x,
6681-
y_plot,
6682-
color=edgecolor,
6683-
linewidth=linewidth,
6684-
zorder=outline_zorder,
6685-
)
66866712
else:
6687-
poly = self.plot(
6688-
x,
6689-
y_plot,
6713+
poly = self.barh(
6714+
bin_edges[:-1],
6715+
bar_heights,
6716+
height=np.diff(bin_edges),
6717+
left=offset,
6718+
align="edge",
66906719
color=colors[i],
6691-
linewidth=linewidth,
6692-
label=labels[i],
6693-
zorder=outline_zorder,
6694-
)[0]
6695-
else:
6696-
# Vertical ridges
6697-
if fill:
6698-
# Fill without edge
6699-
poly = self.fill_betweenx(
6700-
x,
6701-
offset,
6702-
y_plot,
6703-
facecolor=colors[i],
67046720
alpha=alpha,
6705-
edgecolor="none",
6721+
edgecolor=edgecolor,
6722+
linewidth=linewidth,
67066723
label=labels[i],
67076724
zorder=fill_zorder,
67086725
)
6709-
# Draw outline on top (excluding baseline)
6726+
elif is_hist and histtype in ("step", "stepfilled"):
6727+
if vert:
6728+
if histtype == "stepfilled":
6729+
poly = self.fill_between(
6730+
x,
6731+
offset,
6732+
y_plot,
6733+
facecolor=colors[i],
6734+
alpha=alpha,
6735+
edgecolor="none",
6736+
label=labels[i],
6737+
step="mid",
6738+
zorder=fill_zorder,
6739+
)
6740+
else:
6741+
poly = self.plot(
6742+
x,
6743+
y_plot,
6744+
color=edgecolor,
6745+
linewidth=linewidth,
6746+
label=labels[i],
6747+
drawstyle="steps-mid",
6748+
zorder=outline_zorder,
6749+
)[0]
67106750
self.plot(
6711-
y_plot,
67126751
x,
6752+
y_plot,
67136753
color=edgecolor,
67146754
linewidth=linewidth,
6755+
drawstyle="steps-mid",
67156756
zorder=outline_zorder,
67166757
)
67176758
else:
6718-
poly = self.plot(
6759+
if histtype == "stepfilled":
6760+
poly = self.fill_betweenx(
6761+
x,
6762+
offset,
6763+
y_plot,
6764+
facecolor=colors[i],
6765+
alpha=alpha,
6766+
edgecolor="none",
6767+
label=labels[i],
6768+
step="mid",
6769+
zorder=fill_zorder,
6770+
)
6771+
else:
6772+
poly = self.plot(
6773+
y_plot,
6774+
x,
6775+
color=edgecolor,
6776+
linewidth=linewidth,
6777+
label=labels[i],
6778+
drawstyle="steps-mid",
6779+
zorder=outline_zorder,
6780+
)[0]
6781+
self.plot(
67196782
y_plot,
67206783
x,
6721-
color=colors[i],
6784+
color=edgecolor,
67226785
linewidth=linewidth,
6723-
label=labels[i],
6786+
drawstyle="steps-mid",
67246787
zorder=outline_zorder,
6725-
)[0]
6788+
)
6789+
else:
6790+
if vert:
6791+
# Traditional horizontal ridges
6792+
if fill:
6793+
# Fill without edge
6794+
poly = self.fill_between(
6795+
x,
6796+
offset,
6797+
y_plot,
6798+
facecolor=colors[i],
6799+
alpha=alpha,
6800+
edgecolor="none",
6801+
label=labels[i],
6802+
zorder=fill_zorder,
6803+
)
6804+
# Draw outline on top (excluding baseline)
6805+
self.plot(
6806+
x,
6807+
y_plot,
6808+
color=edgecolor,
6809+
linewidth=linewidth,
6810+
zorder=outline_zorder,
6811+
)
6812+
else:
6813+
poly = self.plot(
6814+
x,
6815+
y_plot,
6816+
color=colors[i],
6817+
linewidth=linewidth,
6818+
label=labels[i],
6819+
zorder=outline_zorder,
6820+
)[0]
6821+
else:
6822+
# Vertical ridges
6823+
if fill:
6824+
# Fill without edge
6825+
poly = self.fill_betweenx(
6826+
x,
6827+
offset,
6828+
y_plot,
6829+
facecolor=colors[i],
6830+
alpha=alpha,
6831+
edgecolor="none",
6832+
label=labels[i],
6833+
zorder=fill_zorder,
6834+
)
6835+
# Draw outline on top (excluding baseline)
6836+
self.plot(
6837+
y_plot,
6838+
x,
6839+
color=edgecolor,
6840+
linewidth=linewidth,
6841+
zorder=outline_zorder,
6842+
)
6843+
else:
6844+
poly = self.plot(
6845+
y_plot,
6846+
x,
6847+
color=colors[i],
6848+
linewidth=linewidth,
6849+
label=labels[i],
6850+
zorder=outline_zorder,
6851+
)[0]
67266852

67276853
artists.append(poly)
67286854

ultraplot/tests/test_statistical_plotting.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,26 @@ def test_ridgeline_histogram_colormap(rng):
271271
return fig
272272

273273

274+
def test_ridgeline_histogram_bar(rng):
275+
"""
276+
Test ridgeline plot with histogram bars.
277+
"""
278+
data = [rng.normal(i, 1, 300) for i in range(4)]
279+
labels = [f"Group {i+1}" for i in range(4)]
280+
281+
fig, ax = uplt.subplots()
282+
artists = ax.ridgeline(
283+
data,
284+
labels=labels,
285+
overlap=0.5,
286+
hist=True,
287+
histtype="bar",
288+
bins=12,
289+
)
290+
assert len(artists) == len(data)
291+
uplt.close(fig)
292+
293+
274294
@pytest.mark.mpl_image_compare
275295
def test_ridgeline_comparison_kde_vs_hist(rng):
276296
"""

0 commit comments

Comments
 (0)