|
1109 | 1109 | ) |
1110 | 1110 |
|
1111 | 1111 |
|
| 1112 | +# Ridgeline plot docstrings |
| 1113 | +_ridgeline_docstring = """ |
| 1114 | +Create a {orientation} ridgeline plot (also known as a joyplot). |
| 1115 | +
|
| 1116 | +Ridgeline plots visualize distributions of multiple datasets as stacked, |
| 1117 | +overlapping density curves. They are useful for comparing distributions |
| 1118 | +across categories or over time. |
| 1119 | +
|
| 1120 | +Parameters |
| 1121 | +---------- |
| 1122 | +data : list of array-like |
| 1123 | + List of distributions to plot. Each element should be an array-like |
| 1124 | + object containing the data points for one distribution. |
| 1125 | +labels : list of str, optional |
| 1126 | + Labels for each distribution. If not provided, generates default labels. |
| 1127 | +overlap : float, default: 0.5 |
| 1128 | + Amount of overlap between ridges, from 0 (no overlap) to 1 (full overlap). |
| 1129 | + Higher values create more dramatic visual overlapping. |
| 1130 | +bandwidth : float, optional |
| 1131 | + Bandwidth parameter for kernel density estimation. If None (default), |
| 1132 | + uses automatic bandwidth selection via Scott's rule. Only used when hist=False. |
| 1133 | +hist : bool, default: False |
| 1134 | + If True, uses histograms instead of kernel density estimation. |
| 1135 | +bins : int or sequence or str, default: 'auto' |
| 1136 | + Bin specification for histograms. Can be an integer (number of bins), |
| 1137 | + a sequence defining bin edges, or a string method ('auto', 'sturges', etc.). |
| 1138 | + Only used when hist=True. |
| 1139 | +fill : bool, default: True |
| 1140 | + Whether to fill the area under each density curve. |
| 1141 | +alpha : float, default: 0.7 |
| 1142 | + Transparency level for filled areas (0=transparent, 1=opaque). |
| 1143 | +linewidth : float, default: 1.5 |
| 1144 | + Width of the outline for each ridge. |
| 1145 | +edgecolor : color, default: 'black' |
| 1146 | + Color of the ridge outlines. |
| 1147 | +facecolor : color or list of colors, optional |
| 1148 | + Fill color(s) for the ridges. If a single color, applies to all ridges. |
| 1149 | + If a list, must match the number of distributions. If None, uses the |
| 1150 | + current color cycle or colormap. |
| 1151 | +cmap : str or Colormap, optional |
| 1152 | + Colormap name or object to use for coloring ridges. Overridden by facecolor. |
| 1153 | +
|
| 1154 | +Returns |
| 1155 | +------- |
| 1156 | +list |
| 1157 | + List of artist objects for each ridge (PolyCollection or Line2D). |
| 1158 | +
|
| 1159 | +Examples |
| 1160 | +-------- |
| 1161 | +>>> import ultraplot as uplt |
| 1162 | +>>> import numpy as np |
| 1163 | +>>> fig, ax = uplt.subplots() |
| 1164 | +>>> data = [np.random.normal(i, 1, 1000) for i in range(5)] |
| 1165 | +>>> ax.ridgeline(data, labels=[f'Group {{i+1}}' for i in range(5)]) |
| 1166 | +
|
| 1167 | +>>> # With colormap |
| 1168 | +>>> fig, ax = uplt.subplots() |
| 1169 | +>>> ax.ridgeline(data, cmap='viridis', overlap=0.7) |
| 1170 | +
|
| 1171 | +>>> # With histograms instead of KDE |
| 1172 | +>>> fig, ax = uplt.subplots() |
| 1173 | +>>> ax.ridgeline(data, hist=True, bins=20) |
| 1174 | +
|
| 1175 | +See Also |
| 1176 | +-------- |
| 1177 | +violinplot : Violin plots for distribution visualization |
| 1178 | +hist : Histogram for single distribution |
| 1179 | +""" |
| 1180 | +docstring._snippet_manager["plot.ridgeline"] = _ridgeline_docstring.format( |
| 1181 | + orientation="vertical" |
| 1182 | +) |
| 1183 | +docstring._snippet_manager["plot.ridgelineh"] = _ridgeline_docstring.format( |
| 1184 | + orientation="horizontal" |
| 1185 | +) |
| 1186 | + |
| 1187 | + |
1112 | 1188 | # 1D histogram docstrings |
1113 | 1189 | _hist_docstring = """ |
1114 | 1190 | Plot {orientation} histograms. |
@@ -5262,6 +5338,237 @@ def violinploth(self, *args, **kwargs): |
5262 | 5338 | kwargs = _parse_vert(default_vert=False, **kwargs) |
5263 | 5339 | return self._apply_violinplot(*args, **kwargs) |
5264 | 5340 |
|
| 5341 | + def _apply_ridgeline( |
| 5342 | + self, |
| 5343 | + data, |
| 5344 | + labels=None, |
| 5345 | + overlap=0.5, |
| 5346 | + bandwidth=None, |
| 5347 | + hist=False, |
| 5348 | + bins="auto", |
| 5349 | + fill=True, |
| 5350 | + alpha=0.7, |
| 5351 | + linewidth=1.5, |
| 5352 | + edgecolor="black", |
| 5353 | + facecolor=None, |
| 5354 | + cmap=None, |
| 5355 | + vert=True, |
| 5356 | + **kwargs, |
| 5357 | + ): |
| 5358 | + """ |
| 5359 | + Apply ridgeline plot (joyplot). |
| 5360 | +
|
| 5361 | + Parameters |
| 5362 | + ---------- |
| 5363 | + data : list of array-like |
| 5364 | + List of distributions to plot as ridges. |
| 5365 | + labels : list of str, optional |
| 5366 | + Labels for each distribution. |
| 5367 | + overlap : float, default: 0.5 |
| 5368 | + Amount of overlap between ridges (0-1). Higher values create more overlap. |
| 5369 | + bandwidth : float, optional |
| 5370 | + Bandwidth for kernel density estimation. If None, uses automatic selection. |
| 5371 | + Only used when hist=False. |
| 5372 | + hist : bool, default: False |
| 5373 | + If True, use histograms instead of kernel density estimation. |
| 5374 | + bins : int or sequence or str, default: 'auto' |
| 5375 | + Bin specification for histograms. Passed to numpy.histogram. |
| 5376 | + Only used when hist=True. |
| 5377 | + fill : bool, default: True |
| 5378 | + Whether to fill the area under each curve. |
| 5379 | + alpha : float, default: 0.7 |
| 5380 | + Transparency of filled areas. |
| 5381 | + linewidth : float, default: 1.5 |
| 5382 | + Width of the ridge lines. |
| 5383 | + edgecolor : color, default: 'black' |
| 5384 | + Color of the ridge lines. |
| 5385 | + facecolor : color or list of colors, optional |
| 5386 | + Fill color(s). If None, uses current color cycle or colormap. |
| 5387 | + cmap : str or Colormap, optional |
| 5388 | + Colormap to use for coloring ridges. |
| 5389 | + vert : bool, default: True |
| 5390 | + If True, ridges are horizontal (traditional ridgeline plot). |
| 5391 | + If False, ridges are vertical. |
| 5392 | + **kwargs |
| 5393 | + Additional keyword arguments passed to fill_between or fill_betweenx. |
| 5394 | +
|
| 5395 | + Returns |
| 5396 | + ------- |
| 5397 | + list |
| 5398 | + List of PolyCollection objects for each ridge. |
| 5399 | + """ |
| 5400 | + from scipy.stats import gaussian_kde |
| 5401 | + |
| 5402 | + # Validate input |
| 5403 | + if not isinstance(data, (list, tuple)): |
| 5404 | + data = [data] |
| 5405 | + |
| 5406 | + n_ridges = len(data) |
| 5407 | + if labels is None: |
| 5408 | + labels = [f"Ridge {i+1}" for i in range(n_ridges)] |
| 5409 | + elif len(labels) != n_ridges: |
| 5410 | + raise ValueError( |
| 5411 | + f"Number of labels ({len(labels)}) must match number of data series ({n_ridges})" |
| 5412 | + ) |
| 5413 | + |
| 5414 | + # Determine colors |
| 5415 | + if facecolor is None: |
| 5416 | + if cmap is not None: |
| 5417 | + # Use colormap |
| 5418 | + if isinstance(cmap, str): |
| 5419 | + cmap = mcm.get_cmap(cmap) |
| 5420 | + colors = [cmap(i / (n_ridges - 1)) for i in range(n_ridges)] |
| 5421 | + else: |
| 5422 | + # Use color cycle |
| 5423 | + parser = self._get_patches_for_fill |
| 5424 | + colors = [parser.get_next_color() for _ in range(n_ridges)] |
| 5425 | + elif isinstance(facecolor, (list, tuple)): |
| 5426 | + colors = list(facecolor) |
| 5427 | + else: |
| 5428 | + colors = [facecolor] * n_ridges |
| 5429 | + |
| 5430 | + # Ensure we have enough colors |
| 5431 | + if len(colors) < n_ridges: |
| 5432 | + colors = colors * (n_ridges // len(colors) + 1) |
| 5433 | + colors = colors[:n_ridges] |
| 5434 | + |
| 5435 | + # Calculate KDE or histogram for each distribution |
| 5436 | + ridges = [] |
| 5437 | + for i, dist in enumerate(data): |
| 5438 | + dist = np.asarray(dist).ravel() |
| 5439 | + dist = dist[~np.isnan(dist)] # Remove NaNs |
| 5440 | + |
| 5441 | + if len(dist) < 2: |
| 5442 | + warnings._warn_ultraplot( |
| 5443 | + f"Distribution {i} has less than 2 points, skipping" |
| 5444 | + ) |
| 5445 | + continue |
| 5446 | + |
| 5447 | + if hist: |
| 5448 | + # Use histogram |
| 5449 | + try: |
| 5450 | + counts, bin_edges = np.histogram(dist, bins=bins) |
| 5451 | + # Create x values as bin centers |
| 5452 | + x = (bin_edges[:-1] + bin_edges[1:]) / 2 |
| 5453 | + # Extend to bin edges for proper fill |
| 5454 | + x_extended = np.concatenate([[bin_edges[0]], x, [bin_edges[-1]]]) |
| 5455 | + y_extended = np.concatenate([[0], counts, [0]]) |
| 5456 | + ridges.append((x_extended, y_extended)) |
| 5457 | + except Exception as e: |
| 5458 | + warnings._warn_ultraplot( |
| 5459 | + f"Histogram failed for distribution {i}: {e}, skipping" |
| 5460 | + ) |
| 5461 | + continue |
| 5462 | + else: |
| 5463 | + # Perform KDE |
| 5464 | + try: |
| 5465 | + kde = gaussian_kde(dist, bw_method=bandwidth) |
| 5466 | + # Create smooth x values |
| 5467 | + x_min, x_max = dist.min(), dist.max() |
| 5468 | + x_range = x_max - x_min |
| 5469 | + x_margin = x_range * 0.1 # 10% margin |
| 5470 | + x = np.linspace(x_min - x_margin, x_max + x_margin, 200) |
| 5471 | + y = kde(x) |
| 5472 | + ridges.append((x, y)) |
| 5473 | + except Exception as e: |
| 5474 | + warnings._warn_ultraplot( |
| 5475 | + f"KDE failed for distribution {i}: {e}, skipping" |
| 5476 | + ) |
| 5477 | + continue |
| 5478 | + |
| 5479 | + if not ridges: |
| 5480 | + raise ValueError("No valid distributions to plot") |
| 5481 | + |
| 5482 | + # Normalize heights and add vertical offsets |
| 5483 | + max_height = max(y.max() for x, y in ridges) |
| 5484 | + spacing = max_height * (1 + overlap) |
| 5485 | + |
| 5486 | + artists = [] |
| 5487 | + for i, (x, y) in enumerate(ridges): |
| 5488 | + # Normalize and offset |
| 5489 | + y_normalized = y / max_height |
| 5490 | + offset = i * spacing |
| 5491 | + y_plot = y_normalized + offset |
| 5492 | + |
| 5493 | + if vert: |
| 5494 | + # Traditional horizontal ridges |
| 5495 | + if fill: |
| 5496 | + poly = self.fill_between( |
| 5497 | + x, |
| 5498 | + offset, |
| 5499 | + y_plot, |
| 5500 | + facecolor=colors[i], |
| 5501 | + alpha=alpha, |
| 5502 | + edgecolor=edgecolor, |
| 5503 | + linewidth=linewidth, |
| 5504 | + label=labels[i], |
| 5505 | + **kwargs, |
| 5506 | + ) |
| 5507 | + else: |
| 5508 | + poly = self.plot( |
| 5509 | + x, |
| 5510 | + y_plot, |
| 5511 | + color=colors[i], |
| 5512 | + linewidth=linewidth, |
| 5513 | + label=labels[i], |
| 5514 | + **kwargs, |
| 5515 | + )[0] |
| 5516 | + else: |
| 5517 | + # Vertical ridges |
| 5518 | + if fill: |
| 5519 | + poly = self.fill_betweenx( |
| 5520 | + x, |
| 5521 | + offset, |
| 5522 | + y_plot, |
| 5523 | + facecolor=colors[i], |
| 5524 | + alpha=alpha, |
| 5525 | + edgecolor=edgecolor, |
| 5526 | + linewidth=linewidth, |
| 5527 | + label=labels[i], |
| 5528 | + **kwargs, |
| 5529 | + ) |
| 5530 | + else: |
| 5531 | + poly = self.plot( |
| 5532 | + y_plot, |
| 5533 | + x, |
| 5534 | + color=colors[i], |
| 5535 | + linewidth=linewidth, |
| 5536 | + label=labels[i], |
| 5537 | + **kwargs, |
| 5538 | + )[0] |
| 5539 | + |
| 5540 | + artists.append(poly) |
| 5541 | + |
| 5542 | + # Set appropriate labels and limits |
| 5543 | + if vert: |
| 5544 | + self.set_yticks(np.arange(n_ridges) * spacing) |
| 5545 | + self.set_yticklabels(labels[: len(ridges)]) |
| 5546 | + self.set_ylabel("") |
| 5547 | + else: |
| 5548 | + self.set_xticks(np.arange(n_ridges) * spacing) |
| 5549 | + self.set_xticklabels(labels[: len(ridges)]) |
| 5550 | + self.set_xlabel("") |
| 5551 | + |
| 5552 | + return artists |
| 5553 | + |
| 5554 | + @inputs._preprocess_or_redirect("data") |
| 5555 | + @docstring._snippet_manager |
| 5556 | + def ridgeline(self, data, **kwargs): |
| 5557 | + """ |
| 5558 | + %(plot.ridgeline)s |
| 5559 | + """ |
| 5560 | + kwargs = _parse_vert(default_vert=True, **kwargs) |
| 5561 | + return self._apply_ridgeline(data, **kwargs) |
| 5562 | + |
| 5563 | + @inputs._preprocess_or_redirect("data") |
| 5564 | + @docstring._snippet_manager |
| 5565 | + def ridgelineh(self, data, **kwargs): |
| 5566 | + """ |
| 5567 | + %(plot.ridgelineh)s |
| 5568 | + """ |
| 5569 | + kwargs = _parse_vert(default_vert=False, **kwargs) |
| 5570 | + return self._apply_ridgeline(data, **kwargs) |
| 5571 | + |
5265 | 5572 | def _apply_hist( |
5266 | 5573 | self, |
5267 | 5574 | xs, |
|
0 commit comments