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/hist.py
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""Histograms: counts (or densities…) of values in bins (stat ``bin``), as bars
|
|
2
|
+
(``Rects``) or steps (``Band`` and ``Path``)."""
|
|
3
|
+
|
|
4
|
+
import array
|
|
5
|
+
import math
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .._methods import HistMixin
|
|
9
|
+
from .. import columns, groups, kernels
|
|
10
|
+
from ..color.values import with_alpha
|
|
11
|
+
from ..labels import bar_labels, check_labels, label_size
|
|
12
|
+
from ..layers import stats
|
|
13
|
+
from ..layers.base import Series, common
|
|
14
|
+
from ..layers.core import Layer, Table
|
|
15
|
+
from ..layers.geoms import GEOMS, RECTS
|
|
16
|
+
from ..layers.grammar import Lay, Spec, axis_of, flipped
|
|
17
|
+
from ..layers.paint import Paint, base_colors, role
|
|
18
|
+
from ..layers.positions import Dodge, bin_parts, check_stack
|
|
19
|
+
from ..numbers import NumberStyle, check_format, format_numbers
|
|
20
|
+
from ..settings import Setting, boolean, either, literal, number, one_of, pair, table
|
|
21
|
+
from ..text import font_of
|
|
22
|
+
from ..transform import settings_both as coord_settings
|
|
23
|
+
from ._util import faded, placed_text, set_color_value
|
|
24
|
+
|
|
25
|
+
__all__ = ["Hist"]
|
|
26
|
+
|
|
27
|
+
_STATS = ("count", "density", "probability", "percent", "frequency")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _auto_fill_opacity(h: "Hist") -> Any:
|
|
31
|
+
"""``fill_opacity="auto"``: 0.5 for overlaid sets, else 1."""
|
|
32
|
+
if len(h.cols.sets) > 1 and not h.resolve("stack") and not h.resolve("dodge"):
|
|
33
|
+
return 0.5
|
|
34
|
+
return 1.0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Hist(HistMixin, Series):
|
|
38
|
+
"""A histogram of ``values``: one array, or several (a dict or a list of arrays) that
|
|
39
|
+
share the same bins.
|
|
40
|
+
|
|
41
|
+
Dates draw on a time axis. ``bins``: a count (at most 10⁶), edges, or numpy's rules
|
|
42
|
+
(``auto`` is numpy 2's; a rule asking for more than 10⁶ bins, or a count of bins
|
|
43
|
+
narrower than the floats of the range can tell apart, is an error). Bins are even on
|
|
44
|
+
the axis scale (``bins_scale="axis"``): on a log axis, in log space; edges or a
|
|
45
|
+
``range`` outside the axis' scale (0 on a log axis) are refused. ``stat`` turns counts
|
|
46
|
+
into densities (area 1), probabilities, percents or counts per unit, per unit of the
|
|
47
|
+
axis' space; ``cumulative`` accumulates them. Several sets overlay (``fill="auto"``:
|
|
48
|
+
opacity 0.5), stack (``stack=True``) or dodge (``dodge=True``). ``hist.stats``:
|
|
49
|
+
``edges``, ``counts`` and ``heights`` per set.
|
|
50
|
+
|
|
51
|
+
Example::
|
|
52
|
+
|
|
53
|
+
rng = random.Random(1)
|
|
54
|
+
p = plt.Plot(width=240, height=160)
|
|
55
|
+
p.hist({"a": [rng.gauss(0, 1) for _ in range(400)],
|
|
56
|
+
"b": [rng.gauss(1.5, 0.7) for _ in range(400)]}, bins=24)
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
SETTINGS = table(
|
|
60
|
+
**coord_settings(),
|
|
61
|
+
bins=Setting(stats.check_bins,
|
|
62
|
+
doc="A count, edges, or a rule: `auto`, `fd`, `sturges`, `scott`, `sqrt`, "
|
|
63
|
+
"`rice`, `doane` (numpy's)."),
|
|
64
|
+
range=Setting(either(literal(None), pair(number())), doc="Range of the bins; default "
|
|
65
|
+
"the data range."),
|
|
66
|
+
weights=Setting(columns.weights, doc="A weight per value (per set for several sets)."),
|
|
67
|
+
stat=Setting(one_of(*_STATS),
|
|
68
|
+
doc="`count`, `density` (area 1), `probability`, `percent` or "
|
|
69
|
+
"`frequency` (count per unit); densities and frequencies per unit "
|
|
70
|
+
"of the axis' space (log axes: per log unit, as `kde`)."),
|
|
71
|
+
cumulative=Setting(boolean, doc="Accumulate the bins."),
|
|
72
|
+
shape=Setting(one_of("bar", "step", "stepfilled"),
|
|
73
|
+
doc="`bar` (adjacent bars), `step` (outline) or `stepfilled`."),
|
|
74
|
+
bins_scale=Setting(one_of("axis", "linear"),
|
|
75
|
+
doc="`axis`: bins even on the axis scale (log axes: in log space); "
|
|
76
|
+
"`linear`: even in data units."),
|
|
77
|
+
orientation=Setting(one_of("vertical", "horizontal"),
|
|
78
|
+
doc="`vertical` (bins along x) or `horizontal`."),
|
|
79
|
+
stack=Setting(check_stack, doc="Stack the sets (and other stacked histograms "
|
|
80
|
+
"with the same bins)."),
|
|
81
|
+
dodge=Setting(boolean, doc="Put the sets side by side within each bin."),
|
|
82
|
+
color=Setting(set_color_value,
|
|
83
|
+
doc="Fill color, one per set (a list, or a dict by set name); default "
|
|
84
|
+
"the plot's colors."),
|
|
85
|
+
**groups.Stroke.embed("edge_",
|
|
86
|
+
only=("color", "line_width"),
|
|
87
|
+
docs={"color": "Bar outline color; default none.",
|
|
88
|
+
"line_width": "Bar outline width; default 0 (none)."}),
|
|
89
|
+
**groups.Stroke.embed("line_",
|
|
90
|
+
only=("color", "line_width"),
|
|
91
|
+
inherited=("line_width",),
|
|
92
|
+
docs={"color": "Step and outline color; default each set's "
|
|
93
|
+
"color.",
|
|
94
|
+
"line_width": "Step line width."}),
|
|
95
|
+
outline=Setting(boolean,
|
|
96
|
+
doc="`bar` and `stepfilled` shapes: the outer outline of each set, in "
|
|
97
|
+
"`line_color` (dodged bars: each bar's outline)."),
|
|
98
|
+
fill=Setting(groups.fill_value,
|
|
99
|
+
doc="Fill: `True` (each set's color), a color, or `False` (none)."),
|
|
100
|
+
fill_opacity=Setting(either(literal("auto"), number(0, 1)), auto=_auto_fill_opacity,
|
|
101
|
+
doc="Opacity of the fills, times `opacity` (lines keep "
|
|
102
|
+
"`opacity`); `\"auto\"`: 0.5 for overlaid sets, else 1."),
|
|
103
|
+
saturation=Setting(number(0, 1), doc="Saturation of the fills."),
|
|
104
|
+
value_labels=Setting(check_labels, doc="Value labels on bars (as `Bar.value_labels`)."),
|
|
105
|
+
value_label_format=Setting(check_format, doc="Number format of the value labels."),
|
|
106
|
+
**common("name", "opacity", "z", "rasterize", "simplify", "font_size", "font_family",
|
|
107
|
+
"text_color"),
|
|
108
|
+
**groups.NumberFormat.embed(),
|
|
109
|
+
)
|
|
110
|
+
DATA = {"values": "samples"}
|
|
111
|
+
|
|
112
|
+
def __init__(self, values: Any, plot: Any = None, **settings: Any) -> None:
|
|
113
|
+
super().__init__(plot=plot, **settings)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def sets(self) -> list[tuple[Any, Any]]:
|
|
117
|
+
"""``(name, values)`` of each set."""
|
|
118
|
+
return self.cols.sets
|
|
119
|
+
|
|
120
|
+
def _axis_scale(self) -> Any:
|
|
121
|
+
return stats.axis_scale(axis_of(self))
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def stats(self) -> dict[str, Any]:
|
|
125
|
+
"""``{"edges": bin edges, "heights": [per set], "counts": [per set]}`` (stacked
|
|
126
|
+
calls on the edges they share)."""
|
|
127
|
+
return stats.run(self, "bin")
|
|
128
|
+
|
|
129
|
+
def set_colors(self, ctx: Any) -> list[Any]:
|
|
130
|
+
"""Each set's color (saturation applied), before opacity."""
|
|
131
|
+
return base_colors(self, ctx, [k for k, _ in self.cols.sets])
|
|
132
|
+
|
|
133
|
+
def _fills(self, ctx: Any) -> list[Any]:
|
|
134
|
+
"""Each set's fill: ``fill``'s color at ``fill_opacity`` times ``opacity``."""
|
|
135
|
+
fc, fa = groups.fill_parts(self)
|
|
136
|
+
cols = self.set_colors(ctx)
|
|
137
|
+
if fc is not None:
|
|
138
|
+
cols = [ctx.color(fc)] * len(cols)
|
|
139
|
+
return faded(cols, self._opacity() * fa)
|
|
140
|
+
|
|
141
|
+
def _lines(self, ctx: Any) -> list[Any]:
|
|
142
|
+
"""Each set's line color at ``opacity``."""
|
|
143
|
+
return faded(self.set_colors(ctx), self._opacity())
|
|
144
|
+
|
|
145
|
+
def _opacity(self) -> float:
|
|
146
|
+
a = self.resolve("opacity")
|
|
147
|
+
return 1.0 if a is None else a
|
|
148
|
+
|
|
149
|
+
SPEC = Spec(
|
|
150
|
+
[Lay("rects", ("hist.bars", {"table": "#bars"}), table="bars", when=("shape", "bar"),
|
|
151
|
+
key=False, stat="bin", drop_empty=True,
|
|
152
|
+
position=lambda s: "stack" if s.resolve("stack") else
|
|
153
|
+
"dodge" if s._dodged() else "identity"),
|
|
154
|
+
# the legend symbol
|
|
155
|
+
Lay("path", ("stroke", {"color": "_key_strokes()"}), table="sets", per_set=True,
|
|
156
|
+
when=("shape", "step"), drawn=False),
|
|
157
|
+
Lay("rects", ("hist.key", {}), table="sets", per_set=True,
|
|
158
|
+
when=("shape", ("bar", "stepfilled")), drawn=False)],
|
|
159
|
+
tables={"sets": lambda s, b: _sets(s, b), "bars": lambda s, b: Table()},
|
|
160
|
+
entries="sets", key={"fill": "_key_fills()", "stroke": "_key_strokes()"},
|
|
161
|
+
color_count="sets", zero=("y",), titles={"values": "x"}, finish=lambda s, b: _finish(s, b))
|
|
162
|
+
|
|
163
|
+
def _dodged(self) -> bool:
|
|
164
|
+
return bool(self.resolve("dodge")) and len(self.cols.sets) > 1
|
|
165
|
+
|
|
166
|
+
# -- bars -----------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
def _bars(self, bars: Table, sets: list[Table], edges: Any) -> None:
|
|
169
|
+
"""The bins of every set in one table, set after set: edges ``x0``/``x1`` (dodged
|
|
170
|
+
sets: their part of the bin, even on the axis scale), bottoms and tops; ``owner``:
|
|
171
|
+
the set of each bin. Bins without height are drawn as none (``drop_empty``)."""
|
|
172
|
+
m = len(edges) - 1
|
|
173
|
+
lo_e, hi_e = edges[:-1], edges[1:]
|
|
174
|
+
x0, x1, y0, y1 = (array.array("d") for _ in range(4))
|
|
175
|
+
owner: list[int] = []
|
|
176
|
+
scale = self._axis_scale()
|
|
177
|
+
for k, s in enumerate(sets):
|
|
178
|
+
if s.dodge is not None:
|
|
179
|
+
a, b = bin_parts(s.dodge, lo_e, hi_e, scale)
|
|
180
|
+
x0.extend(a)
|
|
181
|
+
x1.extend(b)
|
|
182
|
+
else:
|
|
183
|
+
x0.extend(lo_e)
|
|
184
|
+
x1.extend(hi_e)
|
|
185
|
+
y0.extend(s["bottom"])
|
|
186
|
+
y1.extend(s["top"])
|
|
187
|
+
owner.extend([k] * m)
|
|
188
|
+
bars.update(x0=x0, x1=x1, y0=y0, y1=y1, owner=owner)
|
|
189
|
+
|
|
190
|
+
def _bar_paint(self, ctx: Any, bars: Table) -> dict[str, Any]:
|
|
191
|
+
fills_by = self._fills(ctx)
|
|
192
|
+
lc, lw = self.resolve("edge_color"), self.resolve("edge_width")
|
|
193
|
+
return {"fill": [fills_by[k] for k in bars["owner"]],
|
|
194
|
+
"stroke": with_alpha(ctx.color(lc), self._opacity()) if lc is not None and lw
|
|
195
|
+
else None,
|
|
196
|
+
"line_width": lw or None, "simplify": self.resolve("simplify")}
|
|
197
|
+
|
|
198
|
+
# -- steps and outlines ------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def _steps(edges: Any, s: Table) -> tuple[array.array, array.array, array.array]:
|
|
202
|
+
"""The step polylines of a set: bin coordinates (each bin's two edges), tops and
|
|
203
|
+
bottoms (each twice; bins without a finite value at 0)."""
|
|
204
|
+
m = len(edges) - 1
|
|
205
|
+
tb = array.array("d", bytes(16 * m))
|
|
206
|
+
tb[0::2] = edges[:-1]
|
|
207
|
+
tb[1::2] = edges[1:]
|
|
208
|
+
steps = []
|
|
209
|
+
for col in ("top", "bottom"):
|
|
210
|
+
v = kernels.scaled(s[col], 1.0, 0.0, None, nan=0.0)
|
|
211
|
+
out = array.array("d", bytes(16 * m))
|
|
212
|
+
out[0::2] = v
|
|
213
|
+
out[1::2] = v
|
|
214
|
+
steps.append(out)
|
|
215
|
+
return tb, steps[0], steps[1]
|
|
216
|
+
|
|
217
|
+
def _outline_colors(self, ctx: Any) -> list[Any]:
|
|
218
|
+
"""Each set's step and outline color: ``line_color`` at ``opacity``, else the
|
|
219
|
+
set's line color."""
|
|
220
|
+
lc = self.resolve("line_color")
|
|
221
|
+
if lc is None:
|
|
222
|
+
return self._lines(ctx)
|
|
223
|
+
return [with_alpha(ctx.color(lc), self._opacity())] * len(self.cols.sets)
|
|
224
|
+
|
|
225
|
+
def _outline_layer(self, s: Table, edges: Any, k: int, flip: bool) -> Layer:
|
|
226
|
+
"""A set's outer outline: down to the bottom at both ends, along the tops."""
|
|
227
|
+
tb, tv, bv = self._steps(edges, s)
|
|
228
|
+
b = array.array("d", [tb[0]]) if len(tb) else array.array("d")
|
|
229
|
+
b.extend(tb)
|
|
230
|
+
v = array.array("d", [bv[0]]) if len(bv) else array.array("d")
|
|
231
|
+
v.extend(tv)
|
|
232
|
+
if len(tb):
|
|
233
|
+
b.append(tb[-1])
|
|
234
|
+
v.append(bv[-1])
|
|
235
|
+
t = Table(x=b, y=v)
|
|
236
|
+
return Layer(GEOMS["path"], t, paint=Paint(self, "stroke", color="_outline_colors()",
|
|
237
|
+
index=k), flip=flip, key=False, ends=True)
|
|
238
|
+
|
|
239
|
+
# -- labels ---------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
def _label_items(self, ctx: Any, bars: Layer) -> list[Any]:
|
|
242
|
+
if self.resolve("shape") != "bar":
|
|
243
|
+
return []
|
|
244
|
+
lay = bars.columns(ctx)
|
|
245
|
+
rects = RECTS.page_rects(ctx, lay)
|
|
246
|
+
keep = RECTS.kept(lay, rects)
|
|
247
|
+
fills = self._bar_paint(ctx, bars.table)["fill"]
|
|
248
|
+
t = bars.table
|
|
249
|
+
texts = format_numbers([t["y1"][i] - t["y0"][i] for i in keep],
|
|
250
|
+
self.resolve("value_label_format"), NumberStyle.of(self))
|
|
251
|
+
font = font_of(self, self.resolve("font_size"),
|
|
252
|
+
color=ctx.color(self.resolve("text_color")))
|
|
253
|
+
return bar_labels(ctx.canvas, [rects[i] for i in keep], texts, [fills[i] for i in keep],
|
|
254
|
+
self.resolve("value_labels"), bars.flip, font,
|
|
255
|
+
stacked=bool(self.resolve("stack")))
|
|
256
|
+
|
|
257
|
+
def _label_room(self, fonts: Any, role: str, sets: list[Table],
|
|
258
|
+
val: str) -> list[tuple[float, float, int]]:
|
|
259
|
+
if self.resolve("value_labels") != "outside" or self.resolve("shape") != "bar" \
|
|
260
|
+
or role != val:
|
|
261
|
+
return []
|
|
262
|
+
pairs = [(lo, hi) for s in sets for lo, hi in zip(s["bottom"], s["top"])
|
|
263
|
+
if math.isfinite(lo) and math.isfinite(hi) and hi != lo]
|
|
264
|
+
texts = format_numbers([hi - lo for lo, hi in pairs], self.resolve("value_label_format"),
|
|
265
|
+
NumberStyle.of(self))
|
|
266
|
+
font = font_of(self, self.resolve("font_size"))
|
|
267
|
+
horizontal = flipped(self)
|
|
268
|
+
return [(hi, label_size(fonts, text, font, horizontal), -1 if hi < lo else 1)
|
|
269
|
+
for (lo, hi), text in zip(pairs, texts) if text not in (None, "")]
|
|
270
|
+
|
|
271
|
+
# -- legend ---------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
def _key_strokes(self, ctx: Any) -> list[Any]:
|
|
274
|
+
"""Each set's symbol outline: the step line, the bar edges or the outline."""
|
|
275
|
+
n = len(self.cols.sets)
|
|
276
|
+
if self.resolve("shape") == "step":
|
|
277
|
+
return self._lines(ctx)
|
|
278
|
+
lc = self.resolve("edge_color")
|
|
279
|
+
if lc is not None and self.resolve("edge_width"):
|
|
280
|
+
return [with_alpha(ctx.color(lc), self._opacity())] * n
|
|
281
|
+
if self.resolve("outline") and self.resolve("line_width"):
|
|
282
|
+
return self._outline_colors(ctx)
|
|
283
|
+
return [None] * n
|
|
284
|
+
|
|
285
|
+
def _key_fills(self, ctx: Any) -> list[Any]:
|
|
286
|
+
return [None] * len(self.cols.sets) if self.resolve("shape") == "step" \
|
|
287
|
+
else self._fills(ctx)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@role("hist.bars")
|
|
291
|
+
def _bars_paint(s: Hist, ctx: Any, glyph: bool, table: Any = None) -> dict[str, Any]:
|
|
292
|
+
return s._bar_paint(ctx, table)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@role("hist.key")
|
|
296
|
+
def _key_paint(s: Hist, ctx: Any, glyph: bool, index: int = 0) -> dict[str, Any]:
|
|
297
|
+
"""A set's legend rectangle."""
|
|
298
|
+
edge = s.resolve("edge_color") is not None and s.resolve("edge_width")
|
|
299
|
+
fills, strokes = s._key_fills(ctx), s._key_strokes(ctx)
|
|
300
|
+
return {"fill": fills[index] if index < len(fills) else None,
|
|
301
|
+
"stroke": strokes[index] if index < len(strokes) else None,
|
|
302
|
+
"line_width": s.resolve("edge_width" if edge else "line_width")}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@role("hist.outlines")
|
|
306
|
+
def _outlines_paint(s: Hist, ctx: Any, glyph: bool, table: Any = None) -> dict[str, Any]:
|
|
307
|
+
"""Dodged bars' outlines: ``line_color`` at ``opacity``, else each set's line color."""
|
|
308
|
+
c = s._outline_colors(ctx)
|
|
309
|
+
return {"fill": None, "stroke": [c[k] for k in table["owner"]],
|
|
310
|
+
"line_width": s.resolve("line_width"), "simplify": s.resolve("simplify")}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _sets(s: Hist, b: Any) -> list[Table]:
|
|
314
|
+
"""A table per set: the heights of the bins, stacked (with the plot's stacked
|
|
315
|
+
histograms), dodged in the bins, or from 0."""
|
|
316
|
+
st = s.stats
|
|
317
|
+
edges = st["edges"]
|
|
318
|
+
stack = s.resolve("stack")
|
|
319
|
+
keys = [(round(a, 12), round(b_, 12)) for a, b_ in zip(edges, edges[1:])]
|
|
320
|
+
dodge = s._dodged() and s.resolve("shape") == "bar"
|
|
321
|
+
out = []
|
|
322
|
+
for k, h in enumerate(st["heights"]):
|
|
323
|
+
t = Table(keys=keys, heights=h)
|
|
324
|
+
if stack:
|
|
325
|
+
t.stack = (("hist", b.flip), stack)
|
|
326
|
+
else:
|
|
327
|
+
t["bottom"], t["top"] = [0.0] * len(h), list(h)
|
|
328
|
+
if dodge:
|
|
329
|
+
t.dodge = Dodge(s, ("hist", id(s)), b.flip, keys, token=k)
|
|
330
|
+
out.append(t)
|
|
331
|
+
return out
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _finish(s: Hist, b: Any) -> None:
|
|
335
|
+
"""The layers that need the stacked values (after the plot's positions), the value
|
|
336
|
+
labels, the value axis' extent."""
|
|
337
|
+
call, flip = b.call, b.flip
|
|
338
|
+
sets = b.tables["sets"]
|
|
339
|
+
edges = s.stats["edges"]
|
|
340
|
+
call.tables = sets
|
|
341
|
+
shape = s.resolve("shape")
|
|
342
|
+
val = b.role("y")
|
|
343
|
+
if shape == "bar":
|
|
344
|
+
bars = b.tables["bars"]
|
|
345
|
+
layer = call.layers[0]
|
|
346
|
+
|
|
347
|
+
def bar_layers() -> None:
|
|
348
|
+
s._bars(bars, sets, edges)
|
|
349
|
+
if s.resolve("outline") and s.resolve("line_width"):
|
|
350
|
+
if s._dodged():
|
|
351
|
+
call.layers.append(Layer(GEOMS["rects"], bars,
|
|
352
|
+
paint=Paint(s, "hist.outlines", table=bars),
|
|
353
|
+
flip=flip, occupy=False, key=False,
|
|
354
|
+
drop_empty=True))
|
|
355
|
+
else:
|
|
356
|
+
for k, t in enumerate(sets):
|
|
357
|
+
call.layers.append(s._outline_layer(t, edges, k, flip))
|
|
358
|
+
if s.resolve("value_labels"):
|
|
359
|
+
call.layers.append(placed_text(
|
|
360
|
+
s, id(s), lambda ctx: s._label_items(ctx, layer),
|
|
361
|
+
lambda ctx: font_of(s, s.resolve("font_size"),
|
|
362
|
+
color=ctx.color(s.resolve("text_color")))))
|
|
363
|
+
call.after.append(bar_layers)
|
|
364
|
+
call.room = lambda fonts, role: s._label_room(fonts, role, sets, val)
|
|
365
|
+
else:
|
|
366
|
+
def step_layers() -> None:
|
|
367
|
+
stack = s.resolve("stack")
|
|
368
|
+
lw = s.resolve("line_width")
|
|
369
|
+
for k, t in enumerate(sets):
|
|
370
|
+
tb, tv, bv = s._steps(edges, t)
|
|
371
|
+
if not len(tb):
|
|
372
|
+
continue
|
|
373
|
+
t["step_t"], t["step_top"], t["step_bottom"] = tb, tv, bv
|
|
374
|
+
if shape == "stepfilled":
|
|
375
|
+
call.layers.append(Layer(GEOMS["band"], t, aes={
|
|
376
|
+
"x": "step_t", "y1": "step_top", "y2": "step_bottom"},
|
|
377
|
+
paint=Paint(s, "band", color="_fills()", index=k), flip=flip,
|
|
378
|
+
stat="bin", position="stack" if stack else "identity", key=False))
|
|
379
|
+
if lw and (shape == "step" or s.resolve("outline")):
|
|
380
|
+
call.layers.append(s._outline_layer(t, edges, k, flip))
|
|
381
|
+
call.after.append(step_layers)
|
|
382
|
+
allv = array.array("d", [0.0])
|
|
383
|
+
|
|
384
|
+
def value_extent() -> None:
|
|
385
|
+
for t in sets:
|
|
386
|
+
allv.extend(x for x in t["top"] if math.isfinite(x))
|
|
387
|
+
allv.extend(x for x in t["bottom"] if math.isfinite(x))
|
|
388
|
+
call.after.insert(0, value_extent)
|
|
389
|
+
call.extra[val] = allv
|
|
390
|
+
call.extra[b.role("x")] = edges
|
|
391
|
+
call.sticky_values[val] = [0.0]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""2-D histograms (stat ``bin2d``): counts of points in rectangular bins, drawn as a
|
|
2
|
+
heatmap (``Cells``)."""
|
|
3
|
+
|
|
4
|
+
import array
|
|
5
|
+
import numbers
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .._methods import Hist2dMixin
|
|
9
|
+
from .. import columns
|
|
10
|
+
from ..layers import stats
|
|
11
|
+
from ..layers.base import Series
|
|
12
|
+
from ..settings import Setting, either, literal, number, one_of, pair, table
|
|
13
|
+
from ..validators import real
|
|
14
|
+
from .heatmap import Coords, Heatmap
|
|
15
|
+
|
|
16
|
+
__all__ = ["Hist2d"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _bins2(v: Any) -> Any:
|
|
20
|
+
if isinstance(v, (tuple, list)) and len(v) == 2 and not all(
|
|
21
|
+
real(x) is not None for x in v):
|
|
22
|
+
return (stats.check_bins(v[0]), stats.check_bins(v[1]))
|
|
23
|
+
if isinstance(v, (tuple, list)) and len(v) == 2 and all(
|
|
24
|
+
isinstance(x, numbers.Integral) and not isinstance(x, bool) for x in v):
|
|
25
|
+
return (stats.check_bins(v[0]), stats.check_bins(v[1]))
|
|
26
|
+
b = stats.check_bins(v)
|
|
27
|
+
return (b, b)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _range2(v: Any) -> Any:
|
|
31
|
+
return (pair(number())(v[0]), pair(number())(v[1]))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Hist2d(Hist2dMixin, Heatmap):
|
|
35
|
+
"""A 2-D histogram of points ``(x, y)``: counts in bins (even on the axis scales),
|
|
36
|
+
drawn as a heatmap; bins with fewer than ``min_count`` points are left empty.
|
|
37
|
+
|
|
38
|
+
Drawn with :class:`Heatmap`'s settings; ``stat="density"`` is per unit area of the axes'
|
|
39
|
+
space; a colorbar source.
|
|
40
|
+
|
|
41
|
+
Example::
|
|
42
|
+
|
|
43
|
+
rng = random.Random(1)
|
|
44
|
+
x = [rng.gauss(0, 1) for _ in range(1000)]
|
|
45
|
+
p = plt.Plot(width=240, height=160)
|
|
46
|
+
p.hist2d(x, [v + rng.gauss(0, 0.7) for v in x], bins=20)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
SETTINGS = table(
|
|
50
|
+
**{k: v for k, v in Heatmap.SETTINGS.items() if k not in ("origin",)},
|
|
51
|
+
bins=Setting(_bins2,
|
|
52
|
+
doc="Bins of both axes, or `(x bins, y bins)`: counts, edges or rules "
|
|
53
|
+
"(as `Hist`)."),
|
|
54
|
+
range=Setting(either(literal(None), _range2),
|
|
55
|
+
doc="`((x0, x1), (y0, y1))`; default the data range."),
|
|
56
|
+
weights=Setting(lambda v: columns.numbers(v, "weights"), doc="A weight per point."),
|
|
57
|
+
stat=Setting(one_of("count", "density", "probability", "percent"),
|
|
58
|
+
doc="`count`, `density` (volume 1), `probability` or `percent`."),
|
|
59
|
+
min_count=Setting(number(lo=0), doc="Bins below this count are transparent."),
|
|
60
|
+
origin=Setting(one_of("top", "bottom"), doc="Row of the first y bin."),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
DATA = {"x": "numbers", "y": "numbers"}
|
|
64
|
+
|
|
65
|
+
def __init__(self, x: Any, y: Any, plot: Any = None, **settings: Any) -> None:
|
|
66
|
+
self.k = 1
|
|
67
|
+
Series.__init__(self, plot=plot, **settings)
|
|
68
|
+
|
|
69
|
+
def _inverts(self) -> bool:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def stats(self) -> dict[str, Any]:
|
|
74
|
+
"""``{"x_edges", "y_edges", "counts": rows (first y bin first), "values": rows}``."""
|
|
75
|
+
sx = stats.axis_scale(self.plot.x_axis)
|
|
76
|
+
sy = stats.axis_scale(self.plot.y_axis)
|
|
77
|
+
return stats.compute(self, "bin2d", (sx, sy))
|
|
78
|
+
|
|
79
|
+
def _grid(self) -> tuple[Any, int, int, int, Coords, Coords]:
|
|
80
|
+
st = self.stats
|
|
81
|
+
rows = st["values"]
|
|
82
|
+
flat = array.array("d")
|
|
83
|
+
for r in rows:
|
|
84
|
+
flat.extend(r)
|
|
85
|
+
h = len(rows)
|
|
86
|
+
w = len(rows[0]) if rows else 0
|
|
87
|
+
return flat, h, w, 1, Coords(st["x_edges"], w, "x"), Coords(st["y_edges"], h, "y")
|
plot_io/presets/kde.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Kernel density estimates (stat ``kde``): a curve per set (``Path``), filled
|
|
2
|
+
(``Band``) or stacked."""
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .._methods import KdeMixin
|
|
8
|
+
from .. import columns, groups
|
|
9
|
+
from ..color.values import with_alpha
|
|
10
|
+
from ..layers import stats
|
|
11
|
+
from ..layers.base import Series, common
|
|
12
|
+
from ..layers.core import Table
|
|
13
|
+
from ..layers.grammar import Lay, Spec
|
|
14
|
+
from ..layers.positions import check_stack
|
|
15
|
+
from ..settings import Setting, boolean, either, literal, number, one_of, pair, table
|
|
16
|
+
from ..transform import settings_both as coord_settings
|
|
17
|
+
from ..layers.paint import base_colors
|
|
18
|
+
from ._util import set_color_value
|
|
19
|
+
|
|
20
|
+
__all__ = ["Kde", "check_bandwidth"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def check_bandwidth(v: Any) -> Any:
|
|
24
|
+
if isinstance(v, str):
|
|
25
|
+
return one_of("scott", "silverman")(v)
|
|
26
|
+
return number(positive=True)(v)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Kde(KdeMixin, Series):
|
|
30
|
+
"""A Gaussian kernel density estimate of ``values`` (one array, or several as a dict or
|
|
31
|
+
a list of arrays, on a common grid).
|
|
32
|
+
|
|
33
|
+
With ``y``, ``Kde(x, y)`` is a :class:`Kde2d`. Each set's density is on a common grid
|
|
34
|
+
of ``points`` points, extending ``cut`` bandwidths past the data (within ``range``);
|
|
35
|
+
dates draw on a time axis. ``fill`` fills under each curve (``True``, or a color, at
|
|
36
|
+
``fill_opacity``). ``bandwidth``: ``scott``, ``silverman`` (as scipy's
|
|
37
|
+
``gaussian_kde``) or a number, times ``adjust``. On log axes the density is computed in
|
|
38
|
+
log space, per log unit. A set without a bandwidth (no values, one, all equal) or a
|
|
39
|
+
``range`` outside the data has no curve (NaN densities), with a warning.
|
|
40
|
+
``kde.stats``: ``grid``, ``densities``, ``bandwidths``.
|
|
41
|
+
|
|
42
|
+
Example::
|
|
43
|
+
|
|
44
|
+
rng = random.Random(1)
|
|
45
|
+
p = plt.Plot(width=240, height=160)
|
|
46
|
+
p.kde({"a": [rng.gauss(0, 1) for _ in range(200)],
|
|
47
|
+
"b": [rng.gauss(1.5, 0.7) for _ in range(200)]}, fill=True)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
SETTINGS = table(
|
|
51
|
+
**coord_settings(),
|
|
52
|
+
bandwidth=Setting(check_bandwidth,
|
|
53
|
+
doc="`scott`, `silverman`, or a bandwidth in data units."),
|
|
54
|
+
adjust=Setting(number(positive=True), doc="Factor applied to the bandwidth."),
|
|
55
|
+
cut=Setting(number(lo=0), doc="The curve extends this many bandwidths past the "
|
|
56
|
+
"data."),
|
|
57
|
+
range=Setting(either(literal(None), pair(number())),
|
|
58
|
+
doc="Limits of the curve (within the data plus `cut`); default none."),
|
|
59
|
+
points=Setting(number(lo=16, hi=65536, integer=True),
|
|
60
|
+
doc="Number of points of the curve."),
|
|
61
|
+
cumulative=Setting(boolean, doc="Plot the cumulative distribution."),
|
|
62
|
+
**groups.FillStyle.embed(docs={"fill": "Fill under the curve: `True` (its color), a "
|
|
63
|
+
"color, or `False` (stacked curves are "
|
|
64
|
+
"filled anyway)."}),
|
|
65
|
+
weights=Setting(columns.weights, doc="A weight per value (per set for several sets)."),
|
|
66
|
+
orientation=Setting(one_of("vertical", "horizontal"),
|
|
67
|
+
doc="`vertical` (values along x) or `horizontal`."),
|
|
68
|
+
stack=Setting(check_stack, doc="Stack the sets (seaborn's `multiple=\"stack\"`)."),
|
|
69
|
+
color=Setting(set_color_value,
|
|
70
|
+
doc="Color, one per set (a list, or a dict by set name); default "
|
|
71
|
+
"the plot's colors."),
|
|
72
|
+
**groups.Stroke.embed(only=("line_width", "line_style"),
|
|
73
|
+
inherited=("line_width",),
|
|
74
|
+
docs={"line_style": "Line style."}),
|
|
75
|
+
**common("name", "opacity", "z", "rasterize", "simplify", "font_size", "font_family",
|
|
76
|
+
"text_color"),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
DATA = {"values": "samples"}
|
|
80
|
+
|
|
81
|
+
def __init__(self, values: Any, plot: Any = None, **settings: Any) -> None:
|
|
82
|
+
super().__init__(plot=plot, **settings)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def stats(self) -> dict[str, Any]:
|
|
86
|
+
"""``{"grid": values, "densities": [per set], "bandwidths": [per set]}`` (densities
|
|
87
|
+
per unit of the axis scale; NaN for a set without a bandwidth; stacked calls on the
|
|
88
|
+
grid they share)."""
|
|
89
|
+
return stats.run(self, "kde")
|
|
90
|
+
|
|
91
|
+
def colors(self, ctx: Any) -> list[Any]:
|
|
92
|
+
return base_colors(self, ctx, [k for k, _ in self.cols.sets])
|
|
93
|
+
|
|
94
|
+
def _fill(self) -> tuple[Any, float, bool]:
|
|
95
|
+
"""``(fill color or None, fill opacity times opacity, filled)``."""
|
|
96
|
+
fill_color, fill_opacity = groups.fill_parts(self)
|
|
97
|
+
stack = self.resolve("stack")
|
|
98
|
+
if stack and self.resolve("fill") is False:
|
|
99
|
+
fill_opacity = self.resolve("fill_opacity")
|
|
100
|
+
opacity = self.resolve("opacity")
|
|
101
|
+
fill_opacity *= 1.0 if opacity is None else opacity
|
|
102
|
+
return fill_color, fill_opacity, fill_opacity > 0 or bool(stack)
|
|
103
|
+
|
|
104
|
+
def _filled(self) -> bool:
|
|
105
|
+
return self._fill()[2]
|
|
106
|
+
|
|
107
|
+
def _fills(self, ctx: Any) -> list[Any]:
|
|
108
|
+
"""Each set's fill: its color (or ``fill``'s) at the fill opacity."""
|
|
109
|
+
fill_color, fill_opacity, _ = self._fill()
|
|
110
|
+
out = []
|
|
111
|
+
for c in self.colors(ctx):
|
|
112
|
+
c = c if fill_color is None else ctx.color(fill_color)
|
|
113
|
+
out.append(with_alpha(c, fill_opacity) if c is not None else None)
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
def _key_fills(self, ctx: Any) -> list[Any]:
|
|
117
|
+
return self._fills(ctx) if self._filled() else [None] * len(self.cols.sets)
|
|
118
|
+
|
|
119
|
+
def _lines(self, ctx: Any) -> list[Any]:
|
|
120
|
+
opacity = self.resolve("opacity")
|
|
121
|
+
return [with_alpha(c, 1.0 if opacity is None else opacity) for c in self.colors(ctx)]
|
|
122
|
+
|
|
123
|
+
SPEC = Spec(
|
|
124
|
+
[Lay("band", ("band", {"color": "_fills()", "extra": ("simplify",)}), table="sets",
|
|
125
|
+
per_set=True,
|
|
126
|
+
when="_filled()", aes={"y1": "top", "y2": "bottom"}, stat="kde",
|
|
127
|
+
position=lambda s: "stack" if s.resolve("stack") else "identity"),
|
|
128
|
+
Lay("path", ("stroke", {"color": "_lines()", "dash": True, "extra": ("simplify",)}),
|
|
129
|
+
table="sets", per_set=True, aes={"y": "top"}, stat="kde",
|
|
130
|
+
position=lambda s: "stack" if s.resolve("stack") else "identity",
|
|
131
|
+
occupy=lambda s: not s._filled(),
|
|
132
|
+
glyph=lambda s: "top" if s._filled() else "line")],
|
|
133
|
+
tables={"sets": lambda s, b: _sets(s, b)},
|
|
134
|
+
entries="sets", key={"fill": "_key_fills()", "stroke": "colors()"}, color_count="sets",
|
|
135
|
+
zero=("y",), titles={"values": "x"}, finish=lambda s, b: _value_extent(b))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _sets(s: Kde, b: Any) -> list[Table]:
|
|
139
|
+
"""A table per set: the densities on the grid (stacked or from 0: the position)."""
|
|
140
|
+
st = b.stats["kde"]
|
|
141
|
+
g = st["grid"]
|
|
142
|
+
keys = [round(v, 12) for v in g]
|
|
143
|
+
return [Table(x=g, keys=keys, heights=d) for d in st["densities"]]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _value_extent(b: Any) -> None:
|
|
147
|
+
"""The densities' axis includes 0 and the stacked tops."""
|
|
148
|
+
tops: list[float] = [0.0]
|
|
149
|
+
|
|
150
|
+
def extent() -> None:
|
|
151
|
+
for t in b.tables["sets"]:
|
|
152
|
+
tops.extend(v for v in t["top"] if math.isfinite(v))
|
|
153
|
+
b.call.after.append(extent)
|
|
154
|
+
b.call.extra[b.role("y")] = tops
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _kde2d() -> type:
|
|
158
|
+
from .kde2d import Kde2d
|
|
159
|
+
return Kde2d
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
Kde.with_y = staticmethod(_kde2d) # type: ignore[attr-defined]
|