plot-io 0.12.0__py3-none-win_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. plot_io/__init__.py +73 -0
  2. plot_io/_methods.py +1691 -0
  3. plot_io/_native/__init__.py +199 -0
  4. plot_io/_native/__main__.py +197 -0
  5. plot_io/_native/_api.py +457 -0
  6. plot_io/_native/bin/windows-arm64.dll +0 -0
  7. plot_io/_native/kernels.py +335 -0
  8. plot_io/_native/manifest.json +49 -0
  9. plot_io/axis.py +1347 -0
  10. plot_io/boxes.py +1156 -0
  11. plot_io/canvas.py +389 -0
  12. plot_io/color/__init__.py +3 -0
  13. plot_io/color/data.py +1646 -0
  14. plot_io/color/maps.py +922 -0
  15. plot_io/color/values.py +295 -0
  16. plot_io/color/variants.py +1246 -0
  17. plot_io/colorbar.py +1426 -0
  18. plot_io/columns.py +614 -0
  19. plot_io/decorations.py +920 -0
  20. plot_io/defaults.py +809 -0
  21. plot_io/density.py +113 -0
  22. plot_io/ext.py +68 -0
  23. plot_io/facets.py +240 -0
  24. plot_io/figure.py +788 -0
  25. plot_io/formatters.py +97 -0
  26. plot_io/genome/__init__.py +17 -0
  27. plot_io/genome/adapters.py +356 -0
  28. plot_io/genome/combine.py +123 -0
  29. plot_io/genome/contacts.py +508 -0
  30. plot_io/genome/intervals.py +750 -0
  31. plot_io/genome/lanes.py +191 -0
  32. plot_io/genome/layout.py +184 -0
  33. plot_io/genome/links.py +364 -0
  34. plot_io/genome/pack.py +27 -0
  35. plot_io/genome/readers.py +145 -0
  36. plot_io/genome/regions.py +228 -0
  37. plot_io/genome/scale.py +349 -0
  38. plot_io/genome/series.py +344 -0
  39. plot_io/genome/signal.py +621 -0
  40. plot_io/genome/sites.py +782 -0
  41. plot_io/genome/sources.py +459 -0
  42. plot_io/genome/testing.py +161 -0
  43. plot_io/geometry.py +289 -0
  44. plot_io/groups.py +308 -0
  45. plot_io/guides.py +503 -0
  46. plot_io/helpers/__init__.py +17 -0
  47. plot_io/helpers/clustermap.py +202 -0
  48. plot_io/helpers/dendrogram.py +172 -0
  49. plot_io/helpers/grids.py +304 -0
  50. plot_io/helpers/qq.py +334 -0
  51. plot_io/helpers/raincloud.py +249 -0
  52. plot_io/helpers/tree.py +214 -0
  53. plot_io/helpers/volcano.py +505 -0
  54. plot_io/hints.py +125 -0
  55. plot_io/kernels.py +740 -0
  56. plot_io/labels.py +566 -0
  57. plot_io/layers/__init__.py +100 -0
  58. plot_io/layers/base.py +630 -0
  59. plot_io/layers/context.py +87 -0
  60. plot_io/layers/core.py +446 -0
  61. plot_io/layers/geoms.py +1362 -0
  62. plot_io/layers/grammar.py +586 -0
  63. plot_io/layers/paint.py +614 -0
  64. plot_io/layers/pipeline.py +170 -0
  65. plot_io/layers/positions.py +431 -0
  66. plot_io/layers/stats.py +1088 -0
  67. plot_io/layout.py +1381 -0
  68. plot_io/legend.py +605 -0
  69. plot_io/levels.py +509 -0
  70. plot_io/limits.py +124 -0
  71. plot_io/lines.py +42 -0
  72. plot_io/mapped.py +522 -0
  73. plot_io/markers.py +110 -0
  74. plot_io/markup.py +128 -0
  75. plot_io/numbers.py +354 -0
  76. plot_io/occupancy.py +295 -0
  77. plot_io/plot.py +597 -0
  78. plot_io/presets/__init__.py +51 -0
  79. plot_io/presets/_util.py +153 -0
  80. plot_io/presets/area.py +145 -0
  81. plot_io/presets/bar.py +261 -0
  82. plot_io/presets/box.py +400 -0
  83. plot_io/presets/catpoints.py +177 -0
  84. plot_io/presets/contour.py +353 -0
  85. plot_io/presets/curves.py +30 -0
  86. plot_io/presets/dots.py +355 -0
  87. plot_io/presets/ecdf.py +97 -0
  88. plot_io/presets/errorbar.py +106 -0
  89. plot_io/presets/errors.py +296 -0
  90. plot_io/presets/fill.py +74 -0
  91. plot_io/presets/heatmap.py +322 -0
  92. plot_io/presets/hexbin.py +198 -0
  93. plot_io/presets/hist.py +391 -0
  94. plot_io/presets/hist2d.py +87 -0
  95. plot_io/presets/kde.py +162 -0
  96. plot_io/presets/kde2d.py +147 -0
  97. plot_io/presets/line.py +143 -0
  98. plot_io/presets/pie.py +502 -0
  99. plot_io/presets/regression.py +115 -0
  100. plot_io/presets/scatter.py +194 -0
  101. plot_io/presets/stem.py +100 -0
  102. plot_io/presets/violin.py +323 -0
  103. plot_io/py.typed +0 -0
  104. plot_io/registry.py +82 -0
  105. plot_io/render.py +480 -0
  106. plot_io/rows.py +491 -0
  107. plot_io/scales.py +103 -0
  108. plot_io/seams.py +138 -0
  109. plot_io/settings.py +807 -0
  110. plot_io/shapes.py +66 -0
  111. plot_io/sharing.py +232 -0
  112. plot_io/sizemaps.py +97 -0
  113. plot_io/spec.py +240 -0
  114. plot_io/states.py +216 -0
  115. plot_io/styles/__init__.py +307 -0
  116. plot_io/styles/library.py +397 -0
  117. plot_io/tables.py +138 -0
  118. plot_io/text.py +412 -0
  119. plot_io/ticks.py +400 -0
  120. plot_io/times.py +287 -0
  121. plot_io/titles.py +225 -0
  122. plot_io/transform.py +114 -0
  123. plot_io/types.py +13604 -0
  124. plot_io/units.py +768 -0
  125. plot_io/validators.py +1029 -0
  126. plot_io/values.py +190 -0
  127. plot_io-0.12.0.dist-info/METADATA +215 -0
  128. plot_io-0.12.0.dist-info/RECORD +130 -0
  129. plot_io-0.12.0.dist-info/WHEEL +4 -0
  130. plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
@@ -0,0 +1,322 @@
1
+ """Heatmap: a grid of values drawn as colored cells (``Cells``), with optional cell labels
2
+ (``Text``)."""
3
+
4
+ import array
5
+ import math
6
+ from typing import Any
7
+
8
+ from vec_io.arrays import doubles
9
+
10
+ from .. import groups
11
+ from .. import kernels
12
+ from .._methods import HeatmapMixin
13
+ from ..color.values import color, ink_for
14
+ from ..layers.base import Series, common
15
+ from ..layers.core import Call, Layer, Table
16
+ from ..layers.geoms import GEOMS
17
+ from ..layers.paint import Paint, role
18
+ from ..numbers import NumberStyle, check_format, format_numbers
19
+ from ..mapped import color_axis_of, trained
20
+ from ..settings import Setting, boolean, one_of, table
21
+ from ..labels import FIT, fits
22
+ from ..text import font_of
23
+ from ..transform import settings_both as coord_settings
24
+ from ._util import columns_meta, gradient_entry, placed_text
25
+
26
+ __all__ = ["Heatmap", "Coords"]
27
+
28
+
29
+ def _labels(v: Any) -> Any:
30
+ if v == "all":
31
+ return "all"
32
+ return boolean(v)
33
+
34
+
35
+ class Coords:
36
+ """Cell coordinates along one axis: categories, centres or edges."""
37
+
38
+ def axis(self, call: Call, role: str) -> None:
39
+ """What the call's axis ``role`` needs: categories, the extent, the coordinates
40
+ given (in the extent), integer ticks for indices."""
41
+ if self.categories is not None:
42
+ call.cats[role] = self.categories
43
+ call.extent_of[role] = self.extent
44
+ if self.kind in ("centers", "edges"):
45
+ call.extra[role] = self.values
46
+ elif self.kind == "index":
47
+ call.whole.add(role)
48
+
49
+ def __init__(self, given: Any, n: int, what: str) -> None:
50
+ self.n = n
51
+ self.categories: list[Any] | None = None
52
+ self.kind = "index"
53
+ self.values: array.array = array.array("d")
54
+ if given is None:
55
+ return
56
+ items = list(given)
57
+ if items and all(isinstance(v, str) for v in items):
58
+ if len(items) != n:
59
+ raise ValueError(f"{len(items)} {what} labels for {n} cells")
60
+ self.categories = items
61
+ self.kind = "category"
62
+ return
63
+ vals = doubles(given)
64
+ if len(vals) == n:
65
+ self.kind = "centers"
66
+ elif len(vals) == n + 1:
67
+ self.kind = "edges"
68
+ else:
69
+ raise ValueError(f"{what} needs {n} centres or {n + 1} edges, got {len(vals)}")
70
+ if any(not math.isfinite(v) for v in vals):
71
+ raise ValueError(f"{what} coordinates must be finite")
72
+ d = [b - a for a, b in zip(vals, vals[1:])]
73
+ if d and not (all(x > 0 for x in d) or all(x < 0 for x in d)):
74
+ raise ValueError(f"{what} coordinates must be monotonic")
75
+ self.values = vals
76
+
77
+ def edges_f(self, f: Any) -> list[float]:
78
+ """Cell edges after the scale function ``f``."""
79
+ if self.kind in ("index", "category"):
80
+ return [i - 0.5 for i in range(self.n + 1)]
81
+ fv = [f(v) for v in self.values]
82
+ if any(not math.isfinite(v) for v in fv):
83
+ raise ValueError("heatmap coordinates outside the axis scale's domain")
84
+ if self.kind == "edges":
85
+ return fv
86
+ if len(fv) == 1:
87
+ return [fv[0] - 0.5, fv[0] + 0.5]
88
+ mids = [(a + b) / 2 for a, b in zip(fv, fv[1:])]
89
+ return [fv[0] - (mids[0] - fv[0])] + mids + [fv[-1] + (fv[-1] - mids[-1])]
90
+
91
+ def extent(self, scale: Any) -> tuple[int, float, float]:
92
+ if self.kind in ("index", "category"):
93
+ return self.n, -0.5, self.n - 0.5
94
+ e = self.edges_f(scale.f)
95
+ lo, hi = min(e), max(e)
96
+ return self.n, scale.inverse(lo), scale.inverse(hi)
97
+
98
+
99
+ def _even(e: list[float]) -> bool:
100
+ if len(e) < 3:
101
+ return True
102
+ d0 = (e[-1] - e[0]) / (len(e) - 1)
103
+ return all(abs((b - a) - d0) <= 1e-6 * max(1.0, abs(d0)) for a, b in zip(e, e[1:]))
104
+
105
+
106
+ class Heatmap(HeatmapMixin, Series):
107
+ """A grid of values (rows first) as colored cells; ``x``/``y`` give cell labels,
108
+ centres or edges.
109
+
110
+ A 3-D ``(h, w, 3|4)`` grid is drawn as colors. ``x``/``y``: ``None`` (cell indices),
111
+ labels (a category axis), n cell centres or n + 1 edges (uneven spacing and any axis
112
+ scale allowed). ``color_map``, ``color_limits`` and ``color_scale`` configure the
113
+ series' `color axis <docs/api.md#6-color-and-size-axes>`_. ``value_labels=True`` writes each value, formatted by
114
+ ``value_label_format`` (any `number format <docs/api.md#number-formats>`_; ``"auto"``: 3
115
+ significant digits, the same decimals for all cells), in black or white, whichever
116
+ contrasts more (at most 2,500 cells unless ``value_labels="all"``). ``kind``:
117
+ ``image``, ``cells`` or ``smooth``.
118
+
119
+ Example::
120
+
121
+ import math
122
+ p = plt.Plot(width=240, height=160)
123
+ p.heatmap([[math.sin(i / 2 + 0.5) * math.cos(j / 3) for j in range(6)] for i in range(5)],
124
+ x=list("abcdef"), value_labels=True, colorbar=True)
125
+ """
126
+
127
+ SETTINGS = table(
128
+ **coord_settings(),
129
+ **common("color_map"),
130
+ kind=Setting(one_of("image", "cells", "smooth"),
131
+ doc="`image` (one image pixel per cell), `cells` (rectangles) or "
132
+ "`smooth` (interpolated)."),
133
+ nan_color=Setting(color, doc="Color of missing cells; default: the map's `bad` "
134
+ "(transparent)."),
135
+ origin=Setting(one_of("top", "bottom"),
136
+ doc="`top`: row 0 at the top (y axis inverted); `bottom`: at the bottom."),
137
+ value_labels=Setting(_labels,
138
+ doc="Write each cell's value (`\"all\"` above 2,500 cells)."),
139
+ value_label_format=Setting(check_format,
140
+ doc="Number format of cell labels (`auto`: 3 significant "
141
+ "digits, the same decimals for all cells)."),
142
+ **groups.TextStyle.embed("value_label_",
143
+ only=("font_size",),
144
+ docs={"font_size": "Font size of cell labels."}),
145
+ **common("name", "opacity", "z", "rasterize", "simplify", "font_size", "font_family",
146
+ "text_color"),
147
+ **groups.NumberFormat.embed(),
148
+ )
149
+
150
+ DATA = {"values": "raw", "x": "raw", "y": "raw"}
151
+
152
+ def __init__(self, values: Any, x: Any = None, y: Any = None, plot: Any = None,
153
+ **settings: Any) -> None:
154
+ values, x, y = self.cols["values"], self.cols["x"], self.cols["y"]
155
+ g = kernels.grid(values)
156
+ if g is not None:
157
+ h, w, k, flat = g
158
+ self.h, self.w, self.k = h, w, k
159
+ self.flat = flat
160
+ else:
161
+ rows = [list(r) for r in values]
162
+ if not rows:
163
+ raise ValueError("a heatmap needs at least one row")
164
+ w = len(rows[0])
165
+ if any(len(r) != w for r in rows):
166
+ raise ValueError("heatmap rows must have the same length")
167
+ if w and rows[0] and isinstance(rows[0][0], (list, tuple)):
168
+ self.k = len(rows[0][0])
169
+ self.flat = array.array("d", [float(c) for r in rows for cell in r for c in cell])
170
+ else:
171
+ self.k = 1
172
+ self.flat = doubles([v for r in rows for v in r])
173
+ self.h, self.w = len(rows), w
174
+ if self.k not in (1, 3, 4):
175
+ raise ValueError("cells must be values or (r, g, b[, a]) colors")
176
+ self.xc = Coords(x, self.w, "x")
177
+ self.yc = Coords(y, self.h, "y")
178
+ super().__init__(plot=plot, **settings)
179
+
180
+ def _bar_source(self) -> Any:
181
+ return self if self.k == 1 else None # RGB cells: no color axis
182
+
183
+ def _grid(self) -> tuple[Any, int, int, int, Coords, Coords]:
184
+ """``(flat values, rows, columns, channels, x coordinates, y coordinates)``."""
185
+ return self.flat, self.h, self.w, self.k, self.xc, self.yc
186
+
187
+ def _inverts(self) -> bool:
188
+ return self.resolve("origin") == "top"
189
+
190
+ def layers(self) -> Call:
191
+ call = Call(self)
192
+ flat, h, w, k, xc, yc = self._grid()
193
+ axis = color_axis_of(self) if self.k == 1 else None
194
+ cmap = axis.color_map() if axis is not None else None
195
+ key = id(self)
196
+
197
+ def page_edges(ctx: Any, c: Coords, role: str) -> list[float]:
198
+ state = ctx.x if role == "x" else ctx.y
199
+ mapper = ctx.xmap if role == "x" else ctx.ymap
200
+ return [mapper.position(e) for e in c.edges_f(state.f)]
201
+
202
+ def grid(ctx: Any) -> dict[str, Any]:
203
+ def make() -> dict[str, Any]:
204
+ xe = page_edges(ctx, xc, "x")
205
+ ye = page_edges(ctx, yc, "y")
206
+ flip_rows = ye[0] > ye[-1] # row 0 must come first from the top
207
+ flip_cols = xe[0] > xe[-1]
208
+ if flip_rows:
209
+ ye = ye[::-1]
210
+ if flip_cols:
211
+ xe = xe[::-1]
212
+ if axis is not None:
213
+ st = trained(axis)
214
+ assert st.scale is not None
215
+ flo, fhi = st.flo, st.fhi
216
+ fn = st.scale.function
217
+ f = kernels.scaled(flat, 1.0, 0.0, fn, **st.scale.options) if fn else flat
218
+ if fn and (math.inf in flat or -math.inf in flat):
219
+ for i, v in enumerate(flat): # infinities: the map's extremes
220
+ if v in (math.inf, -math.inf):
221
+ f[i] = v
222
+ rows: list[Any] = [f[r * w:(r + 1) * w] for r in range(h)]
223
+ else:
224
+ flo = fhi = 0.0
225
+ rows = [[tuple(flat[(r * w + c) * k:(r * w + c + 1) * k]) for c in range(w)]
226
+ for r in range(h)]
227
+ order = list(range(h))[::-1] if flip_rows else list(range(h))
228
+ rows = [rows[i] for i in order]
229
+ if flip_cols:
230
+ rows = [r[::-1] for r in rows]
231
+ return {"rows": rows, "xe": xe, "ye": ye, "flo": flo, "fhi": fhi}
232
+ return memo_grid(ctx, key, make)
233
+
234
+ if h and w: # an empty grid draws nothing
235
+ call.layers.append(Layer(GEOMS["cells"], Table(),
236
+ paint=Paint(self, "heatmap.cells", grid=grid, cmap=cmap),
237
+ stat="identity", occupy="all", key=False))
238
+ if self.resolve("value_labels") and h and w:
239
+ call.layers.append(placed_text(self, key, lambda ctx: self._label_items(ctx, grid(ctx),
240
+ axis),
241
+ lambda ctx: font_of(
242
+ self, self.resolve("value_label_font_size")),
243
+ overlay=False))
244
+ name = self.resolve("name")
245
+ if name is not None:
246
+ call.entries.append(gradient_entry(
247
+ self, name, lambda: axis.color_map() if axis is not None else None))
248
+ if axis is not None:
249
+ call.color_sources = [(axis, flat)]
250
+ xc.axis(call, "x")
251
+ yc.axis(call, "y")
252
+ if self._inverts():
253
+ call.invert = {"y"}
254
+ call.exact = {"x", "y"}
255
+ columns_meta(call, {"values": "x"})
256
+ return call
257
+
258
+ def _label_items(self, ctx: Any, g: dict[str, Any], axis: Any) -> list[Any]:
259
+ _, h, w, _, _, _ = self._grid()
260
+ if h * w > 2500 and self.resolve("value_labels") != "all":
261
+ raise ValueError(f"{h * w} cells is too many for labels "
262
+ "(use value_labels=\"all\" to force)")
263
+ fmt = self.resolve("value_label_format")
264
+ rows, xe, ye, flo, fhi = g["rows"], g["xe"], g["ye"], g["flo"], g["fhi"]
265
+ cmap = axis.color_map() if axis is not None else None
266
+ scale = trained(axis).scale if axis is not None else None
267
+ fn = scale.inverse if scale is not None else None
268
+ cells = [(r, c, v) for r, row in enumerate(rows) for c, v in enumerate(row)]
269
+ if cmap is not None:
270
+ cells = [(r, c, v) for r, c, v in cells if v == v]
271
+ style = NumberStyle.of(self)
272
+ if style.precision is None and fmt == "auto":
273
+ style.precision = 3
274
+ texts = format_numbers([fn(v) if fn else v for _, _, v in cells], fmt, style)
275
+ elif callable(fmt):
276
+ texts = [fmt(v) for _, _, v in cells]
277
+ elif fmt not in ("auto", "percent", "sci", "eng", "si", "int"):
278
+ texts = [fmt.format(*v[:3]) for _, _, v in cells]
279
+ else:
280
+ texts = ["" for _ in cells]
281
+ font = font_of(self, self.resolve("value_label_font_size"))
282
+ fit = FIT * font.size
283
+ out = []
284
+ for (r, c, v), text in zip(cells, texts):
285
+ if not fits(ctx.canvas, text, font, xe[c + 1] - xe[c], ye[r + 1] - ye[r], fit):
286
+ continue # a label that does not fit is left out
287
+ if cmap is None:
288
+ col = v
289
+ elif v != v:
290
+ col = cmap.bad
291
+ else:
292
+ col = cmap((v - flo) / ((fhi - flo) or 1.0))
293
+ out.append((text, (xe[c] + xe[c + 1]) / 2, (ye[r] + ye[r + 1]) / 2, ink_for(col),
294
+ "center", "center"))
295
+ return out
296
+
297
+
298
+ def memo_grid(ctx: Any, key: Any, make: Any) -> Any:
299
+ from ._util import memo
300
+ return memo(ctx, (key, "grid"), make)
301
+
302
+
303
+ @role("heatmap.cells")
304
+ def _cells_paint(s: Heatmap, ctx: Any, glyph: bool, grid: Any = None,
305
+ cmap: Any = None) -> dict[str, Any]:
306
+ """The cells of ``grid(ctx)``: through the color map ``cmap``, or colors."""
307
+ g = grid(ctx)
308
+ opacity = s.resolve("opacity")
309
+ mode = s.resolve("kind")
310
+ p: dict[str, Any] = {"x_edges": g["xe"], "y_edges": g["ye"],
311
+ "even": _even(g["xe"]) and _even(g["ye"]), "mode": mode,
312
+ "opacity": 1.0 if opacity is None else opacity}
313
+ if cmap is not None:
314
+ flat = array.array("d")
315
+ for row in g["rows"]:
316
+ flat.extend(row)
317
+ p["fill"], p["palette"], p["bounds"] = cmap.cells(flat, g["flo"], g["fhi"], mode)
318
+ nan = ctx.color(s.resolve("nan_color"))
319
+ p["bad"] = nan if nan is not None else cmap.bad
320
+ else:
321
+ p["fill"] = [c for row in g["rows"] for c in row]
322
+ return p
@@ -0,0 +1,198 @@
1
+ """Hexagonal 2-D histograms (stat ``hexbin``): counts of points in hexagons
2
+ (``Polygons``, one hexagon repeated), colored by a map."""
3
+
4
+ import math
5
+ from typing import Any
6
+
7
+ import vec_io
8
+
9
+ from .._methods import HexbinMixin
10
+ from .. import groups
11
+ from .. import columns, kernels
12
+ from ..color.values import with_alpha
13
+ from ..layers import stats
14
+ from ..layers.base import Series, common
15
+ from ..layers.core import Call, Layer, Table
16
+ from ..layers.grammar import entry
17
+ from ..layers.paint import Paint, role
18
+ from ..layers.geoms import GEOMS
19
+ from ..mapped import color_axis_of, colors_of
20
+ from ..settings import Setting, number, one_of, table
21
+ from ..layers.pipeline import render_of
22
+ from ..transform import settings_both as coord_settings
23
+ from ._util import columns_meta
24
+
25
+ __all__ = ["Hexbin"]
26
+
27
+
28
+ _S3 = math.sqrt(3.0)
29
+
30
+
31
+ def _frame_of(ctx: Any) -> tuple:
32
+ """The frame a lattice is laid out in: the axes' mappers relative to their top-left
33
+ corner, and their size."""
34
+ x0, y0, x1, y1 = ctx.rect
35
+ xm, ym = ctx.xmap, ctx.ymap
36
+ return (xm.function, xm.a, xm.b - x0, ym.function, ym.a, ym.b - y0, x1 - x0, y1 - y0)
37
+
38
+
39
+ def _same(f: tuple, g: tuple) -> bool:
40
+ return f[0] == g[0] and f[3] == g[3] and all(
41
+ abs(a - b) <= 1e-6 * max(1.0, abs(a), abs(b))
42
+ for a, b in zip(f[1:3] + f[4:], g[1:3] + g[4:]))
43
+
44
+
45
+ class Hexbin(HexbinMixin, Series):
46
+ """A hexagonal 2-D histogram of points ``(x, y)``: ``bins`` regular hexagons across
47
+ the axes width. The lattice is laid out in page units for the axes the layout placed
48
+ (its domain is the data's, its bins the axes'), so the hexagons are regular and tile
49
+ exactly whatever the axes' aspect.
50
+
51
+ The counts depend on the axes, and the color axis on the counts: the layout counts again
52
+ when a round places the axes differently (`layout <docs/api.md#layout>`_). Points outside the
53
+ axes are not counted. Colored by ``color_map``; a colorbar source.
54
+
55
+ Example::
56
+
57
+ rng = random.Random(1)
58
+ x = [rng.gauss(0, 1) for _ in range(1000)]
59
+ p = plt.Plot(width=240, height=160)
60
+ p.hexbin(x, [v + rng.gauss(0, 0.7) for v in x], bins=16)
61
+ """
62
+
63
+ SETTINGS = table(
64
+ **coord_settings(),
65
+ bins=Setting(number(lo=1, integer=True), doc="Hexagons across the axes width."),
66
+ weights=Setting(lambda v: columns.numbers(v, "weights"), doc="A weight per point (sums)."),
67
+ stat=Setting(one_of("count", "probability", "percent"),
68
+ doc="`count`, `probability` or `percent`."),
69
+ min_count=Setting(number(lo=0), doc="Hexagons below this count are not drawn."),
70
+ **common("color_map"),
71
+ **groups.Stroke.embed("edge_",
72
+ only=("color", "line_width"),
73
+ docs={"color": "Hexagon outline color; default none.",
74
+ "line_width": "Outline width."}),
75
+ **common("name", "opacity", "z", "rasterize", "simplify", "font_size", "font_family",
76
+ "text_color"),
77
+ )
78
+
79
+ DATA = {"x": "numbers", "y": "numbers"}
80
+
81
+ def __init__(self, x: Any, y: Any, plot: Any = None, **settings: Any) -> None:
82
+ super().__init__(plot=plot, **settings)
83
+
84
+ def _bar_source(self) -> Any:
85
+ return self
86
+
87
+ def _nominal_frame(self) -> tuple:
88
+ """Outside a render: the lattice over the data range and the figure's nominal axes
89
+ size (``width`` × ``height``)."""
90
+ fig = self.plot.figure
91
+ w, h = fig.reference_size()
92
+ out: list[Any] = []
93
+ for axis, values, (p0, p1) in ((self.plot.x_axis, self.cols["x"], (0.0, w)),
94
+ (self.plot.y_axis, self.cols["y"], (h, 0.0))):
95
+ sc = stats.axis_scale(axis)
96
+ n, lo, hi = kernels.extent(values, sc.function)
97
+ if not n:
98
+ lo, hi = 0.0, 1.0
99
+ if hi == lo:
100
+ lo, hi = lo - 0.5, hi + 0.5
101
+ name = "auto_padding" if axis.get("padding") == "auto" else "padding"
102
+ px, frac = axis.resolve_linear(name) # px + fraction of the range
103
+ span = abs(p1 - p0)
104
+ d = (hi - lo) * frac
105
+ if px and span > 2 * px:
106
+ d = (hi - lo + 2 * d) * px / (span - 2 * px) + d
107
+ lo, hi = lo - d, hi + d
108
+ a = (p1 - p0) / (hi - lo)
109
+ out += [sc.function, a, p0 - a * lo]
110
+ return (*out, w, h)
111
+
112
+ def _frame(self) -> tuple | None:
113
+ """The frame of the axes the layout placed (``None`` in a render before the first
114
+ placement), else, outside a render, the nominal one."""
115
+ r = render_of(self.plot)
116
+ if r is None:
117
+ return self._nominal_frame()
118
+ ctx = r.placed.get(id(self.plot))
119
+ return None if ctx is None else _frame_of(ctx)
120
+
121
+ @property
122
+ def stats(self) -> dict[str, Any]:
123
+ """``{"x", "y": hexagon centres (page units, relative to the axes' top-left corner),
124
+ "values", "dx": hexagon width (px)}`` for the placed axes (see :meth:`_frame`)."""
125
+ frame = self._frame()
126
+ if frame is None:
127
+ raise ValueError(f"{self!r}: the hexagons are counted once the axes are placed")
128
+ return stats.compute(self, "hexbin", (), frame, kind=None)
129
+
130
+ def page_stats(self, ctx: Any) -> dict[str, Any]:
131
+ """The stats for the axes of ``ctx``: those of the placed axes, or (placed
132
+ differently) counted again; the colors keep the color axis trained before."""
133
+ f = _frame_of(ctx)
134
+ placed = self._frame()
135
+ if placed is not None and _same(f, placed):
136
+ return self.stats
137
+ return stats.compute(self, "hexbin", (), f, kind=None)
138
+
139
+ @staticmethod
140
+ def hexagon(dx: float) -> list[tuple[float, float]]:
141
+ """Vertices of the regular hexagon (pointy top) of width ``dx`` centred on 0."""
142
+ r = dx / _S3
143
+ return [(0.5 * dx, -0.5 * r), (0.5 * dx, 0.5 * r), (0.0, r), (-0.5 * dx, 0.5 * r),
144
+ (-0.5 * dx, -0.5 * r), (0.0, -r)]
145
+
146
+ def layers(self) -> Call:
147
+ call = Call(self)
148
+ axis = color_axis_of(self)
149
+
150
+ def page(ctx: Any) -> dict[str, Any]:
151
+ return self.page_stats(ctx)
152
+
153
+ t = Table()
154
+ t.lazy["x"] = lambda ctx: kernels.scaled(page(ctx)["x"], 1.0, ctx.rect[0])
155
+ t.lazy["y"] = lambda ctx: kernels.scaled(page(ctx)["y"], 1.0, ctx.rect[1])
156
+ t.lazy["size"] = lambda ctx: page(ctx)["dx"] * 1.002
157
+ t.lazy["half"] = lambda ctx: (page(ctx)["dx"] / 2, page(ctx)["dx"] / _S3)
158
+
159
+ def shape(ctx: Any) -> Any:
160
+ dx = page(ctx)["dx"]
161
+ path = vec_io.Path()
162
+ pts = [(x / dx, y / dx) for x, y in self.hexagon(dx)]
163
+ path.move_to(*pts[0])
164
+ for p in pts[1:]:
165
+ path.line_to(*p)
166
+ path.close()
167
+ return path
168
+ t.lazy["shape"] = shape
169
+
170
+ layer = Layer(GEOMS["polygons"], t, paint=Paint(self, "hexbin.cells"), stat="hexbin",
171
+ glyph="hexagon")
172
+ call.layers.append(layer)
173
+ name = self.resolve("name")
174
+ if name is not None:
175
+ call.entries.append(entry(self, name, [layer], fill="symbol", marker=layer.paint))
176
+
177
+ def counts() -> Any: # trained once the axes are placed
178
+ return None if self._frame() is None else self.stats["values"]
179
+ call.color_sources = [(axis, counts)]
180
+ call.extra = {"x": self.cols["x"], "y": self.cols["y"]}
181
+ call.resolution = _frame_of
182
+ columns_meta(call, {})
183
+ return call
184
+
185
+
186
+ @role("hexbin.cells")
187
+ def _cells_paint(s: Hexbin, ctx: Any, glyph: bool) -> dict[str, Any]:
188
+ """The hexagons in their counts' colors (the legend's: the map at 0.6)."""
189
+ axis = color_axis_of(s)
190
+ a = s.resolve("opacity")
191
+ if glyph:
192
+ return {"fill": with_alpha(axis.color_map()(0.6), a)}
193
+ lc = ctx.color(s.resolve("edge_color"))
194
+ lw = s.resolve("edge_width")
195
+ return {"fill": colors_of(axis, s.page_stats(ctx)["values"]),
196
+ "stroke": lc if lw and lc is not None else None,
197
+ "line_width": lw or None, "opacity": 1.0 if a is None else a,
198
+ "simplify": s.resolve("simplify")}