plot-io 0.12.0__py3-none-win_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. plot_io/__init__.py +73 -0
  2. plot_io/_methods.py +1691 -0
  3. plot_io/_native/__init__.py +199 -0
  4. plot_io/_native/__main__.py +197 -0
  5. plot_io/_native/_api.py +457 -0
  6. plot_io/_native/bin/windows-arm64.dll +0 -0
  7. plot_io/_native/kernels.py +335 -0
  8. plot_io/_native/manifest.json +49 -0
  9. plot_io/axis.py +1347 -0
  10. plot_io/boxes.py +1156 -0
  11. plot_io/canvas.py +389 -0
  12. plot_io/color/__init__.py +3 -0
  13. plot_io/color/data.py +1646 -0
  14. plot_io/color/maps.py +922 -0
  15. plot_io/color/values.py +295 -0
  16. plot_io/color/variants.py +1246 -0
  17. plot_io/colorbar.py +1426 -0
  18. plot_io/columns.py +614 -0
  19. plot_io/decorations.py +920 -0
  20. plot_io/defaults.py +809 -0
  21. plot_io/density.py +113 -0
  22. plot_io/ext.py +68 -0
  23. plot_io/facets.py +240 -0
  24. plot_io/figure.py +788 -0
  25. plot_io/formatters.py +97 -0
  26. plot_io/genome/__init__.py +17 -0
  27. plot_io/genome/adapters.py +356 -0
  28. plot_io/genome/combine.py +123 -0
  29. plot_io/genome/contacts.py +508 -0
  30. plot_io/genome/intervals.py +750 -0
  31. plot_io/genome/lanes.py +191 -0
  32. plot_io/genome/layout.py +184 -0
  33. plot_io/genome/links.py +364 -0
  34. plot_io/genome/pack.py +27 -0
  35. plot_io/genome/readers.py +145 -0
  36. plot_io/genome/regions.py +228 -0
  37. plot_io/genome/scale.py +349 -0
  38. plot_io/genome/series.py +344 -0
  39. plot_io/genome/signal.py +621 -0
  40. plot_io/genome/sites.py +782 -0
  41. plot_io/genome/sources.py +459 -0
  42. plot_io/genome/testing.py +161 -0
  43. plot_io/geometry.py +289 -0
  44. plot_io/groups.py +308 -0
  45. plot_io/guides.py +503 -0
  46. plot_io/helpers/__init__.py +17 -0
  47. plot_io/helpers/clustermap.py +202 -0
  48. plot_io/helpers/dendrogram.py +172 -0
  49. plot_io/helpers/grids.py +304 -0
  50. plot_io/helpers/qq.py +334 -0
  51. plot_io/helpers/raincloud.py +249 -0
  52. plot_io/helpers/tree.py +214 -0
  53. plot_io/helpers/volcano.py +505 -0
  54. plot_io/hints.py +125 -0
  55. plot_io/kernels.py +740 -0
  56. plot_io/labels.py +566 -0
  57. plot_io/layers/__init__.py +100 -0
  58. plot_io/layers/base.py +630 -0
  59. plot_io/layers/context.py +87 -0
  60. plot_io/layers/core.py +446 -0
  61. plot_io/layers/geoms.py +1362 -0
  62. plot_io/layers/grammar.py +586 -0
  63. plot_io/layers/paint.py +614 -0
  64. plot_io/layers/pipeline.py +170 -0
  65. plot_io/layers/positions.py +431 -0
  66. plot_io/layers/stats.py +1088 -0
  67. plot_io/layout.py +1381 -0
  68. plot_io/legend.py +605 -0
  69. plot_io/levels.py +509 -0
  70. plot_io/limits.py +124 -0
  71. plot_io/lines.py +42 -0
  72. plot_io/mapped.py +522 -0
  73. plot_io/markers.py +110 -0
  74. plot_io/markup.py +128 -0
  75. plot_io/numbers.py +354 -0
  76. plot_io/occupancy.py +295 -0
  77. plot_io/plot.py +597 -0
  78. plot_io/presets/__init__.py +51 -0
  79. plot_io/presets/_util.py +153 -0
  80. plot_io/presets/area.py +145 -0
  81. plot_io/presets/bar.py +261 -0
  82. plot_io/presets/box.py +400 -0
  83. plot_io/presets/catpoints.py +177 -0
  84. plot_io/presets/contour.py +353 -0
  85. plot_io/presets/curves.py +30 -0
  86. plot_io/presets/dots.py +355 -0
  87. plot_io/presets/ecdf.py +97 -0
  88. plot_io/presets/errorbar.py +106 -0
  89. plot_io/presets/errors.py +296 -0
  90. plot_io/presets/fill.py +74 -0
  91. plot_io/presets/heatmap.py +322 -0
  92. plot_io/presets/hexbin.py +198 -0
  93. plot_io/presets/hist.py +391 -0
  94. plot_io/presets/hist2d.py +87 -0
  95. plot_io/presets/kde.py +162 -0
  96. plot_io/presets/kde2d.py +147 -0
  97. plot_io/presets/line.py +143 -0
  98. plot_io/presets/pie.py +502 -0
  99. plot_io/presets/regression.py +115 -0
  100. plot_io/presets/scatter.py +194 -0
  101. plot_io/presets/stem.py +100 -0
  102. plot_io/presets/violin.py +323 -0
  103. plot_io/py.typed +0 -0
  104. plot_io/registry.py +82 -0
  105. plot_io/render.py +480 -0
  106. plot_io/rows.py +491 -0
  107. plot_io/scales.py +103 -0
  108. plot_io/seams.py +138 -0
  109. plot_io/settings.py +807 -0
  110. plot_io/shapes.py +66 -0
  111. plot_io/sharing.py +232 -0
  112. plot_io/sizemaps.py +97 -0
  113. plot_io/spec.py +240 -0
  114. plot_io/states.py +216 -0
  115. plot_io/styles/__init__.py +307 -0
  116. plot_io/styles/library.py +397 -0
  117. plot_io/tables.py +138 -0
  118. plot_io/text.py +412 -0
  119. plot_io/ticks.py +400 -0
  120. plot_io/times.py +287 -0
  121. plot_io/titles.py +225 -0
  122. plot_io/transform.py +114 -0
  123. plot_io/types.py +13604 -0
  124. plot_io/units.py +768 -0
  125. plot_io/validators.py +1029 -0
  126. plot_io/values.py +190 -0
  127. plot_io-0.12.0.dist-info/METADATA +215 -0
  128. plot_io-0.12.0.dist-info/RECORD +130 -0
  129. plot_io-0.12.0.dist-info/WHEEL +4 -0
  130. plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
plot_io/helpers/qq.py ADDED
@@ -0,0 +1,334 @@
1
+ """Quantile–quantile plots (stat ``qq``): a sorted sample against theoretical quantiles,
2
+ or against another sample, with a reference line and a pointwise band of order
3
+ statistics."""
4
+
5
+ import array
6
+ import bisect
7
+ import math
8
+ from typing import Any
9
+
10
+ from .. import Scatter, ext, kernels
11
+ from .._methods import QQMixin
12
+
13
+ __all__ = ["QQ"]
14
+
15
+ #: Quantile functions of the named distributions.
16
+ DISTS: dict[str, Any] = {
17
+ "normal": ext.norm_quantile,
18
+ "uniform": lambda p: p,
19
+ "exponential": lambda p: -math.log1p(-p),
20
+ }
21
+ #: Ranks of the band at most.
22
+ BAND_RANKS = 512
23
+
24
+
25
+ def _dist(v: Any) -> Any:
26
+ if callable(v):
27
+ return v
28
+ return ext.one_of(*DISTS)(v)
29
+
30
+
31
+ def _line(v: Any) -> Any:
32
+ if v is None or v is False:
33
+ return None
34
+ return ext.one_of("auto", "quartiles", "fit", "identity")(v)
35
+
36
+
37
+ def _band(v: Any) -> Any:
38
+ if v is None or v is False or v == "auto":
39
+ return None if v is False else v
40
+ return ext.number(0, 1)(v)
41
+
42
+
43
+ class QQ(QQMixin, Scatter):
44
+ """The sorted ``values`` against the quantiles of ``dist`` at R's ``ppoints``
45
+ (``(i − a) / (n + 1 − 2a)``, a = 3/8 for n ≤ 10, else 1/2), or against the sorted ``y``
46
+ (two samples: the longer interpolated to the shorter's size, as R's ``qqplot``;
47
+ ``values`` on x). ``pvalues=True``: ``values`` are p-values, drawn as −log10 of the
48
+ sorted p-values (y) against −log10 of the expected uniform quantiles (x). Points as
49
+ `Scatter`'s; the reference ``line`` and the pointwise ``band`` of order statistics in
50
+ the series color. The points and the default line are R's (``qqnorm``, ``qqline``), not
51
+ scipy's ``probplot`` (Filliben's medians, a least-squares line).
52
+
53
+ ``dist``: ``normal`` (Wichura's AS241, full double precision), ``uniform``,
54
+ ``exponential``, or a function p → quantile. With ``pvalues=True``, values outside
55
+ (0, 1] are left out.
56
+
57
+ - ``line``: ``quartiles`` (through the first and third quartile points, R's ``qqline``),
58
+ ``fit`` (least squares), ``identity``, or ``None``; ``"auto"``: ``identity`` with
59
+ ``pvalues``, else ``quartiles``.
60
+ - ``band``: the level of the pointwise band of order statistics (beta quantiles, mapped
61
+ through the line), on at most 512 ranks spread evenly along the sample axis;
62
+ ``"auto"``: 0.95 with ``pvalues``, else none.
63
+ - Markers have no edge by default. Above 10,000 points with one marker style, points
64
+ within 1e-4 of the data range of the last one drawn are not drawn (``stats`` keeps
65
+ them all).
66
+
67
+ ``qq.stats``: ``x``, ``y``, ``line`` (slope, intercept), ``band_x``, ``low``, ``high``.
68
+ Uses::
69
+
70
+ p.qq(residuals) # against the normal, quartile line
71
+ p.qq(sample, other_sample) # two samples
72
+ p.qq(pvalues, pvalues=True) # genomics QQ: −log10, identity, 95 % band
73
+ p.qq(x, dist=scipy.stats.t(5).ppf, band=0.9) # any quantile function
74
+
75
+ Example::
76
+
77
+ rng = random.Random(1)
78
+ p = plt.Plot(width=240, height=160)
79
+ p.qq([rng.gauss(0, 1) for _ in range(150)], band=0.95)
80
+ """
81
+
82
+ SETTINGS = ext.table(
83
+ **{k: v for k, v in Scatter.SETTINGS.items()
84
+ if k not in ("x_error", "y_error", "orientation") and not k.startswith("label")},
85
+ dist=ext.Setting(_dist, doc="`normal`, `uniform`, `exponential`, or a function "
86
+ "p → quantile (`scipy.stats.t(5).ppf`)."),
87
+ pvalues=ext.Setting(ext.boolean, doc="`values` are p-values: −log10 of both, the "
88
+ "identity line and a 95 % band by default."),
89
+ line=ext.Setting(_line, doc="`quartiles` (through the first and third quartiles, "
90
+ "R's `qqline`), `fit` (least squares), `identity`, "
91
+ "`None`; `auto`: `identity` with `pvalues`, else "
92
+ "`quartiles`."),
93
+ band=ext.Setting(_band, doc="Level of the pointwise band of order statistics (beta "
94
+ "quantiles, on the line), `None`; `auto`: 0.95 with "
95
+ "`pvalues`, else none."),
96
+ band_opacity=ext.Setting(ext.number(0, 1), doc="Opacity of the band."),
97
+ **ext.common("line_width"),
98
+ )
99
+
100
+ DATA = {"values": "numbers", "y": "raw"}
101
+
102
+ def __init__(self, values: Any, y: Any = None, plot: Any = None,
103
+ **settings: Any) -> None:
104
+ ext.Series.__init__(self, plot=plot, **settings)
105
+
106
+ @property
107
+ def stats(self) -> dict[str, Any]:
108
+ """``x``, ``y`` (the points), ``line`` (``(slope, intercept)`` or ``None``), and the
109
+ band: ``band_x``, ``low``, ``high`` (``None`` without a band)."""
110
+ return ext.compute(self, "qq")
111
+
112
+ def density_points(self) -> tuple[Any, Any]:
113
+ st = self.stats
114
+ return st["x"], st["y"]
115
+
116
+
117
+ def _sorted(values: Any, positive: bool = False) -> array.array:
118
+ """The finite values sorted (``positive``: those in (0, 1])."""
119
+ v = kernels.doubles(values)
120
+ s = kernels.take(v, kernels.argsort(v)) if len(v) else array.array("d")
121
+ i = 0
122
+ while i < len(s) and math.isnan(s[i]):
123
+ i += 1
124
+ lo = bisect.bisect_right(s, 0.0 if positive else -math.inf, i)
125
+ hi = bisect.bisect_right(s, 1.0, lo) if positive else bisect.bisect_left(s, math.inf, lo)
126
+ return s[lo:hi]
127
+
128
+
129
+ def _ppoints(n: int) -> tuple[float, float]:
130
+ """``a`` and the denominator of R's ``ppoints(n)``."""
131
+ a = 3 / 8 if n <= 10 else 0.5
132
+ return a, n + 1 - 2 * a
133
+
134
+
135
+ def _interpolated(s: array.array, n: int) -> array.array:
136
+ """Sorted ``s`` at ``n`` positions spread evenly over its ranks (``np.interp`` at
137
+ ``linspace(1, len, n)``)."""
138
+ m = len(s)
139
+ if n == 1 or m == 1:
140
+ return array.array("d", [s[0]] * n)
141
+ step = (m - 1) / (n - 1)
142
+ out = array.array("d")
143
+ for k in range(n):
144
+ t = 1 + k * step if k < n - 1 else float(m)
145
+ j = min(int(t) - 1, m - 2)
146
+ out.append((s[j + 1] - s[j]) * (t - (j + 1)) + s[j])
147
+ return out
148
+
149
+
150
+ def _quartiles(xs: Any, ys: Any) -> tuple[float, float] | None:
151
+ (x1, x3), (y1, y3) = xs, ys
152
+ if not x3 - x1:
153
+ return None
154
+ slope = (y3 - y1) / (x3 - x1)
155
+ return slope, y1 - slope * x1
156
+
157
+
158
+ def _fit(x: Any, y: Any) -> tuple[float, float] | None:
159
+ n = len(x)
160
+ if n < 2:
161
+ return None
162
+ mx, my = math.fsum(x) / n, math.fsum(y) / n
163
+ sxx = math.fsum((a - mx) * (a - mx) for a in x)
164
+ if not sxx:
165
+ return None
166
+ slope = math.fsum((a - mx) * (b - my) for a, b in zip(x, y)) / sxx
167
+ return slope, my - slope * mx
168
+
169
+
170
+ def _band_ranks(n: int, along: Any) -> list[int]:
171
+ """At most :data:`BAND_RANKS` ranks (1-based), spread evenly along ``along`` (sorted
172
+ positions of the ranks, ascending or descending)."""
173
+ if n <= BAND_RANKS:
174
+ return list(range(1, n + 1))
175
+ sign = -1.0 if along[0] > along[-1] else 1.0
176
+ lo, hi = sign * along[0], sign * along[-1]
177
+ out: list[int] = []
178
+ for k in range(BAND_RANKS):
179
+ t = lo + (hi - lo) * k / (BAND_RANKS - 1)
180
+ i = min(bisect.bisect_left(along, t, key=lambda v: sign * v), n - 1) + 1
181
+ if not out or i > out[-1]:
182
+ out.append(i)
183
+ return out
184
+
185
+
186
+ @ext.stat("qq", reads=("dist", "line", "band", "pvalues"))
187
+ def _qq(cols: Any, p: Any, scales: Any = ()) -> dict[str, Any]:
188
+ pv = bool(p.get("pvalues"))
189
+ line, level = p.get("line", "auto"), p.get("band", "auto")
190
+ if line == "auto":
191
+ line = "identity" if pv else "quartiles"
192
+ if level == "auto":
193
+ level = 0.95 if pv else None
194
+ sample = _sorted(cols["values"], pv)
195
+ other = cols["y"]
196
+ n = len(sample)
197
+ out: dict[str, Any] = {"x": array.array("d"), "y": array.array("d"), "line": None,
198
+ "band_x": None, "low": None, "high": None}
199
+ if not n:
200
+ return out
201
+ if other is not None:
202
+ ys = _sorted(other)
203
+ if not len(ys):
204
+ return out
205
+ xs = _interpolated(sample, len(ys)) if n > len(ys) else sample
206
+ ys = _interpolated(ys, n) if len(ys) > n else ys
207
+ out["x"], out["y"] = xs, ys
208
+ if line == "quartiles":
209
+ out["line"] = _quartiles(kernels.quantiles(sample, (0.25, 0.75)),
210
+ kernels.quantiles(_sorted(other), (0.25, 0.75)))
211
+ elif line == "fit":
212
+ out["line"] = _fit(xs, ys)
213
+ elif line == "identity":
214
+ out["line"] = (1.0, 0.0)
215
+ return out
216
+ a, den = _ppoints(n)
217
+ if pv:
218
+ q = kernels.scaled(array.array("d", range(1, n + 1)), 1 / den, -a / den)
219
+ out["x"] = kernels.scaled(q, -1.0, 0.0, "log10")
220
+ out["y"] = kernels.scaled(sample, -1.0, 0.0, "log10")
221
+ f = None
222
+ else:
223
+ f = p.get("dist", "normal")
224
+ f = DISTS[f] if isinstance(f, str) else f
225
+ out["x"] = array.array("d", (float(f((i - a) / den)) for i in range(1, n + 1)))
226
+ out["y"] = sample
227
+ x, y = out["x"], out["y"]
228
+ if line == "quartiles":
229
+ if f is None:
230
+ qx = [-math.log10(0.75), -math.log10(0.25)]
231
+ else:
232
+ qx = [float(f(0.25)), float(f(0.75))]
233
+ out["line"] = _quartiles(qx, kernels.quantiles(y, (0.25, 0.75)))
234
+ elif line == "fit":
235
+ out["line"] = _fit(x, y)
236
+ elif line == "identity":
237
+ out["line"] = (1.0, 0.0)
238
+ if level is None:
239
+ return out
240
+ slope, icpt = out["line"] or _quartiles(
241
+ [float(f(0.25)), float(f(0.75))] if f is not None else [0.0, 1.0],
242
+ kernels.quantiles(y, (0.25, 0.75))) or (1.0, 0.0)
243
+ ranks = _band_ranks(n, x if pv else y)
244
+ lows, highs, bx = array.array("d"), array.array("d"), array.array("d")
245
+ for i in ranks:
246
+ lo = ext.beta_quantile((1 - level) / 2, i, n + 1 - i)
247
+ hi = ext.beta_quantile((1 + level) / 2, i, n + 1 - i)
248
+ if f is None:
249
+ pair = (-math.log10(hi), -math.log10(lo))
250
+ else:
251
+ pair = (slope * float(f(lo)) + icpt, slope * float(f(hi)) + icpt)
252
+ lows.append(min(pair))
253
+ highs.append(max(pair))
254
+ bx.append(x[i - 1])
255
+ out["band_x"], out["low"], out["high"] = bx, lows, highs
256
+ return out
257
+
258
+
259
+ #: Points of a QQ plot drawn whole at most; above, points closer to the last one kept than
260
+ #: 1e-4 of the data's range on both axes (well under a pixel) are left out, when the
261
+ #: markers are the same for every point.
262
+ THIN = 10_000
263
+
264
+
265
+ def _points(s: QQ, b: Any) -> Any:
266
+ return s.cached("qq_points", lambda: _thinned(s),
267
+ ("dist", "line", "band", "pvalues", "color", "size", "marker",
268
+ "edge_color"))
269
+
270
+
271
+ def _thinned(s: QQ) -> Any:
272
+ st = s.stats
273
+ x, y = st["x"], st["y"]
274
+ n = len(x)
275
+ if not n:
276
+ return None
277
+ each = any(hasattr(v, "__len__") and not isinstance(v, str) and len(v) == n
278
+ for v in (s.resolve(k) for k in ("color", "size", "marker", "edge_color")))
279
+ if n <= THIN or each:
280
+ return ext.Table(x=x, y=y)
281
+ ex = (max(x) - min(x)) * 1e-4
282
+ ey = (max(y) - min(y)) * 1e-4
283
+ keep = array.array("q", [0])
284
+ lx, ly = x[0], y[0]
285
+ for i in range(1, n):
286
+ if abs(x[i] - lx) > ex or abs(y[i] - ly) > ey:
287
+ keep.append(i)
288
+ lx, ly = x[i], y[i]
289
+ if keep[-1] != n - 1:
290
+ keep.append(n - 1)
291
+ return ext.Table(x=kernels.take(x, keep), y=kernels.take(y, keep))
292
+
293
+
294
+ def _line_table(s: QQ, b: Any) -> Any:
295
+ st = s.stats
296
+ if st["line"] is None or not len(st["x"]):
297
+ return None
298
+ slope, icpt = st["line"]
299
+ x0, x1 = min(st["x"]), max(st["x"])
300
+ if st["band_x"] is not None:
301
+ x0, x1 = min(x0, min(st["band_x"])), max(x1, max(st["band_x"]))
302
+ return ext.Table(line_x=[x0, x1], line_y=[slope * x0 + icpt, slope * x1 + icpt])
303
+
304
+
305
+ def _band_table(s: QQ, b: Any) -> Any:
306
+ st = s.stats
307
+ if st["low"] is None:
308
+ return None
309
+ return ext.Table(x=st["band_x"], y1=st["low"], y2=st["high"])
310
+
311
+
312
+ QQ.SPEC = ext.Spec(
313
+ [ext.Lay("band", ("band", {"color": "faded", "opacity": "band_opacity"}), table="band",
314
+ key=False),
315
+ ext.Lay("path", ("stroke", {"color": "faded"}), table="line",
316
+ aes={"x": "line_x", "y": "line_y"}, key=False, bounds=False),
317
+ ext.Lay("points", ("marker", {"order": True, "once": True,
318
+ "n": lambda s: _count(s)}),
319
+ table="points", name="points", radius=lambda s: _radius(s.resolve("size")))],
320
+ tables={"points": _points, "line": _line_table, "band": _band_table},
321
+ key={"fill": "marker", "stroke": "marker", "marker": "@points"},
322
+ color_sources=True, size_sources=True)
323
+
324
+
325
+ def _count(s: QQ) -> int:
326
+ t = _points(s, None)
327
+ return 0 if t is None else len(t["x"])
328
+
329
+
330
+ def _radius(size: Any) -> float:
331
+ return size / 2 if isinstance(size, (int, float)) else 0.0
332
+
333
+
334
+ ext.register("qq", QQ)
@@ -0,0 +1,249 @@
1
+ """Raincloud plots: per category a half violin (the cloud), a narrow box at the slot's
2
+ centre and the values as jittered points on the other side (the rain)."""
3
+
4
+ import array
5
+ import random
6
+ from typing import Any
7
+
8
+ from .. import columns, ext
9
+ from .._methods import RaincloudMixin
10
+
11
+ __all__ = ["Raincloud"]
12
+
13
+ PARTS = ("violin", "box", "points")
14
+
15
+
16
+ def _parts(v: Any) -> tuple[str, ...]:
17
+ if isinstance(v, str):
18
+ v = (v,)
19
+ out = tuple(v)
20
+ for p in out:
21
+ ext.one_of(*PARTS)(p)
22
+ return out
23
+
24
+
25
+ class Raincloud(RaincloudMixin, ext.Series):
26
+ """Rainclouds of ``values`` (a dict ``{category: values}``, a list of arrays, or one
27
+ array; long data with ``x=`` categories): per category a half violin on the high side
28
+ (stat ``violin``), a narrow box at the centre (stat ``box``) and the values as
29
+ jittered points on the low side. ``parts`` keeps some of the three. Widths and
30
+ offsets are `%` of the slot.
31
+
32
+ Long data take ``x=`` the categories (``y=`` with ``orientation="horizontal"``).
33
+ Densities and quartiles are the ``violin`` and ``box`` stats. Rainclouds do not dodge
34
+ with other series and take no levels. The same halves and shifts work on the plain
35
+ types: ``Violin(side="high")``, ``Box(offset=…)``, ``Strip(offset=…)``. Uses::
36
+
37
+ p.raincloud({"control": a, "treated": b})
38
+ p.raincloud("value", x="group", data=df, parts=("violin", "points"))
39
+
40
+ Example::
41
+
42
+ rng = random.Random(1)
43
+ p = plt.Plot(width=240, height=160)
44
+ p.raincloud({"control": [rng.gauss(0, 1) for _ in range(60)],
45
+ "treated": [rng.gauss(1, 0.8) for _ in range(60)]})
46
+ """
47
+
48
+ SETTINGS = ext.table(
49
+ color=ext.Setting(ext.color, doc="Color of the three parts; default the next "
50
+ "plot's color."),
51
+ parts=ext.Setting(_parts, doc="The parts drawn: `violin`, `box`, `points`."),
52
+ orientation=ext.Setting(ext.one_of("vertical", "horizontal"),
53
+ doc="`vertical` (categories on x) or `horizontal`."),
54
+ width=ext.Setting(ext.length(positive=True, data=True, percent="slot"),
55
+ doc="Width of the slot the raincloud takes (the axis limits at "
56
+ "numeric positions)."),
57
+ violin_width=ext.Setting(ext.length(positive=True, percent="slot"),
58
+ doc="Largest width of the half violins."),
59
+ box_width=ext.Setting(ext.length(positive=True, percent="slot"),
60
+ doc="Width of the boxes."),
61
+ points_offset=ext.Setting(ext.length(percent="slot"),
62
+ doc="Centre of the points from the slot's centre "
63
+ "(negative: the low side)."),
64
+ jitter=ext.Setting(ext.length(lo=0, percent="slot"),
65
+ doc="Half-width of the points' jitter."),
66
+ seed=ext.Setting(ext.number(integer=True), doc="Seed of the jitter."),
67
+ fill_opacity=ext.Setting(ext.number(0, 1), doc="Opacity of the violins' and "
68
+ "boxes' fill."),
69
+ bandwidth=ext.Setting(ext.either(ext.one_of("scott", "silverman"),
70
+ ext.number(positive=True)),
71
+ doc="`scott`, `silverman` or a number."),
72
+ adjust=ext.Setting(ext.number(positive=True), doc="Factor applied to the bandwidth."),
73
+ cut=ext.Setting(ext.number(lo=0), doc="The density extends this many bandwidths "
74
+ "past the data."),
75
+ points=ext.Setting(ext.number(lo=16, hi=65536, integer=True),
76
+ doc="Points per violin outline."),
77
+ whiskers=ext.Setting(ext.number(lo=0), doc="Whisker reach in IQRs."),
78
+ weights=ext.Setting(columns.weights,
79
+ doc="A weight per value, counted as repeated values, for the "
80
+ "violins and boxes (a dict or a list per category, as the "
81
+ "values; long data: a weight per row)."),
82
+ median_width=ext.Setting(ext.length(lo=0), doc="Width of the median line."),
83
+ **ext.group("Marker", "marker_"),
84
+ categories=ext.Setting(ext.either(ext.literal(None), lambda v: list(v)),
85
+ doc="Category names of a list of arrays."),
86
+ **ext.common("line_width", "name", "opacity", "z", "rasterize", "simplify", "font_size",
87
+ "font_family", "text_color"),
88
+ )
89
+ DATA = {"values": "categories", "positions": "slots"}
90
+ ITEMS = "rainclouds"
91
+
92
+ def __init__(self, values: Any, positions: Any = None, plot: Any = None,
93
+ **settings: Any) -> None:
94
+ super().__init__(plot=plot, **settings)
95
+
96
+ def value_scale(self) -> Any:
97
+ return ext.axis_scale(ext.axis_of(self, "y"))
98
+
99
+ @property
100
+ def stats(self) -> dict[Any, dict[str, Any]]:
101
+ """``{category: {"violin": the violin stat's, "box": BoxStats}}``."""
102
+ v = ext.compute(self, "violin", (self.value_scale(),))
103
+ b = ext.compute(self, "box")
104
+ return {k: {"violin": v[k], "box": b[k]} for k in self.cols.categories}
105
+
106
+ def base(self, ctx: Any) -> Any:
107
+ """The series color."""
108
+ c = self.resolve("color")
109
+ return ctx.color(c) if c is not None else ext.auto_colors(self, ctx)[0]
110
+
111
+ def fill(self, ctx: Any) -> Any:
112
+ """The violins' and boxes' fill."""
113
+ return ext.with_alpha(self.base(ctx),
114
+ self.resolve("opacity") * self.resolve("fill_opacity"))
115
+
116
+ def _on(self, part: str) -> bool:
117
+ return part in self.resolve("parts")
118
+
119
+ def _scale(self) -> Any:
120
+ """The scale of numeric positions' axis (``None``: none, or linear)."""
121
+ if self.cols["positions"] is None:
122
+ return None
123
+ sc = ext.axis_scale(ext.axis_of(self, "x"))
124
+ return sc if sc.function else None
125
+
126
+ def _slots(self) -> tuple[list[float], float]:
127
+ """The centre of each category's slot and the slot's width, in the space of the
128
+ positions' axis scale (:meth:`_back` maps them to data)."""
129
+ pos = self.cols["positions"]
130
+ if pos is None:
131
+ return [float(i) for i in range(len(self.cols.categories))], 1.0
132
+ sc = self._scale()
133
+ fs = [sc.f(float(p)) if sc is not None else float(p) for p in pos]
134
+ ps = sorted(set(fs))
135
+ gaps = [b - a for a, b in zip(ps, ps[1:]) if b > a]
136
+ return fs, (min(gaps) if gaps else 1.0)
137
+
138
+ def _back(self, v: Any) -> Any:
139
+ """Slot-space positions in data units."""
140
+ sc = self._scale()
141
+ if sc is None:
142
+ return v
143
+ if isinstance(v, list) and v and not isinstance(v[0], float):
144
+ return [self._back(a) for a in v]
145
+ return array.array("d", (sc.inverse(x) for x in v))
146
+
147
+ def _width(self, name: str, unit: float) -> float:
148
+ return float(self.resolve(name, of=unit))
149
+
150
+
151
+ def _clouds(s: Raincloud, b: Any) -> Any:
152
+ st = s.stats
153
+ centres, unit = s._slots()
154
+ keys = [i for i, k in enumerate(s.cols.categories)
155
+ if st[k]["violin"]["count"] and not st[k]["violin"]["line"]]
156
+ if not keys:
157
+ return None
158
+ peak = max(max(st[s.cols.categories[i]]["violin"]["density"]) for i in keys) or 1.0
159
+ w = s._width("violin_width", unit)
160
+ xs, lo, hi = [], [], []
161
+ for i in keys:
162
+ v = st[s.cols.categories[i]]["violin"]
163
+ c = centres[i]
164
+ xs.append(v["grid"])
165
+ lo.append(array.array("d", [c] * len(v["grid"])))
166
+ hi.append(array.array("d", (c + d / peak * w for d in v["density"])))
167
+ return ext.Table(x=xs, y1=s._back(lo), y2=s._back(hi))
168
+
169
+
170
+ def _boxes(s: Raincloud, b: Any) -> Any:
171
+ st = s.stats
172
+ centres, unit = s._slots()
173
+ half = s._width("box_width", unit) / 2
174
+ rows = [(centres[i], st[k]["box"]) for i, k in enumerate(s.cols.categories)
175
+ if st[k]["box"].count]
176
+ if not rows:
177
+ return None
178
+ return ext.Table(x0=s._back([c - half for c, _ in rows]),
179
+ x1=s._back([c + half for c, _ in rows]),
180
+ y0=[bs.q1 for _, bs in rows], y1=[bs.q3 for _, bs in rows])
181
+
182
+
183
+ def _whiskers(s: Raincloud, b: Any) -> Any:
184
+ st = s.stats
185
+ centres, _ = s._slots()
186
+ t: dict[str, list[float]] = {"x0": [], "x1": [], "y0": [], "y1": []}
187
+ for i, k in enumerate(s.cols.categories):
188
+ bs = st[k]["box"]
189
+ if bs.count:
190
+ for a, e in ((bs.q1, bs.low), (bs.q3, bs.high)):
191
+ t["x0"].append(centres[i])
192
+ t["x1"].append(centres[i])
193
+ t["y0"].append(a)
194
+ t["y1"].append(e)
195
+ if not t["x0"]:
196
+ return None
197
+ return ext.Table(x0=s._back(t["x0"]), x1=s._back(t["x1"]), y0=t["y0"], y1=t["y1"])
198
+
199
+
200
+ def _medians(s: Raincloud, b: Any) -> Any:
201
+ st = s.stats
202
+ centres, unit = s._slots()
203
+ half = s._width("box_width", unit) / 2
204
+ rows = [(centres[i], st[k]["box"].median) for i, k in enumerate(s.cols.categories)
205
+ if st[k]["box"].count]
206
+ if not rows:
207
+ return None
208
+ return ext.Table(x0=s._back([c - half for c, _ in rows]),
209
+ x1=s._back([c + half for c, _ in rows]),
210
+ y0=[m for _, m in rows], y1=[m for _, m in rows])
211
+
212
+
213
+ def _rain(s: Raincloud, b: Any) -> Any:
214
+ centres, unit = s._slots()
215
+ off = s._width("points_offset", unit)
216
+ jit = s._width("jitter", unit)
217
+ rng = random.Random(s.resolve("seed"))
218
+ x, y = array.array("d"), array.array("d")
219
+ for c, vals in zip(centres, s.cols.values):
220
+ for v in vals:
221
+ x.append(c + off + jit * rng.uniform(-1.0, 1.0))
222
+ y.append(v)
223
+ return ext.Table(x=s._back(x), y=y) if len(y) else None
224
+
225
+
226
+ @ext.role("raincloud.box")
227
+ def _box_paint(s: Raincloud, ctx: Any, glyph: bool) -> dict[str, Any]:
228
+ return {"fill": s.fill(ctx), "stroke": ext.with_alpha(s.base(ctx), s.resolve("opacity")),
229
+ "line_width": s.resolve("line_width")}
230
+
231
+
232
+ Raincloud.SPEC = ext.Spec(
233
+ [ext.Lay("band", ("band", {"color": "fill()", "extra": ("simplify",)}),
234
+ table="clouds", when=lambda s: s._on("violin"), turn=True, key=False),
235
+ ext.Lay("segments", ("stroke", {"color": "faded"}), table="whiskers",
236
+ when=lambda s: s._on("box"), key=False),
237
+ ext.Lay("rects", "raincloud.box", table="boxes", when=lambda s: s._on("box"),
238
+ key=False),
239
+ ext.Lay("segments", ("stroke", {"color": "faded", "width": "median_width"}),
240
+ table="medians", when=lambda s: s._on("box"), key=False),
241
+ ext.Lay("points", ("marker", {"prefix": "marker_", "color": "base",
242
+ "n": lambda s: sum(map(len, s.cols.values))}),
243
+ table="rain", name="rain", when=lambda s: s._on("points"))],
244
+ tables={"clouds": _clouds, "boxes": _boxes, "whiskers": _whiskers, "medians": _medians,
245
+ "rain": _rain},
246
+ key={"fill": "base", "stroke": "base"},
247
+ titles={"values": "y", "categories": "x"}, slots="positions")
248
+
249
+ ext.register("raincloud", Raincloud)