From 6f0c46578b9b5a613dbe0ce7c930a12bab5ea5f9 Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Fri, 16 Oct 2026 22:15:45 +1000 Subject: [PATCH 01/10] Add composable raincloud plots with reproducible jitter --- docs/examples/plot_types/13_raincloud.py | 53 +++++++ docs/stats.py | 11 ++ ultraplot/axes/plot.py | 110 +++++++++++++++ ultraplot/axes/plot_types/raincloud.py | 169 +++++++++++++++++++++++ ultraplot/tests/test_raincloud.py | 147 ++++++++++++++++++++ 5 files changed, 490 insertions(+) create mode 100644 docs/examples/plot_types/13_raincloud.py create mode 100644 ultraplot/axes/plot_types/raincloud.py create mode 100644 ultraplot/tests/test_raincloud.py diff --git a/docs/examples/plot_types/13_raincloud.py b/docs/examples/plot_types/13_raincloud.py new file mode 100644 index 000000000..d598ecf89 --- /dev/null +++ b/docs/examples/plot_types/13_raincloud.py @@ -0,0 +1,53 @@ +""" +Raincloud plots +=============== + +These synthetic samples have exactly the same sample mean and standard +deviation, but different shapes. Half violins reveal the shapes, points show +the observations, and boxes show the median and interquartile range. +""" + +import numpy as np + +import ultraplot as uplt + +# Fixed seeds make both the observations and the point positions reproducible. +rng = np.random.default_rng(42) +data = np.column_stack( + [ + rng.normal(size=100), + rng.lognormal(mean=0, sigma=0.65, size=100), + np.r_[rng.normal(-1.2, 0.3, 50), rng.normal(1.2, 0.3, 50)], + ] +) +data = (data - data.mean(axis=0)) / data.std(axis=0, ddof=1) +labels = ["Normal", "Right-skewed", "Bimodal"] +colors = ["#4477AA", "#228833", "#AA3377"] +limits = (np.floor(data.min()), np.ceil(data.max())) + +fig, axs = uplt.subplots(ncols=2, refwidth=3.8, refaspect=1.1, share=False) + +artists = axs[0].raincloud( + data, + labels=labels, + orientation="horizontal", + cycle=colors, + seed=7, +) +axs[0].format( + title="Horizontal raincloud", + xlabel="Standardised value", + yreverse=True, + xlim=limits, + grid=False, +) + +axs[1].raincloud(data, labels=labels, cycle=colors, side="low", seed=7) +axs[1].format( + title="Vertical raincloud ยท cloud on low side", + ylabel="Standardised value", + ylim=limits, + grid=False, +) +axs.format(suptitle="Same mean and standard deviation, different distributions") +fig.show() diff --git a/docs/stats.py b/docs/stats.py index 1d7ef151a..2f5217418 100644 --- a/docs/stats.py +++ b/docs/stats.py @@ -300,6 +300,17 @@ # %% [raw] raw_mimetype="text/restructuredtext" +# Raincloud plots +# --------------- +# :meth:`~ultraplot.axes.PlotAxes.raincloud` combines a half violin, jittered +# observations, and a narrow boxplot. Columns of arrays and DataFrames represent +# distributions; a list of vectors also supports unequal sample sizes. Colours +# and positions are coordinated across components. Use ``orientation='horizontal'`` +# to put values on the x axis, and ``cloud=False``, ``rain=False``, or ``box=False`` +# to disable individual components. ``seed`` makes the jitter reproducible. +# The boxplot describes sample spread, not uncertainty in an estimated mean. +# See the :ref:`sphx_glr_gallery_plot_types_13_raincloud.py` gallery example. +# # .. _ug_ridgeline: # # Ridgeline plots diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index b90420e45..4164283c7 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -6722,6 +6722,116 @@ def violinploth(self, *args, **kwargs): kwargs = _parse_vert(default_vert=False, **kwargs) return self._apply_violinplot(*args, **kwargs) + def raincloud( + self, + data, + *, + positions=None, + labels=None, + orientation="vertical", + side="high", + jitter=0.08, + offset=0.15, + cloud=True, + rain=True, + box=True, + seed=0, + color=None, + cycle=None, + cycle_kw=None, + cloud_kw=None, + rain_kw=None, + box_kw=None, + ): + """ + Plot half violins, individual observations, and narrow boxplots. + + Parameters + ---------- + data : array-like, sequence of array-like, or DataFrame + A vector represents one distribution. Columns of a 2D array or + DataFrame represent distributions. A list or tuple of vectors + represents separate distributions, including unequal sample sizes. + Masked and non-finite observations are ignored. Empty groups retain + their positions and labels but are skipped with a warning. + positions : sequence of float, optional + Distinct group centres. Defaults to ``range(number_of_groups)``. + labels : sequence of str, optional + Group tick labels. Defaults to DataFrame column labels, if present. + orientation : {'vertical', 'horizontal'}, default: 'vertical' + Direction of the measured variable. For horizontal plots, group + centres lie on the y axis. + side : {'low', 'high'}, default: 'high' + Place the cloud below or above its group centre on the categorical + axis. Observations are placed on the opposite side. + jitter : float, default: 0.08 + Half-width of uniform jitter in categorical-axis data units. + offset : float, default: 0.15 + Distance of the cloud baseline and the centre of the observations + from the group centre, in categorical-axis data units. + cloud, rain, box : bool, default: True + Whether to draw each component. At least one must be enabled. + Boxplots show the median, interquartile range, and Tukey whiskers; + these are distribution summaries, not confidence intervals. + seed : int, numpy.random.Generator, or None, default: 0 + Random seed for reproducible jitter, or a generator to use directly. + Does not change NumPy's global random state. + color : color-spec, optional + Colour shared by all groups. Otherwise each group uses one cycle + colour shared across its three components. + cycle : cycle-spec, optional + Property cycle used for group colours. + cycle_kw : dict, optional + Additional arguments for constructing the cycle. + cloud_kw, rain_kw, box_kw : dict, optional + Component options passed to `violinplot`, `scatter`, and `boxplot`, + respectively. Override default styling and widths. Layout options + such as positions and orientation must be supplied to this method. + + Returns + ------- + dict + ``cloud`` contains violin PolyCollections, ``rain`` contains scatter + PathCollections, and ``box`` contains boxplot artist dictionaries. + Each list follows input group order, with ``None`` for disabled + components or empty groups. + + Examples + -------- + >>> rng = np.random.default_rng(42) + >>> data = [rng.normal(size=80), rng.normal(1, size=60)] + >>> fig, ax = uplt.subplots() + >>> artists = ax.raincloud(data, labels=['Control', 'Treatment']) + + See also + -------- + PlotAxes.violinplot + PlotAxes.scatter + PlotAxes.boxplot + """ + from .plot_types.raincloud import raincloud + + return raincloud( + self, + data, + positions=positions, + labels=labels, + orientation=orientation, + side=side, + jitter=jitter, + offset=offset, + cloud=cloud, + rain=rain, + box=box, + seed=seed, + color=color, + cycle=cycle, + cycle_kw=cycle_kw, + cloud_kw=cloud_kw, + rain_kw=rain_kw, + box_kw=box_kw, + ) + def _apply_ridgeline( self, data, diff --git a/ultraplot/axes/plot_types/raincloud.py b/ultraplot/axes/plot_types/raincloud.py new file mode 100644 index 000000000..d89607b99 --- /dev/null +++ b/ultraplot/axes/plot_types/raincloud.py @@ -0,0 +1,169 @@ +"""Composition of half violins, observations, and boxplots.""" + +import numpy as np + +from ...internals import warnings + + +def raincloud( + ax, + data, + *, + positions=None, + labels=None, + orientation="vertical", + side="high", + jitter=0.08, + offset=0.15, + cloud=True, + rain=True, + box=True, + seed=0, + color=None, + cycle=None, + cycle_kw=None, + cloud_kw=None, + rain_kw=None, + box_kw=None, +): + """Draw the components of :meth:`PlotAxes.raincloud`.""" + if orientation not in ("vertical", "horizontal"): + raise ValueError("orientation must be 'vertical' or 'horizontal'") + if side not in ("low", "high"): + raise ValueError("side must be 'low' or 'high'") + for name, value in (("jitter", jitter), ("offset", offset)): + if not np.isscalar(value) or not np.isfinite(value) or value < 0: + raise ValueError(f"{name} must be a finite non-negative number") + if not any((cloud, rain, box)): + raise ValueError("At least one of cloud, rain, or box must be enabled") + + # Arrays and labelled tables use columns; sequences of vectors permit + # unequal sample sizes, following Matplotlib's distribution convention. + columns = getattr(data, "columns", None) + if labels is None and columns is not None: + labels = list(columns) + if isinstance(data, (list, tuple)): + distributions = data if data and np.ndim(data[0]) else [data] + else: + values = np.ma.asarray(data) + if values.ndim == 1: + distributions = ( + values + if values.dtype == object and any(np.ndim(value) for value in values) + else [values] + ) + elif values.ndim == 2: + distributions = values.T + else: + raise ValueError("data must be one- or two-dimensional") + cleaned = [] + for values in distributions: + if np.ndim(values) != 1: + raise ValueError("Each distribution must be one-dimensional") + values = np.ma.masked_invalid(np.ma.asarray(values, dtype=float)).compressed() + cleaned.append(values) + n = len(cleaned) + if not n or not any(values.size for values in cleaned): + raise ValueError("No finite observations to plot") + if labels is not None and (isinstance(labels, str) or len(labels) != n): + raise ValueError("Number of labels must match number of distributions") + positions = ( + np.arange(n, dtype=float) + if positions is None + else np.asarray(positions, dtype=float) + ) + if positions.shape != (n,) or not np.all(np.isfinite(positions)): + raise ValueError("positions must contain one finite value per distribution") + if np.unique(positions).size != n: + raise ValueError("positions must be distinct") + + # Layout belongs to this function; component dictionaries style the artists. + styles = [dict(kw or {}) for kw in (cloud_kw, rain_kw, box_kw)] + reserved = { + "positions", + "orientation", + "vert", + "side", + "labels", + "tick_labels", + "manage_ticks", + "data", + "cycle", + "cycle_kw", + } + for name, style in zip(("cloud_kw", "rain_kw", "box_kw"), styles): + conflicts = reserved.intersection(style) + if conflicts: + raise ValueError( + f"{name} cannot override layout options: {sorted(conflicts)}" + ) + + ax._parse_cycle(n, cycle=cycle, cycle_kw=cycle_kw) + colors = [ + color if color is not None else ax._get_patches_for_fill.get_next_color() + for _ in range(n) + ] + rng = np.random.default_rng(seed) + vert = orientation == "vertical" + direction = 1 if side == "high" else -1 + artists = {key: [None] * n for key in ("cloud", "rain", "box")} + + for i, (position, values, group_color) in enumerate( + zip(positions, cleaned, colors) + ): + if not values.size: + warnings._warn_ultraplot( + f"Distribution {i} has no finite observations, skipping" + ) + continue + if cloud: + kw = {"fillcolor": group_color, "widths": 0.6, **styles[0]} + alpha = kw.pop("alpha", 0.4) + bodies = ax.violinplot( + [position + direction * offset], + values, + vert=vert, + side=side, + **kw, + ) + artists["cloud"][i] = bodies[0] + bodies[0].set_alpha(alpha) + if rain: + locations = ( + position + - direction * offset + + rng.uniform(-jitter, jitter, values.size) + ) + kw = { + "color": group_color, + "s": 12, + "alpha": 0.5, + "linewidth": 0, + **styles[1], + } + coords = (locations, values) if vert else (values, locations) + artists["rain"][i] = ax.scatter(*coords, **kw) + if box: + kw = { + "fillcolor": group_color, + "widths": 0.12, + "fill": True, + "showfliers": False, + "zorder": 3, + **styles[2], + } + artists["box"][i] = ax.boxplot( + [position], + values, + vert=vert, + manage_ticks=False, + **kw, + ) + + axis = ax.xaxis if vert else ax.yaxis + axis.set_ticks(positions) + if labels is not None: + axis.set_ticklabels(labels) + if columns is not None and columns.name and axis.isDefault_label: + axis.set_label_text(str(columns.name)) + return artists diff --git a/ultraplot/tests/test_raincloud.py b/ultraplot/tests/test_raincloud.py new file mode 100644 index 000000000..b50c4dcf8 --- /dev/null +++ b/ultraplot/tests/test_raincloud.py @@ -0,0 +1,147 @@ +"""Check raincloud geometry, data preservation, and component composition.""" + +import numpy as np +import pandas as pd +import pytest +from matplotlib.colors import to_rgba + +import ultraplot as uplt + + +@pytest.mark.parametrize("orientation", ["vertical", "horizontal"]) +@pytest.mark.parametrize("side,direction", [("low", -1), ("high", 1)]) +def test_raincloud_geometry(orientation, side, direction): + data = [np.linspace(-2, 2, 11), np.linspace(0, 3, 17)] + positions = [2.0, 5.0] + fig, ax = uplt.subplots() + result = ax.raincloud( + data, + positions=positions, + labels=["A", "B"], + orientation=orientation, + side=side, + seed=42, + color="teal", + ) + category = 0 if orientation == "vertical" else 1 + for i, values in enumerate(data): + points = result["rain"][i].get_offsets() + np.testing.assert_array_equal(points[:, 1 - category], values) + centre = positions[i] - direction * 0.15 + assert np.all(np.abs(points[:, category] - centre) <= 0.08) + vertices = result["cloud"][i].get_paths()[0].vertices[:, category] + baseline = positions[i] + direction * 0.15 + assert np.all(direction * (vertices - baseline) >= -1e-12) + median = result["box"][i]["medians"][0] + median_values = median.get_ydata() if category == 0 else median.get_xdata() + np.testing.assert_allclose(median_values, np.median(values)) + np.testing.assert_allclose( + result["cloud"][i].get_facecolor()[0, :3], to_rgba("teal")[:3] + ) + np.testing.assert_allclose( + result["rain"][i].get_facecolor()[0, :3], to_rgba("teal")[:3] + ) + ticks = ax.get_xticklabels() if category == 0 else ax.get_yticklabels() + assert [tick.get_text() for tick in ticks] == ["A", "B"] + fig.canvas.draw() + + +def test_raincloud_columns_and_cycle(): + data = pd.DataFrame({"Alpha": [1, 2, 3], "Beta": [2, 4, 8]}) + data.columns.name = "Condition" + fig, ax = uplt.subplots() + result = ax.raincloud(data, cycle=["red", "blue"]) + assert [tick.get_text() for tick in ax.get_xticklabels()] == ["Alpha", "Beta"] + assert ax.get_xlabel() == "Condition" + np.testing.assert_array_equal(result["rain"][0].get_offsets()[:, 1], data["Alpha"]) + for i, color in enumerate(["red", "blue"]): + np.testing.assert_allclose( + result["cloud"][i].get_facecolor()[0, :3], to_rgba(color)[:3] + ) + np.testing.assert_allclose( + ax.patches[i].get_facecolor()[:3], to_rgba(color)[:3] + ) + + +def test_raincloud_masks_empty_and_degenerate_groups(): + data = [ + np.ma.array([1.0, 999.0, 2.0, np.inf, np.nan], mask=[0, 1, 0, 0, 0]), + [], + [4.0], + [5.0, 5.0, 5.0], + ] + fig, ax = uplt.subplots() + with pytest.warns(UserWarning, match="Distribution 1.*skipping"): + result = ax.raincloud(data, labels=["A", "Empty", "Single", "Constant"]) + np.testing.assert_array_equal(result["rain"][0].get_offsets()[:, 1], [1.0, 2.0]) + assert all(result[key][1] is None for key in result) + assert result["rain"][2].get_offsets().shape == (1, 2) + np.testing.assert_array_equal(ax.get_xticks(), [0, 1, 2, 3]) + fig.canvas.draw() + + +@pytest.mark.parametrize("component", ["cloud", "rain", "box"]) +def test_raincloud_disabled_components(component): + fig, ax = uplt.subplots() + result = ax.raincloud(np.arange(5.0), **{component: False}) + assert result[component] == [None] + assert all(result[key][0] is not None for key in result if key != component) + + +def test_raincloud_jitter_reproducible_without_global_rng(): + data = np.arange(12.0).reshape(6, 2) + state = np.random.get_state() + fig, axes = uplt.subplots(ncols=2) + first = axes[0].raincloud(data, seed=24) + second = axes[1].raincloud(data, seed=24) + for a, b in zip(first["rain"], second["rain"]): + np.testing.assert_array_equal(a.get_offsets(), b.get_offsets()) + after = np.random.get_state() + assert state[0] == after[0] + np.testing.assert_array_equal(state[1], after[1]) + assert state[2:] == after[2:] + + +def test_raincloud_component_styles_are_not_mutated(): + cloud_kw = {"alpha": 0.2, "widths": 0.8, "fillcolor": "red"} + rain_kw = {"s": 35, "alpha": 0.7} + box_kw = {"widths": 0.2, "showfliers": True} + originals = [dict(kw) for kw in (cloud_kw, rain_kw, box_kw)] + fig, ax = uplt.subplots() + result = ax.raincloud( + np.arange(8.0), cloud_kw=cloud_kw, rain_kw=rain_kw, box_kw=box_kw + ) + assert [cloud_kw, rain_kw, box_kw] == originals + assert result["cloud"][0].get_alpha() == 0.2 + np.testing.assert_allclose( + result["cloud"][0].get_facecolor()[0, :3], to_rgba("red")[:3] + ) + np.testing.assert_array_equal(result["rain"][0].get_sizes(), [35]) + + +@pytest.mark.parametrize( + "kwargs,match", + [ + ({"orientation": "diagonal"}, "orientation"), + ({"side": "both"}, "side"), + ({"jitter": -1}, "jitter"), + ({"offset": np.inf}, "offset"), + ({"labels": ["A", "B"]}, "labels"), + ({"positions": [0, 1]}, "positions"), + ({"positions": [np.nan]}, "positions"), + ({"cloud": False, "rain": False, "box": False}, "At least one"), + ({"cloud_kw": {"side": "low"}}, "layout"), + ], +) +def test_raincloud_invalid_options(kwargs, match): + fig, ax = uplt.subplots() + with pytest.raises(ValueError, match=match): + ax.raincloud(np.arange(5.0), **kwargs) + assert not ax.collections and not ax.lines and not ax.patches + + +@pytest.mark.parametrize("data", [[], [[], []], [np.nan, np.inf]]) +def test_raincloud_no_observations(data): + fig, ax = uplt.subplots() + with pytest.raises(ValueError, match="No finite observations"): + ax.raincloud(data) From 7cc7c69128bb9be8c988929147a0e581a33ef821 Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Fri, 16 Oct 2026 22:18:10 +1000 Subject: [PATCH 02/10] Include raincloud example image and CSV data --- docs/_static/plot_types/raincloud.png | Bin 0 -> 138848 bytes .../plot_types/data/raincloud_data.csv | 101 ++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 docs/_static/plot_types/raincloud.png create mode 100644 docs/examples/plot_types/data/raincloud_data.csv diff --git a/docs/_static/plot_types/raincloud.png b/docs/_static/plot_types/raincloud.png new file mode 100644 index 0000000000000000000000000000000000000000..a9d82f6befe589cd83030467350291352c43fc5e GIT binary patch literal 138848 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/examples/plot_types/data/raincloud_data.csv b/docs/examples/plot_types/data/raincloud_data.csv new file mode 100644 index 000000000..1fac500d2 --- /dev/null +++ b/docs/examples/plot_types/data/raincloud_data.csv @@ -0,0 +1,101 @@ +Normal,Right-skewed,Bimodal +4.570581997558644249e-01,-4.704771519386133916e-01,-8.559799267119158062e-01 +-1.274293195803511081e+00,1.138952090630544811e+00,-6.015920822406437551e-01 +1.030956989994613382e+00,-4.587958982791404683e-01,-9.147057264835560808e-01 +1.275735021431826821e+00,3.974157264691976499e-01,-7.831368506209369640e-01 +-2.447304404085562712e+00,-7.174129398016750869e-01,-1.423705482270590350e+00 +-1.611879253958110159e+00,-3.736624002970062031e-01,-9.478273511765962223e-01 +2.293230835527209632e-01,-7.234361082343604421e-01,-1.136735454930538181e+00 +-3.424498292918945475e-01,-4.494869022936150982e-01,-1.226036364723646077e+00 +4.309184457353500836e-02,5.136880814107286364e-01,-1.145038750724206356e+00 +-1.033600956819865191e+00,-9.462964488929621609e-01,-1.015687035622965917e+00 +1.196980630188664074e+00,9.651613721120903822e-02,-7.184730511559564992e-01 +1.066159180781707683e+00,-6.931917160114066390e-02,-1.251615175909854383e+00 +1.497408528127340821e-01,-5.772051130609557879e-01,-9.289606246938378886e-01 +1.516087752465650640e+00,-8.782917700815950912e-01,-1.051362907907346100e+00 +6.666591235871132115e-01,-1.934138132038103197e-01,-1.014153360993360176e+00 +-1.041646171951116973e+00,-5.468114701765517394e-01,-6.978217966484038248e-01 +5.395039092606185305e-01,-7.331149736608608136e-02,-8.082980727026064516e-01 +-1.169872075305822490e+00,-2.285203381991025295e-01,-6.182556332521671472e-01 +1.195760466222100193e+00,1.665809697347728102e+00,-9.729799949010423665e-01 +4.425268173928683410e-04,-3.935412019388941918e-01,-1.101715421367026604e+00 +-1.732930345720433973e-01,-7.496396846790880364e-01,-9.894698300323672147e-01 +-8.119974655757373760e-01,-1.146584881047419680e-01,-8.785862081268797086e-01 +1.638790117678551761e+00,-8.325932053744133921e-02,-8.942605752224513926e-01 +-1.342383529650313101e-01,1.235299222177552947e+00,-1.194074640012727118e+00 +-4.867636152026627583e-01,5.076213741588365691e-01,-9.147283440627537177e-01 +-3.886607355652625384e-01,2.858622825709987089e-02,-8.819787665559276624e-01 +7.500912647201866124e-01,1.411785454488908575e+00,-3.376734102344343680e-01 +5.352463880664395379e-01,-8.041992633936616564e-01,-4.899934635165507091e-01 +5.961321023772855332e-01,-5.978858870926533342e-01,-1.139116227428844885e+00 +6.194215603182601226e-01,-7.148076690632824137e-01,-1.004573822499995384e+00 +2.822172661171865382e+00,-4.766226817060956744e-01,-1.284200869605070405e+00 +-4.585500854903775125e-01,-8.595188469581481749e-01,-1.076693948432645875e+00 +-5.948070918481080938e-01,2.890961617116470372e-01,-8.612035429862002767e-01 +-9.830378678109574775e-01,-3.835546625004089827e-01,-6.495324719046926543e-01 +8.578197200408353984e-01,-8.847867915066891520e-01,-1.109595286253645119e+00 +1.518316587961045405e+00,-7.468755285868634575e-01,-1.091777702578822895e+00 +-8.198752926125690843e-02,-7.925180864466540437e-03,-1.446796609760443442e+00 +-1.017007898203546201e+00,5.111388815816818587e-01,-9.749202248341597432e-01 +-9.968254325669719806e-01,2.529546407374880790e+00,-1.188850045291061974e+00 +9.023856792690770900e-01,5.638996382318423706e+00,-1.062126584411327901e+00 +1.021690560604885611e+00,7.865613159352038974e-02,-1.144731654514046681e+00 +7.640547008745716129e-01,-7.376841757844221359e-01,-9.586562132677844961e-01 +-7.921438677751014845e-01,-1.024713490581300901e+00,-1.354172239471838068e+00 +3.636400397174379684e-01,-4.539421653322759515e-02,-1.285057599295654107e+00 +2.149611404415538995e-01,-6.710740102651947536e-01,-4.299806686201645967e-01 +3.462934174420068123e-01,-4.899406421032219683e-01,-1.242349392885232495e+00 +1.186720001029302862e+00,-5.854174150341603378e-01,-1.197022348517528423e+00 +3.526112956150126987e-01,-3.345386581970978823e-01,-4.994877271811409836e-01 +9.388497013568343208e-01,7.979132260534113064e-01,-2.455168466205926991e-01 +1.517344376520620153e-01,-1.314499144577696288e-01,-1.214802758080658185e+00 +4.369756214405638439e-01,-3.455024736788078754e-01,8.783290306176130269e-01 +8.775303577422558510e-01,-7.538555981223742375e-01,1.047096583287798754e+00 +-1.811416860211617852e+00,-9.344968018512614938e-01,1.376912150491032216e+00 +-3.468643066738036906e-01,-5.257882034401262228e-01,7.312449002487961813e-01 +-5.408978567292930251e-01,-2.792181120110966419e-01,9.075674099187166499e-01 +-7.578543863611353348e-01,2.002371568483547470e+00,1.150710839152028209e+00 +-2.895316540929496196e-01,-1.513568693237695717e-01,1.069258899485182601e+00 +1.989514931157301714e+00,6.881927401494664087e-01,8.764489857058929134e-01 +-1.050064923853462062e+00,-5.321730491692476805e-01,9.340676185911264140e-01 +1.311417332620210985e+00,-8.030034183478125254e-01,6.389823884156635714e-01 +-2.102031733945068481e+00,-7.289219558930716270e-01,9.092565255694495807e-01 +-3.664526417862984942e-01,-6.350362263457114409e-01,9.025482491101989302e-01 +2.742743980887019095e-01,2.870607201687502830e+00,1.021088365311988877e+00 +8.195063892119740512e-01,-6.744366690753046401e-01,8.338481346850163956e-01 +9.804538786842861287e-01,5.115623770537101045e-01,1.078002146393776339e+00 +1.086187191649340633e+00,-7.059704193686625606e-01,1.206675839910399617e+00 +-3.842721965956688024e-01,6.236371835178988743e-01,1.002842050036256483e+00 +-5.305707073832119924e-01,5.278746792890732370e-02,1.049522002864169856e+00 +1.169398910140914793e+00,-3.442818088111796460e-01,9.785247498024703949e-01 +-1.815872927061168152e-01,-2.706674567356631700e-01,9.659062682219407003e-01 +-1.577768322291283276e+00,-6.045716506898286546e-01,7.943240580218666747e-01 +-1.394424320486017610e+00,1.070183868127083104e-01,1.041137854637398830e+00 +-1.119104119335565528e+00,-5.101652784000815233e-01,9.427547331747206938e-01 +7.048363754735339493e-01,-8.155837925047412229e-01,1.463570971902018902e+00 +2.481022270083786119e-01,-8.312929621843503369e-01,1.339976890014089816e+00 +9.537488006500377535e-01,-1.197362601463014820e-01,1.057627488440771923e+00 +-4.853792874522327994e-01,1.622609050371250916e+00,7.844569547269026888e-01 +2.688495266898576208e-01,-1.292405013727374319e-01,7.013923035174706166e-01 +8.701941931990735402e-01,-3.207493878621509031e-01,1.249099789915552039e+00 +-3.335709122078411859e-01,-3.070855577884405718e-02,1.028359099101289420e+00 +6.528385756938763773e-01,1.149648988545044492e+00,1.080048096603538621e+00 +-7.875296391907027349e-01,-8.373910361808210856e-02,5.510825427399892451e-01 +-4.027210115949690361e-01,-4.876470764786985934e-01,1.186399767861819665e+00 +-4.267773981997991517e-01,8.532159083934596211e-01,1.073932019979804364e+00 +-1.474962837778363012e+00,9.143531189917711299e-02,7.018632679403923325e-01 +6.917186181521239874e-01,1.541842032059796530e+00,8.537735193798491329e-01 +-5.396485422559562650e-01,-1.116422779602090631e-01,1.028589251883392341e+00 +8.081057191294913067e-02,-8.152365990722728606e-01,9.783610360393693028e-01 +6.837026473831977480e-01,-8.571523716562271522e-01,8.964177565722291074e-01 +6.396489760962126292e-01,1.761610697880518606e+00,9.412801854671187796e-01 +9.214313275737230358e-01,1.909127291118590808e+00,9.059744872871325905e-01 +-6.207967979827594407e-02,-3.581739371783070891e-01,1.002160418063280378e+00 +-4.802879390791325309e-01,-4.731341166693074363e-01,1.315757323740101237e+00 +-3.791613649077135717e-02,1.408529096417256543e+00,3.556214620321523356e-01 +-2.107780148133603149e+00,-7.779538858059780182e-01,9.095712043125148139e-01 +-1.798485687268077893e+00,-7.028742654072184237e-01,1.007854898064685534e+00 +-1.638299631697614833e+00,2.974852716034528943e-01,1.036263581940459533e+00 +-1.219267404360247165e+00,-4.791393841433880252e-01,8.774574688358123264e-01 +5.794477133346088715e-01,-2.468875589676320370e-01,5.481970602096852829e-01 +-1.101113079672860096e+00,-3.484349953217553697e-01,1.043872457017924749e+00 From 879da6c256a92dfce8869ea5317b5be7ffcba1cf Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 19 Oct 2026 12:59:49 +1000 Subject: [PATCH 03/10] Clarify rain and cloud in the docs --- docs/stats.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/stats.py b/docs/stats.py index 2f5217418..ca4ae04b0 100644 --- a/docs/stats.py +++ b/docs/stats.py @@ -302,8 +302,8 @@ # %% [raw] raw_mimetype="text/restructuredtext" # Raincloud plots # --------------- -# :meth:`~ultraplot.axes.PlotAxes.raincloud` combines a half violin, jittered -# observations, and a narrow boxplot. Columns of arrays and DataFrames represent +# :meth:`~ultraplot.axes.PlotAxes.raincloud` combines a half violin (the cloud), jittered +# observations (the rain), and a narrow boxplot. Columns of arrays and DataFrames represent # distributions; a list of vectors also supports unequal sample sizes. Colours # and positions are coordinated across components. Use ``orientation='horizontal'`` # to put values on the x axis, and ``cloud=False``, ``rain=False``, or ``box=False`` @@ -336,7 +336,7 @@ # Sample data with different distributions state = np.random.RandomState(51423) data = [state.normal(i, 1, 500) for i in range(5)] -labels = [f"Distribution {i+1}" for i in range(5)] +labels = [f"Distribution {i + 1}" for i in range(5)] # Create figure with two subplots fig, axs = uplt.subplots(ncols=2, figsize=(10, 5)) @@ -372,7 +372,7 @@ state = np.random.RandomState(51423) data1 = [state.normal(i * 0.5, 1, 400) for i in range(6)] data2 = [state.normal(i, 0.8, 400) for i in range(4)] -labels1 = [f"Group {i+1}" for i in range(6)] +labels1 = [f"Group {i + 1}" for i in range(6)] labels2 = ["Alpha", "Beta", "Gamma", "Delta"] # Create figure with vertical and horizontal orientations From d56ac78a99119baf4f7a506182445817bfd31e1b Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Mon, 19 Oct 2026 13:01:54 +1000 Subject: [PATCH 04/10] Honor component styling aliases in raincloud plots --- ultraplot/axes/plot.py | 3 + ultraplot/axes/plot_types/raincloud.py | 44 ++++++++---- ultraplot/tests/test_raincloud.py | 98 ++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 13 deletions(-) diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index 4164283c7..dd66b515b 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -6787,6 +6787,9 @@ def raincloud( Component options passed to `violinplot`, `scatter`, and `boxplot`, respectively. Override default styling and widths. Layout options such as positions and orientation must be supplied to this method. + Artist aliases supported by these methods are accepted, e.g. + ``rain_kw={'ms': 25, 'lw': 1}`` or + ``cloud_kw={'fc': 'blue', 'ec': 'navy', 'lw': 1}``. Returns ------- diff --git a/ultraplot/axes/plot_types/raincloud.py b/ultraplot/axes/plot_types/raincloud.py index d89607b99..82865fb9e 100644 --- a/ultraplot/axes/plot_types/raincloud.py +++ b/ultraplot/axes/plot_types/raincloud.py @@ -2,7 +2,21 @@ import numpy as np -from ...internals import warnings +from ...internals import _canonicalize_kwargs, _pop_props, warnings + + +def _component_style(options, category): + """Resolve artist aliases before applying raincloud's component defaults.""" + style = dict(options or {}) + style.update(_pop_props(style, category)) + if category == "patch": + # Box/violin wrappers style faces and edges separately rather than + # accepting the generic Patch colour argument themselves. + color = style.pop("color", None) + if color is not None: + style.setdefault("facecolor", color) + style.setdefault("edgecolor", color) + return style def raincloud( @@ -78,7 +92,11 @@ def raincloud( raise ValueError("positions must be distinct") # Layout belongs to this function; component dictionaries style the artists. - styles = [dict(kw or {}) for kw in (cloud_kw, rain_kw, box_kw)] + styles = [ + _component_style(cloud_kw, "patch"), + _component_style(rain_kw, "collection"), + _canonicalize_kwargs("plot.boxplot", _component_style(box_kw, "patch")), + ] reserved = { "positions", "orientation", @@ -117,35 +135,35 @@ def raincloud( ) continue if cloud: - kw = {"fillcolor": group_color, "widths": 0.6, **styles[0]} - alpha = kw.pop("alpha", 0.4) + kw = {"widths": 0.6, **styles[0]} + props = _pop_props(kw, "patch") + props.setdefault("alpha", 0.4) bodies = ax.violinplot( [position + direction * offset], values, vert=vert, side=side, + fillcolor=group_color, **kw, ) artists["cloud"][i] = bodies[0] - bodies[0].set_alpha(alpha) + bodies[0].update(props) if rain: locations = ( position - direction * offset + rng.uniform(-jitter, jitter, values.size) ) - kw = { - "color": group_color, - "s": 12, - "alpha": 0.5, - "linewidth": 0, - **styles[1], - } + kw = {"alpha": 0.5, "linewidths": 0, **styles[1]} + if not any(key in kw for key in ("sizes", "size")): + kw["sizes"] = 12 + if not any(key in kw for key in ("colors", "facecolors")): + kw["colors"] = group_color coords = (locations, values) if vert else (values, locations) artists["rain"][i] = ax.scatter(*coords, **kw) if box: kw = { - "fillcolor": group_color, + "facecolor": group_color, "widths": 0.12, "fill": True, "showfliers": False, diff --git a/ultraplot/tests/test_raincloud.py b/ultraplot/tests/test_raincloud.py index b50c4dcf8..fb179f1a2 100644 --- a/ultraplot/tests/test_raincloud.py +++ b/ultraplot/tests/test_raincloud.py @@ -1,5 +1,7 @@ """Check raincloud geometry, data preservation, and component composition.""" +import warnings + import numpy as np import pandas as pd import pytest @@ -119,6 +121,102 @@ def test_raincloud_component_styles_are_not_mutated(): np.testing.assert_array_equal(result["rain"][0].get_sizes(), [35]) +@pytest.mark.parametrize( + "alias", ["s", "ms", "markersize", "markersizes", "size", "sizes"] +) +def test_raincloud_scatter_size_aliases(alias): + fig, ax = uplt.subplots() + style = {alias: 35} + with warnings.catch_warnings(): + warnings.simplefilter("error", UserWarning) + result = ax.raincloud(np.arange(8.0), rain_kw=style) + np.testing.assert_array_equal(result["rain"][0].get_sizes(), [35]) + assert style == {alias: 35} + + +@pytest.mark.parametrize( + "alias", ["lw", "linewidth", "linewidths", "mew", "markeredgewidth"] +) +def test_raincloud_scatter_linewidth_aliases(alias): + fig, ax = uplt.subplots() + with warnings.catch_warnings(): + warnings.simplefilter("error", UserWarning) + result = ax.raincloud(np.arange(8.0), rain_kw={alias: 2}) + np.testing.assert_array_equal(result["rain"][0].get_linewidths(), [2]) + + +@pytest.mark.parametrize( + "alias", ["c", "color", "colors", "fc", "facecolor", "facecolors", "mfc"] +) +def test_raincloud_scatter_colour_aliases(alias): + fig, ax = uplt.subplots() + with warnings.catch_warnings(): + warnings.simplefilter("error", UserWarning) + result = ax.raincloud(np.arange(8.0), rain_kw={alias: "red"}) + np.testing.assert_allclose( + result["rain"][0].get_facecolor()[0, :3], to_rgba("red")[:3] + ) + + +def test_raincloud_scatter_artist_aliases(): + fig, ax = uplt.subplots() + with warnings.catch_warnings(): + warnings.simplefilter("error", UserWarning) + result = ax.raincloud(np.arange(8.0), rain_kw={"a": 0.2, "z": 6, "ec": "navy"}) + artist = result["rain"][0] + assert artist.get_alpha() == 0.2 + assert artist.get_zorder() == 6 + np.testing.assert_allclose(artist.get_edgecolor()[0], to_rgba("navy", alpha=0.2)) + + +@pytest.mark.parametrize("component", ["cloud", "box"]) +@pytest.mark.parametrize( + "aliases", + [ + {"fc": "red", "ec": "navy", "lw": 2, "a": 0.2, "z": 6}, + { + "fillcolor": "red", + "edgecolors": "navy", + "linewidths": 2, + "fillalpha": 0.2, + "zorders": 6, + }, + ], +) +def test_raincloud_patch_style_aliases(component, aliases): + fig, ax = uplt.subplots() + original = dict(aliases) + with warnings.catch_warnings(): + warnings.simplefilter("error", UserWarning) + result = ax.raincloud(np.arange(8.0), **{component + "_kw": aliases}) + assert aliases == original + artist = result[component][0] if component == "cloud" else ax.patches[0] + np.testing.assert_allclose( + np.asarray(artist.get_facecolor()).reshape(-1, 4)[0], to_rgba("red", alpha=0.2) + ) + if component == "box": + artist = result[component][0]["boxes"][0] + np.testing.assert_allclose(to_rgba(artist.get_color()), to_rgba("navy")) + else: + np.testing.assert_allclose( + artist.get_edgecolor()[0], to_rgba("navy", alpha=0.2) + ) + np.testing.assert_allclose(artist.get_linewidth(), 2) + assert artist.get_zorder() == 6 + + +@pytest.mark.parametrize("component", ["cloud", "box"]) +def test_raincloud_patch_generic_colour(component): + fig, ax = uplt.subplots() + with warnings.catch_warnings(): + warnings.simplefilter("error", UserWarning) + result = ax.raincloud(np.arange(8.0), **{component + "_kw": {"c": "red"}}) + artist = result[component][0] if component == "cloud" else ax.patches[0] + np.testing.assert_allclose( + np.asarray(artist.get_facecolor()).reshape(-1, 4)[0, :3], to_rgba("red")[:3] + ) + + @pytest.mark.parametrize( "kwargs,match", [ From c961c645656ebd2b60b2fb029d8915925b9c1ade Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Mon, 19 Oct 2026 13:16:27 +1000 Subject: [PATCH 05/10] Integrate raincloud inputs, guides, metadata, and component options --- docs/plot_types.rst | 8 + docs/stats.py | 6 + tools/cheatsheet/parts/icons.py | 21 ++- ultraplot/axes/plot.py | 90 +++++++++-- ultraplot/axes/plot_types/raincloud.py | 146 ++++++++++++++++-- ultraplot/tests/test_raincloud.py | 198 +++++++++++++++++++++++++ 6 files changed, 442 insertions(+), 27 deletions(-) diff --git a/docs/plot_types.rst b/docs/plot_types.rst index a13f0cfe9..a1c100e0c 100644 --- a/docs/plot_types.rst +++ b/docs/plot_types.rst @@ -167,6 +167,14 @@ The shape and spread of a sample. :meth:`~ultraplot.axes.PlotAxes.ridgeline` + .. grid-item-card:: + :img-top: _static/plot_types/raincloud.png + :text-align: center + + :meth:`~ultraplot.axes.PlotAxes.raincloud` + + :doc:`example ` + .. grid-item-card:: :img-top: _static/plot_types/errorbars.png :text-align: center diff --git a/docs/stats.py b/docs/stats.py index ca4ae04b0..0f2b986e5 100644 --- a/docs/stats.py +++ b/docs/stats.py @@ -308,6 +308,12 @@ # and positions are coordinated across components. Use ``orientation='horizontal'`` # to put values on the x axis, and ``cloud=False``, ``rain=False``, or ``box=False`` # to disable individual components. ``seed`` makes the jitter reproducible. +# Use ``legend='r'`` for a group legend, and ``data=table`` to look up a named +# input, e.g. ``ax.raincloud('samples', data={'samples': samples})``. Automatic +# pandas/xarray labels respect :rc:`autoformat`; explicit ``labels`` override them. +# Component dictionaries accept artist aliases and native options, e.g. +# ``rain_kw={'ms': 20, 'lw': 0.5}`` and +# ``box_kw={'medianprops': {'color': 'black', 'lw': 2}}``. # The boxplot describes sample spread, not uncertainty in an estimated mean. # See the :ref:`sphx_glr_gallery_plot_types_13_raincloud.py` gallery example. # diff --git a/tools/cheatsheet/parts/icons.py b/tools/cheatsheet/parts/icons.py index b9e84b6aa..c187a74b0 100644 --- a/tools/cheatsheet/parts/icons.py +++ b/tools/cheatsheet/parts/icons.py @@ -199,6 +199,18 @@ def icon_ridgeline(ax): ax.ridgeline(data, overlap=0.6, cmap=ICON_SEQUENTIAL, lw=0.7) +def icon_raincloud(ax): + data = np.random.default_rng(42).normal(size=(45, 3)) + [0, 0.7, -0.5] + ax.raincloud( + data, + orientation="horizontal", + color=ICON_LINE, + rain_kw={"ms": 3}, + cloud_kw={"lw": 0.5}, + box_kw={"lw": 0.7}, + ) + + def icon_errorbars(ax): ax.plot( WAVE_X, @@ -349,7 +361,7 @@ def icon_curved_quiver(ax): linewidth=1.4, arrowsize=1.2, arrow_at_end=True, - scale = 3, + scale=3, ) @@ -587,6 +599,13 @@ def icon_map_track(ax): "violin": (icon_violin, None, SAME, None, "distribution"), "beeswarm": (icon_beeswarm, None, NEW, None, "distribution"), "ridgeline": (icon_ridgeline, None, NEW, None, "distribution"), + "raincloud": ( + icon_raincloud, + None, + BETTER, + "violinplot + scatter + boxplot", + "distribution", + ), "errorbars": ( icon_errorbars, None, diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index dd66b515b..6d676ebce 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -6400,7 +6400,7 @@ def _apply_boxplot( ) if fill is None: fill = fillcolor is not None or fillalpha is not None - fill = fill or kw.get("cycle") is not None + fill = fill or kw.get("cycle") is not None or kw.get("patch_artist", False) # Parse non-color properties # NOTE: Output dict keys are plural but we use singular for keyword args @@ -6416,9 +6416,19 @@ def _apply_boxplot( ): prefix = key.rstrip("es") # singular form props[key] = iprops = _pop_props(kw, "line", prefix=prefix) - iprops.setdefault("color", edgecolor) + patch_box = key == "boxes" and kw.get("patch_artist", False) + artist_type = mpatches.PathPatch if patch_box else mlines.Line2D + iprops.update( + cbook.normalize_kwargs(kw.pop(prefix + "props", {}), artist_type) + ) + if patch_box: + if "color" not in iprops: + iprops.setdefault("edgecolor", edgecolor) + else: + iprops.setdefault("color", edgecolor) iprops.setdefault("linewidth", linewidth) - iprops.setdefault("markeredgecolor", edgecolor) + if not patch_box: + iprops.setdefault("markeredgecolor", edgecolor) # Parse color properties x, y, kw = self._parse_1d_args( @@ -6509,11 +6519,27 @@ def _apply_boxplot( for key, value in aprops.items() } obj.update(iprops) - # "Filled" boxplot by adding patch beneath line path - if key == "boxes" and ( - fillcolor[i] is not None - or fillalpha is not None - or hatch[i] is not None + # Filled boxes may already be patches when patch_artist=True. + if key == "boxes" and isinstance(obj, mpatches.Patch): + obj.set_fill(fill) + if fillcolor[i] is not None and not { + "facecolor", + "color", + }.intersection(aprops): + obj.set_facecolor(fillcolor[i]) + if fillalpha is not None and "alpha" not in aprops: + obj.set_alpha(fillalpha) + if hatch[i] is not None: + obj.set_hatch(hatch[i]) + artists.setdefault("patches", []).append(obj) + elif ( + key == "boxes" + and fill + and ( + fillcolor[i] is not None + or fillalpha is not None + or hatch[i] is not None + ) ): patch = mpatches.PathPatch( obj.get_path(), @@ -6523,6 +6549,7 @@ def _apply_boxplot( hatch=hatch[i], ) self.add_artist(patch) + artists.setdefault("patches", []).append(patch) # Outlier markers if key == "fliers": if marker is not None: @@ -6724,11 +6751,16 @@ def violinploth(self, *args, **kwargs): def raincloud( self, - data, + dataset=None, *, + data=None, positions=None, labels=None, - orientation="vertical", + orientation=None, + vert=None, + autoformat=None, + legend=None, + legend_kw=None, side="high", jitter=0.08, offset=0.15, @@ -6748,19 +6780,32 @@ def raincloud( Parameters ---------- - data : array-like, sequence of array-like, or DataFrame + dataset : array-like, sequence of array-like, DataFrame, or str A vector represents one distribution. Columns of a 2D array or DataFrame represent distributions. A list or tuple of vectors represents separate distributions, including unequal sample sizes. Masked and non-finite observations are ignored. Empty groups retain their positions and labels but are skipped with a warning. + data : mapping, optional + Resolve a string ``dataset`` as a key in this mapping, following + other plotting methods. ``raincloud(data=array)`` is also accepted + as an alternative to passing the observations positionally. positions : sequence of float, optional Distinct group centres. Defaults to ``range(number_of_groups)``. labels : sequence of str, optional - Group tick labels. Defaults to DataFrame column labels, if present. + Group tick and legend labels. With automatic formatting, infer + column labels from pandas or xarray input. orientation : {'vertical', 'horizontal'}, default: 'vertical' Direction of the measured variable. For horizontal plots, group centres lie on the y axis. + vert : bool, optional + Alternative to ``orientation``, as with box and violin plots. + autoformat : bool, optional + Infer labels from input metadata. Defaults to :rc:`autoformat`. + legend : bool or str, optional + Automatically add a legend using one representative per group. + legend_kw : dict, optional + Additional legend options, e.g. ``{'title': 'Condition'}``. side : {'low', 'high'}, default: 'high' Place the cloud below or above its group centre on the categorical axis. Observations are placed on the opposite side. @@ -6798,6 +6843,8 @@ def raincloud( PathCollections, and ``box`` contains boxplot artist dictionaries. Each list follows input group order, with ``None`` for disabled components or empty groups. + Filled box patches are included under ``box[i]['patches']``. + Optional violin summary artists are available as ``cloud_extras``. Examples -------- @@ -6814,12 +6861,29 @@ def raincloud( """ from .plot_types.raincloud import raincloud + if dataset is None: + dataset = data + elif data is not None: + dataset, positions = inputs._from_data(data, dataset, positions) + if orientation not in (None, "vertical", "horizontal"): + raise ValueError("orientation must be 'vertical' or 'horizontal'") + orientation = ( + "vertical" + if _parse_vert(vert=vert, orientation=orientation, default_vert=True)[ + "vert" + ] + else "horizontal" + ) + return raincloud( self, - data, + dataset, positions=positions, labels=labels, orientation=orientation, + autoformat=autoformat, + legend=legend, + legend_kw=legend_kw, side=side, jitter=jitter, offset=offset, diff --git a/ultraplot/axes/plot_types/raincloud.py b/ultraplot/axes/plot_types/raincloud.py index 82865fb9e..d03e9b5f5 100644 --- a/ultraplot/axes/plot_types/raincloud.py +++ b/ultraplot/axes/plot_types/raincloud.py @@ -1,14 +1,44 @@ """Composition of half violins, observations, and boxplots.""" +from numbers import Number + import numpy as np +import matplotlib.colors as mcolors + +from ... import _sharing as psharing, colors as pcolors +from ...config import rc +from ...internals import _canonicalize_kwargs, _pop_props, inputs, warnings -from ...internals import _canonicalize_kwargs, _pop_props, warnings + +def _resolve_color(color): + """Match the (colormap, index) colour shorthand of other plot methods.""" + if ( + isinstance(color, tuple) + and len(color) == 2 + and isinstance(color[0], (str, mcolors.Colormap)) + and isinstance(color[1], Number) + ): + cmap, index = color + return mcolors.to_hex( + pcolors._cmap_database.get_cmap(cmap)(index), keep_alpha=True + ) + return color def _component_style(options, category): """Resolve artist aliases before applying raincloud's component defaults.""" style = dict(options or {}) style.update(_pop_props(style, category)) + for key in ( + "color", + "facecolor", + "edgecolor", + "colors", + "facecolors", + "edgecolors", + ): + if key in style: + style[key] = _resolve_color(style[key]) if category == "patch": # Box/violin wrappers style faces and edges separately rather than # accepting the generic Patch colour argument themselves. @@ -19,6 +49,44 @@ def _component_style(options, category): return style +def _filter_point_style(style, valid): + """Keep per-observation scatter properties aligned with cleaned values.""" + style = dict(style) + for key in ( + "sizes", + "size", + "colors", + "facecolors", + "edgecolors", + "linewidths", + "alpha", + ): + if key not in style: + continue + value = style[key] + if key in ("colors", "facecolors", "edgecolors") and mcolors.is_color_like( + value + ): + continue + array = np.asarray(value) + if array.ndim and array.shape[0] == valid.size: + style[key] = array[valid] + return style + + +def _group_options(style, index, count): + """Select native box/violin options that contain one value per group.""" + style = dict(style) + for key in ("widths", "capwidths"): + value = style.get(key) + if np.ndim(value) == 1 and len(value) == count: + style[key] = value[index] + quantiles = style.get("quantiles") + if quantiles is not None and len(quantiles) == count and np.ndim(quantiles[0]): + style["quantiles"] = quantiles[index] + return style + + def raincloud( ax, data, @@ -26,6 +94,9 @@ def raincloud( positions=None, labels=None, orientation="vertical", + autoformat=None, + legend=None, + legend_kw=None, side="high", jitter=0.08, offset=0.15, @@ -53,9 +124,19 @@ def raincloud( # Arrays and labelled tables use columns; sequences of vectors permit # unequal sample sizes, following Matplotlib's distribution convention. - columns = getattr(data, "columns", None) - if labels is None and columns is not None: - labels = list(columns) + autoformat = rc["autoformat"] if autoformat is None else autoformat + group_title = value_title = None + if not isinstance(data, (list, tuple)): + metadata = inputs._to_duck_array(data) + if autoformat: + value_title = inputs._meta_title(metadata) + if metadata.ndim == 2: + inferred = inputs._meta_labels(metadata, axis=1, always=False) + if inferred is not None: + group_title = inputs._meta_title(inferred) + if labels is None: + labels = inputs._to_numpy_array(inferred).tolist() + data = inputs._to_numpy_array(metadata, strip_units=True) if isinstance(data, (list, tuple)): distributions = data if data and np.ndim(data[0]) else [data] else: @@ -70,12 +151,14 @@ def raincloud( distributions = values.T else: raise ValueError("data must be one- or two-dimensional") - cleaned = [] + cleaned, masks = [], [] for values in distributions: if np.ndim(values) != 1: raise ValueError("Each distribution must be one-dimensional") - values = np.ma.masked_invalid(np.ma.asarray(values, dtype=float)).compressed() - cleaned.append(values) + values = np.ma.masked_invalid(np.ma.asarray(values, dtype=float)) + valid = ~np.ma.getmaskarray(values) + masks.append(valid) + cleaned.append(values.compressed()) n = len(cleaned) if not n or not any(values.size for values in cleaned): raise ValueError("No finite observations to plot") @@ -117,6 +200,7 @@ def raincloud( ) ax._parse_cycle(n, cycle=cycle, cycle_kw=cycle_kw) + color = _resolve_color(color) colors = [ color if color is not None else ax._get_patches_for_fill.get_next_color() for _ in range(n) @@ -125,6 +209,7 @@ def raincloud( vert = orientation == "vertical" direction = 1 if side == "high" else -1 artists = {key: [None] * n for key in ("cloud", "rain", "box")} + handles = [] for i, (position, values, group_color) in enumerate( zip(positions, cleaned, colors) @@ -135,26 +220,40 @@ def raincloud( ) continue if cloud: - kw = {"widths": 0.6, **styles[0]} + kw = {"widths": 0.6, **_group_options(styles[0], i, n)} props = _pop_props(kw, "patch") props.setdefault("alpha", 0.4) + before = set(ax.get_children()) bodies = ax.violinplot( [position + direction * offset], - values, + values[:, None], vert=vert, side=side, fillcolor=group_color, **kw, ) + if isinstance(bodies, tuple): + bodies = bodies[0] artists["cloud"][i] = bodies[0] bodies[0].update(props) + extras = [ + artist + for artist in ax.get_children() + if artist not in before and artist is not bodies[0] + ] + if extras: + artists.setdefault("cloud_extras", [None] * n)[i] = extras if rain: locations = ( position - direction * offset + rng.uniform(-jitter, jitter, values.size) ) - kw = {"alpha": 0.5, "linewidths": 0, **styles[1]} + kw = { + "alpha": 0.5, + "linewidths": 0, + **_filter_point_style(styles[1], masks[i]), + } if not any(key in kw for key in ("sizes", "size")): kw["sizes"] = 12 if not any(key in kw for key in ("colors", "facecolors")): @@ -168,7 +267,7 @@ def raincloud( "fill": True, "showfliers": False, "zorder": 3, - **styles[2], + **_group_options(styles[2], i, n), } artists["box"][i] = ax.boxplot( [position], @@ -177,11 +276,32 @@ def raincloud( manage_ticks=False, **kw, ) + if cloud: + handle = artists["cloud"][i] + elif rain: + handle = artists["rain"][i] + else: + group = artists["box"][i] + handle = (group.get("patches") or group["boxes"] or group["medians"])[0] + if labels is not None or legend: + handle.set_label(str(labels[i] if labels is not None else position)) + handles.append(handle) axis = ax.xaxis if vert else ax.yaxis axis.set_ticks(positions) if labels is not None: axis.set_ticklabels(labels) - if columns is not None and columns.name and axis.isDefault_label: - axis.set_label_text(str(columns.name)) + value_axis = ax.yaxis if vert else ax.xaxis + formatting = {} + if group_title and axis.isDefault_label: + formatting["xlabel" if vert else "ylabel"] = group_title + if value_title and value_axis.isDefault_label: + formatting["ylabel" if vert else "xlabel"] = value_title + if formatting: + with psharing.preserve_axis_sharing(): + ax.format(**formatting) + legend_kw = dict(legend_kw or {}) + if group_title: + legend_kw.setdefault("title", group_title) + ax._update_guide(handles, legend=legend, legend_kw=legend_kw) return artists diff --git a/ultraplot/tests/test_raincloud.py b/ultraplot/tests/test_raincloud.py index fb179f1a2..b5d69bd41 100644 --- a/ultraplot/tests/test_raincloud.py +++ b/ultraplot/tests/test_raincloud.py @@ -243,3 +243,201 @@ def test_raincloud_no_observations(data): fig, ax = uplt.subplots() with pytest.raises(ValueError, match="No finite observations"): ax.raincloud(data) + + +@pytest.mark.parametrize("keyword", [False, True]) +def test_raincloud_data_lookup_and_keyword_input(keyword): + frame = pd.DataFrame({"A": [1, 2, 3], "B": [2, 4, 5]}) + frame.columns.name = "Condition" + fig, ax = uplt.subplots() + result = ( + ax.raincloud(data=frame) + if keyword + else ax.raincloud("samples", data={"samples": frame}) + ) + assert ax.get_xlabel() == "Condition" + assert [tick.get_text() for tick in ax.get_xticklabels()] == ["A", "B"] + np.testing.assert_array_equal(result["rain"][1].get_offsets()[:, 1], frame["B"]) + + +@pytest.mark.parametrize("explicit", [False, True]) +def test_raincloud_respects_autoformat(explicit): + frame = pd.DataFrame({"A": [1, 2, 3], "B": [2, 4, 5]}) + frame.columns.name = "Condition" + fig, ax = uplt.subplots() + labels = ["First", "Second"] if explicit else None + with uplt.rc.context({"autoformat": False}): + ax.raincloud(frame, labels=labels) + assert ax.get_xlabel() == "" + assert [tick.get_text() for tick in ax.get_xticklabels()] == (labels or ["0", "1"]) + + +def test_raincloud_xarray_metadata_and_existing_labels(): + xr = pytest.importorskip("xarray") + samples = xr.DataArray( + np.arange(12.0).reshape(6, 2), + dims=["sample", "condition"], + coords={"condition": ["A", "B"]}, + name="rate", + attrs={"long_name": "Growth", "units": "mm"}, + ) + fig, axes = uplt.subplots(ncols=2, share=False) + axes[0].raincloud(samples, vert=False) + assert axes[0].get_xlabel() == "Growth (mm)" + assert axes[0].get_ylabel() == "condition" + assert [tick.get_text() for tick in axes[0].get_yticklabels()] == ["A", "B"] + axes[1].format(xlabel="Custom group", ylabel="Custom value") + axes[1].raincloud(samples) + assert axes[1].get_xlabel() == "Custom group" + assert axes[1].get_ylabel() == "Custom value" + + +@pytest.mark.parametrize( + "disabled", [{}, {"cloud": False}, {"cloud": False, "rain": False}] +) +def test_raincloud_legend_one_entry_per_group(disabled): + from matplotlib.legend import Legend + + fig, ax = uplt.subplots() + options = {"title": "Condition"} + result = ax.raincloud( + np.arange(12.0).reshape(6, 2), + labels=["A", "B"], + legend=True, + legend_kw=options, + **disabled, + ) + fig.canvas.draw() + legends = fig.findobj(Legend) + assert len(legends) == 1 + assert [text.get_text() for text in legends[0].get_texts()] == ["A", "B"] + assert legends[0].get_title().get_text() == "Condition" + assert options == {"title": "Condition"} + + +def test_raincloud_filters_observation_styles(): + values = np.ma.array([1.0, 2.0, np.nan, 3.0, 4.0, 99.0], mask=[0, 0, 0, 0, 0, 1]) + sizes = np.arange(10.0, 70.0, 10.0) + colours = np.arange(6.0) + fig, ax = uplt.subplots() + result = ax.raincloud( + values, rain_kw={"ms": sizes, "c": colours, "absolute_size": True} + ) + points = result["rain"][0] + np.testing.assert_array_equal(points.get_sizes(), sizes[[0, 1, 3, 4]]) + np.testing.assert_array_equal(points.get_array(), colours[[0, 1, 3, 4]]) + np.testing.assert_array_equal(points.get_offsets()[:, 1], [1.0, 2.0, 3.0, 4.0]) + + +@pytest.mark.parametrize( + "option,value", + [ + ("showmeans", True), + ("showmedians", True), + ("showextrema", True), + ("quantiles", [0.25, 0.75]), + ], +) +def test_raincloud_violin_summary_options(option, value): + fig, ax = uplt.subplots() + result = ax.raincloud(np.arange(8.0), cloud_kw={option: value}) + assert result["cloud"][0] in ax.collections + assert result["cloud_extras"][0] + fig.canvas.draw() + + +def test_raincloud_nested_boxplot_properties(): + fig, ax = uplt.subplots() + result = ax.raincloud( + np.r_[np.arange(8.0), 100], + box_kw={ + "showfliers": True, + "showmeans": True, + "boxprops": {"lw": 4, "color": "red"}, + "medianprops": {"lw": 3, "color": "blue"}, + "whiskerprops": {"lw": 2, "color": "green"}, + "flierprops": {"marker": "x", "ms": 9}, + "meanprops": {"color": "purple"}, + }, + ) + artists = result["box"][0] + for name, width, colour in [ + ("boxes", 4, "red"), + ("medians", 3, "blue"), + ("whiskers", 2, "green"), + ]: + for artist in artists[name]: + assert artist.get_linewidth() == width + assert artist.get_color() == colour + assert artists["fliers"][0].get_marker() == "x" + assert artists["fliers"][0].get_markersize() == 9 + assert artists["means"][0].get_color() == "purple" + assert artists["patches"][0] in ax.patches + + +@pytest.mark.parametrize("filled", [False, True]) +def test_raincloud_box_fill_and_patch_artist(filled): + fig, ax = uplt.subplots() + result = ax.raincloud( + np.arange(8.0), + box_kw={ + "patch_artist": True, + "filled": filled, + "boxprops": {"fc": "red", "ec": "navy"}, + }, + ) + patch = result["box"][0]["boxes"][0] + assert patch.get_fill() == filled + np.testing.assert_allclose(patch.get_facecolor()[:3], to_rgba("red")[:3]) + np.testing.assert_allclose(patch.get_edgecolor()[:3], to_rgba("navy")[:3]) + assert result["box"][0]["patches"] == [patch] + assert len(ax.patches) == 1 + + +def test_raincloud_unfilled_boxes_and_hidden_box_legend(): + fig, ax = uplt.subplots() + ax.raincloud(np.arange(8.0), box_kw={"filled": False}) + assert not ax.patches + result = ax.raincloud( + np.arange(8.0), cloud=False, rain=False, box_kw={"showbox": False}, legend=True + ) + assert not result["box"][0]["boxes"] + fig.canvas.draw() + + +def test_raincloud_colormap_colour_shorthand(): + fig, ax = uplt.subplots() + result = ax.raincloud(np.arange(8.0), color=("reds", 0.6)) + expected = uplt.Colormap("reds")(0.6) + np.testing.assert_allclose( + result["cloud"][0].get_facecolor()[0, :3], expected[:3], atol=1 / 255 + ) + np.testing.assert_allclose( + result["rain"][0].get_facecolor()[0, :3], expected[:3], atol=1 / 255 + ) + + +def test_raincloud_subplot_grid_and_shared_ticks(): + fig, axes = uplt.subplots(ncols=2) + axes.raincloud(np.arange(12.0).reshape(6, 2), labels=["A", "B"]) + for ax in axes: + assert [tick.get_text() for tick in ax.get_xticklabels()] == ["A", "B"] + assert len(ax.get_xticks()) == 2 + fig.canvas.draw() + + +def test_raincloud_per_group_widths_and_quantiles(): + fig, ax = uplt.subplots() + result = ax.raincloud( + np.arange(12.0).reshape(6, 2), + cloud_kw={"widths": [0.4, 0.8], "quantiles": [[0.25, 0.75], [0.5]]}, + box_kw={"widths": [0.1, 0.2], "capwidths": [0.05, 0.1]}, + ) + for i, width in enumerate([0.4, 0.8]): + vertices = result["cloud"][i].get_paths()[0].vertices + np.testing.assert_allclose(np.ptp(vertices[:, 0]), width / 2) + np.testing.assert_allclose( + np.ptp(result["box"][i]["boxes"][0].get_xdata()), [0.1, 0.2][i] + ) + assert result["cloud_extras"][i] + fig.canvas.draw() From dd158b3b304901fb9925b8ba8689723e36f0ef17 Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Mon, 19 Oct 2026 13:27:55 +1000 Subject: [PATCH 06/10] Remove redundant generated raincloud CSV data --- .../plot_types/data/raincloud_data.csv | 101 ------------------ 1 file changed, 101 deletions(-) delete mode 100644 docs/examples/plot_types/data/raincloud_data.csv diff --git a/docs/examples/plot_types/data/raincloud_data.csv b/docs/examples/plot_types/data/raincloud_data.csv deleted file mode 100644 index 1fac500d2..000000000 --- a/docs/examples/plot_types/data/raincloud_data.csv +++ /dev/null @@ -1,101 +0,0 @@ -Normal,Right-skewed,Bimodal -4.570581997558644249e-01,-4.704771519386133916e-01,-8.559799267119158062e-01 --1.274293195803511081e+00,1.138952090630544811e+00,-6.015920822406437551e-01 -1.030956989994613382e+00,-4.587958982791404683e-01,-9.147057264835560808e-01 -1.275735021431826821e+00,3.974157264691976499e-01,-7.831368506209369640e-01 --2.447304404085562712e+00,-7.174129398016750869e-01,-1.423705482270590350e+00 --1.611879253958110159e+00,-3.736624002970062031e-01,-9.478273511765962223e-01 -2.293230835527209632e-01,-7.234361082343604421e-01,-1.136735454930538181e+00 --3.424498292918945475e-01,-4.494869022936150982e-01,-1.226036364723646077e+00 -4.309184457353500836e-02,5.136880814107286364e-01,-1.145038750724206356e+00 --1.033600956819865191e+00,-9.462964488929621609e-01,-1.015687035622965917e+00 -1.196980630188664074e+00,9.651613721120903822e-02,-7.184730511559564992e-01 -1.066159180781707683e+00,-6.931917160114066390e-02,-1.251615175909854383e+00 -1.497408528127340821e-01,-5.772051130609557879e-01,-9.289606246938378886e-01 -1.516087752465650640e+00,-8.782917700815950912e-01,-1.051362907907346100e+00 -6.666591235871132115e-01,-1.934138132038103197e-01,-1.014153360993360176e+00 --1.041646171951116973e+00,-5.468114701765517394e-01,-6.978217966484038248e-01 -5.395039092606185305e-01,-7.331149736608608136e-02,-8.082980727026064516e-01 --1.169872075305822490e+00,-2.285203381991025295e-01,-6.182556332521671472e-01 -1.195760466222100193e+00,1.665809697347728102e+00,-9.729799949010423665e-01 -4.425268173928683410e-04,-3.935412019388941918e-01,-1.101715421367026604e+00 --1.732930345720433973e-01,-7.496396846790880364e-01,-9.894698300323672147e-01 --8.119974655757373760e-01,-1.146584881047419680e-01,-8.785862081268797086e-01 -1.638790117678551761e+00,-8.325932053744133921e-02,-8.942605752224513926e-01 --1.342383529650313101e-01,1.235299222177552947e+00,-1.194074640012727118e+00 --4.867636152026627583e-01,5.076213741588365691e-01,-9.147283440627537177e-01 --3.886607355652625384e-01,2.858622825709987089e-02,-8.819787665559276624e-01 -7.500912647201866124e-01,1.411785454488908575e+00,-3.376734102344343680e-01 -5.352463880664395379e-01,-8.041992633936616564e-01,-4.899934635165507091e-01 -5.961321023772855332e-01,-5.978858870926533342e-01,-1.139116227428844885e+00 -6.194215603182601226e-01,-7.148076690632824137e-01,-1.004573822499995384e+00 -2.822172661171865382e+00,-4.766226817060956744e-01,-1.284200869605070405e+00 --4.585500854903775125e-01,-8.595188469581481749e-01,-1.076693948432645875e+00 --5.948070918481080938e-01,2.890961617116470372e-01,-8.612035429862002767e-01 --9.830378678109574775e-01,-3.835546625004089827e-01,-6.495324719046926543e-01 -8.578197200408353984e-01,-8.847867915066891520e-01,-1.109595286253645119e+00 -1.518316587961045405e+00,-7.468755285868634575e-01,-1.091777702578822895e+00 --8.198752926125690843e-02,-7.925180864466540437e-03,-1.446796609760443442e+00 --1.017007898203546201e+00,5.111388815816818587e-01,-9.749202248341597432e-01 --9.968254325669719806e-01,2.529546407374880790e+00,-1.188850045291061974e+00 -9.023856792690770900e-01,5.638996382318423706e+00,-1.062126584411327901e+00 -1.021690560604885611e+00,7.865613159352038974e-02,-1.144731654514046681e+00 -7.640547008745716129e-01,-7.376841757844221359e-01,-9.586562132677844961e-01 --7.921438677751014845e-01,-1.024713490581300901e+00,-1.354172239471838068e+00 -3.636400397174379684e-01,-4.539421653322759515e-02,-1.285057599295654107e+00 -2.149611404415538995e-01,-6.710740102651947536e-01,-4.299806686201645967e-01 -3.462934174420068123e-01,-4.899406421032219683e-01,-1.242349392885232495e+00 -1.186720001029302862e+00,-5.854174150341603378e-01,-1.197022348517528423e+00 -3.526112956150126987e-01,-3.345386581970978823e-01,-4.994877271811409836e-01 -9.388497013568343208e-01,7.979132260534113064e-01,-2.455168466205926991e-01 -1.517344376520620153e-01,-1.314499144577696288e-01,-1.214802758080658185e+00 -4.369756214405638439e-01,-3.455024736788078754e-01,8.783290306176130269e-01 -8.775303577422558510e-01,-7.538555981223742375e-01,1.047096583287798754e+00 --1.811416860211617852e+00,-9.344968018512614938e-01,1.376912150491032216e+00 --3.468643066738036906e-01,-5.257882034401262228e-01,7.312449002487961813e-01 --5.408978567292930251e-01,-2.792181120110966419e-01,9.075674099187166499e-01 --7.578543863611353348e-01,2.002371568483547470e+00,1.150710839152028209e+00 --2.895316540929496196e-01,-1.513568693237695717e-01,1.069258899485182601e+00 -1.989514931157301714e+00,6.881927401494664087e-01,8.764489857058929134e-01 --1.050064923853462062e+00,-5.321730491692476805e-01,9.340676185911264140e-01 -1.311417332620210985e+00,-8.030034183478125254e-01,6.389823884156635714e-01 --2.102031733945068481e+00,-7.289219558930716270e-01,9.092565255694495807e-01 --3.664526417862984942e-01,-6.350362263457114409e-01,9.025482491101989302e-01 -2.742743980887019095e-01,2.870607201687502830e+00,1.021088365311988877e+00 -8.195063892119740512e-01,-6.744366690753046401e-01,8.338481346850163956e-01 -9.804538786842861287e-01,5.115623770537101045e-01,1.078002146393776339e+00 -1.086187191649340633e+00,-7.059704193686625606e-01,1.206675839910399617e+00 --3.842721965956688024e-01,6.236371835178988743e-01,1.002842050036256483e+00 --5.305707073832119924e-01,5.278746792890732370e-02,1.049522002864169856e+00 -1.169398910140914793e+00,-3.442818088111796460e-01,9.785247498024703949e-01 --1.815872927061168152e-01,-2.706674567356631700e-01,9.659062682219407003e-01 --1.577768322291283276e+00,-6.045716506898286546e-01,7.943240580218666747e-01 --1.394424320486017610e+00,1.070183868127083104e-01,1.041137854637398830e+00 --1.119104119335565528e+00,-5.101652784000815233e-01,9.427547331747206938e-01 -7.048363754735339493e-01,-8.155837925047412229e-01,1.463570971902018902e+00 -2.481022270083786119e-01,-8.312929621843503369e-01,1.339976890014089816e+00 -9.537488006500377535e-01,-1.197362601463014820e-01,1.057627488440771923e+00 --4.853792874522327994e-01,1.622609050371250916e+00,7.844569547269026888e-01 -2.688495266898576208e-01,-1.292405013727374319e-01,7.013923035174706166e-01 -8.701941931990735402e-01,-3.207493878621509031e-01,1.249099789915552039e+00 --3.335709122078411859e-01,-3.070855577884405718e-02,1.028359099101289420e+00 -6.528385756938763773e-01,1.149648988545044492e+00,1.080048096603538621e+00 --7.875296391907027349e-01,-8.373910361808210856e-02,5.510825427399892451e-01 --4.027210115949690361e-01,-4.876470764786985934e-01,1.186399767861819665e+00 --4.267773981997991517e-01,8.532159083934596211e-01,1.073932019979804364e+00 --1.474962837778363012e+00,9.143531189917711299e-02,7.018632679403923325e-01 -6.917186181521239874e-01,1.541842032059796530e+00,8.537735193798491329e-01 --5.396485422559562650e-01,-1.116422779602090631e-01,1.028589251883392341e+00 -8.081057191294913067e-02,-8.152365990722728606e-01,9.783610360393693028e-01 -6.837026473831977480e-01,-8.571523716562271522e-01,8.964177565722291074e-01 -6.396489760962126292e-01,1.761610697880518606e+00,9.412801854671187796e-01 -9.214313275737230358e-01,1.909127291118590808e+00,9.059744872871325905e-01 --6.207967979827594407e-02,-3.581739371783070891e-01,1.002160418063280378e+00 --4.802879390791325309e-01,-4.731341166693074363e-01,1.315757323740101237e+00 --3.791613649077135717e-02,1.408529096417256543e+00,3.556214620321523356e-01 --2.107780148133603149e+00,-7.779538858059780182e-01,9.095712043125148139e-01 --1.798485687268077893e+00,-7.028742654072184237e-01,1.007854898064685534e+00 --1.638299631697614833e+00,2.974852716034528943e-01,1.036263581940459533e+00 --1.219267404360247165e+00,-4.791393841433880252e-01,8.774574688358123264e-01 -5.794477133346088715e-01,-2.468875589676320370e-01,5.481970602096852829e-01 --1.101113079672860096e+00,-3.484349953217553697e-01,1.043872457017924749e+00 From ad79695c831c0f4d90ac3ca428e90c9cc683a80b Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Mon, 19 Oct 2026 20:00:28 +1000 Subject: [PATCH 07/10] Move raincloud defaults into validated rc settings --- docs/stats.py | 5 ++ ultraplot/axes/plot.py | 45 ++++++---- ultraplot/axes/plot_types/raincloud.py | 47 ++++++---- ultraplot/internals/rcsetup.py | 62 +++++++++++++ ultraplot/tests/test_raincloud.py | 115 +++++++++++++++++++++++++ 5 files changed, 238 insertions(+), 36 deletions(-) diff --git a/docs/stats.py b/docs/stats.py index 0f2b986e5..d7e1a7432 100644 --- a/docs/stats.py +++ b/docs/stats.py @@ -314,6 +314,11 @@ # Component dictionaries accept artist aliases and native options, e.g. # ``rain_kw={'ms': 20, 'lw': 0.5}`` and # ``box_kw={'medianprops': {'color': 'black', 'lw': 2}}``. +# Layout, visibility, jitter seed, and component appearance defaults live under +# ``raincloud.*`` rc settings. For example, +# ``uplt.rc['raincloud.rain.markersize'] = 20`` changes observation marker areas +# globally, or use ``uplt.rc.context`` to change defaults temporarily. Explicit +# arguments and component dictionary entries override the rc defaults. # The boxplot describes sample spread, not uncertainty in an estimated mean. # See the :ref:`sphx_glr_gallery_plot_types_13_raincloud.py` gallery example. # diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index 6d676ebce..0be910162 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -6761,13 +6761,13 @@ def raincloud( autoformat=None, legend=None, legend_kw=None, - side="high", - jitter=0.08, - offset=0.15, - cloud=True, - rain=True, - box=True, - seed=0, + side=None, + jitter=None, + offset=None, + cloud=None, + rain=None, + box=None, + seed=None, color=None, cycle=None, cycle_kw=None, @@ -6795,7 +6795,7 @@ def raincloud( labels : sequence of str, optional Group tick and legend labels. With automatic formatting, infer column labels from pandas or xarray input. - orientation : {'vertical', 'horizontal'}, default: 'vertical' + orientation : {'vertical', 'horizontal'}, default: :rc:`raincloud.orientation` Direction of the measured variable. For horizontal plots, group centres lie on the y axis. vert : bool, optional @@ -6806,21 +6806,25 @@ def raincloud( Automatically add a legend using one representative per group. legend_kw : dict, optional Additional legend options, e.g. ``{'title': 'Condition'}``. - side : {'low', 'high'}, default: 'high' + side : {'low', 'high'}, default: :rc:`raincloud.side` Place the cloud below or above its group centre on the categorical axis. Observations are placed on the opposite side. - jitter : float, default: 0.08 + jitter : float, default: :rc:`raincloud.jitter` Half-width of uniform jitter in categorical-axis data units. - offset : float, default: 0.15 + offset : float, default: :rc:`raincloud.offset` Distance of the cloud baseline and the centre of the observations from the group centre, in categorical-axis data units. - cloud, rain, box : bool, default: True - Whether to draw each component. At least one must be enabled. + cloud, rain, box : bool, optional + Whether to draw each component. Defaults to :rc:`raincloud.cloud`, + :rc:`raincloud.rain`, and :rc:`raincloud.box`, respectively. + At least one must be enabled. Boxplots show the median, interquartile range, and Tukey whiskers; these are distribution summaries, not confidence intervals. - seed : int, numpy.random.Generator, or None, default: 0 + seed : int, numpy.random.Generator, or None, optional Random seed for reproducible jitter, or a generator to use directly. - Does not change NumPy's global random state. + Defaults to :rc:`raincloud.seed`. Set that rc value to ``None`` or + pass a fresh generator for unseeded jitter. Does not change NumPy's + global random state. color : color-spec, optional Colour shared by all groups. Otherwise each group uses one cycle colour shared across its three components. @@ -6835,6 +6839,9 @@ def raincloud( Artist aliases supported by these methods are accepted, e.g. ``rain_kw={'ms': 25, 'lw': 1}`` or ``cloud_kw={'fc': 'blue', 'ec': 'navy', 'lw': 1}``. + Defaults come from the ``raincloud.cloud.*``, ``raincloud.rain.*``, + and ``raincloud.box.*`` rc settings. Explicit component options take + precedence over these defaults. Returns ------- @@ -6869,9 +6876,11 @@ def raincloud( raise ValueError("orientation must be 'vertical' or 'horizontal'") orientation = ( "vertical" - if _parse_vert(vert=vert, orientation=orientation, default_vert=True)[ - "vert" - ] + if _parse_vert( + vert=vert, + orientation=orientation, + default_vert=rc["raincloud.orientation"] == "vertical", + )["vert"] else "horizontal" ) diff --git a/ultraplot/axes/plot_types/raincloud.py b/ultraplot/axes/plot_types/raincloud.py index d03e9b5f5..33fa89aa5 100644 --- a/ultraplot/axes/plot_types/raincloud.py +++ b/ultraplot/axes/plot_types/raincloud.py @@ -7,7 +7,7 @@ from ... import _sharing as psharing, colors as pcolors from ...config import rc -from ...internals import _canonicalize_kwargs, _pop_props, inputs, warnings +from ...internals import _canonicalize_kwargs, _not_none, _pop_props, inputs, warnings def _resolve_color(color): @@ -93,17 +93,17 @@ def raincloud( *, positions=None, labels=None, - orientation="vertical", + orientation=None, autoformat=None, legend=None, legend_kw=None, - side="high", - jitter=0.08, - offset=0.15, - cloud=True, - rain=True, - box=True, - seed=0, + side=None, + jitter=None, + offset=None, + cloud=None, + rain=None, + box=None, + seed=None, color=None, cycle=None, cycle_kw=None, @@ -112,6 +112,14 @@ def raincloud( box_kw=None, ): """Draw the components of :meth:`PlotAxes.raincloud`.""" + orientation = _not_none(orientation, rc["raincloud.orientation"]) + side = _not_none(side, rc["raincloud.side"]) + jitter = _not_none(jitter, rc["raincloud.jitter"]) + offset = _not_none(offset, rc["raincloud.offset"]) + cloud = _not_none(cloud, rc["raincloud.cloud"]) + rain = _not_none(rain, rc["raincloud.rain"]) + box = _not_none(box, rc["raincloud.box"]) + seed = _not_none(seed, rc["raincloud.seed"]) if orientation not in ("vertical", "horizontal"): raise ValueError("orientation must be 'vertical' or 'horizontal'") if side not in ("low", "high"): @@ -220,9 +228,12 @@ def raincloud( ) continue if cloud: - kw = {"widths": 0.6, **_group_options(styles[0], i, n)} + kw = { + "widths": rc["raincloud.cloud.width"], + **_group_options(styles[0], i, n), + } props = _pop_props(kw, "patch") - props.setdefault("alpha", 0.4) + props.setdefault("alpha", rc["raincloud.cloud.alpha"]) before = set(ax.get_children()) bodies = ax.violinplot( [position + direction * offset], @@ -250,12 +261,12 @@ def raincloud( + rng.uniform(-jitter, jitter, values.size) ) kw = { - "alpha": 0.5, - "linewidths": 0, + "alpha": rc["raincloud.rain.alpha"], + "linewidths": rc["raincloud.rain.linewidth"], **_filter_point_style(styles[1], masks[i]), } if not any(key in kw for key in ("sizes", "size")): - kw["sizes"] = 12 + kw["sizes"] = rc["raincloud.rain.markersize"] if not any(key in kw for key in ("colors", "facecolors")): kw["colors"] = group_color coords = (locations, values) if vert else (values, locations) @@ -263,10 +274,10 @@ def raincloud( if box: kw = { "facecolor": group_color, - "widths": 0.12, - "fill": True, - "showfliers": False, - "zorder": 3, + "widths": rc["raincloud.box.width"], + "fill": rc["raincloud.box.fill"], + "showfliers": rc["raincloud.box.showfliers"], + "zorder": rc["raincloud.box.zorder"], **_group_options(styles[2], i, n), } artists["box"][i] = ax.boxplot( diff --git a/ultraplot/internals/rcsetup.py b/ultraplot/internals/rcsetup.py index 39a39f6ad..27737fbce 100644 --- a/ultraplot/internals/rcsetup.py +++ b/ultraplot/internals/rcsetup.py @@ -1992,6 +1992,68 @@ def _validator_accepts(validator, value): _validate_float, "Z-order for internal political border lines.", ), + # Raincloud plot settings + "raincloud.orientation": ( + "vertical", + _validate_belongs("vertical", "horizontal"), + "Direction of raincloud observations; categories lie on the other axis.", + ), + "raincloud.side": ( + "high", + _validate_belongs("low", "high"), + "Side of each group centre occupied by the raincloud density.", + ), + "raincloud.jitter": ( + 0.08, + _validate_float, + "Half-width of raincloud point jitter in categorical-axis data units.", + ), + "raincloud.offset": ( + 0.15, + _validate_float, + "Offset of clouds and points from group centres in categorical-axis data units.", + ), + "raincloud.cloud": ( + True, + _validate_bool, + "Whether to draw raincloud half violins.", + ), + "raincloud.rain": (True, _validate_bool, "Whether to draw raincloud observations."), + "raincloud.box": (True, _validate_bool, "Whether to draw raincloud boxplots."), + "raincloud.seed": ( + 0, + _validate_or_none(_validate_int), + "Random seed for raincloud jitter. None uses fresh random entropy.", + ), + "raincloud.cloud.width": ( + 0.6, + _validate_float, + "Full violin width before halving, in categorical-axis data units.", + ), + "raincloud.cloud.alpha": (0.4, _validate_float, "Raincloud density opacity."), + "raincloud.rain.markersize": ( + 12.0, + _validate_float, + "Raincloud observation marker area in points squared, as for scatter's s.", + ), + "raincloud.rain.linewidth": ( + 0.0, + _validate_pt, + "Raincloud observation marker edge width." + _addendum_pt, + ), + "raincloud.rain.alpha": (0.5, _validate_float, "Raincloud observation opacity."), + "raincloud.box.width": ( + 0.12, + _validate_float, + "Raincloud box width in categorical-axis data units.", + ), + "raincloud.box.fill": (True, _validate_bool, "Whether to fill raincloud boxes."), + "raincloud.box.showfliers": ( + False, + _validate_bool, + "Whether raincloud boxplots show outlier markers as well as the rain.", + ), + "raincloud.box.zorder": (3.0, _validate_float, "Raincloud boxplot drawing order."), # Kernel density settings "kde.points": ( 200, diff --git a/ultraplot/tests/test_raincloud.py b/ultraplot/tests/test_raincloud.py index b5d69bd41..32a49de75 100644 --- a/ultraplot/tests/test_raincloud.py +++ b/ultraplot/tests/test_raincloud.py @@ -441,3 +441,118 @@ def test_raincloud_per_group_widths_and_quantiles(): ) assert result["cloud_extras"][i] fig.canvas.draw() + + +def test_raincloud_rc_layout_and_appearance(): + fig, ax = uplt.subplots() + settings = { + "raincloud.orientation": "horizontal", + "raincloud.side": "low", + "raincloud.jitter": 0, + "raincloud.offset": 0.3, + "raincloud.cloud.width": 0.8, + "raincloud.cloud.alpha": 0.2, + "raincloud.rain.markersize": 36, + "raincloud.rain.linewidth": 1.5, + "raincloud.rain.alpha": 0.7, + "raincloud.box.width": 0.2, + "raincloud.box.fill": False, + "raincloud.box.showfliers": True, + "raincloud.box.zorder": 5, + } + with uplt.rc.context(settings): + result = ax.raincloud(np.r_[np.arange(8.0), 100]) + points = result["rain"][0] + np.testing.assert_allclose(points.get_offsets()[:, 1], 0.3) + np.testing.assert_array_equal(points.get_sizes(), [36]) + np.testing.assert_allclose(points.get_linewidths(), [1.5]) + assert points.get_alpha() == 0.7 + density = result["cloud"][0] + vertices = density.get_paths()[0].vertices[:, 1] + assert vertices.max() == pytest.approx(-0.3) + assert np.ptp(vertices) == pytest.approx(0.4) + assert density.get_alpha() == 0.2 + box = result["box"][0] + assert np.ptp(box["boxes"][0].get_ydata()) == pytest.approx(0.2) + assert box["boxes"][0].get_zorder() == 5 + np.testing.assert_array_equal(box["fliers"][0].get_xdata(), [100]) + assert not ax.patches + fig.canvas.draw() + + +def test_raincloud_explicit_options_override_rc(): + fig, ax = uplt.subplots() + settings = { + "raincloud.orientation": "horizontal", + "raincloud.side": "low", + "raincloud.jitter": 0.5, + "raincloud.offset": 0.4, + "raincloud.rain.markersize": 40, + "raincloud.rain.linewidth": 3, + "raincloud.rain.alpha": 0.8, + "raincloud.cloud.alpha": 0.9, + "raincloud.box.fill": False, + "raincloud.box.width": 0.5, + } + with uplt.rc.context(settings): + result = ax.raincloud( + np.arange(8.0), + orientation="vertical", + side="high", + jitter=0, + offset=0, + rain_kw={"ms": 20, "lw": 1, "a": 0.3}, + cloud_kw={"a": 0.25}, + box_kw={"filled": True, "widths": 0.1}, + ) + points = result["rain"][0] + np.testing.assert_array_equal(points.get_offsets()[:, 0], np.zeros(8)) + np.testing.assert_array_equal(points.get_sizes(), [20]) + np.testing.assert_array_equal(points.get_linewidths(), [1]) + assert points.get_alpha() == 0.3 + assert result["cloud"][0].get_alpha() == 0.25 + assert result["cloud"][0].get_paths()[0].vertices[:, 0].min() == 0 + assert np.ptp(result["box"][0]["boxes"][0].get_xdata()) == pytest.approx(0.1) + assert len(ax.patches) == 1 + + +def test_raincloud_rc_visibility_and_explicit_false(): + fig, axes = uplt.subplots(ncols=2) + with uplt.rc.context({"raincloud.cloud": False, "raincloud.box": False}): + first = axes[0].raincloud(np.arange(8.0)) + second = axes[1].raincloud(np.arange(8.0), cloud=True, rain=False) + assert first["cloud"] == first["box"] == [None] + assert first["rain"][0] is not None + assert second["cloud"][0] is not None + assert second["rain"] == second["box"] == [None] + + +def test_raincloud_rc_seed_and_generator_override(): + fig, axes = uplt.subplots(ncols=3) + with uplt.rc.context({"raincloud.seed": 17}): + first = axes[0].raincloud(np.arange(8.0)) + second = axes[1].raincloud(np.arange(8.0), seed=17) + third = axes[2].raincloud(np.arange(8.0), seed=np.random.default_rng(7)) + np.testing.assert_array_equal( + first["rain"][0].get_offsets(), second["rain"][0].get_offsets() + ) + assert not np.array_equal( + first["rain"][0].get_offsets(), third["rain"][0].get_offsets() + ) + + +def test_raincloud_rc_file_and_context_restore(tmp_path): + original = uplt.rc["raincloud.side"] + path = tmp_path / "raincloud.rc" + path.write_text( + "raincloud.side: low\nraincloud.cloud.alpha: 0.25\nraincloud.seed: None\n" + ) + fig, ax = uplt.subplots() + with uplt.rc.context(file=str(path)): + result = ax.raincloud(np.arange(8.0)) + assert uplt.rc["raincloud.seed"] is None + assert result["cloud"][0].get_alpha() == 0.25 + assert result["cloud"][0].get_paths()[0].vertices[:, 0].max() == pytest.approx( + -0.15 + ) + assert uplt.rc["raincloud.side"] == original From 0dfe0e3fbd2c86dc5fe1d8736f7bab74c20658d0 Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Mon, 19 Oct 2026 20:15:24 +1000 Subject: [PATCH 08/10] Align raincloud rc resolution and annotate all added functions --- tools/cheatsheet/parts/icons.py | 2 +- ultraplot/axes/plot.py | 48 +++++++-------- ultraplot/axes/plot_types/raincloud.py | 83 +++++++++++++++----------- ultraplot/tests/test_raincloud.py | 68 +++++++++++---------- 4 files changed, 110 insertions(+), 91 deletions(-) diff --git a/tools/cheatsheet/parts/icons.py b/tools/cheatsheet/parts/icons.py index c187a74b0..eed24a8fe 100644 --- a/tools/cheatsheet/parts/icons.py +++ b/tools/cheatsheet/parts/icons.py @@ -199,7 +199,7 @@ def icon_ridgeline(ax): ax.ridgeline(data, overlap=0.6, cmap=ICON_SEQUENTIAL, lw=0.7) -def icon_raincloud(ax): +def icon_raincloud(ax: uplt.PlotAxes) -> None: data = np.random.default_rng(42).normal(size=(45, 3)) + [0, 0.7, -0.5] ax.raincloud( data, diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index 0be910162..405d027e0 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -11,7 +11,7 @@ import sys from collections.abc import Callable, Iterable from numbers import Integral, Number -from typing import Any, Iterable, Mapping, Optional, Sequence, TypeAlias, Union +from typing import Any, Literal, Iterable, Mapping, Optional, Sequence, TypeAlias, Union import matplotlib as mpl import matplotlib.artist as martist @@ -6751,30 +6751,30 @@ def violinploth(self, *args, **kwargs): def raincloud( self, - dataset=None, + dataset: Any = None, *, - data=None, - positions=None, - labels=None, - orientation=None, - vert=None, - autoformat=None, - legend=None, - legend_kw=None, - side=None, - jitter=None, - offset=None, - cloud=None, - rain=None, - box=None, - seed=None, - color=None, - cycle=None, - cycle_kw=None, - cloud_kw=None, - rain_kw=None, - box_kw=None, - ): + data: Any = None, + positions: ArrayLike | None = None, + labels: Sequence[Any] | None = None, + orientation: Literal["vertical", "horizontal"] | None = None, + vert: bool | None = None, + autoformat: bool | None = None, + legend: bool | str | int | None = None, + legend_kw: Mapping[str, Any] | None = None, + side: Literal["low", "high"] | None = None, + jitter: float | None = None, + offset: float | None = None, + cloud: bool | None = None, + rain: bool | None = None, + box: bool | None = None, + seed: int | np.random.Generator | None = None, + color: Any = None, + cycle: Any = None, + cycle_kw: Mapping[str, Any] | None = None, + cloud_kw: Mapping[str, Any] | None = None, + rain_kw: Mapping[str, Any] | None = None, + box_kw: Mapping[str, Any] | None = None, + ) -> dict[str, list[Any]]: """ Plot half violins, individual observations, and narrow boxplots. diff --git a/ultraplot/axes/plot_types/raincloud.py b/ultraplot/axes/plot_types/raincloud.py index 33fa89aa5..a64c3320b 100644 --- a/ultraplot/axes/plot_types/raincloud.py +++ b/ultraplot/axes/plot_types/raincloud.py @@ -1,16 +1,25 @@ """Composition of half violins, observations, and boxplots.""" +from __future__ import annotations + +from collections.abc import Mapping, Sequence from numbers import Number +from typing import TYPE_CHECKING, Any, Literal + +from numpy.typing import ArrayLike, NDArray import numpy as np import matplotlib.colors as mcolors from ... import _sharing as psharing, colors as pcolors from ...config import rc -from ...internals import _canonicalize_kwargs, _not_none, _pop_props, inputs, warnings +from ...internals import _canonicalize_kwargs, _pop_props, inputs, warnings + +if TYPE_CHECKING: + from ..plot import PlotAxes -def _resolve_color(color): +def _resolve_color(color: Any) -> Any: """Match the (colormap, index) colour shorthand of other plot methods.""" if ( isinstance(color, tuple) @@ -25,7 +34,9 @@ def _resolve_color(color): return color -def _component_style(options, category): +def _component_style( + options: Mapping[str, Any] | None, category: str +) -> dict[str, Any]: """Resolve artist aliases before applying raincloud's component defaults.""" style = dict(options or {}) style.update(_pop_props(style, category)) @@ -49,7 +60,9 @@ def _component_style(options, category): return style -def _filter_point_style(style, valid): +def _filter_point_style( + style: Mapping[str, Any], valid: NDArray[np.bool_] +) -> dict[str, Any]: """Keep per-observation scatter properties aligned with cleaned values.""" style = dict(style) for key in ( @@ -74,7 +87,7 @@ def _filter_point_style(style, valid): return style -def _group_options(style, index, count): +def _group_options(style: Mapping[str, Any], index: int, count: int) -> dict[str, Any]: """Select native box/violin options that contain one value per group.""" style = dict(style) for key in ("widths", "capwidths"): @@ -88,38 +101,38 @@ def _group_options(style, index, count): def raincloud( - ax, - data, + ax: PlotAxes, + data: Any, *, - positions=None, - labels=None, - orientation=None, - autoformat=None, - legend=None, - legend_kw=None, - side=None, - jitter=None, - offset=None, - cloud=None, - rain=None, - box=None, - seed=None, - color=None, - cycle=None, - cycle_kw=None, - cloud_kw=None, - rain_kw=None, - box_kw=None, -): + positions: ArrayLike | None = None, + labels: Sequence[Any] | None = None, + orientation: Literal["vertical", "horizontal"] | None = None, + autoformat: bool | None = None, + legend: bool | str | int | None = None, + legend_kw: Mapping[str, Any] | None = None, + side: Literal["low", "high"] | None = None, + jitter: float | None = None, + offset: float | None = None, + cloud: bool | None = None, + rain: bool | None = None, + box: bool | None = None, + seed: int | np.random.Generator | None = None, + color: Any = None, + cycle: Any = None, + cycle_kw: Mapping[str, Any] | None = None, + cloud_kw: Mapping[str, Any] | None = None, + rain_kw: Mapping[str, Any] | None = None, + box_kw: Mapping[str, Any] | None = None, +) -> dict[str, list[Any]]: """Draw the components of :meth:`PlotAxes.raincloud`.""" - orientation = _not_none(orientation, rc["raincloud.orientation"]) - side = _not_none(side, rc["raincloud.side"]) - jitter = _not_none(jitter, rc["raincloud.jitter"]) - offset = _not_none(offset, rc["raincloud.offset"]) - cloud = _not_none(cloud, rc["raincloud.cloud"]) - rain = _not_none(rain, rc["raincloud.rain"]) - box = _not_none(box, rc["raincloud.box"]) - seed = _not_none(seed, rc["raincloud.seed"]) + orientation = rc["raincloud.orientation"] if orientation is None else orientation + side = rc["raincloud.side"] if side is None else side + jitter = rc["raincloud.jitter"] if jitter is None else jitter + offset = rc["raincloud.offset"] if offset is None else offset + cloud = rc["raincloud.cloud"] if cloud is None else cloud + rain = rc["raincloud.rain"] if rain is None else rain + box = rc["raincloud.box"] if box is None else box + seed = rc["raincloud.seed"] if seed is None else seed if orientation not in ("vertical", "horizontal"): raise ValueError("orientation must be 'vertical' or 'horizontal'") if side not in ("low", "high"): diff --git a/ultraplot/tests/test_raincloud.py b/ultraplot/tests/test_raincloud.py index 32a49de75..a6c5dc083 100644 --- a/ultraplot/tests/test_raincloud.py +++ b/ultraplot/tests/test_raincloud.py @@ -1,6 +1,10 @@ """Check raincloud geometry, data preservation, and component composition.""" import warnings +from pathlib import Path +from typing import Any + +from numpy.typing import ArrayLike import numpy as np import pandas as pd @@ -12,7 +16,7 @@ @pytest.mark.parametrize("orientation", ["vertical", "horizontal"]) @pytest.mark.parametrize("side,direction", [("low", -1), ("high", 1)]) -def test_raincloud_geometry(orientation, side, direction): +def test_raincloud_geometry(orientation: str, side: str, direction: int) -> None: data = [np.linspace(-2, 2, 11), np.linspace(0, 3, 17)] positions = [2.0, 5.0] fig, ax = uplt.subplots() @@ -48,7 +52,7 @@ def test_raincloud_geometry(orientation, side, direction): fig.canvas.draw() -def test_raincloud_columns_and_cycle(): +def test_raincloud_columns_and_cycle() -> None: data = pd.DataFrame({"Alpha": [1, 2, 3], "Beta": [2, 4, 8]}) data.columns.name = "Condition" fig, ax = uplt.subplots() @@ -65,7 +69,7 @@ def test_raincloud_columns_and_cycle(): ) -def test_raincloud_masks_empty_and_degenerate_groups(): +def test_raincloud_masks_empty_and_degenerate_groups() -> None: data = [ np.ma.array([1.0, 999.0, 2.0, np.inf, np.nan], mask=[0, 1, 0, 0, 0]), [], @@ -83,14 +87,14 @@ def test_raincloud_masks_empty_and_degenerate_groups(): @pytest.mark.parametrize("component", ["cloud", "rain", "box"]) -def test_raincloud_disabled_components(component): +def test_raincloud_disabled_components(component: str) -> None: fig, ax = uplt.subplots() result = ax.raincloud(np.arange(5.0), **{component: False}) assert result[component] == [None] assert all(result[key][0] is not None for key in result if key != component) -def test_raincloud_jitter_reproducible_without_global_rng(): +def test_raincloud_jitter_reproducible_without_global_rng() -> None: data = np.arange(12.0).reshape(6, 2) state = np.random.get_state() fig, axes = uplt.subplots(ncols=2) @@ -104,7 +108,7 @@ def test_raincloud_jitter_reproducible_without_global_rng(): assert state[2:] == after[2:] -def test_raincloud_component_styles_are_not_mutated(): +def test_raincloud_component_styles_are_not_mutated() -> None: cloud_kw = {"alpha": 0.2, "widths": 0.8, "fillcolor": "red"} rain_kw = {"s": 35, "alpha": 0.7} box_kw = {"widths": 0.2, "showfliers": True} @@ -124,7 +128,7 @@ def test_raincloud_component_styles_are_not_mutated(): @pytest.mark.parametrize( "alias", ["s", "ms", "markersize", "markersizes", "size", "sizes"] ) -def test_raincloud_scatter_size_aliases(alias): +def test_raincloud_scatter_size_aliases(alias: str) -> None: fig, ax = uplt.subplots() style = {alias: 35} with warnings.catch_warnings(): @@ -137,7 +141,7 @@ def test_raincloud_scatter_size_aliases(alias): @pytest.mark.parametrize( "alias", ["lw", "linewidth", "linewidths", "mew", "markeredgewidth"] ) -def test_raincloud_scatter_linewidth_aliases(alias): +def test_raincloud_scatter_linewidth_aliases(alias: str) -> None: fig, ax = uplt.subplots() with warnings.catch_warnings(): warnings.simplefilter("error", UserWarning) @@ -148,7 +152,7 @@ def test_raincloud_scatter_linewidth_aliases(alias): @pytest.mark.parametrize( "alias", ["c", "color", "colors", "fc", "facecolor", "facecolors", "mfc"] ) -def test_raincloud_scatter_colour_aliases(alias): +def test_raincloud_scatter_colour_aliases(alias: str) -> None: fig, ax = uplt.subplots() with warnings.catch_warnings(): warnings.simplefilter("error", UserWarning) @@ -158,7 +162,7 @@ def test_raincloud_scatter_colour_aliases(alias): ) -def test_raincloud_scatter_artist_aliases(): +def test_raincloud_scatter_artist_aliases() -> None: fig, ax = uplt.subplots() with warnings.catch_warnings(): warnings.simplefilter("error", UserWarning) @@ -183,7 +187,7 @@ def test_raincloud_scatter_artist_aliases(): }, ], ) -def test_raincloud_patch_style_aliases(component, aliases): +def test_raincloud_patch_style_aliases(component: str, aliases: dict[str, Any]) -> None: fig, ax = uplt.subplots() original = dict(aliases) with warnings.catch_warnings(): @@ -206,7 +210,7 @@ def test_raincloud_patch_style_aliases(component, aliases): @pytest.mark.parametrize("component", ["cloud", "box"]) -def test_raincloud_patch_generic_colour(component): +def test_raincloud_patch_generic_colour(component: str) -> None: fig, ax = uplt.subplots() with warnings.catch_warnings(): warnings.simplefilter("error", UserWarning) @@ -231,7 +235,7 @@ def test_raincloud_patch_generic_colour(component): ({"cloud_kw": {"side": "low"}}, "layout"), ], ) -def test_raincloud_invalid_options(kwargs, match): +def test_raincloud_invalid_options(kwargs: dict[str, Any], match: str) -> None: fig, ax = uplt.subplots() with pytest.raises(ValueError, match=match): ax.raincloud(np.arange(5.0), **kwargs) @@ -239,14 +243,14 @@ def test_raincloud_invalid_options(kwargs, match): @pytest.mark.parametrize("data", [[], [[], []], [np.nan, np.inf]]) -def test_raincloud_no_observations(data): +def test_raincloud_no_observations(data: ArrayLike) -> None: fig, ax = uplt.subplots() with pytest.raises(ValueError, match="No finite observations"): ax.raincloud(data) @pytest.mark.parametrize("keyword", [False, True]) -def test_raincloud_data_lookup_and_keyword_input(keyword): +def test_raincloud_data_lookup_and_keyword_input(keyword: bool) -> None: frame = pd.DataFrame({"A": [1, 2, 3], "B": [2, 4, 5]}) frame.columns.name = "Condition" fig, ax = uplt.subplots() @@ -261,7 +265,7 @@ def test_raincloud_data_lookup_and_keyword_input(keyword): @pytest.mark.parametrize("explicit", [False, True]) -def test_raincloud_respects_autoformat(explicit): +def test_raincloud_respects_autoformat(explicit: bool) -> None: frame = pd.DataFrame({"A": [1, 2, 3], "B": [2, 4, 5]}) frame.columns.name = "Condition" fig, ax = uplt.subplots() @@ -272,7 +276,7 @@ def test_raincloud_respects_autoformat(explicit): assert [tick.get_text() for tick in ax.get_xticklabels()] == (labels or ["0", "1"]) -def test_raincloud_xarray_metadata_and_existing_labels(): +def test_raincloud_xarray_metadata_and_existing_labels() -> None: xr = pytest.importorskip("xarray") samples = xr.DataArray( np.arange(12.0).reshape(6, 2), @@ -295,7 +299,7 @@ def test_raincloud_xarray_metadata_and_existing_labels(): @pytest.mark.parametrize( "disabled", [{}, {"cloud": False}, {"cloud": False, "rain": False}] ) -def test_raincloud_legend_one_entry_per_group(disabled): +def test_raincloud_legend_one_entry_per_group(disabled: dict[str, bool]) -> None: from matplotlib.legend import Legend fig, ax = uplt.subplots() @@ -315,7 +319,7 @@ def test_raincloud_legend_one_entry_per_group(disabled): assert options == {"title": "Condition"} -def test_raincloud_filters_observation_styles(): +def test_raincloud_filters_observation_styles() -> None: values = np.ma.array([1.0, 2.0, np.nan, 3.0, 4.0, 99.0], mask=[0, 0, 0, 0, 0, 1]) sizes = np.arange(10.0, 70.0, 10.0) colours = np.arange(6.0) @@ -338,7 +342,9 @@ def test_raincloud_filters_observation_styles(): ("quantiles", [0.25, 0.75]), ], ) -def test_raincloud_violin_summary_options(option, value): +def test_raincloud_violin_summary_options( + option: str, value: bool | list[float] +) -> None: fig, ax = uplt.subplots() result = ax.raincloud(np.arange(8.0), cloud_kw={option: value}) assert result["cloud"][0] in ax.collections @@ -346,7 +352,7 @@ def test_raincloud_violin_summary_options(option, value): fig.canvas.draw() -def test_raincloud_nested_boxplot_properties(): +def test_raincloud_nested_boxplot_properties() -> None: fig, ax = uplt.subplots() result = ax.raincloud( np.r_[np.arange(8.0), 100], @@ -376,7 +382,7 @@ def test_raincloud_nested_boxplot_properties(): @pytest.mark.parametrize("filled", [False, True]) -def test_raincloud_box_fill_and_patch_artist(filled): +def test_raincloud_box_fill_and_patch_artist(filled: bool) -> None: fig, ax = uplt.subplots() result = ax.raincloud( np.arange(8.0), @@ -394,7 +400,7 @@ def test_raincloud_box_fill_and_patch_artist(filled): assert len(ax.patches) == 1 -def test_raincloud_unfilled_boxes_and_hidden_box_legend(): +def test_raincloud_unfilled_boxes_and_hidden_box_legend() -> None: fig, ax = uplt.subplots() ax.raincloud(np.arange(8.0), box_kw={"filled": False}) assert not ax.patches @@ -405,7 +411,7 @@ def test_raincloud_unfilled_boxes_and_hidden_box_legend(): fig.canvas.draw() -def test_raincloud_colormap_colour_shorthand(): +def test_raincloud_colormap_colour_shorthand() -> None: fig, ax = uplt.subplots() result = ax.raincloud(np.arange(8.0), color=("reds", 0.6)) expected = uplt.Colormap("reds")(0.6) @@ -417,7 +423,7 @@ def test_raincloud_colormap_colour_shorthand(): ) -def test_raincloud_subplot_grid_and_shared_ticks(): +def test_raincloud_subplot_grid_and_shared_ticks() -> None: fig, axes = uplt.subplots(ncols=2) axes.raincloud(np.arange(12.0).reshape(6, 2), labels=["A", "B"]) for ax in axes: @@ -426,7 +432,7 @@ def test_raincloud_subplot_grid_and_shared_ticks(): fig.canvas.draw() -def test_raincloud_per_group_widths_and_quantiles(): +def test_raincloud_per_group_widths_and_quantiles() -> None: fig, ax = uplt.subplots() result = ax.raincloud( np.arange(12.0).reshape(6, 2), @@ -443,7 +449,7 @@ def test_raincloud_per_group_widths_and_quantiles(): fig.canvas.draw() -def test_raincloud_rc_layout_and_appearance(): +def test_raincloud_rc_layout_and_appearance() -> None: fig, ax = uplt.subplots() settings = { "raincloud.orientation": "horizontal", @@ -480,7 +486,7 @@ def test_raincloud_rc_layout_and_appearance(): fig.canvas.draw() -def test_raincloud_explicit_options_override_rc(): +def test_raincloud_explicit_options_override_rc() -> None: fig, ax = uplt.subplots() settings = { "raincloud.orientation": "horizontal", @@ -516,7 +522,7 @@ def test_raincloud_explicit_options_override_rc(): assert len(ax.patches) == 1 -def test_raincloud_rc_visibility_and_explicit_false(): +def test_raincloud_rc_visibility_and_explicit_false() -> None: fig, axes = uplt.subplots(ncols=2) with uplt.rc.context({"raincloud.cloud": False, "raincloud.box": False}): first = axes[0].raincloud(np.arange(8.0)) @@ -527,7 +533,7 @@ def test_raincloud_rc_visibility_and_explicit_false(): assert second["rain"] == second["box"] == [None] -def test_raincloud_rc_seed_and_generator_override(): +def test_raincloud_rc_seed_and_generator_override() -> None: fig, axes = uplt.subplots(ncols=3) with uplt.rc.context({"raincloud.seed": 17}): first = axes[0].raincloud(np.arange(8.0)) @@ -541,7 +547,7 @@ def test_raincloud_rc_seed_and_generator_override(): ) -def test_raincloud_rc_file_and_context_restore(tmp_path): +def test_raincloud_rc_file_and_context_restore(tmp_path: Path) -> None: original = uplt.rc["raincloud.side"] path = tmp_path / "raincloud.rc" path.write_text( From d3986ac4f09672cf5f5654e11a378d35f35412c1 Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Mon, 19 Oct 2026 21:05:48 +1000 Subject: [PATCH 09/10] Restore standard _not_none resolution for raincloud rc defaults --- ultraplot/axes/plot_types/raincloud.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ultraplot/axes/plot_types/raincloud.py b/ultraplot/axes/plot_types/raincloud.py index a64c3320b..015a396ed 100644 --- a/ultraplot/axes/plot_types/raincloud.py +++ b/ultraplot/axes/plot_types/raincloud.py @@ -13,7 +13,7 @@ from ... import _sharing as psharing, colors as pcolors from ...config import rc -from ...internals import _canonicalize_kwargs, _pop_props, inputs, warnings +from ...internals import _canonicalize_kwargs, _not_none, _pop_props, inputs, warnings if TYPE_CHECKING: from ..plot import PlotAxes @@ -125,14 +125,14 @@ def raincloud( box_kw: Mapping[str, Any] | None = None, ) -> dict[str, list[Any]]: """Draw the components of :meth:`PlotAxes.raincloud`.""" - orientation = rc["raincloud.orientation"] if orientation is None else orientation - side = rc["raincloud.side"] if side is None else side - jitter = rc["raincloud.jitter"] if jitter is None else jitter - offset = rc["raincloud.offset"] if offset is None else offset - cloud = rc["raincloud.cloud"] if cloud is None else cloud - rain = rc["raincloud.rain"] if rain is None else rain - box = rc["raincloud.box"] if box is None else box - seed = rc["raincloud.seed"] if seed is None else seed + orientation = _not_none(orientation, rc["raincloud.orientation"]) + side = _not_none(side, rc["raincloud.side"]) + jitter = _not_none(jitter, rc["raincloud.jitter"]) + offset = _not_none(offset, rc["raincloud.offset"]) + cloud = _not_none(cloud, rc["raincloud.cloud"]) + rain = _not_none(rain, rc["raincloud.rain"]) + box = _not_none(box, rc["raincloud.box"]) + seed = _not_none(seed, rc["raincloud.seed"]) if orientation not in ("vertical", "horizontal"): raise ValueError("orientation must be 'vertical' or 'horizontal'") if side not in ("low", "high"): From 8753c55ff5fec5aacd33d63536aad5b670d22db8 Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Mon, 19 Oct 2026 21:11:01 +1000 Subject: [PATCH 10/10] Refactor raincloud into typed preparation and drawing helpers --- ultraplot/axes/plot_types/raincloud.py | 455 +++++++++++++++++-------- 1 file changed, 320 insertions(+), 135 deletions(-) diff --git a/ultraplot/axes/plot_types/raincloud.py b/ultraplot/axes/plot_types/raincloud.py index 015a396ed..3de04e9d3 100644 --- a/ultraplot/axes/plot_types/raincloud.py +++ b/ultraplot/axes/plot_types/raincloud.py @@ -2,13 +2,16 @@ from __future__ import annotations -from collections.abc import Mapping, Sequence +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import dataclass from numbers import Number from typing import TYPE_CHECKING, Any, Literal from numpy.typing import ArrayLike, NDArray import numpy as np +import matplotlib.artist as martist +import matplotlib.collections as mcollections import matplotlib.colors as mcolors from ... import _sharing as psharing, colors as pcolors @@ -100,31 +103,44 @@ def _group_options(style: Mapping[str, Any], index: int, count: int) -> dict[str return style -def raincloud( - ax: PlotAxes, - data: Any, - *, - positions: ArrayLike | None = None, - labels: Sequence[Any] | None = None, - orientation: Literal["vertical", "horizontal"] | None = None, - autoformat: bool | None = None, - legend: bool | str | int | None = None, - legend_kw: Mapping[str, Any] | None = None, - side: Literal["low", "high"] | None = None, - jitter: float | None = None, - offset: float | None = None, - cloud: bool | None = None, - rain: bool | None = None, - box: bool | None = None, - seed: int | np.random.Generator | None = None, - color: Any = None, - cycle: Any = None, - cycle_kw: Mapping[str, Any] | None = None, - cloud_kw: Mapping[str, Any] | None = None, - rain_kw: Mapping[str, Any] | None = None, - box_kw: Mapping[str, Any] | None = None, -) -> dict[str, list[Any]]: - """Draw the components of :meth:`PlotAxes.raincloud`.""" +@dataclass(frozen=True) +class _RaincloudOptions: + """Validated layout and component controls.""" + + vert: bool + side: Literal["low", "high"] + direction: int + jitter: float + offset: float + cloud: bool + rain: bool + box: bool + seed: int | np.random.Generator | None + + +@dataclass(frozen=True) +class _RaincloudData: + """Cleaned observations, original masks, and group metadata.""" + + values: list[NDArray[np.float64]] + masks: list[NDArray[np.bool_]] + positions: NDArray[np.float64] + labels: Sequence[Any] | None + group_title: str | None + value_title: str | None + + +def _resolve_options( + orientation: Literal["vertical", "horizontal"] | None, + side: Literal["low", "high"] | None, + jitter: float | None, + offset: float | None, + cloud: bool | None, + rain: bool | None, + box: bool | None, + seed: int | np.random.Generator | None, +) -> _RaincloudOptions: + """Resolve rc defaults and validate options before creating artists.""" orientation = _not_none(orientation, rc["raincloud.orientation"]) side = _not_none(side, rc["raincloud.side"]) jitter = _not_none(jitter, rc["raincloud.jitter"]) @@ -142,10 +158,24 @@ def raincloud( raise ValueError(f"{name} must be a finite non-negative number") if not any((cloud, rain, box)): raise ValueError("At least one of cloud, rain, or box must be enabled") + return _RaincloudOptions( + vert=orientation == "vertical", + side=side, + direction=1 if side == "high" else -1, + jitter=jitter, + offset=offset, + cloud=cloud, + rain=rain, + box=box, + seed=seed, + ) - # Arrays and labelled tables use columns; sequences of vectors permit - # unequal sample sizes, following Matplotlib's distribution convention. - autoformat = rc["autoformat"] if autoformat is None else autoformat + +def _infer_metadata( + data: Any, labels: Sequence[Any] | None, autoformat: bool | None +) -> tuple[Any, Sequence[Any] | None, str | None, str | None]: + """Extract labelled-array metadata without overriding explicit labels.""" + autoformat = _not_none(autoformat, rc["autoformat"]) group_title = value_title = None if not isinstance(data, (list, tuple)): metadata = inputs._to_duck_array(data) @@ -158,27 +188,40 @@ def raincloud( if labels is None: labels = inputs._to_numpy_array(inferred).tolist() data = inputs._to_numpy_array(metadata, strip_units=True) + return data, labels, group_title, value_title + + +def _split_distributions(data: Any) -> Iterable[ArrayLike]: + """Use array columns or sequence elements as individual distributions.""" if isinstance(data, (list, tuple)): - distributions = data if data and np.ndim(data[0]) else [data] - else: - values = np.ma.asarray(data) - if values.ndim == 1: - distributions = ( - values - if values.dtype == object and any(np.ndim(value) for value in values) - else [values] - ) - elif values.ndim == 2: - distributions = values.T - else: - raise ValueError("data must be one- or two-dimensional") - cleaned, masks = [], [] - for values in distributions: + return data if data and np.ndim(data[0]) else [data] + values = np.ma.asarray(data) + if values.ndim == 1: + return ( + values + if values.dtype == object and any(np.ndim(value) for value in values) + else [values] + ) + if values.ndim == 2: + return values.T + raise ValueError("data must be one- or two-dimensional") + + +def _prepare_data( + data: Any, + positions: ArrayLike | None, + labels: Sequence[Any] | None, + autoformat: bool | None, +) -> _RaincloudData: + """Clean observations and validate group labels and positions.""" + data, labels, group_title, value_title = _infer_metadata(data, labels, autoformat) + cleaned: list[NDArray[np.float64]] = [] + masks: list[NDArray[np.bool_]] = [] + for values in _split_distributions(data): if np.ndim(values) != 1: raise ValueError("Each distribution must be one-dimensional") values = np.ma.masked_invalid(np.ma.asarray(values, dtype=float)) - valid = ~np.ma.getmaskarray(values) - masks.append(valid) + masks.append(~np.ma.getmaskarray(values)) cleaned.append(values.compressed()) n = len(cleaned) if not n or not any(values.size for values in cleaned): @@ -194,13 +237,20 @@ def raincloud( raise ValueError("positions must contain one finite value per distribution") if np.unique(positions).size != n: raise ValueError("positions must be distinct") + return _RaincloudData(cleaned, masks, positions, labels, group_title, value_title) - # Layout belongs to this function; component dictionaries style the artists. - styles = [ - _component_style(cloud_kw, "patch"), - _component_style(rain_kw, "collection"), - _canonicalize_kwargs("plot.boxplot", _component_style(box_kw, "patch")), - ] + +def _prepare_styles( + cloud_kw: Mapping[str, Any] | None, + rain_kw: Mapping[str, Any] | None, + box_kw: Mapping[str, Any] | None, +) -> dict[str, dict[str, Any]]: + """Normalize component aliases and reject options owned by the layout.""" + styles = { + "cloud": _component_style(cloud_kw, "patch"), + "rain": _component_style(rain_kw, "collection"), + "box": _canonicalize_kwargs("plot.boxplot", _component_style(box_kw, "patch")), + } reserved = { "positions", "orientation", @@ -213,119 +263,254 @@ def raincloud( "cycle", "cycle_kw", } - for name, style in zip(("cloud_kw", "rain_kw", "box_kw"), styles): + for name, style in styles.items(): conflicts = reserved.intersection(style) if conflicts: raise ValueError( - f"{name} cannot override layout options: {sorted(conflicts)}" + f"{name}_kw cannot override layout options: {sorted(conflicts)}" ) + return styles + - ax._parse_cycle(n, cycle=cycle, cycle_kw=cycle_kw) +def _group_colors( + ax: PlotAxes, + count: int, + color: Any, + cycle: Any, + cycle_kw: Mapping[str, Any] | None, +) -> list[Any]: + """Assign one cycle colour to all components of each group.""" + ax._parse_cycle(count, cycle=cycle, cycle_kw=cycle_kw) color = _resolve_color(color) - colors = [ + return [ color if color is not None else ax._get_patches_for_fill.get_next_color() - for _ in range(n) + for _ in range(count) + ] + + +def _draw_cloud( + ax: PlotAxes, + values: NDArray[np.float64], + position: float, + color: Any, + options: _RaincloudOptions, + style: Mapping[str, Any], +) -> tuple[mcollections.PolyCollection, list[martist.Artist]]: + """Draw one half violin and collect any optional summary artists.""" + kw = {"widths": rc["raincloud.cloud.width"], **style} + props = _pop_props(kw, "patch") + props.setdefault("alpha", rc["raincloud.cloud.alpha"]) + before = set(ax.get_children()) + bodies = ax.violinplot( + [position + options.direction * options.offset], + values[:, None], + vert=options.vert, + side=options.side, + fillcolor=color, + **kw, + ) + if isinstance(bodies, tuple): + bodies = bodies[0] + body = bodies[0] + body.update(props) + extras = [ + artist + for artist in ax.get_children() + if artist not in before and artist is not body ] - rng = np.random.default_rng(seed) - vert = orientation == "vertical" - direction = 1 if side == "high" else -1 - artists = {key: [None] * n for key in ("cloud", "rain", "box")} - handles = [] - - for i, (position, values, group_color) in enumerate( - zip(positions, cleaned, colors) + return body, extras + + +def _draw_rain( + ax: PlotAxes, + values: NDArray[np.float64], + position: float, + color: Any, + options: _RaincloudOptions, + style: Mapping[str, Any], + rng: np.random.Generator, +) -> mcollections.PathCollection: + """Draw one group's observations using the shared jitter generator.""" + locations = ( + position + - options.direction * options.offset + + rng.uniform(-options.jitter, options.jitter, values.size) + ) + kw = { + "alpha": rc["raincloud.rain.alpha"], + "linewidths": rc["raincloud.rain.linewidth"], + **style, + } + if not any(key in kw for key in ("sizes", "size")): + kw["sizes"] = rc["raincloud.rain.markersize"] + if not any(key in kw for key in ("colors", "facecolors")): + kw["colors"] = color + coords = (locations, values) if options.vert else (values, locations) + return ax.scatter(*coords, **kw) + + +def _draw_box( + ax: PlotAxes, + values: NDArray[np.float64], + position: float, + color: Any, + options: _RaincloudOptions, + style: Mapping[str, Any], +) -> dict[str, list[martist.Artist]]: + """Draw one distribution's boxplot at its category centre.""" + kw = { + "facecolor": color, + "widths": rc["raincloud.box.width"], + "fill": rc["raincloud.box.fill"], + "showfliers": rc["raincloud.box.showfliers"], + "zorder": rc["raincloud.box.zorder"], + **style, + } + return ax.boxplot([position], values, vert=options.vert, manage_ticks=False, **kw) + + +def _legend_handle(artists: Mapping[str, list[Any]], index: int) -> martist.Artist: + """Choose the first visible component as a group's legend representative.""" + for component in ("cloud", "rain"): + if artists[component][index] is not None: + return artists[component][index] + group = artists["box"][index] + return (group.get("patches") or group["boxes"] or group["medians"])[0] + + +def _draw_groups( + ax: PlotAxes, + samples: _RaincloudData, + options: _RaincloudOptions, + styles: Mapping[str, Mapping[str, Any]], + colors: Sequence[Any], + legend: bool | str | int | None, +) -> tuple[dict[str, list[Any]], list[martist.Artist]]: + """Compose each group in input order and preserve slots for empty groups.""" + n = len(samples.values) + artists: dict[str, list[Any]] = { + key: [None] * n for key in ("cloud", "rain", "box") + } + handles: list[martist.Artist] = [] + rng = np.random.default_rng(options.seed) + for i, (position, values, color) in enumerate( + zip(samples.positions, samples.values, colors) ): if not values.size: warnings._warn_ultraplot( f"Distribution {i} has no finite observations, skipping" ) continue - if cloud: - kw = { - "widths": rc["raincloud.cloud.width"], - **_group_options(styles[0], i, n), - } - props = _pop_props(kw, "patch") - props.setdefault("alpha", rc["raincloud.cloud.alpha"]) - before = set(ax.get_children()) - bodies = ax.violinplot( - [position + direction * offset], - values[:, None], - vert=vert, - side=side, - fillcolor=group_color, - **kw, + if options.cloud: + body, extras = _draw_cloud( + ax, + values, + position, + color, + options, + _group_options(styles["cloud"], i, n), ) - if isinstance(bodies, tuple): - bodies = bodies[0] - artists["cloud"][i] = bodies[0] - bodies[0].update(props) - extras = [ - artist - for artist in ax.get_children() - if artist not in before and artist is not bodies[0] - ] + artists["cloud"][i] = body if extras: artists.setdefault("cloud_extras", [None] * n)[i] = extras - if rain: - locations = ( - position - - direction * offset - + rng.uniform(-jitter, jitter, values.size) + if options.rain: + artists["rain"][i] = _draw_rain( + ax, + values, + position, + color, + options, + _filter_point_style(styles["rain"], samples.masks[i]), + rng, ) - kw = { - "alpha": rc["raincloud.rain.alpha"], - "linewidths": rc["raincloud.rain.linewidth"], - **_filter_point_style(styles[1], masks[i]), - } - if not any(key in kw for key in ("sizes", "size")): - kw["sizes"] = rc["raincloud.rain.markersize"] - if not any(key in kw for key in ("colors", "facecolors")): - kw["colors"] = group_color - coords = (locations, values) if vert else (values, locations) - artists["rain"][i] = ax.scatter(*coords, **kw) - if box: - kw = { - "facecolor": group_color, - "widths": rc["raincloud.box.width"], - "fill": rc["raincloud.box.fill"], - "showfliers": rc["raincloud.box.showfliers"], - "zorder": rc["raincloud.box.zorder"], - **_group_options(styles[2], i, n), - } - artists["box"][i] = ax.boxplot( - [position], + if options.box: + artists["box"][i] = _draw_box( + ax, values, - vert=vert, - manage_ticks=False, - **kw, + position, + color, + options, + _group_options(styles["box"], i, n), + ) + handle = _legend_handle(artists, i) + if samples.labels is not None or legend: + handle.set_label( + str(samples.labels[i] if samples.labels is not None else position) ) - if cloud: - handle = artists["cloud"][i] - elif rain: - handle = artists["rain"][i] - else: - group = artists["box"][i] - handle = (group.get("patches") or group["boxes"] or group["medians"])[0] - if labels is not None or legend: - handle.set_label(str(labels[i] if labels is not None else position)) handles.append(handle) + return artists, handles + +def _format_axes(ax: PlotAxes, samples: _RaincloudData, vert: bool) -> None: + """Set categorical ticks and inferred titles while preserving shared axes.""" axis = ax.xaxis if vert else ax.yaxis - axis.set_ticks(positions) - if labels is not None: - axis.set_ticklabels(labels) + axis.set_ticks(samples.positions) + if samples.labels is not None: + axis.set_ticklabels(samples.labels) value_axis = ax.yaxis if vert else ax.xaxis formatting = {} - if group_title and axis.isDefault_label: - formatting["xlabel" if vert else "ylabel"] = group_title - if value_title and value_axis.isDefault_label: - formatting["ylabel" if vert else "xlabel"] = value_title + if samples.group_title and axis.isDefault_label: + formatting["xlabel" if vert else "ylabel"] = samples.group_title + if samples.value_title and value_axis.isDefault_label: + formatting["ylabel" if vert else "xlabel"] = samples.value_title if formatting: with psharing.preserve_axis_sharing(): ax.format(**formatting) + + +def _update_legend( + ax: PlotAxes, + handles: Sequence[martist.Artist], + group_title: str | None, + legend: bool | str | int | None, + legend_kw: Mapping[str, Any] | None, +) -> None: + """Queue one legend entry per nonempty group with its inferred title.""" legend_kw = dict(legend_kw or {}) if group_title: legend_kw.setdefault("title", group_title) ax._update_guide(handles, legend=legend, legend_kw=legend_kw) + + +def raincloud( + ax: PlotAxes, + data: Any, + *, + positions: ArrayLike | None = None, + labels: Sequence[Any] | None = None, + orientation: Literal["vertical", "horizontal"] | None = None, + autoformat: bool | None = None, + legend: bool | str | int | None = None, + legend_kw: Mapping[str, Any] | None = None, + side: Literal["low", "high"] | None = None, + jitter: float | None = None, + offset: float | None = None, + cloud: bool | None = None, + rain: bool | None = None, + box: bool | None = None, + seed: int | np.random.Generator | None = None, + color: Any = None, + cycle: Any = None, + cycle_kw: Mapping[str, Any] | None = None, + cloud_kw: Mapping[str, Any] | None = None, + rain_kw: Mapping[str, Any] | None = None, + box_kw: Mapping[str, Any] | None = None, +) -> dict[str, list[Any]]: + """Draw the components of :meth:`PlotAxes.raincloud`.""" + options = _resolve_options( + orientation=orientation, + side=side, + jitter=jitter, + offset=offset, + cloud=cloud, + rain=rain, + box=box, + seed=seed, + ) + samples = _prepare_data(data, positions, labels, autoformat) + styles = _prepare_styles(cloud_kw, rain_kw, box_kw) + colors = _group_colors(ax, len(samples.values), color, cycle, cycle_kw) + artists, handles = _draw_groups(ax, samples, options, styles, colors, legend) + _format_axes(ax, samples, options.vert) + _update_legend(ax, handles, samples.group_title, legend, legend_kw) return artists