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,344 @@
1
+ """The base of the genome series: the region read and the axes width the data are binned
2
+ and packed for, and the ``bins`` setting.
3
+
4
+ Each series reads its data when the figure is drawn, for the region drawn (the series'
5
+ ``region``, else its x axis'), from a reader (see :mod:`plot_io.genome.sources`) or from
6
+ plain data. x values are positions in bp; they ask their x axis for the genome scale
7
+ (:mod:`plot_io.genome.scale`), which draws them less its ``origin``.
8
+
9
+ On an x axis over several regions (:class:`~plot_io.genome.scale.Segments`), a series
10
+ reads and lays out each region as if it were drawn alone, on axes as wide as the region's
11
+ segment: ``layers`` is made once per segment (the series' ``region()`` and ``axes_px()``
12
+ those of the segment) and the calls are merged into one, each layer drawn through its
13
+ segment's x map and cut at its edges (:class:`SegmentScope`). ``track_height`` and
14
+ ``lane_rows`` are the largest of the segments'. A series that places its data across
15
+ the regions itself returns ``False`` from ``segmented()``.
16
+ """
17
+
18
+ import contextlib
19
+ import functools
20
+ import math
21
+ from array import array
22
+ from collections.abc import Generator, Mapping
23
+ from typing import Any, NamedTuple
24
+
25
+ from vec_io import Path
26
+
27
+ from ..settings import Setting, number
28
+ from ..layers.base import Series
29
+ from ..layers.core import Call
30
+ from .regions import Region, parse_bp, parse_region, pending, whole
31
+
32
+ __all__ = ["GenomeSeries", "Bins", "check_bins", "axes_width", "region_setting",
33
+ "SegmentScope", "in_segment", "axis_region", "known_sizes"]
34
+
35
+
36
+ class Bins(NamedTuple):
37
+ """A ``bins`` value: ``kind`` ``"count"`` (``value`` bins over the region), ``"bp"``
38
+ (bins of ``value`` bp) or ``"px"`` (bins ``value`` px wide on the axes)."""
39
+
40
+ kind: str
41
+ value: float
42
+
43
+ def __repr__(self) -> str:
44
+ return f"Bins({self.kind}={self.value:g})"
45
+
46
+
47
+ def check_bins(v: Any) -> Bins:
48
+ """A bin count (``200``), a bin size in bp (``"10kb"``, ``"5000bp"``, ``"5000d"``) or a
49
+ bin width on the axes (``"0.5px"``)."""
50
+ if isinstance(v, Bins):
51
+ return v
52
+ if isinstance(v, str):
53
+ t = v.strip()
54
+ if t.lower().endswith("px"):
55
+ try:
56
+ w = float(t[:-2])
57
+ except ValueError:
58
+ w = math.nan
59
+ if not (math.isfinite(w) and w > 0):
60
+ raise ValueError(f"expected a positive bin width such as \"0.5px\", got {v!r}")
61
+ return Bins("px", w)
62
+ size = t[:-1] if t.endswith("d") and not t.lower().endswith("bp") else t
63
+ return Bins("bp", float(parse_bp(size, "bin size", positive=True)))
64
+ return Bins("count", float(number(positive=True, integer=True)(v)))
65
+
66
+
67
+ def _region(v: Any) -> Any:
68
+ return parse_region(v, whole=True)
69
+
70
+
71
+ def region_setting() -> Setting:
72
+ return Setting(_region,
73
+ doc="The region read: `\"chr2:143,120,001-143,780,000\"` (1-based, "
74
+ "inclusive), `(chr, start, end)` (0-based, half-open), a mapping, "
75
+ "or a chromosome name (the whole chromosome, as the x axis' "
76
+ "`chr_sizes` gives it); unset: the x axis' `region`.")
77
+
78
+
79
+ def known_sizes(owner: Any, axis: Any = None) -> dict[str, int]:
80
+ """The chromosome sizes known to the figure of ``owner`` (a plot or a figure): the
81
+ largest each reader of its genome series gives (``chr_sizes``), then ``axis``'
82
+ ``chr_sizes``, which win."""
83
+ fig = owner if hasattr(owner, "plots") else getattr(owner, "figure", None)
84
+ plots = fig.plots if fig is not None else [owner]
85
+ sizes: dict[str, int] = {}
86
+ for p in plots:
87
+ for s in getattr(p, "series", ()):
88
+ got = getattr(getattr(s, "reader", None), "chr_sizes", None)
89
+ if isinstance(got, Mapping):
90
+ for k, n in got.items():
91
+ sizes[str(k)] = max(sizes.get(str(k), 0), int(n))
92
+ given = axis.resolve("chr_sizes") if axis is not None else None
93
+ if given:
94
+ sizes.update(given)
95
+ return sizes
96
+
97
+
98
+ def axis_region(axis: Any) -> Any:
99
+ """The ``region`` of a genome axis, a whole chromosome (``"chr1"``) made the region
100
+ of its size (:func:`known_sizes`)."""
101
+ r = axis.resolve("region")
102
+ if not pending(r):
103
+ return r
104
+ return whole(r, lambda: known_sizes(axis._owner, axis))
105
+
106
+
107
+ def axes_width(plot: Any) -> float:
108
+ """The axes width (px) genome data are binned and packed for: in a render, the width
109
+ of the plot's axes as laid out when its calls were made (the layout makes them again
110
+ when it places the axes at another width); outside a render, the figure's ``width``."""
111
+ from ..spec import NeedsBox, current
112
+ main = getattr(plot, "twin_of", None)
113
+ plot = main[0] if main else plot
114
+ fig = getattr(plot, "figure", None)
115
+ r = current()
116
+ if r is not None and fig is not None and r.root is fig:
117
+ w = r.widths.get(id(plot))
118
+ if w is None:
119
+ boxes = r.boxes.get(id(plot))
120
+ if not boxes:
121
+ raise NeedsBox("the axes width genome data are binned for is known once the "
122
+ "layout sizes the axes")
123
+ w = r.widths[id(plot)] = boxes["axes_width"]
124
+ return w
125
+ if fig is None or "width" not in fig.SETTINGS:
126
+ return 480.0
127
+ return float(fig.resolve("width"))
128
+
129
+
130
+ @contextlib.contextmanager
131
+ def in_segment(series: Any, seg: Any) -> Generator[None]:
132
+ """``series``, and the other genome series of its plot (a series may read another's
133
+ data), reading and drawing the region of segment ``seg``."""
134
+ plot = getattr(series, "plot", None)
135
+ every = [s for s in (plot.series if plot is not None else ()) if
136
+ isinstance(s, GenomeSeries)] or [series]
137
+ if series not in every:
138
+ every.append(series)
139
+ before = [s.__dict__.get("_segment") for s in every]
140
+ for s in every:
141
+ s.__dict__["_segment"] = seg
142
+ try:
143
+ yield
144
+ finally:
145
+ for s, b in zip(every, before):
146
+ s.__dict__["_segment"] = b
147
+
148
+
149
+ class SegmentScope:
150
+ """A layer of a series drawn in a segment of an axis over several regions:
151
+ :meth:`scope` gives the drawing context whose x map draws the segment's positions, the
152
+ series reading the segment's region, and the canvas cut at the segment's edges."""
153
+
154
+ __slots__ = ("series", "segment")
155
+
156
+ def __init__(self, series: Any, segment: Any) -> None:
157
+ self.series, self.segment = series, segment
158
+
159
+ def context(self, ctx: Any) -> Any:
160
+ """``ctx`` (a call's context) with the segment's x map; one per plot context and
161
+ segment (its own ``root``: values a layer keeps per context are per segment)."""
162
+ seg = self.segment
163
+ made = ctx.root.__dict__.setdefault("_segments", {})
164
+ key = (id(ctx), seg.number)
165
+ got = made.get(key)
166
+ if got is None:
167
+ import copy
168
+ c = copy.copy(ctx)
169
+ for k in ("_segments", "_lazy", "_seams"):
170
+ c.__dict__.pop(k, None)
171
+ c.xmap = ctx.xmap.affine(seg.region.start - seg.u0 / seg.k, seg.k, "x")
172
+ c.root = c
173
+ got = made[key] = (ctx, c) # ctx kept alive while its id is a key
174
+ return got[1]
175
+
176
+ @contextlib.contextmanager
177
+ def scope(self, ctx: Any, clip: bool = False) -> Generator[Any]:
178
+ seg = self.segment
179
+ c = self.context(ctx)
180
+ with in_segment(self.series, seg), contextlib.ExitStack() as stack:
181
+ if clip:
182
+ canvas = ctx.canvas
183
+ stack.enter_context(canvas.state())
184
+ a, b = sorted((ctx.root.xmap(seg.u0), ctx.root.xmap(seg.u1)))
185
+ y0, y1 = ctx.rect[1], ctx.rect[3]
186
+ canvas.clip(Path().rect(a, y0 - 1e5, b - a, y1 - y0 + 2e5))
187
+ yield c
188
+
189
+
190
+ def _merged(calls: list[Any], series: Any, segs: Any) -> Any:
191
+ """One call of the calls a series made for each segment: their layers, tables and
192
+ color and size sources, the extents of all (legend entries, colors taken and the rest of
193
+ the first)."""
194
+ out = calls[0]
195
+ for c, seg in zip(calls, segs):
196
+ scope = SegmentScope(series, seg)
197
+ for la in c.layers:
198
+ la.options["segment"] = scope
199
+ for t in c.tables:
200
+ t.segment = seg
201
+ for c in calls[1:]:
202
+ out.layers += c.layers
203
+ out.tables += c.tables
204
+ out.color_sources += c.color_sources
205
+ out.size_sources += c.size_sources
206
+ out.after += c.after
207
+ for role, v in c.extra.items():
208
+ have = out.extra.get(role)
209
+ out.extra[role] = v if have is None else array("d", list(have) + list(v))
210
+ for role, v in c.sticky_values.items():
211
+ out.sticky_values[role] = out.sticky_values.get(role, []) + list(v)
212
+ for role, v in c.ticks.items():
213
+ out.ticks[role] = out.ticks.get(role, []) + list(v)
214
+ for role, f in c.extent_of.items():
215
+ g = out.extent_of.get(role)
216
+ out.extent_of[role] = f if g is None else _union(g, f)
217
+ for name in ("zero", "exact", "invert", "whole", "hide"):
218
+ getattr(out, name).update(getattr(c, name))
219
+ return out
220
+
221
+
222
+ def _union(f: Any, g: Any) -> Any:
223
+ def extent(scale: Any) -> tuple[int, float, float]:
224
+ n1, a1, b1 = f(scale)
225
+ n2, a2, b2 = g(scale)
226
+ if not n1:
227
+ return n2, a2, b2
228
+ if not n2:
229
+ return n1, a1, b1
230
+ return n1 + n2, min(a1, a2), max(b1, b2)
231
+ return extent
232
+
233
+
234
+ def _per_segment(name: str, method: Any) -> Any:
235
+ """``method`` of a genome series run for each segment of its x axis when it has
236
+ several: ``layers`` merged (:func:`_merged`), other methods the largest result."""
237
+ @functools.wraps(method)
238
+ def run(self: Any, *args: Any) -> Any:
239
+ if self.__dict__.get("_segment") is not None or self.plot is None \
240
+ or not self.segmented():
241
+ return method(self, *args)
242
+ from .scale import segments_of
243
+ width = args[0] if name == "track_height" else None
244
+ segs = segments_of(self.plot, width)
245
+ if segs is None:
246
+ return method(self, *args)
247
+ got = []
248
+ for seg in segs:
249
+ with in_segment(self, seg):
250
+ got.append(method(self, *((seg.px,) if name == "track_height" else args)))
251
+ if name == "layers":
252
+ return _merged(got, self, segs)
253
+ known = [g for g in got if g is not None]
254
+ return max(known) if known else None
255
+ return run
256
+
257
+
258
+ class GenomeSeries(Series):
259
+ """Base of the genome series: the region and the width the data are binned for."""
260
+
261
+ #: The data argument (a reader or plain data).
262
+ reader: Any = None
263
+ #: The methods made per segment (see :func:`_per_segment`).
264
+ _PER_SEGMENT = ("layers", "track_height", "lane_rows")
265
+
266
+ def __init_subclass__(cls, **kw: Any) -> None:
267
+ super().__init_subclass__(**kw)
268
+ for name in GenomeSeries._PER_SEGMENT:
269
+ m = cls.__dict__.get(name)
270
+ if m is not None and not getattr(m, "_per_segment", False):
271
+ w = _per_segment(name, m)
272
+ w._per_segment = True
273
+ setattr(cls, name, w)
274
+
275
+ def segmented(self) -> bool:
276
+ """Whether the series is made once per region of an axis over several (see the
277
+ module); ``False``: it places its data across the regions itself."""
278
+ return True
279
+
280
+ def segment(self) -> Any:
281
+ """The segment being read or drawn (:class:`~plot_io.genome.scale.Segment`), or
282
+ ``None``."""
283
+ return self.__dict__.get("_segment")
284
+
285
+ def cached(self, key: Any, compute: Any, reads: tuple[str, ...] = (),
286
+ kind: Any = None) -> Any:
287
+ seg = self.__dict__.get("_segment")
288
+ if seg is not None and kind is not None: # one result per kind and segment
289
+ kind = (kind, seg.number)
290
+ return super().cached(key, compute, reads, kind)
291
+
292
+ def _given_region(self) -> Region | None:
293
+ seg = self.__dict__.get("_segment")
294
+ if seg is not None:
295
+ return seg.region
296
+ r = self.resolve("region")
297
+ if r is None and self.plot is not None:
298
+ return axis_region(self.plot.x_axis)
299
+ if pending(r):
300
+ plot = self.plot
301
+ return whole(r, lambda: known_sizes(plot if plot is not None else self,
302
+ None if plot is None else plot.x_axis))
303
+ return r
304
+
305
+ def region(self) -> Region:
306
+ r = self._given_region()
307
+ if r is None:
308
+ raise ValueError(f"{self!r} has no region: give region= to the x axis "
309
+ f"(plt.Tracks(region), figure.x_axis) or the series")
310
+ return r
311
+
312
+ def axes_px(self) -> float:
313
+ """The width (px) the data are binned for (see :func:`axes_width`; in a segment, the
314
+ segment's)."""
315
+ seg = self.__dict__.get("_segment")
316
+ return axes_width(self.plot) if seg is None else seg.px
317
+
318
+ def genome_call(self) -> Call:
319
+ """A call asking its x axis for the genome scale, over the series' region (``None``:
320
+ neither the series nor its x axis has one)."""
321
+ call = Call(self)
322
+ call.scales["x"] = "genome"
323
+ call.region = self._given_region()
324
+ return call
325
+
326
+ def _what(self) -> str:
327
+ label = self.plot.y_axis.resolve("label") if self.plot is not None else None
328
+ name = self.resolve("name") if "name" in self.SETTINGS else None
329
+ tag = label or name
330
+ return f"{self.kind}" + (f" {str(tag)!r}" if tag else "")
331
+
332
+ def _guard(self, read: Any) -> Any:
333
+ """``read()``, an error naming the track."""
334
+ try:
335
+ return read()
336
+ except Exception as e:
337
+ if self._what() in str(e):
338
+ raise
339
+ raise type(e)(f"{self._what()}: {e}") from e
340
+
341
+ def track_height(self, width: float) -> float | None:
342
+ """The height (px) the series asks of its track's value lane when the track's
343
+ ``height`` is ``"auto"``; ``None``: the track's ``auto_height``."""
344
+ return None