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,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]