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
plot_io/occupancy.py ADDED
@@ -0,0 +1,295 @@
1
+ """What the series cover inside the axes, on a coarse grid: used to place legends."""
2
+
3
+ import math
4
+ from typing import Any
5
+
6
+ from . import kernels
7
+
8
+
9
+ __all__ = ["Occupancy", "thin", "extremes", "N", "MAX_VERTICES", "MAX_POINTS"]
10
+
11
+ #: Cells across each side of the axes.
12
+ N = 64
13
+ #: Polylines and curves are thinned to at most this many vertices before they are marked.
14
+ MAX_VERTICES = 4096
15
+
16
+
17
+ #: Points beyond this many are sampled (every k-th) before they are marked.
18
+ MAX_POINTS = 50_000
19
+
20
+
21
+ def _stride(n: int) -> int:
22
+ return max(1, -(-n // MAX_VERTICES))
23
+
24
+
25
+ def thin(*columns: Any, limit: int = MAX_POINTS) -> tuple[Any, ...]:
26
+ """Every k-th item of each column, so that at most ``limit`` are left (data are thinned
27
+ before they are mapped to the page, which keeps legend placement fast)."""
28
+ n = min(len(c) for c in columns)
29
+ k = max(1, -(-n // limit))
30
+ if k == 1:
31
+ return columns
32
+ return tuple(c[:n:k] for c in columns)
33
+
34
+
35
+ def extremes(a: Any, b: Any, limit: int = MAX_VERTICES) -> tuple[Any, Any]:
36
+ """A polyline of more than ``4 · limit`` vertices thinned to strides of ``n / limit``
37
+ vertices, each kept by its first vertex and those of least and largest ``a`` and ``b``
38
+ (in order, so that narrow spikes stay); a stride with NaN keeps its first NaN (a
39
+ break) and the extremes of its other vertices."""
40
+ n = min(len(a), len(b))
41
+ k = -(-n // limit)
42
+ if k <= 4:
43
+ return a[:n], b[:n]
44
+ keep: list[int] = []
45
+ for i in range(0, n, k):
46
+ sa, sb = a[i:i + k], b[i:i + k]
47
+ ta, tb = sum(sa), sum(sb)
48
+ if ta == ta and tb == tb and abs(ta) != math.inf and abs(tb) != math.inf:
49
+ idx = {0, sa.index(min(sa)), sa.index(max(sa)), sb.index(min(sb)),
50
+ sb.index(max(sb))}
51
+ else:
52
+ good = [m for m in range(len(sa)) if math.isfinite(sa[m]) and math.isfinite(sb[m])]
53
+ idx = {m for m in range(len(sa)) if not (sa[m] == sa[m] and sb[m] == sb[m])}
54
+ idx = {min(idx)} if idx else set()
55
+ if good:
56
+ idx |= {good[0], min(good, key=sa.__getitem__), max(good, key=sa.__getitem__),
57
+ min(good, key=sb.__getitem__), max(good, key=sb.__getitem__)}
58
+ keep.extend(i + m for m in sorted(idx))
59
+ return [a[i] for i in keep], [b[i] for i in keep]
60
+
61
+
62
+ class Occupancy:
63
+ """An ``N`` × ``N`` grid of cells over the axes rectangle (page units); a cell is
64
+ occupied when some series draws in it. ``marks``: the same grid of the cells under
65
+ markers and solid boxes only (texts and guides inside the axes; a marker or a text
66
+ under a legend is hidden, a line or an area is not); ``discs`` and ``solids``: those
67
+ markers (page positions and radius) and boxes themselves, which :meth:`solid_in` tests
68
+ a box against exactly."""
69
+
70
+ def __init__(self, rect: tuple[float, float, float, float]) -> None:
71
+ x0, y0, x1, y1 = rect
72
+ self.x0, self.y0 = x0, y0
73
+ self.cw = max(x1 - x0, 1e-9) / N
74
+ self.ch = max(y1 - y0, 1e-9) / N
75
+ self.cells = bytearray(N * N)
76
+ self.marks = bytearray(N * N)
77
+ self.discs: list[tuple[Any, Any, float]] = []
78
+ self.solids: list[tuple[float, float, float, float]] = []
79
+ self._xe = [x0 + i * self.cw for i in range(N + 1)]
80
+ self._ye = [y0 + j * self.ch for j in range(N + 1)]
81
+
82
+ # -- marking ---------------------------------------------------------------------
83
+
84
+ def _col(self, x: float) -> int:
85
+ return min(N - 1, max(0, int((x - self.x0) / self.cw)))
86
+
87
+ def _row(self, y: float) -> int:
88
+ return min(N - 1, max(0, int((y - self.y0) / self.ch)))
89
+
90
+ def _span(self, r: int, c0: int, c1: int, grid: bytearray | None = None) -> None:
91
+ if c1 >= c0:
92
+ (self.cells if grid is None else grid)[r * N + c0:r * N + c1 + 1] = \
93
+ b"\x01" * (c1 - c0 + 1)
94
+
95
+ def points(self, px: Any, py: Any, radius: float = 0.0, marker: bool = False) -> None:
96
+ """Marks the cells holding points (counted in C; points outside are ignored), and
97
+ the cells within ``radius`` (px, a marker's half size) of those; ``marker``: in
98
+ ``marks`` too."""
99
+ if not len(px):
100
+ return
101
+ if marker:
102
+ self.discs.append((px, py, radius))
103
+ _, _, counts = kernels.histogram2d(px, py, self._xe, self._ye)
104
+ kx = int(radius / self.cw + 0.5)
105
+ ky = int(radius / self.ch + 0.5)
106
+ grids = (self.cells, self.marks) if marker else (self.cells,)
107
+ for k, c in enumerate(counts):
108
+ if not c:
109
+ continue
110
+ if not kx and not ky:
111
+ for g in grids:
112
+ g[k] = 1
113
+ continue
114
+ r, col = divmod(k, N)
115
+ c0, c1 = max(0, col - kx), min(N - 1, col + kx)
116
+ for rr in range(max(0, r - ky), min(N - 1, r + ky) + 1):
117
+ for g in grids:
118
+ self._span(rr, c0, c1, g)
119
+
120
+ def lines(self, px: Any, py: Any) -> None:
121
+ """Marks the cells a polyline passes through (NaN breaks it; a long one thinned by
122
+ :func:`extremes`); segments are sampled at half a cell, so long segments count
123
+ along their whole length (at most ``8 · MAX_VERTICES`` samples in all, spread by
124
+ length)."""
125
+ n = min(len(px), len(py))
126
+ if n == 0:
127
+ return
128
+ verts = list(zip(*extremes(px, py)))
129
+ step = 0.5 * min(self.cw, self.ch)
130
+ counts = []
131
+ for (x0, y0), (x1, y1) in zip(verts, verts[1:]):
132
+ d = max(abs(x1 - x0), abs(y1 - y0))
133
+ counts.append(int(d / step) if d == d else -1)
134
+ total = sum(c for c in counts if c > 0)
135
+ budget = 8 * MAX_VERTICES
136
+ scale = budget / total if total > budget else 1.0
137
+ xs, ys = [], []
138
+ for (x0, y0), (x1, y1), k in zip(verts, verts[1:], counts):
139
+ if k < 0:
140
+ continue
141
+ k = int(k * scale)
142
+ for j in range(k + 1):
143
+ t = j / (k + 1)
144
+ xs.append(x0 + (x1 - x0) * t)
145
+ ys.append(y0 + (y1 - y0) * t)
146
+ x, y = verts[-1]
147
+ if x == x and y == y:
148
+ xs.append(x)
149
+ ys.append(y)
150
+ self.points(xs, ys)
151
+
152
+ def rects(self, x0: Any, y0: Any, x1: Any, y1: Any, solid: bool = False) -> None:
153
+ """Marks the cells under rectangles (any corner order; NaN rectangles skipped);
154
+ ``solid``: texts and guides, in ``marks`` and ``solids`` too."""
155
+ X0, Y0 = self.x0, self.y0
156
+ X1, Y1 = X0 + N * self.cw, Y0 + N * self.ch
157
+ for a, b, c, d in zip(x0, y0, x1, y1):
158
+ if a != a or b != b or c != c or d != d:
159
+ continue
160
+ a, c = min(a, c), max(a, c)
161
+ b, d = min(b, d), max(b, d)
162
+ if c < X0 or a > X1 or d < Y0 or b > Y1:
163
+ continue
164
+ c0, c1 = self._col(a), self._col(c)
165
+ for r in range(self._row(b), self._row(d) + 1):
166
+ self._span(r, c0, c1)
167
+ if solid:
168
+ self._span(r, c0, c1, self.marks)
169
+ if solid:
170
+ self.solids.append((a, b, c, d))
171
+
172
+ def band(self, t: Any, lo: Any, hi: Any, horizontal: bool = False) -> None:
173
+ """Marks the region between two curves sharing the coordinate ``t`` (page x, or page
174
+ y when ``horizontal``): per grid column (row), from the least to the largest value
175
+ of the samples in it, with samples added at half a cell along ``t``."""
176
+ n = min(len(t), len(lo), len(hi))
177
+ if n == 0:
178
+ return
179
+ size = self.ch if horizontal else self.cw
180
+ index = self._row if horizontal else self._col
181
+ mins = [math.inf] * N
182
+ maxs = [-math.inf] * N
183
+ s = _stride(n)
184
+ prev: tuple[float, float, float] | None = None
185
+ T0 = self.y0 if horizontal else self.x0
186
+ T1 = T0 + N * size
187
+
188
+ def put(tv: float, a: float, b: float) -> None:
189
+ if not T0 <= tv <= T1: # outside the view
190
+ return
191
+ k = index(tv)
192
+ lo_, hi_ = (a, b) if a <= b else (b, a)
193
+ if lo_ < mins[k]:
194
+ mins[k] = lo_
195
+ if hi_ > maxs[k]:
196
+ maxs[k] = hi_
197
+
198
+ for i in range(0, n, s):
199
+ tv, a, b = t[i], lo[i], hi[i]
200
+ if tv != tv or a != a or b != b:
201
+ prev = None
202
+ continue
203
+ if prev is not None:
204
+ k = int(abs(tv - prev[0]) / (0.5 * size))
205
+ for j in range(1, k + 1):
206
+ f = j / (k + 1)
207
+ put(prev[0] + (tv - prev[0]) * f, prev[1] + (a - prev[1]) * f,
208
+ prev[2] + (b - prev[2]) * f)
209
+ put(tv, a, b)
210
+ prev = (tv, a, b)
211
+ for k in range(N):
212
+ if mins[k] > maxs[k]:
213
+ continue
214
+ centre = T0 + (k + 0.5) * size
215
+ if not T0 <= centre <= T1:
216
+ continue
217
+ if horizontal:
218
+ self._span(k, self._col(mins[k]), self._col(maxs[k]))
219
+ else:
220
+ for r in range(self._row(mins[k]), self._row(maxs[k]) + 1):
221
+ self.cells[r * N + k] = 1
222
+
223
+ # -- queries ---------------------------------------------------------------------
224
+
225
+ def _sums(self) -> list[int]:
226
+ """Summed-area table: ``s[(r) * (N + 1) + c]`` = occupied cells above and left."""
227
+ s = [0] * ((N + 1) * (N + 1))
228
+ cells = self.cells
229
+ for r in range(N):
230
+ run = 0
231
+ base, prev = (r + 1) * (N + 1), r * (N + 1)
232
+ for c in range(N):
233
+ run += cells[r * N + c]
234
+ s[base + c + 1] = s[prev + c + 1] + run
235
+ return s
236
+
237
+ def covered(self, x: float, y: float, w: float, h: float, sums: list[int] | None = None,
238
+ markers: bool = False) -> float:
239
+ """Fraction of the cells whose centres lie in the box ``(x, y, w, h)`` that are
240
+ occupied (``markers``: under markers; 0 when the box holds no cell centre).
241
+ ``sums`` (from :meth:`_sums`, of all cells) makes repeated queries constant time."""
242
+ c0 = max(0, math.ceil((x - self.x0) / self.cw - 0.5))
243
+ c1 = min(N - 1, math.floor((x + w - self.x0) / self.cw - 0.5))
244
+ r0 = max(0, math.ceil((y - self.y0) / self.ch - 0.5))
245
+ r1 = min(N - 1, math.floor((y + h - self.y0) / self.ch - 0.5))
246
+ if c1 < c0 or r1 < r0:
247
+ return 0.0
248
+ if sums is None or markers:
249
+ cells = self.marks if markers else self.cells
250
+ hit = sum(cells[r * N + c0:r * N + c1 + 1].count(1) for r in range(r0, r1 + 1))
251
+ else:
252
+ M = N + 1
253
+ hit = (sums[(r1 + 1) * M + c1 + 1] - sums[r0 * M + c1 + 1]
254
+ - sums[(r1 + 1) * M + c0] + sums[r0 * M + c0])
255
+ return hit / ((c1 - c0 + 1) * (r1 - r0 + 1))
256
+
257
+ def solid_in(self, x: float, y: float, w: float, h: float) -> int:
258
+ """The markers whose disc meets the box ``(x, y, w, h)`` (by the box grown by
259
+ each marker's radius) and the solid boxes it meets."""
260
+ n = sum(1 for a, b, c, d in self.solids if a < x + w and x < c and b < y + h and y < d)
261
+ for px, py, r in self.discs:
262
+ r = max(r, 0.5)
263
+ n += int(kernels.histogram2d(px, py, (x - r, x + w + r),
264
+ (y - r, y + h + r))[2][0])
265
+ return n
266
+
267
+ def free_boxes(self, w: float, h: float, pad: float) -> list[tuple[float, float]]:
268
+ """Top-left corners, one cell apart, of the boxes ``w`` × ``h`` inside the axes
269
+ (``pad`` from their edges) that cover no occupied cell."""
270
+ sums = self._sums()
271
+ x_lo, x_hi = self.x0 + pad, self.x0 + N * self.cw - pad - w
272
+ y_lo, y_hi = self.y0 + pad, self.y0 + N * self.ch - pad - h
273
+ if x_hi < x_lo or y_hi < y_lo:
274
+ return []
275
+ nx = int((x_hi - x_lo) / self.cw)
276
+ ny = int((y_hi - y_lo) / self.ch)
277
+ xs = [x_lo + i * self.cw for i in range(nx + 1)] + [x_hi]
278
+ ys = [y_lo + j * self.ch for j in range(ny + 1)] + [y_hi]
279
+ # the cells whose centres a box at each x (y) holds, as :meth:`covered` takes them
280
+ cols = [(max(0, math.ceil((x - self.x0) / self.cw - 0.5)),
281
+ min(N - 1, math.floor((x + w - self.x0) / self.cw - 0.5))) for x in xs]
282
+ rows = [(max(0, math.ceil((y - self.y0) / self.ch - 0.5)),
283
+ min(N - 1, math.floor((y + h - self.y0) / self.ch - 0.5))) for y in ys]
284
+ M = N + 1
285
+ out = []
286
+ for y, (r0, r1) in zip(ys, rows):
287
+ if r1 < r0:
288
+ out += [(x, y) for x in xs]
289
+ continue
290
+ top, bottom = r0 * M, (r1 + 1) * M
291
+ for x, (c0, c1) in zip(xs, cols):
292
+ if c1 < c0 or (sums[bottom + c1 + 1] - sums[top + c1 + 1]
293
+ - sums[bottom + c0] + sums[top + c0]) == 0:
294
+ out.append((x, y))
295
+ return out