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.
- plot_io/__init__.py +73 -0
- plot_io/_methods.py +1691 -0
- plot_io/_native/__init__.py +199 -0
- plot_io/_native/__main__.py +197 -0
- plot_io/_native/_api.py +457 -0
- plot_io/_native/bin/windows-arm64.dll +0 -0
- plot_io/_native/kernels.py +335 -0
- plot_io/_native/manifest.json +49 -0
- plot_io/axis.py +1347 -0
- plot_io/boxes.py +1156 -0
- plot_io/canvas.py +389 -0
- plot_io/color/__init__.py +3 -0
- plot_io/color/data.py +1646 -0
- plot_io/color/maps.py +922 -0
- plot_io/color/values.py +295 -0
- plot_io/color/variants.py +1246 -0
- plot_io/colorbar.py +1426 -0
- plot_io/columns.py +614 -0
- plot_io/decorations.py +920 -0
- plot_io/defaults.py +809 -0
- plot_io/density.py +113 -0
- plot_io/ext.py +68 -0
- plot_io/facets.py +240 -0
- plot_io/figure.py +788 -0
- plot_io/formatters.py +97 -0
- plot_io/genome/__init__.py +17 -0
- plot_io/genome/adapters.py +356 -0
- plot_io/genome/combine.py +123 -0
- plot_io/genome/contacts.py +508 -0
- plot_io/genome/intervals.py +750 -0
- plot_io/genome/lanes.py +191 -0
- plot_io/genome/layout.py +184 -0
- plot_io/genome/links.py +364 -0
- plot_io/genome/pack.py +27 -0
- plot_io/genome/readers.py +145 -0
- plot_io/genome/regions.py +228 -0
- plot_io/genome/scale.py +349 -0
- plot_io/genome/series.py +344 -0
- plot_io/genome/signal.py +621 -0
- plot_io/genome/sites.py +782 -0
- plot_io/genome/sources.py +459 -0
- plot_io/genome/testing.py +161 -0
- plot_io/geometry.py +289 -0
- plot_io/groups.py +308 -0
- plot_io/guides.py +503 -0
- plot_io/helpers/__init__.py +17 -0
- plot_io/helpers/clustermap.py +202 -0
- plot_io/helpers/dendrogram.py +172 -0
- plot_io/helpers/grids.py +304 -0
- plot_io/helpers/qq.py +334 -0
- plot_io/helpers/raincloud.py +249 -0
- plot_io/helpers/tree.py +214 -0
- plot_io/helpers/volcano.py +505 -0
- plot_io/hints.py +125 -0
- plot_io/kernels.py +740 -0
- plot_io/labels.py +566 -0
- plot_io/layers/__init__.py +100 -0
- plot_io/layers/base.py +630 -0
- plot_io/layers/context.py +87 -0
- plot_io/layers/core.py +446 -0
- plot_io/layers/geoms.py +1362 -0
- plot_io/layers/grammar.py +586 -0
- plot_io/layers/paint.py +614 -0
- plot_io/layers/pipeline.py +170 -0
- plot_io/layers/positions.py +431 -0
- plot_io/layers/stats.py +1088 -0
- plot_io/layout.py +1381 -0
- plot_io/legend.py +605 -0
- plot_io/levels.py +509 -0
- plot_io/limits.py +124 -0
- plot_io/lines.py +42 -0
- plot_io/mapped.py +522 -0
- plot_io/markers.py +110 -0
- plot_io/markup.py +128 -0
- plot_io/numbers.py +354 -0
- plot_io/occupancy.py +295 -0
- plot_io/plot.py +597 -0
- plot_io/presets/__init__.py +51 -0
- plot_io/presets/_util.py +153 -0
- plot_io/presets/area.py +145 -0
- plot_io/presets/bar.py +261 -0
- plot_io/presets/box.py +400 -0
- plot_io/presets/catpoints.py +177 -0
- plot_io/presets/contour.py +353 -0
- plot_io/presets/curves.py +30 -0
- plot_io/presets/dots.py +355 -0
- plot_io/presets/ecdf.py +97 -0
- plot_io/presets/errorbar.py +106 -0
- plot_io/presets/errors.py +296 -0
- plot_io/presets/fill.py +74 -0
- plot_io/presets/heatmap.py +322 -0
- plot_io/presets/hexbin.py +198 -0
- plot_io/presets/hist.py +391 -0
- plot_io/presets/hist2d.py +87 -0
- plot_io/presets/kde.py +162 -0
- plot_io/presets/kde2d.py +147 -0
- plot_io/presets/line.py +143 -0
- plot_io/presets/pie.py +502 -0
- plot_io/presets/regression.py +115 -0
- plot_io/presets/scatter.py +194 -0
- plot_io/presets/stem.py +100 -0
- plot_io/presets/violin.py +323 -0
- plot_io/py.typed +0 -0
- plot_io/registry.py +82 -0
- plot_io/render.py +480 -0
- plot_io/rows.py +491 -0
- plot_io/scales.py +103 -0
- plot_io/seams.py +138 -0
- plot_io/settings.py +807 -0
- plot_io/shapes.py +66 -0
- plot_io/sharing.py +232 -0
- plot_io/sizemaps.py +97 -0
- plot_io/spec.py +240 -0
- plot_io/states.py +216 -0
- plot_io/styles/__init__.py +307 -0
- plot_io/styles/library.py +397 -0
- plot_io/tables.py +138 -0
- plot_io/text.py +412 -0
- plot_io/ticks.py +400 -0
- plot_io/times.py +287 -0
- plot_io/titles.py +225 -0
- plot_io/transform.py +114 -0
- plot_io/types.py +13604 -0
- plot_io/units.py +768 -0
- plot_io/validators.py +1029 -0
- plot_io/values.py +190 -0
- plot_io-0.12.0.dist-info/METADATA +215 -0
- plot_io-0.12.0.dist-info/RECORD +130 -0
- plot_io-0.12.0.dist-info/WHEEL +4 -0
- plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
plot_io/genome/links.py
ADDED
|
@@ -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)
|