Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1c0a42e

Browse files
authored
Add ridgeline plot feature (#451)
* Add ridgeline plot feature with histogram support Implements ridgeline plots (also known as joyplots) for visualizing distributions of multiple datasets as stacked, overlapping density curves. Features: - Support for both vertical (traditional) and horizontal orientations - Kernel density estimation (KDE) for smooth curves - Histogram mode for binned bar charts (hist=True) - Customizable overlap between ridges - Color specification via colormap or custom colors - Integration with UltraPlot's color cycle - Transparent error handling for invalid distributions - Follows UltraPlot's docstring snippet manager pattern Methods added: - ridgeline(): Create vertical ridgeline plots - ridgelineh(): Create horizontal ridgeline plots - _apply_ridgeline(): Internal implementation Tests added: - test_ridgeline_basic: Basic KDE functionality - test_ridgeline_colormap: Colormap support - test_ridgeline_horizontal: Horizontal orientation - test_ridgeline_custom_colors: Custom color specification - test_ridgeline_histogram: Histogram mode - test_ridgeline_histogram_colormap: Histogram with colormap - test_ridgeline_comparison_kde_vs_hist: KDE vs histogram comparison - test_ridgeline_empty_data: Error handling for empty data - test_ridgeline_label_mismatch: Error handling for label mismatch Docstrings registered with snippet manager following UltraPlot conventions. * Fix ridgeline plot outline to exclude baseline The ridge outlines now only trace the top curve of each distribution, not the baseline. This is achieved by: - Using fill_between/fill_betweenx with edgecolor='none' - Drawing a separate plot() line on top for the outline - Proper z-ordering to ensure outline appears above fill This creates cleaner ridgeline plots where the baseline doesn't have a visible edge line connecting the endpoints. * Improve z-ordering for ridgeline plots Implements explicit z-ordering to ensure proper layering: - Each ridge i gets: fill at base+i*2, outline at base+i*2+1 - Later ridges appear on top of earlier ridges - Outline always appears on top of its corresponding fill - Base zorder defaults to 2 (above grid/axes elements) - User can override base zorder via zorder parameter This ensures clean visual layering even with high overlap values and when other plot elements are present (e.g., grids). * Fix z-ordering: lower ridges now correctly appear in front Reversed the z-order assignment so that visually lower ridges (smaller index, closer to viewer) have higher z-order values. Z-order formula: fill_zorder = base + (n_ridges - i - 1) * 2 This ensures proper visual layering where: - Ridge 0 (bottom, front) has highest z-order - Ridge n-1 (top, back) has lowest z-order This prevents ridges from incorrectly popping in front of others when overlap is high, maintaining the correct visual depth. * Add kde_kw parameter for flexible KDE control Replaced explicit bandwidth/weights parameters with a more flexible kde_kw dictionary that passes all kwargs to scipy.stats.gaussian_kde. Features: - kde_kw: dict parameter for passing any KDE arguments (bw_method, weights, etc.) - points: int parameter to control number of evaluation points (default 200) - More maintainable and extensible than exposing individual parameters - Follows UltraPlot's convention of using *_kw parameters Example usage: - Custom bandwidth: kde_kw={'bw_method': 0.5} - With weights: kde_kw={'weights': weight_array} - Silverman method: kde_kw={'bw_method': 'silverman'} - Smoother curves: points=500 Tests added: - test_ridgeline_kde_kw: Tests various kde_kw configurations - test_ridgeline_points: Tests points parameter * Add continuous coordinate-based positioning for scientific ridgeline plots Implements two distinct positioning modes for ridgeline plots: 1. Categorical Positioning (default): Evenly-spaced ridges with discrete labels - Uses overlap parameter to control spacing - Traditional 'joyplot' aesthetic 2. Continuous Positioning: Ridges anchored to specific Y-coordinates - Enabled by providing 'positions' parameter - 'height' parameter controls ridge height in Y-axis units - Essential for scientific plots where Y-axis represents physical variables - Supports: time series, depth profiles, redshift distributions, etc. Parameters: - positions: Array of Y-coordinates for each ridge - height: Ridge height in Y-axis units (auto-determined if not provided) Scientific use cases: - Ocean temperature profiles vs depth - Galaxy distributions vs redshift - Climate data over time - Atmospheric profiles vs altitude - Any data where the vertical axis has physical meaning Tests added: - test_ridgeline_continuous_positioning: Visual test of continuous mode - test_ridgeline_continuous_vs_categorical: Side-by-side comparison - test_ridgeline_continuous_errors: Error handling validation - test_ridgeline_continuous_auto_height: Auto height calculation * Add user guide documentation for ridgeline plots and fix deprecated API - Add comprehensive ridgeline plot examples to docs/stats.py - Include examples for KDE vs histogram modes - Demonstrate categorical vs continuous positioning for scientific use cases - Replace deprecated mcm.get_cmap() with constructor.Colormap() - All 15 ridgeline tests still passing
1 parent d7ec9ea commit 1c0a42e

3 files changed

Lines changed: 977 additions & 5 deletions

File tree

docs/stats.py

Lines changed: 176 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,10 @@
7979
shadedata = np.percentile(data, (25, 75), axis=0) # dark shading
8080

8181
# %%
82-
import ultraplot as uplt
8382
import numpy as np
8483

84+
import ultraplot as uplt
85+
8586
# Loop through "vertical" and "horizontal" versions
8687
varray = [[1], [2], [3]]
8788
harray = [[1, 1], [2, 3], [2, 3]]
@@ -164,10 +165,11 @@
164165
# with the same keywords used for :ref:`on-the-fly error bars <ug_errorbars>`.
165166

166167
# %%
167-
import ultraplot as uplt
168168
import numpy as np
169169
import pandas as pd
170170

171+
import ultraplot as uplt
172+
171173
# Sample data
172174
N = 500
173175
state = np.random.RandomState(51423)
@@ -221,9 +223,10 @@
221223
# will use the same algorithm for kernel density estimation as the `kde` commands.
222224

223225
# %%
224-
import ultraplot as uplt
225226
import numpy as np
226227

228+
import ultraplot as uplt
229+
227230
# Sample data
228231
M, N = 300, 3
229232
state = np.random.RandomState(51423)
@@ -244,9 +247,10 @@
244247
)
245248

246249
# %%
247-
import ultraplot as uplt
248250
import numpy as np
249251

252+
import ultraplot as uplt
253+
250254
# Sample data
251255
N = 500
252256
state = np.random.RandomState(51423)
@@ -284,3 +288,171 @@
284288
px = ax.panel("t", space=0)
285289
px.hist(x, bins, color=color, fill=True, ec="k")
286290
px.format(grid=False, ylocator=[], title=title, titleloc="l")
291+
292+
293+
# %% [raw] raw_mimetype="text/restructuredtext"
294+
# .. _ug_ridgeline:
295+
#
296+
# Ridgeline plots
297+
# ---------------
298+
#
299+
# Ridgeline plots (also known as joyplots) visualize distributions of multiple
300+
# datasets as stacked, overlapping density curves. They are useful for comparing
301+
# distributions across categories or over time. UltraPlot provides
302+
# :func:`~ultraplot.axes.PlotAxes.ridgeline` and :func:`~ultraplot.axes.PlotAxes.ridgelineh`
303+
# for creating vertical and horizontal ridgeline plots.
304+
#
305+
# Ridgeline plots support two display modes: smooth kernel density estimation (KDE)
306+
# by default, or histograms with the `hist` keyword. They also support two positioning
307+
# modes: categorical positioning with evenly-spaced ridges (traditional joyplots),
308+
# or continuous positioning where ridges are anchored to specific physical coordinates
309+
# (useful for scientific plots like depth profiles or time series).
310+
311+
# %%
312+
import numpy as np
313+
314+
import ultraplot as uplt
315+
316+
# Sample data with different distributions
317+
state = np.random.RandomState(51423)
318+
data = [state.normal(i, 1, 500) for i in range(5)]
319+
labels = [f"Distribution {i+1}" for i in range(5)]
320+
321+
# Create figure with two subplots
322+
fig, axs = uplt.subplots(ncols=2, figsize=(10, 5))
323+
axs.format(
324+
abc="A.", abcloc="ul", grid=False, suptitle="Ridgeline plots: KDE vs Histogram"
325+
)
326+
327+
# KDE ridgeline (default)
328+
axs[0].ridgeline(
329+
data, labels=labels, overlap=0.6, cmap="viridis", alpha=0.7, linewidth=1.5
330+
)
331+
axs[0].format(title="Kernel Density Estimation", xlabel="Value")
332+
333+
# Histogram ridgeline
334+
axs[1].ridgeline(
335+
data,
336+
labels=labels,
337+
overlap=0.6,
338+
cmap="plasma",
339+
alpha=0.7,
340+
hist=True,
341+
bins=20,
342+
linewidth=1.5,
343+
)
344+
axs[1].format(title="Histogram", xlabel="Value")
345+
346+
# %%
347+
import numpy as np
348+
349+
import ultraplot as uplt
350+
351+
# Sample data
352+
state = np.random.RandomState(51423)
353+
data1 = [state.normal(i * 0.5, 1, 400) for i in range(6)]
354+
data2 = [state.normal(i, 0.8, 400) for i in range(4)]
355+
labels1 = [f"Group {i+1}" for i in range(6)]
356+
labels2 = ["Alpha", "Beta", "Gamma", "Delta"]
357+
358+
# Create figure with vertical and horizontal orientations
359+
fig, axs = uplt.subplots(ncols=2, figsize=(10, 5))
360+
axs.format(abc="A.", abcloc="ul", grid=False, suptitle="Ridgeline plot orientations")
361+
362+
# Vertical ridgeline (default - ridges are horizontal)
363+
axs[0].ridgeline(
364+
data1, labels=labels1, overlap=0.7, cmap="coolwarm", alpha=0.8, linewidth=2
365+
)
366+
axs[0].format(title="Vertical (ridgeline)", xlabel="Value")
367+
368+
# Horizontal ridgeline (ridges are vertical)
369+
axs[1].ridgelineh(
370+
data2, labels=labels2, overlap=0.6, facecolor="skyblue", alpha=0.7, linewidth=1.5
371+
)
372+
axs[1].format(title="Horizontal (ridgelineh)", ylabel="Value")
373+
374+
375+
# %% [raw] raw_mimetype="text/restructuredtext"
376+
# .. _ug_ridgeline_continuous:
377+
#
378+
# Continuous positioning
379+
# ^^^^^^^^^^^^^^^^^^^^^^
380+
#
381+
# For scientific applications, ridgeline plots can use continuous (coordinate-based)
382+
# positioning where each ridge is anchored to a specific numerical coordinate along
383+
# the axis. This is useful for visualizing how distributions change with physical
384+
# variables like depth, time, altitude, or redshift. Use the `positions` parameter
385+
# to specify coordinates, and optionally the `height` parameter to control ridge height
386+
# in axis units.
387+
388+
# %%
389+
import numpy as np
390+
391+
import ultraplot as uplt
392+
393+
# Simulate ocean temperature data at different depths
394+
state = np.random.RandomState(51423)
395+
depths = [0, 10, 25, 50, 100] # meters
396+
mean_temps = [25, 22, 18, 12, 8] # decreasing with depth
397+
data = [state.normal(temp, 2, 400) for temp in mean_temps]
398+
labels = ["Surface", "10m", "25m", "50m", "100m"]
399+
400+
fig, ax = uplt.subplots(figsize=(8, 6))
401+
ax.ridgeline(
402+
data,
403+
labels=labels,
404+
positions=depths,
405+
height=8, # height in axis units
406+
cmap="coolwarm",
407+
alpha=0.75,
408+
linewidth=2,
409+
)
410+
ax.format(
411+
title="Ocean Temperature Distribution by Depth",
412+
xlabel="Temperature (°C)",
413+
ylabel="Depth (m)",
414+
yreverse=True, # depth increases downward
415+
grid=True,
416+
gridcolor="gray5",
417+
gridalpha=0.3,
418+
)
419+
420+
# %%
421+
import numpy as np
422+
423+
import ultraplot as uplt
424+
425+
# Simulate climate data over time
426+
state = np.random.RandomState(51423)
427+
years = [1950, 1970, 1990, 2010, 2030]
428+
mean_temps = [14.0, 14.2, 14.5, 15.0, 15.5] # warming trend
429+
data = [state.normal(temp, 0.8, 500) for temp in mean_temps]
430+
431+
fig, axs = uplt.subplots(ncols=2, figsize=(11, 5))
432+
axs.format(abc="A.", abcloc="ul", suptitle="Categorical vs Continuous positioning")
433+
434+
# Categorical positioning (default)
435+
axs[0].ridgeline(
436+
data, labels=[str(y) for y in years], overlap=0.6, cmap="fire", alpha=0.7
437+
)
438+
axs[0].format(
439+
title="Categorical (traditional joyplot)", xlabel="Temperature (°C)", grid=False
440+
)
441+
442+
# Continuous positioning
443+
axs[1].ridgeline(
444+
data,
445+
labels=[str(y) for y in years],
446+
positions=years,
447+
height=15, # height in year units
448+
cmap="fire",
449+
alpha=0.7,
450+
)
451+
axs[1].format(
452+
title="Continuous (scientific)",
453+
xlabel="Temperature (°C)",
454+
ylabel="Year",
455+
grid=True,
456+
gridcolor="gray5",
457+
gridalpha=0.3,
458+
)

0 commit comments

Comments
 (0)