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,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}