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.
- plot_io/__init__.py +73 -0
- plot_io/_methods.py +1691 -0
- plot_io/_native/__init__.py +199 -0
- plot_io/_native/__main__.py +197 -0
- plot_io/_native/_api.py +457 -0
- plot_io/_native/bin/windows-arm64.dll +0 -0
- plot_io/_native/kernels.py +335 -0
- plot_io/_native/manifest.json +49 -0
- plot_io/axis.py +1347 -0
- plot_io/boxes.py +1156 -0
- plot_io/canvas.py +389 -0
- plot_io/color/__init__.py +3 -0
- plot_io/color/data.py +1646 -0
- plot_io/color/maps.py +922 -0
- plot_io/color/values.py +295 -0
- plot_io/color/variants.py +1246 -0
- plot_io/colorbar.py +1426 -0
- plot_io/columns.py +614 -0
- plot_io/decorations.py +920 -0
- plot_io/defaults.py +809 -0
- plot_io/density.py +113 -0
- plot_io/ext.py +68 -0
- plot_io/facets.py +240 -0
- plot_io/figure.py +788 -0
- plot_io/formatters.py +97 -0
- plot_io/genome/__init__.py +17 -0
- plot_io/genome/adapters.py +356 -0
- plot_io/genome/combine.py +123 -0
- plot_io/genome/contacts.py +508 -0
- plot_io/genome/intervals.py +750 -0
- plot_io/genome/lanes.py +191 -0
- plot_io/genome/layout.py +184 -0
- plot_io/genome/links.py +364 -0
- plot_io/genome/pack.py +27 -0
- plot_io/genome/readers.py +145 -0
- plot_io/genome/regions.py +228 -0
- plot_io/genome/scale.py +349 -0
- plot_io/genome/series.py +344 -0
- plot_io/genome/signal.py +621 -0
- plot_io/genome/sites.py +782 -0
- plot_io/genome/sources.py +459 -0
- plot_io/genome/testing.py +161 -0
- plot_io/geometry.py +289 -0
- plot_io/groups.py +308 -0
- plot_io/guides.py +503 -0
- plot_io/helpers/__init__.py +17 -0
- plot_io/helpers/clustermap.py +202 -0
- plot_io/helpers/dendrogram.py +172 -0
- plot_io/helpers/grids.py +304 -0
- plot_io/helpers/qq.py +334 -0
- plot_io/helpers/raincloud.py +249 -0
- plot_io/helpers/tree.py +214 -0
- plot_io/helpers/volcano.py +505 -0
- plot_io/hints.py +125 -0
- plot_io/kernels.py +740 -0
- plot_io/labels.py +566 -0
- plot_io/layers/__init__.py +100 -0
- plot_io/layers/base.py +630 -0
- plot_io/layers/context.py +87 -0
- plot_io/layers/core.py +446 -0
- plot_io/layers/geoms.py +1362 -0
- plot_io/layers/grammar.py +586 -0
- plot_io/layers/paint.py +614 -0
- plot_io/layers/pipeline.py +170 -0
- plot_io/layers/positions.py +431 -0
- plot_io/layers/stats.py +1088 -0
- plot_io/layout.py +1381 -0
- plot_io/legend.py +605 -0
- plot_io/levels.py +509 -0
- plot_io/limits.py +124 -0
- plot_io/lines.py +42 -0
- plot_io/mapped.py +522 -0
- plot_io/markers.py +110 -0
- plot_io/markup.py +128 -0
- plot_io/numbers.py +354 -0
- plot_io/occupancy.py +295 -0
- plot_io/plot.py +597 -0
- plot_io/presets/__init__.py +51 -0
- plot_io/presets/_util.py +153 -0
- plot_io/presets/area.py +145 -0
- plot_io/presets/bar.py +261 -0
- plot_io/presets/box.py +400 -0
- plot_io/presets/catpoints.py +177 -0
- plot_io/presets/contour.py +353 -0
- plot_io/presets/curves.py +30 -0
- plot_io/presets/dots.py +355 -0
- plot_io/presets/ecdf.py +97 -0
- plot_io/presets/errorbar.py +106 -0
- plot_io/presets/errors.py +296 -0
- plot_io/presets/fill.py +74 -0
- plot_io/presets/heatmap.py +322 -0
- plot_io/presets/hexbin.py +198 -0
- plot_io/presets/hist.py +391 -0
- plot_io/presets/hist2d.py +87 -0
- plot_io/presets/kde.py +162 -0
- plot_io/presets/kde2d.py +147 -0
- plot_io/presets/line.py +143 -0
- plot_io/presets/pie.py +502 -0
- plot_io/presets/regression.py +115 -0
- plot_io/presets/scatter.py +194 -0
- plot_io/presets/stem.py +100 -0
- plot_io/presets/violin.py +323 -0
- plot_io/py.typed +0 -0
- plot_io/registry.py +82 -0
- plot_io/render.py +480 -0
- plot_io/rows.py +491 -0
- plot_io/scales.py +103 -0
- plot_io/seams.py +138 -0
- plot_io/settings.py +807 -0
- plot_io/shapes.py +66 -0
- plot_io/sharing.py +232 -0
- plot_io/sizemaps.py +97 -0
- plot_io/spec.py +240 -0
- plot_io/states.py +216 -0
- plot_io/styles/__init__.py +307 -0
- plot_io/styles/library.py +397 -0
- plot_io/tables.py +138 -0
- plot_io/text.py +412 -0
- plot_io/ticks.py +400 -0
- plot_io/times.py +287 -0
- plot_io/titles.py +225 -0
- plot_io/transform.py +114 -0
- plot_io/types.py +13604 -0
- plot_io/units.py +768 -0
- plot_io/validators.py +1029 -0
- plot_io/values.py +190 -0
- plot_io-0.12.0.dist-info/METADATA +215 -0
- plot_io-0.12.0.dist-info/RECORD +130 -0
- plot_io-0.12.0.dist-info/WHEEL +4 -0
- plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
plot_io/presets/dots.py
ADDED
|
@@ -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
|
plot_io/presets/ecdf.py
ADDED
|
@@ -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)))
|