plot-io 0.12.0__py3-none-win_amd64.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-x86_64.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
@@ -0,0 +1,355 @@
1
+ """Dots: one marker per cell of a grid (rows × columns), its size and color mapping values
2
+ through the series' size and color axes (the dot plot)."""
3
+
4
+ import array
5
+ import math
6
+ from typing import TYPE_CHECKING, Any, Self, Unpack
7
+
8
+ from vec_io.arrays import doubles
9
+
10
+ from .. import groups, kernels
11
+ from ..layers.base import Series, common
12
+ from ..layers.core import Call, Layer, Table
13
+ from ..layers.geoms import GEOMS
14
+ from ..layers.grammar import entry
15
+ from ..layers.paint import Paint, color_sources, size_sources
16
+ from ..settings import Setting, canonical_kwargs, one_of, table
17
+ from ..transform import settings_both as coord_settings
18
+ from ._util import columns_meta
19
+ from .heatmap import Coords
20
+
21
+ if TYPE_CHECKING:
22
+ from ..types import DotsSettings
23
+
24
+ __all__ = ["Dots", "group_summary"]
25
+
26
+ _NAN = float("nan")
27
+
28
+
29
+ def _grid(v: Any, what: str) -> tuple[int, int, array.array] | None:
30
+ """``(rows, columns, values)`` of a 2-D grid of numbers, else ``None`` (one value)."""
31
+ if v is None or isinstance(v, (str, int, float, tuple)):
32
+ return None
33
+ g = kernels.grid(v)
34
+ if g is not None:
35
+ h, w, k, flat = g
36
+ if k != 1:
37
+ raise ValueError(f"{what}: a grid of numbers, one per cell")
38
+ return h, w, flat
39
+ rows = list(v)
40
+ if not rows or not all(not isinstance(r, (str, bytes)) and hasattr(r, "__len__")
41
+ for r in rows):
42
+ raise ValueError(f"{what}: a grid of values (rows of cells), or long data (x, y "
43
+ f"and {what} per cell)")
44
+ w = len(rows[0])
45
+ if any(len(r) != w for r in rows):
46
+ raise ValueError(f"{what}: rows of one length")
47
+ try:
48
+ flat = doubles([x for r in rows for x in r])
49
+ except (TypeError, ValueError):
50
+ raise ValueError(f"{what}: numbers per cell") from None
51
+ return len(rows), w, flat
52
+
53
+
54
+ def _is_grid(v: Any) -> bool:
55
+ if v is None or isinstance(v, (str, int, float, tuple)):
56
+ return False
57
+ if kernels.grid(v) is not None:
58
+ return True
59
+ try:
60
+ first = next(iter(v))
61
+ except (TypeError, StopIteration):
62
+ return False
63
+ return not isinstance(first, (str, bytes)) and hasattr(first, "__len__")
64
+
65
+
66
+ def _categories(given: Any, values: list[Any], order: list[Any] | None) -> list[Any]:
67
+ from ..tables import is_missing
68
+ if given is not None:
69
+ return list(given)
70
+ out = list(order or [])
71
+ seen = set(out)
72
+ for v in values:
73
+ if not is_missing(v) and v not in seen:
74
+ seen.add(v)
75
+ out.append(v)
76
+ return out
77
+
78
+
79
+ def _center(c: Coords, i: int) -> float:
80
+ if c.kind in ("index", "category"):
81
+ return float(i)
82
+ if c.kind == "centers":
83
+ return c.values[i]
84
+ return (c.values[i] + c.values[i + 1]) / 2
85
+
86
+
87
+ class Dots(Series):
88
+ """One marker per cell of a grid: ``color`` and ``size`` are grids of values (row 0
89
+ first) of one shape, or one value; ``x``/``y`` label the columns and rows, as
90
+ ``heatmap``'s. With long data (``x``, ``y`` and values per cell), the cells come from
91
+ the rows; cells without a row get no dot.
92
+
93
+ Values always map: colors through the series' `color axis <docs/api.md#6-color-and-size-axes>`_ (a colorbar by default),
94
+ sizes through its size axis (``size_map`` ``(0, 12)`` px and ``size_limits``
95
+ ``"0/max"``: area ∝ value; fractions: ``"0/1"``) and its
96
+ `size key <docs/api.md#size-keys>`_, on the colorbar's line when both fit. A missing size
97
+ leaves the cell empty; a missing color takes the map's ``bad``.
98
+
99
+ - Long data: ``x``, ``y`` and ``color``/``size`` per cell (columns with ``data=``, or
100
+ sequences), one row per cell; two rows for one cell are an error. Categories in order
101
+ of first appearance (pandas or polars categoricals: theirs); ``x_categories=`` /
102
+ ``y_categories=`` lists select and order them.
103
+ - ``scale_color``: ``"rows"`` / ``"columns"`` rescales color values to 0–1 in each row or
104
+ column before mapping (one variable's range per column).
105
+ - Cells: the axes' width (``Figure(width=…)``) sets the space per column; rotated labels
106
+ closer than ``category_label_spacing`` are thinned out. ``data_ratio=1`` on the plot
107
+ makes cells square.
108
+
109
+ Example::
110
+
111
+ rng = random.Random(1)
112
+ genes, clusters = ["Cd3e", "Ms4a1", "Lyz2", "Nkg7"], ["T", "B", "Mono"]
113
+ p = plt.Plot(width=240, height=160)
114
+ p.dots([[rng.random() for _ in genes] for _ in clusters],
115
+ size=[[rng.random() for _ in genes] for _ in clusters], x=genes, y=clusters,
116
+ size_limits="0/1")
117
+ """
118
+
119
+ DATA = {"color": "raw", "size": "raw", "x": "cells", "y": "cells"}
120
+ SETTINGS = table(
121
+ **coord_settings(),
122
+ **groups.Marker.embed(),
123
+ **common("color_map", "size_map", "size_key"),
124
+ scale_color=Setting(one_of("rows", "columns"),
125
+ doc="`rows` / `columns`: color values rescaled to 0–1 in each "
126
+ "row or column before mapping; unset: as given."),
127
+ origin=Setting(one_of("top", "bottom"),
128
+ doc="`top`: row 0 at the top (y axis inverted); `bottom`: at the "
129
+ "bottom."),
130
+ **common("name", "opacity", "z", "rasterize", "simplify", "font_size", "font_family",
131
+ "text_color"),
132
+ )
133
+
134
+ def __init__(self, color: Any = None, size: Any = None, x: Any = None, y: Any = None,
135
+ plot: Any = None, **settings: Any) -> None:
136
+ args = {n: self.cols[n] for n in ("color", "size", "x", "y")}
137
+ _cells(args, settings, self.cols.orders)
138
+ color, size, x, y = args["color"], args["size"], args["x"], args["y"]
139
+ present = settings.pop("_present", None)
140
+ shape = settings.pop("_shape", None)
141
+ for k in ("x_categories", "y_categories"):
142
+ if k in settings:
143
+ raise ValueError(f"{k} applies to long data (x, y and values per cell)")
144
+ shapes = {}
145
+ for name, v in (("color", color), ("size", size)):
146
+ g = _grid(v, name)
147
+ if g is not None:
148
+ shapes[name] = (g[0], g[1])
149
+ elif v is not None:
150
+ settings[name] = v
151
+ if len(set(shapes.values())) > 1:
152
+ raise ValueError("color and size grids must have one shape, got "
153
+ + " and ".join(f"{h}×{w}" for h, w in shapes.values()))
154
+ if shapes:
155
+ shape = next(iter(shapes.values()))
156
+ if shape is None:
157
+ raise ValueError("dots need color or size as a grid of values (rows of cells), "
158
+ "or long data: x, y and values per cell")
159
+ self.h, self.w = shape
160
+ self.xc = Coords(x, self.w, "x")
161
+ self.yc = Coords(y, self.h, "y")
162
+ #: the cells drawn, row by row: ``(row, column)``
163
+ self.cells = [(r, c) for r in range(self.h) for c in range(self.w)
164
+ if present is None or present[r * self.w + c]]
165
+ self.x = array.array("d", (_center(self.xc, c) for _, c in self.cells))
166
+ self.y = array.array("d", (_center(self.yc, r) for r, _ in self.cells))
167
+ self._given: dict[str, array.array] = {}
168
+ for name, v in (("color", color), ("size", size)):
169
+ if name in shapes:
170
+ settings[name] = v
171
+ super().__init__(plot=plot, **settings)
172
+
173
+ # -- values per cell ------------------------------------------------------------
174
+
175
+ def set(self, **given: "Unpack[DotsSettings]") -> Self: # type: ignore[override]
176
+ settings = canonical_kwargs(dict(given), self.SETTINGS, self.kind)
177
+ for name in ("color", "size"):
178
+ if name not in settings:
179
+ continue
180
+ g = _grid(settings[name], name) if _is_grid(settings[name]) else None
181
+ if g is None:
182
+ self._given.pop(name, None)
183
+ continue
184
+ h, w, flat = g
185
+ if (h, w) != (self.h, self.w):
186
+ raise ValueError(f"{name}: a {self.h}×{self.w} grid, got {h}×{w}")
187
+ settings[name] = self._given[name] = array.array(
188
+ "d", (flat[r * w + c] for r, c in self.cells))
189
+ super().set(**settings)
190
+ if ("color" in settings or "scale_color" in settings) and "color" in self._given:
191
+ raw = self._given["color"]
192
+ super().set(color=raw if self.resolve("scale_color") is None
193
+ else self._scaled(raw))
194
+ return self
195
+
196
+ def _scaled(self, values: array.array) -> array.array:
197
+ """Color values rescaled to 0–1 in each row (``scale_color="rows"``) or column."""
198
+ by = 0 if self.resolve("scale_color") == "rows" else 1
199
+ spans: dict[int, list[float]] = {}
200
+ for (r, c), v in zip(self.cells, values):
201
+ if math.isfinite(v):
202
+ k = (r, c)[by]
203
+ lo, hi = spans.get(k, (v, v))
204
+ spans[k] = [min(lo, v), max(hi, v)]
205
+ out = array.array("d")
206
+ for (r, c), v in zip(self.cells, values):
207
+ lo, hi = spans.get((r, c)[by], (0.0, 0.0))
208
+ out.append(_NAN if not math.isfinite(v) else
209
+ (v - lo) / (hi - lo) if hi > lo else 0.0)
210
+ return out
211
+
212
+ def _bar_source(self) -> Any:
213
+ return self if "color" in self._given else None # color values only
214
+
215
+ # -- drawing --------------------------------------------------------------------
216
+
217
+ def layers(self) -> Call:
218
+ call = Call(self)
219
+ n = len(self.cells)
220
+ t = Table(x=self.x, y=self.y)
221
+ size = self.resolve("size")
222
+ call.layers.append(Layer(GEOMS["points"], t, paint=Paint(self, "marker", n=n, once=True),
223
+ occupy="all",
224
+ radius=size / 2 if isinstance(size, (int, float)) else 0.0))
225
+ name = self.resolve("name")
226
+ if name is not None:
227
+ call.entries.append(entry(self, name, call.layers, fill="marker",
228
+ stroke="marker", marker=call.layers[0].paint))
229
+ call.color_count = 0 if self.is_set("color") else 1
230
+ call.color_sources = color_sources(self)
231
+ call.size_sources = size_sources(self)
232
+ self.xc.axis(call, "x")
233
+ self.yc.axis(call, "y")
234
+ if self.resolve("origin") == "top":
235
+ call.invert = {"y"}
236
+ call.exact = {"x", "y"}
237
+ columns_meta(call, {})
238
+ return call
239
+
240
+
241
+ def group_summary(values: Any, groups: Any, names: Any = None, threshold: float = 0.0,
242
+ mean: str = "all") -> dict[str, list[Any]]:
243
+ """Per group and variable of an observations × variables matrix: the ``mean`` and the
244
+ ``fraction`` of observations above ``threshold``, as columns for ``data=``
245
+ (``group``, ``name``, ``mean``, ``fraction``).
246
+
247
+ ``groups``: one label per observation (row); ``names``: the variables (default:
248
+ their indices). ``mean="expressed"`` averages only the observations above
249
+ ``threshold`` (none: a missing mean). Non-finite values are left out of both, per
250
+ cell. Groups in order of first appearance (a pandas or polars categorical: its
251
+ order); missing labels are left out."""
252
+ from ..tables import is_missing
253
+ if mean not in ("all", "expressed"):
254
+ raise ValueError("mean is 'all' or 'expressed'")
255
+ g = kernels.grid(values)
256
+ if g is not None:
257
+ h, w, k, flat = g
258
+ if k != 1:
259
+ raise ValueError("values: a 2-D matrix")
260
+ else:
261
+ rows = [list(r) for r in values]
262
+ h, w = len(rows), len(rows[0]) if rows else 0
263
+ if any(len(r) != w for r in rows):
264
+ raise ValueError("values: rows of one length")
265
+ flat = doubles([v for r in rows for v in r])
266
+ order = None
267
+ cat = getattr(groups, "cat", None)
268
+ if cat is not None:
269
+ try:
270
+ order = list(cat.categories)
271
+ except (AttributeError, TypeError):
272
+ try:
273
+ order = list(cat.get_categories())
274
+ except (AttributeError, TypeError):
275
+ order = None
276
+ if hasattr(groups, "to_numpy"):
277
+ groups = groups.to_numpy()
278
+ labels = list(groups.tolist() if hasattr(groups, "tolist") else groups)
279
+ if len(labels) != h:
280
+ raise ValueError(f"{len(labels)} group labels for {h} observations")
281
+ names = list(range(w)) if names is None else list(names)
282
+ if len(names) != w:
283
+ raise ValueError(f"{len(names)} names for {w} variables")
284
+ from .._native import kernels as _native
285
+ keys = _categories(None, labels, order)
286
+ index = {k: i for i, k in enumerate(keys)}
287
+ rows_group = array.array("q", [-1]) * h
288
+ for r, lab in enumerate(labels):
289
+ if is_missing(lab) or lab not in index:
290
+ continue
291
+ rows_group[r] = index[lab]
292
+ total, count, above, total_above = _native.group_summary(flat, h, w, rows_group, len(keys),
293
+ float(threshold))
294
+ out: dict[str, list[Any]] = {"group": [], "name": [], "mean": [], "fraction": []}
295
+ for gi, k in enumerate(keys):
296
+ for c in range(w):
297
+ i = gi * w + c
298
+ n = count[i]
299
+ out["group"].append(k)
300
+ out["name"].append(names[c])
301
+ if mean == "all":
302
+ out["mean"].append(total[i] / n if n else _NAN)
303
+ else:
304
+ m = above[i]
305
+ out["mean"].append(total_above[i] / m if m else _NAN)
306
+ out["fraction"].append(above[i] / n if n else _NAN)
307
+ return out
308
+
309
+
310
+ def _cells(pos: dict[str, Any], kw: dict[str, Any], orders: dict[str, Any]) -> None:
311
+ """Long data: ``x``, ``y`` and values per cell (one row per cell) into grids (in
312
+ ``pos``; the grid's shape and the cells present in ``kw``)."""
313
+ from ..tables import is_missing
314
+ x, y = pos.get("x"), pos.get("y")
315
+ given = {n: pos[n] for n in ("color", "size") if pos.get(n) is not None}
316
+ if x is None or y is None or any(_is_grid(v) for v in given.values()):
317
+ return
318
+ per_cell = {n: v for n, v in given.items()
319
+ if not isinstance(v, (str, int, float, tuple))}
320
+ xs, ys = list(x), list(y)
321
+ if len(xs) != len(ys):
322
+ raise ValueError(f"x has {len(xs)} values and y {len(ys)}")
323
+ vals: dict[str, array.array] = {}
324
+ for n, v in per_cell.items():
325
+ try:
326
+ vals[n] = doubles(v)
327
+ except (TypeError, ValueError):
328
+ raise ValueError(f"{n}: numbers per cell") from None
329
+ if len(vals[n]) != len(xs):
330
+ raise ValueError(f"{n} has {len(vals[n])} values for {len(xs)} cells")
331
+ xcats = _categories(kw.pop("x_categories", None), xs, orders.get("x"))
332
+ ycats = _categories(kw.pop("y_categories", None), ys, orders.get("y"))
333
+ xi = {v: i for i, v in enumerate(xcats)}
334
+ yi = {v: i for i, v in enumerate(ycats)}
335
+ h, w = len(ycats), len(xcats)
336
+ if not h or not w:
337
+ raise ValueError("dots: no cells")
338
+ present = bytearray(h * w)
339
+ grids = {n: array.array("d", [_NAN]) * (h * w) for n in vals}
340
+ for k, (a, b) in enumerate(zip(xs, ys)):
341
+ if is_missing(a) or is_missing(b) or a not in xi or b not in yi:
342
+ continue
343
+ i = yi[b] * w + xi[a]
344
+ if present[i]:
345
+ raise ValueError(f"two rows for the cell ({a!r}, {b!r})")
346
+ present[i] = 1
347
+ for n, g in grids.items():
348
+ g[i] = vals[n][k]
349
+ pos["x"] = [v if isinstance(v, str) else str(v) for v in xcats]
350
+ pos["y"] = [v if isinstance(v, str) else str(v) for v in ycats]
351
+ kw["_shape"] = (h, w)
352
+ for n, g in grids.items():
353
+ pos[n] = [g[r * w:(r + 1) * w] for r in range(h)]
354
+ if not all(present):
355
+ kw["_present"] = present
@@ -0,0 +1,97 @@
1
+ """Empirical cumulative distributions (stat ``ecdf``): a step line (``Path``) per set."""
2
+
3
+ import array
4
+ from typing import Any
5
+
6
+ from .._methods import EcdfMixin
7
+ from .. import columns, groups
8
+ from ..layers import stats
9
+ from ..layers.base import Series, common
10
+ from ..layers.core import Table
11
+ from ..layers.grammar import Lay, Spec
12
+ from ..layers.positions import check_stack
13
+ from ..settings import Setting, boolean, one_of, table
14
+ from ..transform import settings_both as coord_settings
15
+ from ..layers.paint import base_colors
16
+ from ._util import faded, set_color_value
17
+
18
+ __all__ = ["Ecdf"]
19
+
20
+
21
+ class Ecdf(EcdfMixin, Series):
22
+ """The empirical cumulative distribution of ``values`` (one array, or several as a dict
23
+ or a list of arrays).
24
+
25
+ A step line through the sorted values of each set: ``stat`` ``proportion``, ``count`` or
26
+ ``percent``; ``complementary`` plots 1 − F. ``color``: one, a list (one per set) or a
27
+ dict by set name.
28
+
29
+ Example::
30
+
31
+ rng = random.Random(1)
32
+ p = plt.Plot(width=240, height=160)
33
+ p.ecdf({"a": [rng.gauss(0, 1) for _ in range(80)],
34
+ "b": [rng.gauss(0.8, 1.2) for _ in range(80)]})
35
+ """
36
+
37
+ SETTINGS = table(
38
+ **coord_settings(),
39
+ weights=Setting(columns.weights, doc="A weight per value (per set for several sets)."),
40
+ stat=Setting(one_of("proportion", "count", "percent"),
41
+ doc="`proportion` (0–1), `count` or `percent`."),
42
+ complementary=Setting(boolean, doc="Plot 1 − F (survival)."),
43
+ stack=Setting(check_stack, doc="Stack the sets, and the plot's other stacked ecdf "
44
+ "calls, on their values (`\"normalize\"`: as "
45
+ "shares)."),
46
+ orientation=Setting(one_of("vertical", "horizontal"),
47
+ doc="`vertical`: values along x; `horizontal`: along y."),
48
+ color=Setting(set_color_value,
49
+ doc="Line color, one per set (a list, or a dict by set name); default "
50
+ "the plot's colors."),
51
+ **groups.Stroke.embed(only=("line_width", "line_style"),
52
+ inherited=("line_width",),
53
+ docs={"line_style": "Line style."}),
54
+ **common("name", "opacity", "z", "rasterize", "simplify", "font_size", "font_family",
55
+ "text_color"),
56
+ )
57
+
58
+ DATA = {"values": "samples"}
59
+
60
+ def __init__(self, values: Any, plot: Any = None, **settings: Any) -> None:
61
+ super().__init__(plot=plot, **settings)
62
+
63
+ @property
64
+ def stats(self) -> list[tuple[array.array, array.array, float]]:
65
+ """Per set: sorted values, the plotted cumulative values, and the value before the
66
+ first step (0, or the total for complementary curves)."""
67
+ return stats.run(self, "ecdf")
68
+
69
+ def _colors(self, ctx: Any) -> list[Any]:
70
+ cols = base_colors(self, ctx, [k for k, _ in self.cols.sets])
71
+ return faded(cols, self.resolve("opacity"))
72
+
73
+ def _line_colors(self, ctx: Any) -> list[Any]:
74
+ """Each set's line color (none for a set without values)."""
75
+ return [c if len(st[0]) else None for c, st in zip(self._colors(ctx), self.stats)]
76
+
77
+ SPEC = Spec(
78
+ [Lay("path", ("stroke", {"color": "_line_colors()", "dash": True,
79
+ "extra": ("simplify",),
80
+ "fixed": {"interpolation": "step-post"}}),
81
+ table="sets", per_set=True, aes={"y": "top"}, stat="ecdf",
82
+ position=lambda s: "stack" if s.resolve("stack") else "identity",
83
+ interpolation="step-post")],
84
+ tables={"sets": lambda s, b: [_steps(xs, ys, start) for xs, ys, start in b.stats["ecdf"]]},
85
+ entries="sets", key={"stroke": "_colors()"}, color_count="sets", titles={"values": "x"})
86
+
87
+
88
+ def _steps(xs: Any, ys: Any, start: float) -> Table:
89
+ """A set's step line: from the value before the first step (its key ``None``)."""
90
+ if len(xs):
91
+ vx = array.array("d", [xs[0]])
92
+ vx.extend(xs)
93
+ cy = array.array("d", [start])
94
+ cy.extend(ys)
95
+ else:
96
+ vx, cy = array.array("d"), array.array("d")
97
+ return Table(x=vx, keys=[None, *xs[:len(vx) - 1]], heights=cy)
@@ -0,0 +1,106 @@
1
+ """Error bars on their own (``Segments``, or a ``Band`` for y errors), with optional markers
2
+ (``Points``)."""
3
+
4
+ import array
5
+ from typing import Any
6
+
7
+ from .._methods import ErrorbarMixin
8
+ from .. import groups
9
+ from ..color.values import color
10
+ from ..layers.base import column, common
11
+ from ..layers.core import Table
12
+ from ..layers.grammar import Lay, Spec
13
+ from ..layers.paint import xy_density_points
14
+ from ..settings import Setting, number, one_of, table
15
+ from ..transform import settings_both as coord_settings
16
+ from ..validators import length
17
+ from .errors import Summarized, error_value, errors_of
18
+
19
+ __all__ = ["Errorbar"]
20
+
21
+
22
+ def _marker(v: Any) -> Any:
23
+ from ..markers import check_marker
24
+ return check_marker(v)
25
+
26
+
27
+ class Errorbar(ErrorbarMixin, Summarized):
28
+ """Error bars at ``(x, y)``: ``y_error``/``x_error`` as a value, a sequence,
29
+ ``(low, high)`` or ``Errors``; ``marker`` adds markers at the points (numbers per
30
+ point in ``marker_color`` map through the series' color axis).
31
+
32
+ Markers only with ``marker=``; ``y_error_style="band"`` fills the band between the y
33
+ error ends instead. With replicates, ``estimate`` and ``interval`` draw points at their
34
+ estimates with bars of their interval (a point plot; `summaries <docs/api.md#summaries-estimate-interval>`_).
35
+
36
+ Example::
37
+
38
+ p = plt.Plot(width=240, height=160)
39
+ p.errorbar(range(6), [2, 3, 2.5, 4, 3.5, 5], y_error=[0.4, 0.6, 0.3, 0.8, 0.5, 0.7], marker="o")
40
+ """
41
+
42
+ SETTINGS = table(
43
+ **coord_settings(),
44
+ **{**groups.Marker.embed("marker_"),
45
+ "marker": Setting(_marker, doc="Marker at the points; default none.")},
46
+ **common("color_map"),
47
+ color=Setting(color, doc="Bar color; default: the plot's next color."),
48
+ line_width=Setting(length(lo=0), inherited=True, doc="Bar width."),
49
+ cap_length=Setting(length(lo=0, data=True),
50
+ doc="Cap width (`data`: in data units across the bar); 0: no "
51
+ "caps."),
52
+ x_error=Setting(error_value, doc="Horizontal errors."),
53
+ y_error=Setting(error_value, doc="Vertical errors."),
54
+ y_error_style=Setting(one_of("bars", "band"),
55
+ doc="`bars`, or `band`: a filled band between the y error ends "
56
+ "(points sorted by x)."),
57
+ band_opacity=Setting(number(0, 1), doc="Opacity of a band."),
58
+ **common("estimate"),
59
+ orientation=Setting(one_of("vertical", "horizontal"),
60
+ doc="`vertical` (x along the x axis) or `horizontal` (x along "
61
+ "the y axis)."),
62
+ **common("name", "opacity", "z", "rasterize", "simplify", "font_size", "font_family",
63
+ "text_color"),
64
+ )
65
+
66
+ DATA = {"x": "position", "y": "position"}
67
+ x = column("x", "x of each point (categories at their index among their own).")
68
+ y = column("y", "y of each point (as x).")
69
+
70
+ def __init__(self, x: Any, y: Any, plot: Any = None, **settings: Any) -> None:
71
+ super().__init__(plot=plot, **settings)
72
+
73
+ def density_points(self) -> tuple[Any, Any]:
74
+ """The markers' positions on the plot's axes (a density in ``marker_color``)."""
75
+ return xy_density_points(self, self.x, self.y)
76
+
77
+ SPEC = Spec(
78
+ [Lay("band", ("band", {"color": "faded", "opacity": "band_opacity"}), table="band",
79
+ key=False, occupy=False),
80
+ Lay("errors", ("error_source", {"color": "faded", "width": "line_width",
81
+ "opacity": 1.0}),
82
+ order=("y", "x"), skip=("y", ("y_error_style", "band"))),
83
+ # the legend symbol: a bar with caps (not drawn on the axes) and a marker
84
+ Lay("segments", ("stroke", {"color": "faded"}), table="symbol", drawn=False,
85
+ glyph="errorbar"),
86
+ Lay("points", ("marker", {"prefix": "marker_", "color": "faded", "when": "marker",
87
+ "n": lambda s: len(s.x)}),
88
+ glyph_max=0.8)],
89
+ tables={"band": lambda s, b: _band(s), "symbol": lambda s, b: Table()},
90
+ key={"stroke": "faded"}, color_sources=lambda s: s.resolve("marker") is not None)
91
+
92
+
93
+ def _band(s: Errorbar) -> Table | None:
94
+ """``y_error_style="band"``: the band between the y error ends, points sorted by x."""
95
+ e = errors_of(s, "y_error")
96
+ if e is None or s.resolve("y_error_style") != "band":
97
+ return None
98
+ e.bind(len(s.x), s, "y_error")
99
+ if "x" in s.cols.keys:
100
+ raise ValueError("Errorbar: y_error_style=\"band\" needs numbers in x, not "
101
+ "categories")
102
+ lo, hi = e.ends(s.y)
103
+ order = sorted(range(len(s.x)), key=lambda i: s.x[i])
104
+ return Table(x=array.array("d", (s.x[i] for i in order)),
105
+ y1=array.array("d", (lo[i] for i in order)),
106
+ y2=array.array("d", (hi[i] for i in order)))