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.
- 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-x86_64.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
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"""Hi-C contact matrices (:class:`HiC`): the upper triangle of a contact matrix, the
|
|
2
|
+
diagonal along the x axis, or (``shape="square"``) the whole matrix of ``region`` (x)
|
|
3
|
+
against ``region2`` (y), both axes on the genome scale. Series called with
|
|
4
|
+
``lane="over"`` after one draw in its coordinates (:func:`under_hic`): intervals as
|
|
5
|
+
outlined triangles or squares on the diagonal (TADs, :func:`tads`), links as loop marks
|
|
6
|
+
(:mod:`plot_io.genome.links`)."""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
from array import array
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Self, Unpack
|
|
11
|
+
|
|
12
|
+
from .. import kernels
|
|
13
|
+
from ..color.values import color, with_alpha
|
|
14
|
+
from ..settings import DECLINE, Setting, number, one_of, table
|
|
15
|
+
from ..transform import settings_both as coord_settings
|
|
16
|
+
from ..layers import add_layer
|
|
17
|
+
from ..layers.base import common
|
|
18
|
+
from ..layers.core import Call, Layer, Table
|
|
19
|
+
from ..layers.geoms import GEOMS
|
|
20
|
+
from ..layers.paint import Paint, role
|
|
21
|
+
from ..mapped import color_axis_of, trained
|
|
22
|
+
from ..presets._util import gradient_entry
|
|
23
|
+
from ..validators import length
|
|
24
|
+
from . import readers, sources
|
|
25
|
+
from .lanes import is_lane, value_height
|
|
26
|
+
from .adapters import Matrix
|
|
27
|
+
from .regions import Region, Regions, parse_region, pending, whole
|
|
28
|
+
from .series import GenomeSeries, check_bins, known_sizes, region_setting
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from ..types import HiCSettings
|
|
32
|
+
|
|
33
|
+
__all__ = ["HiC", "under_hic", "tads"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _max_height(v: Any) -> Any:
|
|
37
|
+
"""``None``, a fraction of the axes width (``"33%"``, kept as ``("%", 0.33)``) or a
|
|
38
|
+
length (px)."""
|
|
39
|
+
if v is None:
|
|
40
|
+
return None
|
|
41
|
+
if isinstance(v, str) and v.strip().endswith("%"):
|
|
42
|
+
f = float(v.strip()[:-1]) / 100
|
|
43
|
+
if not f > 0:
|
|
44
|
+
raise ValueError(f"expected a positive percentage, got {v!r}")
|
|
45
|
+
return ("%", f)
|
|
46
|
+
if isinstance(v, tuple) and len(v) == 2 and v[0] == "%":
|
|
47
|
+
return v
|
|
48
|
+
return length(positive=True)(v)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _region2(v: Any) -> Any:
|
|
52
|
+
return parse_region(v, whole=True)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _oe(rule: Any) -> Any:
|
|
56
|
+
"""A class rule of ``matrix_type="oe"`` (declines for other matrices)."""
|
|
57
|
+
def r(s: "HiC") -> Any:
|
|
58
|
+
return rule(s) if s.resolve("matrix_type") == "oe" else DECLINE
|
|
59
|
+
return r
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _oe_limits(s: "HiC") -> Any:
|
|
63
|
+
"""Limits symmetric about 1 on a log scale: ``1 / m`` to ``m``, ``log m`` the 0.99
|
|
64
|
+
quantile of the values' ``|log v|``."""
|
|
65
|
+
def compute() -> Any:
|
|
66
|
+
vals = s.cell_values()
|
|
67
|
+
logs = array("d", [abs(math.log(v)) for v in vals if v > 0 and math.isfinite(v)])
|
|
68
|
+
q = kernels.quantiles(logs, [0.99])[0] if logs else math.nan
|
|
69
|
+
m = math.exp(q) if math.isfinite(q) and q > 0 else 2.0
|
|
70
|
+
return (1 / m, m)
|
|
71
|
+
return s.cached(s.data_key(), compute, kind="oe_limits")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class HiC(GenomeSeries):
|
|
75
|
+
"""The upper triangle of a contact matrix, the diagonal along the x axis (each cell a
|
|
76
|
+
diamond), or with ``shape="square"`` the whole matrix of ``region`` (x) against
|
|
77
|
+
``region2`` (y, its start at the top). ``reader`` is a Hi-C reader, a path (opened
|
|
78
|
+
with gwseq_io), or a square matrix over the region (bins of ``span / n``).
|
|
79
|
+
|
|
80
|
+
On the triangle, the matrix is drawn as an image under a 45° transform and clipped to
|
|
81
|
+
the track. Readers and paths: see `readers <docs/api.md#readers>`_. ``bins="0.5px"`` takes the finest of the
|
|
82
|
+
reader's ``bin_sizes`` whose cells are at least 0.5 px wide; the cells of a size
|
|
83
|
+
(``bins="5kb"``) sit on the file's grid, those past the region cut by the track's edges,
|
|
84
|
+
and ``bins=n`` reads n cells over the region exactly. The color axis' limits default to
|
|
85
|
+
quantiles of the cells drawn (``color_limits="q:0.01/q:0.99"``), on a log scale (cells of
|
|
86
|
+
0 take the map's low end; cells without a value take ``nan_color``, transparent),
|
|
87
|
+
through the ``sequential_map``. A region without a finite positive value draws no
|
|
88
|
+
cells, its color axis at its default limits. With ``matrix_type="oe"`` the defaults are
|
|
89
|
+
a diverging map about 1 (``center=1``: the ``diverging_map``) on a log scale, the limits
|
|
90
|
+
``1/m`` to ``m``, ``log m`` the 0.99 quantile of the cells' ``|log v|``; values given
|
|
91
|
+
(``center``, ``color_map``, ``color_limits``, ``color_scale``, or a style's) win.
|
|
92
|
+
|
|
93
|
+
Series called on the track after the triangle with ``lane="over"`` draw in its
|
|
94
|
+
coordinates: a ``bed`` draws each interval as an outlined triangle (a TAD: its sides
|
|
95
|
+
from the interval's ends up to the apex at their distance, cut at the track's depth), in
|
|
96
|
+
the features' colors and ``line_width``; ``links`` draw a mark at each pair's contact
|
|
97
|
+
cell, an outlined square one bin wide (at least half an em) at the cell of the anchors'
|
|
98
|
+
centres::
|
|
99
|
+
|
|
100
|
+
h = fig.hic(reader, depth=2_000_000)
|
|
101
|
+
h.plot.bed("tads.bed", lane="over")
|
|
102
|
+
h.plot.links(loops, lane="over")
|
|
103
|
+
|
|
104
|
+
``shape="square"`` draws the whole matrix of ``region`` (x) against ``region2`` (y;
|
|
105
|
+
default ``region``, another chromosome draws a rectangle) as cells, row 0 at the top,
|
|
106
|
+
the diagonal from the top left. Both axes take the genome scale. In a tracks figure
|
|
107
|
+
the track is ``width × span2 / span`` high; in an ordinary plot the aspect is equal
|
|
108
|
+
(``data_ratio``). ``depth``, ``direction``, ``max_height`` and ``fit`` do not apply;
|
|
109
|
+
``bins``, ``normalization``, ``matrix_type`` and the color settings do. Over a square, a
|
|
110
|
+
``bed`` draws each interval as an outlined square on the diagonal (none when
|
|
111
|
+
``region2`` is on another chromosome) and ``links`` a mark at each pair's cell (first
|
|
112
|
+
anchor on x, second on y) and at its mirror. A square cannot span several regions side
|
|
113
|
+
by side::
|
|
114
|
+
|
|
115
|
+
fig = plt.Figure()
|
|
116
|
+
h = fig.plot().hic("cells.hic", region="chr2:30M-70M", shape="square", colorbar=True)
|
|
117
|
+
h.plot.bed("tads.bed", region="chr2:30M-70M", lane="over")
|
|
118
|
+
|
|
119
|
+
Example::
|
|
120
|
+
|
|
121
|
+
rng = random.Random(1)
|
|
122
|
+
n = 48
|
|
123
|
+
fig = plt.Tracks(("chr1", 0, 2_400_000), width=300)
|
|
124
|
+
fig.hic([[100 / (1 + abs(i - j)) * (3 if i // 16 == j // 16 else 1) * rng.uniform(0.7, 1.3)
|
|
125
|
+
for j in range(n)] for i in range(n)])
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
SETTINGS = table(
|
|
129
|
+
region=region_setting(),
|
|
130
|
+
shape=Setting(one_of("triangle", "square"),
|
|
131
|
+
doc="`triangle`: the upper triangle, the diagonal along x; `square`: "
|
|
132
|
+
"the matrix of `region` (x) against `region2` (y, from the top), "
|
|
133
|
+
"the diagonal from the top left, in a track as high as "
|
|
134
|
+
"`width × span2 / span`, elsewhere at equal bp per px on both "
|
|
135
|
+
"axes (`depth`, `direction`, `max_height` and `fit` do not "
|
|
136
|
+
"apply)."),
|
|
137
|
+
region2=Setting(_region2,
|
|
138
|
+
doc="The region of the y axis of `shape=\"square\"`, in the forms "
|
|
139
|
+
"of `region`; unset: `region`."),
|
|
140
|
+
depth=Setting(number(positive=True, integer=True), doc="Largest distance shown (bp); default the region's span "
|
|
141
|
+
"(the whole triangle)."),
|
|
142
|
+
bins=Setting(check_bins,
|
|
143
|
+
doc="A bin count over the region (exactly, from the coarsest "
|
|
144
|
+
"resolution giving that many); one of the reader's `bin_sizes` "
|
|
145
|
+
"(`\"10kb\"`), read on its grid (the region grown to it, then cut "
|
|
146
|
+
"by the track); or a cell width on the axes (`\"0.5px\"`): the "
|
|
147
|
+
"finest resolution whose cells are at least that wide."),
|
|
148
|
+
normalization=Setting(str, doc="`\"none\"` or one of the reader's "
|
|
149
|
+
"`normalizations`."),
|
|
150
|
+
matrix_type=Setting(one_of("observed", "oe", "expected"),
|
|
151
|
+
doc="`observed`, `oe` (observed / expected) or `expected`."),
|
|
152
|
+
**common("color_map"),
|
|
153
|
+
nan_color=Setting(color, doc="Color of cells without a value; default: the map's "
|
|
154
|
+
"`bad` (transparent)."),
|
|
155
|
+
direction=Setting(one_of("up", "down"),
|
|
156
|
+
doc="`up`: the apex up; `down`: the triangle hangs from the top "
|
|
157
|
+
"(a triangle)."),
|
|
158
|
+
max_height=Setting(_max_height,
|
|
159
|
+
doc="Largest height of the track: a percentage of its width "
|
|
160
|
+
"(`\"33%\"`) or a length; `None`: the triangle's (square "
|
|
161
|
+
"cells)."),
|
|
162
|
+
fit=Setting(one_of("clip", "squash"),
|
|
163
|
+
doc="How the triangle fits a height below its own (`max_height`, or the "
|
|
164
|
+
"track's `height`): `clip` keeps square cells and cuts the "
|
|
165
|
+
"distances above; `squash` flattens the whole triangle."),
|
|
166
|
+
**common("name", "opacity", "z", "rasterize", "simplify", "font_size", "font_family",
|
|
167
|
+
"text_color"),
|
|
168
|
+
**coord_settings(),
|
|
169
|
+
)
|
|
170
|
+
#: ``matrix_type="oe"``: a diverging map about 1 on a log scale
|
|
171
|
+
RULES = {"center": _oe(lambda s: 1), "color_scale": _oe(lambda s: "log"),
|
|
172
|
+
"color_limits": _oe(_oe_limits)}
|
|
173
|
+
|
|
174
|
+
def __init__(self, reader: Any, plot: Any = None, **settings: Any) -> None:
|
|
175
|
+
self.reader = readers.adapt(reader, "contacts", "HiC", owner=self)
|
|
176
|
+
super().__init__(plot=plot, **settings)
|
|
177
|
+
|
|
178
|
+
def set(self, **settings: "Unpack[HiCSettings]") -> Self: # type: ignore[override]
|
|
179
|
+
super().set(**settings)
|
|
180
|
+
if getattr(self, "plot", None) is not None:
|
|
181
|
+
self._attach_bar()
|
|
182
|
+
return self
|
|
183
|
+
|
|
184
|
+
def _bar_source(self) -> Any:
|
|
185
|
+
return self
|
|
186
|
+
|
|
187
|
+
# -- data ------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
def square(self) -> bool:
|
|
190
|
+
"""Whether the whole matrix is drawn (``shape="square"``)."""
|
|
191
|
+
return self.resolve("shape") == "square"
|
|
192
|
+
|
|
193
|
+
def segmented(self) -> bool:
|
|
194
|
+
return not self.square()
|
|
195
|
+
|
|
196
|
+
def region(self) -> Region:
|
|
197
|
+
r = super().region()
|
|
198
|
+
if isinstance(r, Regions) and self.square():
|
|
199
|
+
raise ValueError(f"{self._what()}: shape=\"square\" needs one region on the x "
|
|
200
|
+
f"axis, got {r}")
|
|
201
|
+
return r
|
|
202
|
+
|
|
203
|
+
def region2(self) -> Region:
|
|
204
|
+
"""The region of the y axis of a square (``region2``, else ``region``)."""
|
|
205
|
+
r = self.resolve("region2")
|
|
206
|
+
if r is None:
|
|
207
|
+
return self.region()
|
|
208
|
+
if pending(r):
|
|
209
|
+
plot = self.plot
|
|
210
|
+
r = whole(r, lambda: known_sizes(plot if plot is not None else self,
|
|
211
|
+
None if plot is None else plot.x_axis))
|
|
212
|
+
if isinstance(r, Regions):
|
|
213
|
+
raise ValueError(f"{self._what()}: region2 is one region, got {r}")
|
|
214
|
+
return r
|
|
215
|
+
|
|
216
|
+
def equal_aspect(self) -> bool:
|
|
217
|
+
"""Whether the plot's ``data_ratio`` is 1 (a square outside a tracks figure)."""
|
|
218
|
+
return self.square() and self.plot is not None and "height" not in self.plot.SETTINGS
|
|
219
|
+
|
|
220
|
+
def full_depth(self) -> float:
|
|
221
|
+
"""The largest distance asked (``depth``, at most the span)."""
|
|
222
|
+
span = self.region().span
|
|
223
|
+
d = self.resolve("depth")
|
|
224
|
+
return float(span if d is None else min(d, span))
|
|
225
|
+
|
|
226
|
+
def _square_height(self, width: float) -> float:
|
|
227
|
+
return width * self.full_depth() / (2 * self.region().span)
|
|
228
|
+
|
|
229
|
+
def _height_cap(self, width: float) -> float | None:
|
|
230
|
+
"""The height (px) the triangle must fit: ``max_height`` or its lane in the track's
|
|
231
|
+
``height``, the lower."""
|
|
232
|
+
caps = []
|
|
233
|
+
m = self.resolve("max_height")
|
|
234
|
+
if m is not None:
|
|
235
|
+
caps.append(m[1] * width if isinstance(m, tuple) else float(m))
|
|
236
|
+
h = self.plot.get("height") if "height" in self.plot.SETTINGS else "auto"
|
|
237
|
+
if h != "auto" and not isinstance(h, tuple): # not a share ("fr")
|
|
238
|
+
caps.append(value_height(self.plot, float(self.plot.resolve("height"))))
|
|
239
|
+
return min(caps) if caps else None
|
|
240
|
+
|
|
241
|
+
def depth(self) -> float:
|
|
242
|
+
"""The largest distance drawn: ``full_depth``, less what ``fit="clip"`` cuts."""
|
|
243
|
+
d = self.full_depth()
|
|
244
|
+
if self.resolve("fit") != "clip":
|
|
245
|
+
return d
|
|
246
|
+
width = self.axes_px()
|
|
247
|
+
cap = self._height_cap(width)
|
|
248
|
+
natural = self._square_height(width)
|
|
249
|
+
if cap is None or cap >= natural:
|
|
250
|
+
return d
|
|
251
|
+
return d * cap / natural
|
|
252
|
+
|
|
253
|
+
def binning(self) -> tuple[int | None, int | None]:
|
|
254
|
+
"""``(bin count, bin size)``: one of them."""
|
|
255
|
+
b = self.resolve("bins")
|
|
256
|
+
if b.kind == "count":
|
|
257
|
+
return int(b.value), None
|
|
258
|
+
if b.kind == "bp":
|
|
259
|
+
return None, int(b.value)
|
|
260
|
+
return None, sources.bin_size_for(self.reader, self.region().span,
|
|
261
|
+
self.axes_px() / b.value)
|
|
262
|
+
|
|
263
|
+
def data(self) -> tuple[int, array, float, float]:
|
|
264
|
+
"""``(n, values, start, bin)``: an n × n matrix (rows first) whose bins start at
|
|
265
|
+
``start`` (bp) and are ``bin`` bp wide."""
|
|
266
|
+
region = self.region()
|
|
267
|
+
return self.cached(self.data_key(), lambda: self._guard(lambda: self._read(region)),
|
|
268
|
+
kind="data")
|
|
269
|
+
|
|
270
|
+
def data_key(self) -> Any:
|
|
271
|
+
"""What the matrix read depends on."""
|
|
272
|
+
region = self.region()
|
|
273
|
+
if self.square():
|
|
274
|
+
region2 = self.region2()
|
|
275
|
+
if isinstance(self.reader, Matrix):
|
|
276
|
+
return (region, region2, "square")
|
|
277
|
+
return (region, region2, self.binning(), self.resolve("normalization"),
|
|
278
|
+
self.resolve("matrix_type"), "square")
|
|
279
|
+
if isinstance(self.reader, Matrix):
|
|
280
|
+
return (region,)
|
|
281
|
+
return (region, self.binning(), self.resolve("normalization"),
|
|
282
|
+
self.resolve("matrix_type"), self.depth())
|
|
283
|
+
|
|
284
|
+
def square_data(self) -> tuple[int, int, array, float, float, float, float]:
|
|
285
|
+
"""``(h, w, values, start, bin, start2, bin2)``: the h × w matrix (rows first) of
|
|
286
|
+
``region`` (rows: x) against ``region2`` (columns: y), rows from ``start`` in bins
|
|
287
|
+
of ``bin`` bp, columns from ``start2`` in bins of ``bin2``."""
|
|
288
|
+
return self.cached(self.data_key(), lambda: self._guard(self._read_square),
|
|
289
|
+
kind="data")
|
|
290
|
+
|
|
291
|
+
def _read_square(self) -> tuple[int, int, array, float, float, float, float]:
|
|
292
|
+
region, region2 = self.region(), self.region2()
|
|
293
|
+
if isinstance(self.reader, Matrix): # over the region, whatever it is
|
|
294
|
+
if region2 != region:
|
|
295
|
+
raise ValueError("a plain matrix has no coordinates: region2 needs a reader")
|
|
296
|
+
n, b = self.reader.n, region.span / self.reader.n
|
|
297
|
+
return n, n, self.reader.values, float(region.start), b, float(region.start), b
|
|
298
|
+
count, size = self.binning()
|
|
299
|
+
counts = None if count is None else \
|
|
300
|
+
(count, max(1, round(count * region2.span / region.span)))
|
|
301
|
+
m, start, start2 = sources.read_square(self.reader, region, region2, size,
|
|
302
|
+
self.resolve("normalization"),
|
|
303
|
+
self.resolve("matrix_type"), counts)
|
|
304
|
+
h, w, _, flat = sources.matrix(m)
|
|
305
|
+
if counts is not None:
|
|
306
|
+
return h, w, flat, start, region.span / counts[0], start2, \
|
|
307
|
+
region2.span / counts[1]
|
|
308
|
+
return h, w, flat, start, float(size or 0), start2, float(size or 0)
|
|
309
|
+
|
|
310
|
+
def _read(self, region: Region) -> tuple[int, array, float, float]:
|
|
311
|
+
if isinstance(self.reader, Matrix): # over the region, whatever it is
|
|
312
|
+
n, flat = self.reader.n, self.reader.values
|
|
313
|
+
return n, flat, float(region.start), region.span / n
|
|
314
|
+
count, size = self.binning()
|
|
315
|
+
b = float(size) if size is not None else region.span / (count or 1)
|
|
316
|
+
d = self.depth()
|
|
317
|
+
m, start = sources.read_contacts(self.reader, region, size,
|
|
318
|
+
self.resolve("normalization"),
|
|
319
|
+
self.resolve("matrix_type"),
|
|
320
|
+
None if d >= region.span else int(d + b),
|
|
321
|
+
bin_count=count)
|
|
322
|
+
h, w, _, flat = sources.matrix(m)
|
|
323
|
+
if h != w:
|
|
324
|
+
raise ValueError(f"the reader gave a {h} × {w} matrix for one region")
|
|
325
|
+
return h, flat, float(start), b
|
|
326
|
+
|
|
327
|
+
def cell_values(self) -> array:
|
|
328
|
+
"""The values of the cells drawn: the upper triangle within ``depth``, or the whole
|
|
329
|
+
matrix of a square."""
|
|
330
|
+
if self.square():
|
|
331
|
+
return self.square_data()[2]
|
|
332
|
+
n, flat, _, b = self.data()
|
|
333
|
+
reach = int(self.depth() / b) + 1
|
|
334
|
+
out = array("d")
|
|
335
|
+
for i in range(n):
|
|
336
|
+
out.extend(flat[i * n + i:i * n + min(n, i + reach + 1)])
|
|
337
|
+
return out
|
|
338
|
+
|
|
339
|
+
def band(self) -> array:
|
|
340
|
+
"""The matrix with NaN outside the cells that can show: the lower triangle and the
|
|
341
|
+
cells past ``depth`` (they fall outside the axes)."""
|
|
342
|
+
n, flat, _, b = self.data()
|
|
343
|
+
reach = int(self.depth() / b) + 2
|
|
344
|
+
out = array("d")
|
|
345
|
+
nan = array("d", [math.nan])
|
|
346
|
+
for i in range(n):
|
|
347
|
+
end = min(n, i + reach + 1)
|
|
348
|
+
out.extend(nan * i)
|
|
349
|
+
out.extend(flat[i * n + i:i * n + end])
|
|
350
|
+
out.extend(nan * (n - end))
|
|
351
|
+
return out
|
|
352
|
+
|
|
353
|
+
def track_height(self, width: float) -> float | None:
|
|
354
|
+
if self.square():
|
|
355
|
+
return width * self.region2().span / self.region().span
|
|
356
|
+
natural = self._square_height(width)
|
|
357
|
+
cap = self._height_cap(width)
|
|
358
|
+
return natural if cap is None else min(natural, cap)
|
|
359
|
+
|
|
360
|
+
# -- the preset (stat contact_matrix) ---------------------------------------------
|
|
361
|
+
|
|
362
|
+
def _cells(self, call: Call, values: array, grid: Any) -> None:
|
|
363
|
+
"""The cells layer of ``call``: ``grid(ctx)`` gives the values drawn (NaN: none),
|
|
364
|
+
the transform placing the grid and its columns and rows."""
|
|
365
|
+
axis = color_axis_of(self)
|
|
366
|
+
cmap = axis.color_map()
|
|
367
|
+
if kernels.extent(values, "log")[0]: # no finite positive value: no cells
|
|
368
|
+
call.layers.append(Layer(GEOMS["cells"], Table(),
|
|
369
|
+
paint=Paint(self, "contacts.cells", grid=grid, cmap=cmap),
|
|
370
|
+
stat="contact_matrix", occupy="all", key=False))
|
|
371
|
+
name = self.resolve("name")
|
|
372
|
+
if name is not None:
|
|
373
|
+
call.entries.append(gradient_entry(self, name, axis.color_map))
|
|
374
|
+
call.color_sources = [(axis, values)]
|
|
375
|
+
|
|
376
|
+
def layers(self) -> Call:
|
|
377
|
+
call = self.genome_call()
|
|
378
|
+
if self.square():
|
|
379
|
+
return self._square_layers(call)
|
|
380
|
+
n, flat, start, b = self.data()
|
|
381
|
+
|
|
382
|
+
def grid(ctx: Any) -> tuple[array, Any, int, int]:
|
|
383
|
+
x0 = ctx.xmap(start)
|
|
384
|
+
xb = ctx.xmap(start + b) - x0 # px per bin along x
|
|
385
|
+
y0 = ctx.ymap(0.0)
|
|
386
|
+
yb = ctx.ymap(b) - y0 # px per bin of distance
|
|
387
|
+
# cell (row v, col u) → x = start + b (u + v) / 2 + …, distance b (u − v)
|
|
388
|
+
return self.band(), (xb / 2, yb, xb / 2, -yb, x0, y0), n, n
|
|
389
|
+
self._cells(call, self.cell_values(), grid)
|
|
390
|
+
r = self.region()
|
|
391
|
+
call.extra = {"x": array("d", [r.start, r.end]),
|
|
392
|
+
"y": array("d", [0.0, self.depth()])}
|
|
393
|
+
call.hide = {"y"} # distances: no y axis
|
|
394
|
+
if self.resolve("direction") == "down":
|
|
395
|
+
call.invert = {"y"}
|
|
396
|
+
call.hide.add("x") # hanging from the top
|
|
397
|
+
call.exact = {"x", "y"}
|
|
398
|
+
return call
|
|
399
|
+
|
|
400
|
+
def _square_layers(self, call: Call) -> Call:
|
|
401
|
+
h, w, flat, start, b, start2, b2 = self.square_data()
|
|
402
|
+
|
|
403
|
+
def grid(ctx: Any) -> tuple[array, Any, int, int]:
|
|
404
|
+
x0, y0 = ctx.xmap(start), ctx.ymap(start2)
|
|
405
|
+
# cell (row v, column u): x from start + b v, y from start2 + b2 u
|
|
406
|
+
return flat, (0.0, ctx.ymap(start2 + b2) - y0, ctx.xmap(start + b) - x0, 0.0,
|
|
407
|
+
x0, y0), w, h
|
|
408
|
+
self._cells(call, flat, grid)
|
|
409
|
+
r, r2 = self.region(), self.region2()
|
|
410
|
+
call.scales["y"] = "genome"
|
|
411
|
+
call.regions["y"] = r2
|
|
412
|
+
call.extra = {"x": array("d", [r.start, r.end]), "y": array("d", [r2.start, r2.end])}
|
|
413
|
+
call.invert = {"y"} # row 0 at the top
|
|
414
|
+
call.exact = {"x", "y"}
|
|
415
|
+
return call
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def under_hic(series: Any) -> HiC | None:
|
|
419
|
+
"""The Hi-C triangle a series with ``lane="over"`` is drawn over: the last series
|
|
420
|
+
with a y scale called before it, when no lane series of its own came between them;
|
|
421
|
+
``None`` otherwise."""
|
|
422
|
+
if series.plot is None or series.resolve("lane") != "over":
|
|
423
|
+
return None
|
|
424
|
+
under = None
|
|
425
|
+
for s in series.plot.series:
|
|
426
|
+
if s is series:
|
|
427
|
+
break
|
|
428
|
+
if not is_lane(s):
|
|
429
|
+
under = s
|
|
430
|
+
elif s.resolve("lane") != "over":
|
|
431
|
+
under = None
|
|
432
|
+
return under if isinstance(under, HiC) else None
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def tads(bed: Any, hic: HiC) -> Call:
|
|
436
|
+
"""The call of a ``Bed`` over ``hic``: each interval an outlined triangle (a TAD) in
|
|
437
|
+
the triangle's coordinates, its apex at the interval's span, cut at ``hic``'s depth;
|
|
438
|
+
over a square, an outlined square on the diagonal."""
|
|
439
|
+
call = bed.genome_call()
|
|
440
|
+
cols = bed.entries()
|
|
441
|
+
|
|
442
|
+
def squares(ctx: Any) -> list[Any]:
|
|
443
|
+
if hic.region2().chr != bed.region().chr:
|
|
444
|
+
return [] # no diagonal in view
|
|
445
|
+
cs = bed._colors(ctx, cols)
|
|
446
|
+
opacity = bed.resolve("opacity")
|
|
447
|
+
opacity = 1.0 if opacity is None else opacity
|
|
448
|
+
lw = bed.resolve("line_width")
|
|
449
|
+
return [(array("d", [ctx.xmap(x) for x in (s, e, e, s, s)]),
|
|
450
|
+
array("d", [ctx.ymap(y) for y in (s, s, e, e, s)]),
|
|
451
|
+
with_alpha(cs[i], opacity), lw)
|
|
452
|
+
for i, (s, e) in enumerate(zip(cols["start"], cols["end"]))]
|
|
453
|
+
|
|
454
|
+
def lines(ctx: Any) -> list[Any]:
|
|
455
|
+
if hic.square():
|
|
456
|
+
return squares(ctx)
|
|
457
|
+
depth = hic.depth()
|
|
458
|
+
cs = bed._colors(ctx, cols)
|
|
459
|
+
opacity = bed.resolve("opacity")
|
|
460
|
+
opacity = 1.0 if opacity is None else opacity
|
|
461
|
+
lw = bed.resolve("line_width")
|
|
462
|
+
out = []
|
|
463
|
+
for i, (s, e) in enumerate(zip(cols["start"], cols["end"])):
|
|
464
|
+
h = min(e - s, depth) # the sides cut at the depth
|
|
465
|
+
xs = [s, s + h / 2, math.nan, e - h / 2, e] if h < e - s else [s, (s + e) / 2, e]
|
|
466
|
+
ys = [0.0, h, math.nan, h, 0.0] if h < e - s else [0.0, h, 0.0]
|
|
467
|
+
out.append((array("d", [ctx.xmap(x) for x in xs]), # NaN: a break
|
|
468
|
+
array("d", [ctx.ymap(y) for y in ys]),
|
|
469
|
+
with_alpha(cs[i], opacity), lw))
|
|
470
|
+
return out
|
|
471
|
+
|
|
472
|
+
t = Table()
|
|
473
|
+
t.lazy["page_lines"] = lines
|
|
474
|
+
call.layers.append(Layer(GEOMS["path"], t, paint={}, stat="intervals", occupy=False,
|
|
475
|
+
key=False))
|
|
476
|
+
if bed.resolve("color") == "score":
|
|
477
|
+
scores = [v for v in cols.get("score", []) if v is not None]
|
|
478
|
+
call.color_sources = [(color_axis_of(bed), array("d", scores))]
|
|
479
|
+
r = bed.region()
|
|
480
|
+
call.extra = {"x": array("d", [r.start, r.end])}
|
|
481
|
+
call.hide = {"y"}
|
|
482
|
+
call.exact = {"x"}
|
|
483
|
+
return call
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
add_layer("hic", HiC)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
@role("contacts.cells")
|
|
490
|
+
def _cells_paint(s: Any, ctx: Any, glyph: bool, grid: Any = None,
|
|
491
|
+
cmap: Any = None) -> dict[str, Any]:
|
|
492
|
+
"""The cells of ``grid(ctx)`` (values, the transform placing them, columns, rows)."""
|
|
493
|
+
st = trained(color_axis_of(s))
|
|
494
|
+
assert st.scale is not None
|
|
495
|
+
flo, fhi = st.flo, st.fhi
|
|
496
|
+
fn = st.scale.function
|
|
497
|
+
f, transform, cols, rows = grid(ctx)
|
|
498
|
+
if st.scale.base is not None:
|
|
499
|
+
# no contacts is the low end of the map, not a missing value
|
|
500
|
+
f = kernels.scaled(f, low=5e-324)
|
|
501
|
+
vals = kernels.scaled(f, 1.0, 0.0, fn, low=flo) if fn else f
|
|
502
|
+
fill, table, _ = cmap.cells(vals, flo, fhi, "image")
|
|
503
|
+
nan = ctx.color(s.resolve("nan_color"))
|
|
504
|
+
opacity = s.resolve("opacity")
|
|
505
|
+
return {"transform": transform, "x_edges": range(cols + 1),
|
|
506
|
+
"y_edges": range(rows + 1), "fill": fill, "palette": table,
|
|
507
|
+
"bad": nan if nan is not None else cmap.bad, "mode": "image",
|
|
508
|
+
"opacity": 1.0 if opacity is None else opacity}
|