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,364 @@
1
+ """Links between two places of the genome (:class:`Links`, BEDPE pairs): arcs in a lane of
2
+ their own, or loop marks on a Hi-C triangle or square (``lane="over"`` after it).
3
+
4
+ An arc joins the centres of its two anchors; its height is proportional to their distance,
5
+ the tallest of the links with both anchors in view reaching the lane's edge (links with an
6
+ anchor out of view are cut at it). The lane is as high as that arc at half its anchors'
7
+ distance (a half circle), at most ``max_height``. Arcs are page polylines, cut at the
8
+ axes' edges.
9
+ """
10
+
11
+ import math
12
+ from array import array
13
+ from typing import Any
14
+
15
+ from .._methods import LinksMixin
16
+ from ..color.values import color, with_alpha
17
+ from ..settings import Setting, one_of, table
18
+ from ..transform import settings_both as coord_settings
19
+ from ..layers import add_layer
20
+ from ..layers.base import common
21
+ from ..layers.core import Call, Layer, Table
22
+ from ..layers.geoms import GEOMS
23
+ from ..mapped import color_axis_of, unit_of
24
+ from ..validators import length
25
+ from . import readers, sources
26
+ from .lanes import lane_mapper, lane_setting
27
+ from .series import GenomeSeries, region_setting
28
+
29
+ __all__ = ["Links"]
30
+
31
+ _STEPS = 48 # segments of a whole arc
32
+
33
+
34
+ def _link_color(v: Any) -> Any:
35
+ if isinstance(v, str) and v.strip().lower() == "score":
36
+ return "score"
37
+ return color(v)
38
+
39
+
40
+ class Links(LinksMixin, GenomeSeries):
41
+ """Links between pairs of places (BEDPE): an arc from anchor centre to anchor centre
42
+ in a lane of its own, or, ``lane="over"`` a Hi-C triangle or square, a mark at each
43
+ pair's contact cell (on a square, and at its mirror). ``pairs`` is a reader whose ``read_entries`` gives the BEDPE columns, a
44
+ path gwseq_io reads them from, or plain data (the pairs with an anchor in view are
45
+ read): a dict of columns, a DataFrame, or rows in BEDPE order (``chr1``,
46
+ ``start1``, ``end1``, ``chr2``, ``start2``, ``end2``, then optionally ``name``,
47
+ ``score``).
48
+
49
+ Readers: see `readers <docs/api.md#readers>`_. Strand columns are not drawn. Each arc's height is
50
+ proportional to the anchors' distance: the tallest arc of the links with both anchors in
51
+ view reaches the lane's edge (taller arcs of links with an anchor out of view are cut
52
+ at it), and the lane is as high as that arc at half the distance (a half circle), at
53
+ most ``max_height``. A link with one anchor out of view is drawn to the axes' edge
54
+ (``outside="clip"``) or left out (``"hide"``); links with both anchors out of view, or
55
+ an anchor on another chromosome, are not drawn. Over a Hi-C triangle or square
56
+ (``lane="over"``), see :class:`HiC`.
57
+
58
+ Example::
59
+
60
+ fig = plt.Tracks(("chr2", 143_000_000, 144_000_000), width=300)
61
+ fig.links([("chr2", 143_200_000, 143_210_000, "chr2", 143_500_000, 143_510_000, "L1", 12),
62
+ ("chr2", 143_300_000, 143_310_000, "chr2", 143_900_000, 143_910_000, "L2", 4),
63
+ ("chr2", 143_600_000, 143_610_000, "chr2", 143_700_000, 143_710_000, "L3", 8)],
64
+ color="score")
65
+ """
66
+
67
+ SETTINGS = table(
68
+ region=region_setting(),
69
+ lane=lane_setting(),
70
+ direction=Setting(one_of("up", "down"),
71
+ doc="`up`: arcs rise from the lane's bottom; `down`: they hang "
72
+ "from its top."),
73
+ color=Setting(_link_color,
74
+ doc="A color, or `\"score\"` (the `score` column through "
75
+ "`color_map`)."),
76
+ max_height=Setting(length(positive=True, percent="em"),
77
+ doc="Largest height of the lane: arcs are flattened to fit it."),
78
+ outside=Setting(one_of("clip", "hide"),
79
+ doc="A link with one anchor out of view: `clip` draws it to the "
80
+ "axes' edge; `hide` leaves it out."),
81
+ nan_color=Setting(color, doc="Color of links without a score under "
82
+ "`color=\"score\"`; `None`: the map's `bad` "
83
+ "(transparent)."),
84
+ **common("color_map", "name", "opacity", "z", "line_width"),
85
+ **coord_settings(),
86
+ )
87
+
88
+ def __init__(self, pairs: Any, plot: Any = None, **settings: Any) -> None:
89
+ self.reader = readers.adapt(pairs, "pairs", "Links", owner=self)
90
+ super().__init__(plot=plot, **settings)
91
+
92
+ # -- data ------------------------------------------------------------------------
93
+
94
+ def segmented(self) -> bool:
95
+ """Arcs join regions of an axis over several; loop marks are per Hi-C triangle."""
96
+ from .contacts import under_hic
97
+ return under_hic(self) is not None
98
+
99
+ def _segments(self) -> Any:
100
+ """The segments of an axis over several regions the arcs are drawn across, or
101
+ ``None``."""
102
+ if self.segment() is not None or self.plot is None:
103
+ return None
104
+ from .scale import segments_of
105
+ return segments_of(self.plot)
106
+
107
+ def pairs(self) -> dict[str, list[Any]]:
108
+ """The BEDPE columns of the pairs read for the region (for each region of an axis
109
+ over several, each pair once)."""
110
+ segs = self._segments()
111
+ regions = tuple(g.region for g in segs) if segs is not None else (self.region(),)
112
+ if segs is None:
113
+ from .contacts import under_hic
114
+ hic = under_hic(self)
115
+ if hic is not None and hic.square() and hic.region2() != regions[0]:
116
+ regions += (hic.region2(),) # the pairs with an anchor on y
117
+ return self.cached(regions, lambda: self._guard(
118
+ lambda: sources.read_pairs(self.reader, regions)), kind="pairs")
119
+ if len(regions) == 1:
120
+ return self.cached(regions, lambda: self._guard(
121
+ lambda: sources.read_pairs(self.reader, regions)), kind="pairs")
122
+ return self.cached(regions, lambda: self._guard(lambda: _union(
123
+ [sources.read_pairs(self.reader, regions)])), kind="pairs")
124
+
125
+ def anchor_x(self, chr: str, pos: float) -> float | None:
126
+ """Where position ``pos`` of chromosome ``chr`` is on the x axis (bp; over several
127
+ regions, drawn units); ``None`` where the axis does not show that chromosome.
128
+ Over several regions, a position out of every region is placed from the nearest
129
+ region of its chromosome when that falls past the axis' ends, else ``None``."""
130
+ segs = self._segments()
131
+ if segs is None:
132
+ return float(pos) if chr == self.region().chr else None
133
+ seg = segs.at(chr, pos)
134
+ if seg is not None:
135
+ return seg.u(pos)
136
+ mine = [g for g in segs if g.region.chr == chr]
137
+ if not mine:
138
+ return None
139
+ near = min(mine, key=lambda g: min(abs(pos - g.region.start), abs(pos - g.region.end)))
140
+ u = near.u(pos)
141
+ return u if u < 0 or u > segs.total else None
142
+
143
+ def in_view(self, x: float) -> bool:
144
+ segs = self._segments()
145
+ if segs is not None:
146
+ return any(g.u0 <= x <= g.u1 for g in segs)
147
+ r = self.region()
148
+ return r.start <= x <= r.end
149
+
150
+ def links(self) -> list[tuple[int, float, float]]:
151
+ """The links drawn: ``(i, x1, x2)``, the pair's row and its anchors' centres on the
152
+ x axis, ``x1 <= x2``; with ``outside="clip"`` the links with one anchor in view,
153
+ with ``"hide"`` both."""
154
+ cols = self.pairs()
155
+ both = self.resolve("outside") == "hide"
156
+ out = []
157
+ for i in range(len(cols["start1"])):
158
+ a = self.anchor_x(cols["chr1"][i], (cols["start1"][i] + cols["end1"][i]) / 2)
159
+ b = self.anchor_x(cols["chr2"][i], (cols["start2"][i] + cols["end2"][i]) / 2)
160
+ if a is None or b is None:
161
+ continue
162
+ seen = self.in_view(a) + self.in_view(b)
163
+ if seen == 2 or (seen == 1 and not both):
164
+ out.append((i, min(a, b), max(a, b)))
165
+ return out
166
+
167
+ def _visible(self, x1: float, x2: float, lo: float, hi: float) -> tuple[float, float]:
168
+ """The angles (0 at ``x1``, π at ``x2``) of the part of the half ellipse over
169
+ ``[x1, x2]`` within ``[lo, hi]``."""
170
+ c, r = (x1 + x2) / 2, (x2 - x1) / 2
171
+ if r <= 0:
172
+ return 0.0, math.pi
173
+ return (math.acos(min(1.0, max(-1.0, (c - lo) / r))),
174
+ math.acos(min(1.0, max(-1.0, (c - hi) / r))))
175
+
176
+ def _tallest(self, spans: Any, lo: float, hi: float) -> float:
177
+ """The tallest part within ``[lo, hi]`` of the half circles over ``spans``
178
+ (``(x1, x2)`` pairs)."""
179
+ top = 0.0
180
+ for x1, x2 in spans:
181
+ t0, t1 = self._visible(x1, x2, lo, hi)
182
+ s = 1.0 if t0 <= math.pi / 2 <= t1 else max(math.sin(t0), math.sin(t1))
183
+ top = max(top, (x2 - x1) / 2 * s)
184
+ return top
185
+
186
+ def _scale_spans(self, spans: list[tuple[float, float, bool]]) -> list[tuple[float, float]]:
187
+ """The ``(x1, x2)`` of ``spans`` (``(x1, x2, both anchors in view)``) that set the
188
+ arcs' scale: those with both anchors in view, else all."""
189
+ both = [(a, b) for a, b, inside in spans if inside]
190
+ return both or [(a, b) for a, b, _ in spans]
191
+
192
+ def _inside(self, a: float, b: float) -> bool:
193
+ return self.in_view(a) and self.in_view(b)
194
+
195
+ def lane_px(self) -> float:
196
+ """The lane's height (px): the tallest arc part in view of the links with both
197
+ anchors in view, at most ``max_height``."""
198
+ spans = self._scale_spans([(a, b, self._inside(a, b)) for _, a, b in self.links()])
199
+ segs = self._segments()
200
+ lo, hi = (0.0, segs.total) if segs is not None else (self.region().start,
201
+ self.region().end)
202
+ tallest = self._tallest(spans, lo, hi) * self.axes_px() / ((hi - lo) or 1.0)
203
+ return max(1.0, min(tallest, float(self.resolve("max_height"))))
204
+
205
+ def lane_rows(self) -> int:
206
+ return 1
207
+
208
+ def drawn_rows(self) -> tuple[float, float]:
209
+ return 0.0, 1.0
210
+
211
+ # -- drawing ---------------------------------------------------------------------
212
+
213
+ def _colors(self, ctx: Any) -> list[Any]:
214
+ cols = self.pairs()
215
+ n = len(cols["start1"])
216
+ opacity = self.resolve("opacity")
217
+ opacity = 1.0 if opacity is None else opacity
218
+ c = self.resolve("color")
219
+ if c != "score":
220
+ return [with_alpha(ctx.color(c), opacity)] * n
221
+ axis = color_axis_of(self)
222
+ m, unit = axis.color_map(), unit_of(axis)
223
+ nan = ctx.color(self.resolve("nan_color"))
224
+ bad = nan if nan is not None else m.bad
225
+ out = []
226
+ for v in cols.get("score") or [None] * n:
227
+ t = math.nan if v is None else unit(v)
228
+ out.append(with_alpha(m(min(1.0, max(0.0, t))), opacity) if math.isfinite(t)
229
+ else bad)
230
+ return out
231
+
232
+ def layers(self) -> Call:
233
+ from .contacts import under_hic
234
+ call = self.genome_call()
235
+ hic = under_hic(self)
236
+ lines = Table()
237
+ lines.lazy["page_lines"] = self._arcs if hic is None \
238
+ else (lambda ctx: self._marks(ctx, hic))
239
+ call.layers.append(Layer(GEOMS["path"], lines, paint={}, stat="links",
240
+ occupy=False, key=False))
241
+ if self.resolve("color") == "score":
242
+ scores = [v for v in self.pairs().get("score") or () if v is not None]
243
+ call.color_sources = [(color_axis_of(self), array("d", scores))]
244
+ if self._segments() is None:
245
+ r = self.region()
246
+ call.extra = {"x": array("d", [r.start, r.end])}
247
+ else:
248
+ call.region = None
249
+ call.hide = {"y"}
250
+ if hic is None:
251
+ call.lane = lane_mapper(self)
252
+ call.exact = {"x"}
253
+ return call
254
+
255
+ def _bar_source(self) -> Any:
256
+ """``color="score"`` maps the scores through the color axis, as numbers do."""
257
+ return self if self.resolve("color") == "score" else super()._bar_source()
258
+
259
+ def _arcs(self, ctx: Any) -> list[Any]:
260
+ """The arcs (page polylines), the tallest of the links with both anchors in view at
261
+ the lane's edge, taller ones cut at it."""
262
+ lo, hi = ctx.rect[0], ctx.rect[2]
263
+ ym = ctx.ymap
264
+ top, bottom = ym.position(ym.flo), ym.position(ym.fhi)
265
+ base, room = (bottom, top - bottom) if self.resolve("direction") == "up" \
266
+ else (top, bottom - top)
267
+ links = self.links()
268
+ xs = [(i, *sorted((ctx.xmap(a), ctx.xmap(b)))) for i, a, b in links]
269
+ tallest = self._tallest(self._scale_spans([
270
+ (x1, x2, self._inside(a, b)) for (_, x1, x2), (_, a, b) in zip(xs, links)]),
271
+ lo, hi)
272
+ k = room / tallest if tallest > 0 else 0.0
273
+ cs = self._colors(ctx)
274
+ lw = self.resolve("line_width")
275
+ out: list[Any] = []
276
+ for i, x1, x2 in xs:
277
+ t0, t1 = self._visible(x1, x2, lo, hi)
278
+ c, r = (x1 + x2) / 2, (x2 - x1) / 2
279
+ n = max(2, math.ceil(_STEPS * (t1 - t0) / math.pi))
280
+ ts = [t0 + (t1 - t0) * j / n for j in range(n + 1)]
281
+ out.append((array("d", [c - r * math.cos(t) for t in ts]),
282
+ array("d", [base + math.copysign(min(abs(k * r * math.sin(t)),
283
+ abs(room)), room)
284
+ for t in ts]), cs[i], lw))
285
+ return out
286
+
287
+ def _marks(self, ctx: Any, hic: Any) -> list[Any]:
288
+ """Loop marks: an outlined square at each pair's contact cell of ``hic`` (a
289
+ diamond in the triangle), one bin wide, at least half an em."""
290
+ if hic.square():
291
+ return self._square_marks(ctx, hic)
292
+ n, _, start, b = hic.data()
293
+ depth = hic.depth()
294
+ ppb = abs(ctx.xmap(start + b) - ctx.xmap(start)) / b if b else 0.0
295
+ side = max(b, 0.5 * float(hic.resolve("font_size")) / ppb) if ppb else b
296
+ cs = self._colors(ctx)
297
+ lw = self.resolve("line_width")
298
+ out: list[Any] = []
299
+ for i, a, c in self.links():
300
+ u = start + math.floor((a - start) / b) * b + b / 2 # the cells' centres
301
+ v = start + math.floor((c - start) / b) * b + b / 2
302
+ x, d = (u + v) / 2, v - u
303
+ if d - side / 2 > depth:
304
+ continue
305
+ pts = [(x - side / 2, d), (x, d - side), (x + side / 2, d), (x, d + side),
306
+ (x - side / 2, d)]
307
+ out.append((array("d", [ctx.xmap(p) for p, _ in pts]),
308
+ array("d", [ctx.ymap(q) for _, q in pts]), cs[i], lw))
309
+ return out
310
+
311
+
312
+ def _square_marks(self, ctx: Any, hic: Any) -> list[Any]:
313
+ """Loop marks on a square: an outlined square at each pair's cell (first anchor on
314
+ x, second on y) and at its mirror, where in view, one bin wide, at least half an
315
+ em."""
316
+ _, _, _, start, b, start2, b2 = hic.square_data()
317
+ r, r2 = hic.region(), hic.region2()
318
+ em = 0.5 * float(hic.resolve("font_size"))
319
+ ppx = abs(ctx.xmap(start + b) - ctx.xmap(start)) / b if b else 0.0
320
+ ppy = abs(ctx.ymap(start2 + b2) - ctx.ymap(start2)) / b2 if b2 else 0.0
321
+ sx = max(b, em / ppx) if ppx else b
322
+ sy = max(b2, em / ppy) if ppy else b2
323
+ cols = self.pairs()
324
+ cs = self._colors(ctx)
325
+ lw = self.resolve("line_width")
326
+ out: list[Any] = []
327
+ for i in range(len(cols["start1"])):
328
+ a = (cols["chr1"][i], (cols["start1"][i] + cols["end1"][i]) / 2)
329
+ c = (cols["chr2"][i], (cols["start2"][i] + cols["end2"][i]) / 2)
330
+ seen = set()
331
+ for (cx, px), (cy, py) in ((a, c), (c, a)):
332
+ if cx != r.chr or cy != r2.chr or not (r.start <= px <= r.end and
333
+ r2.start <= py <= r2.end):
334
+ continue
335
+ u = start + math.floor((px - start) / b) * b + b / 2 # the cell's centre
336
+ v = start2 + math.floor((py - start2) / b2) * b2 + b2 / 2
337
+ if (u, v) in seen:
338
+ continue
339
+ seen.add((u, v))
340
+ xs = (u - sx / 2, u + sx / 2, u + sx / 2, u - sx / 2, u - sx / 2)
341
+ ys = (v - sy / 2, v - sy / 2, v + sy / 2, v + sy / 2, v - sy / 2)
342
+ out.append((array("d", [ctx.xmap(p) for p in xs]),
343
+ array("d", [ctx.ymap(q) for q in ys]), cs[i], lw))
344
+ return out
345
+
346
+
347
+ def _union(tables: list[dict[str, list[Any]]]) -> dict[str, list[Any]]:
348
+ """The rows of BEDPE column tables, each pair once (in the order first read)."""
349
+ keys = ("chr1", "start1", "end1", "chr2", "start2", "end2")
350
+ names = list(tables[0]) if tables else []
351
+ out: dict[str, list[Any]] = {k: [] for k in names}
352
+ seen = set()
353
+ for t in tables:
354
+ for i in range(len(t["start1"])):
355
+ k = tuple(t[c][i] for c in keys)
356
+ if k in seen:
357
+ continue
358
+ seen.add(k)
359
+ for c in names:
360
+ out[c].append(t[c][i] if c in t else None)
361
+ return out
362
+
363
+
364
+ add_layer("links", Links)
plot_io/genome/pack.py ADDED
@@ -0,0 +1,27 @@
1
+ """Packing intervals onto rows (IGV's "expanded" mode)."""
2
+
3
+ import heapq
4
+ from collections.abc import Sequence
5
+
6
+ __all__ = ["pack_rows"]
7
+
8
+
9
+ def pack_rows(lo: Sequence[float], hi: Sequence[float], gap: float = 0.0) -> list[int]:
10
+ """The row of each interval ``[lo, hi)``: greedily, by start, the first row whose last
11
+ interval ends at least ``gap`` before it. Starts only grow, so a row that has room
12
+ keeps it: rows are freed from a heap of their ends into a heap of free row numbers."""
13
+ order = sorted(range(len(lo)), key=lambda i: (lo[i], hi[i]))
14
+ busy: list[tuple[float, int]] = [] # (end + gap, row)
15
+ free: list[int] = []
16
+ rows = [0] * len(lo)
17
+ count = 0
18
+ for i in order:
19
+ while busy and busy[0][0] <= lo[i]:
20
+ heapq.heappush(free, heapq.heappop(busy)[1])
21
+ if free:
22
+ r = heapq.heappop(free)
23
+ else:
24
+ r, count = count, count + 1
25
+ rows[i] = r
26
+ heapq.heappush(busy, (hi[i] + gap, r))
27
+ return rows
@@ -0,0 +1,145 @@
1
+ """What plot_io asks of a reader, and the one door a series' data come in by.
2
+
3
+ The protocols name the members plot_io calls, with the arguments it passes
4
+ (:mod:`plot_io.genome.sources` makes the calls). gwseq_io's readers meet them; any
5
+ object with the same members can be drawn, and :func:`plot_io.genome.testing.check`
6
+ says whether it does. A reader is recognised by its ``type`` attribute (``"bigwig"``,
7
+ ``"bed"``, ``"hic"``, …) or, without it, by its members
8
+ (:func:`~plot_io.genome.sources.kind_of`).
9
+
10
+ :func:`adapt` turns a series' data argument into a reader: a reader passes through; a
11
+ path is opened with gwseq_io (:func:`~plot_io.genome.adapters.open_path`) at draw, once per
12
+ figure; plain data
13
+ (columns, a DataFrame, rows, a matrix) becomes an in-memory reader
14
+ (:mod:`plot_io.genome.adapters`).
15
+ """
16
+
17
+ from collections.abc import Mapping, Sequence
18
+ from typing import Any, Protocol, runtime_checkable
19
+
20
+ from . import sources
21
+
22
+ __all__ = ["SignalReader", "IntervalReader", "AnnotationReader", "PairReader",
23
+ "ContactReader", "Resolving", "Sized", "Region3", "adapt", "WANTS"]
24
+
25
+ #: A region as plot_io passes it: ``(chr, start, end)``, 0-based, half-open.
26
+ Region3 = tuple[str, int, int]
27
+
28
+
29
+ @runtime_checkable
30
+ class SignalReader(Protocol):
31
+ """Values binned over regions (bigWig; the coverage of bigBed, BED and peaks)."""
32
+
33
+ def read_values(self, regions: Sequence[Region3], *, bin_count: int | None = ...,
34
+ bin_size: int | None = ..., bin_mode: str = ..., def_value: float = ...,
35
+ zoom: Any = ..., snap_to_grid: Any = ..., full_bin: bool = ...) -> Any:
36
+ """One row per region: ``bin_count`` bins over it, or bins of ``bin_size`` from its
37
+ start (``snap_to_grid="expand"``: grown to the genome-wide grid of the size;
38
+ ``full_bin``: a last partial bin kept), each the ``bin_mode`` reduction (``mean``,
39
+ ``min``, ``max``, ``sum``, …) of its bases, ``def_value`` for bases no record
40
+ covers. A 2-D buffer or a sequence of rows."""
41
+ ...
42
+
43
+
44
+ @runtime_checkable
45
+ class IntervalReader(Protocol):
46
+ """Intervals (bigBed, BED, peaks): the entries overlapping regions."""
47
+
48
+ def read_entries(self, regions: Sequence[Region3], **kw: Any) -> Any:
49
+ """A mapping of columns (``chr``, ``start``, ``end``, then any of the BED columns,
50
+ by their BED or autoSql names), the entries overlapping the regions. Called with
51
+ ``columns=n`` (the first ``n`` columns) when the reader has an ``auto_sql``
52
+ mapping."""
53
+ ...
54
+
55
+
56
+ @runtime_checkable
57
+ class AnnotationReader(Protocol):
58
+ """Gene annotation (GTF, GFF3): transcripts and their exons. ``tss`` only marks the
59
+ kind."""
60
+
61
+ def read_entries(self, regions: Sequence[Region3], *, model: str = ...,
62
+ gene_type: Any = ..., transcript_type: Any = ...) -> Any:
63
+ """With ``model="transcript"``: a table of the transcripts overlapping the regions
64
+ (``start``, ``end``, ``strand``, ``cds_start``, ``cds_end`` (-1: non-coding),
65
+ ``gene_id``, ``transcript_id``, optional ``gene_name``, ``transcript_name``),
66
+ whose ``nested["exons"]`` is a table (``start``, ``end``) with ``groups``:
67
+ transcript ``i``'s exons are rows ``groups[i]:groups[i + 1]``."""
68
+ ...
69
+
70
+ def tss(self, *args: Any, **kw: Any) -> Any: ...
71
+
72
+
73
+ @runtime_checkable
74
+ class PairReader(Protocol):
75
+ """Pairs of places (BEDPE), for links."""
76
+
77
+ def read_entries(self, regions: Sequence[Region3]) -> Any:
78
+ """A mapping of the BEDPE columns (``chr1``, ``start1``, ``end1``, ``chr2``,
79
+ ``start2``, ``end2``, optional ``name``, ``score``; the first anchor's may be
80
+ ``chr``, ``start``, ``end``): the pairs with an anchor overlapping the regions
81
+ (at least those whose first anchor does)."""
82
+ ...
83
+
84
+
85
+ @runtime_checkable
86
+ class ContactReader(Protocol):
87
+ """A contact matrix (Hi-C)."""
88
+
89
+ bin_sizes: Sequence[int]
90
+ normalizations: Sequence[str]
91
+
92
+ def read_values(self, region1: Region3, region2: Region3 | None = ..., *,
93
+ bin_size: int | None = ...,
94
+ bin_count: int | tuple[int, int] | None = ...,
95
+ snap_to_grid: Any = ..., full_bin: bool = ...,
96
+ normalization: str = ..., matrix_type: str = ...,
97
+ max_distance: int | None = ...) -> Any:
98
+ """The matrix of ``region1`` against ``region2`` (default ``region1``): rows of
99
+ ``region1``'s bins, columns of ``region2``'s, in bins of one of ``bin_sizes`` (on
100
+ its genome-wide grid with ``snap_to_grid="expand"``) or ``bin_count`` bins (with
101
+ ``region2``: a ``(rows, columns)`` pair);
102
+ ``normalization`` ``"none"`` or one of ``normalizations``; ``matrix_type``
103
+ ``observed``, ``oe`` or ``expected``; cells farther than ``max_distance`` from the
104
+ diagonal may be left out (NaN or 0)."""
105
+ ...
106
+
107
+
108
+ @runtime_checkable
109
+ class Resolving(Protocol):
110
+ """Optional: the bounds of a binned read, so its bins are placed exactly."""
111
+
112
+ def resolve_regions(self, regions: Any, *more: Any, **kw: Any) -> Any:
113
+ """With the arguments of the read: the ``start`` and ``end`` columns of the rows it
114
+ gives (a contact reader: under ``"rows"``, and those of ``region2`` under
115
+ ``"cols"``)."""
116
+ ...
117
+
118
+
119
+ @runtime_checkable
120
+ class Sized(Protocol):
121
+ """Optional: chromosome sizes, giving the end of a whole chromosome (``"chr1"``)."""
122
+
123
+ chr_sizes: Mapping[str, int]
124
+
125
+
126
+ #: The reader kinds each data role takes (:func:`~plot_io.genome.sources.kind_of`).
127
+ WANTS = {"signal": ("signal", "intervals"), "intervals": ("intervals",),
128
+ "annotation": ("annotation",), "pairs": ("intervals",), "contacts": ("contacts",)}
129
+
130
+
131
+ def adapt(value: Any, want: str, what: str, owner: Any = None) -> Any:
132
+ """``value`` as a reader of role ``want`` (``"signal"``, ``"intervals"``,
133
+ ``"annotation"``, ``"pairs"``, ``"contacts"``) for series ``what``: a reader of a kind
134
+ the role takes passes through; a path (``str``, ``os.PathLike``) is opened with
135
+ gwseq_io when first read, once per figure of ``owner`` (its series), and closed with
136
+ the figure (:class:`~plot_io.genome.adapters.Deferred`); plain data becomes an in-memory reader
137
+ (an interval table for ``annotation``: BED12 models)."""
138
+ from . import adapters
139
+ if isinstance(value, (str, bytes)) or hasattr(value, "__fspath__"):
140
+ adapters.check_path(value, what)
141
+ return adapters.Deferred(value, WANTS[want], what, owner)
142
+ if sources.kind_of(value) is not None:
143
+ sources.check_reader(value, WANTS[want], what)
144
+ return value
145
+ return adapters.from_data(value, want, what)