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/sites.py
ADDED
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
"""Site matrices: a signal read in windows around sites (peaks, TSSs), one row per site.
|
|
2
|
+
|
|
3
|
+
:class:`SiteHeatmap` draws the rows as a heat map (``plot.site_heatmap``),
|
|
4
|
+
:class:`SiteProfile` their column mean (``plot.site_profile``), :func:`SignalMatrix` a
|
|
5
|
+
figure of both for each of several signals.
|
|
6
|
+
|
|
7
|
+
A window is ``flank`` around the site's ``anchor``, read as ``bins`` bins in one
|
|
8
|
+
``read_values`` call per signal (no strand passed to the reader); a ``-`` site's row
|
|
9
|
+
(from a ``strand`` column) is reversed, so rows run 5′ to 3′. Bins wholly past a
|
|
10
|
+
chromosome end (the reader's ``chr_sizes``) are NaN. The read and the row order are kept
|
|
11
|
+
by the figure: a heat map and a profile of the same signal, sites and window share them.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import math
|
|
15
|
+
import operator
|
|
16
|
+
import os
|
|
17
|
+
from array import array
|
|
18
|
+
from collections.abc import Mapping
|
|
19
|
+
from typing import TYPE_CHECKING, Any, Self, Unpack
|
|
20
|
+
|
|
21
|
+
from .. import kernels
|
|
22
|
+
from ..color.values import color
|
|
23
|
+
from ..layers import add_layer
|
|
24
|
+
from ..layers.base import Series, common
|
|
25
|
+
from ..layers.core import Call, Layer, Table
|
|
26
|
+
from ..layers.grammar import entry
|
|
27
|
+
from ..layers.paint import Paint, role
|
|
28
|
+
from ..layers.geoms import GEOMS
|
|
29
|
+
from ..mapped import color_axis_of, trained
|
|
30
|
+
from ..presets._util import gradient_entry, memo
|
|
31
|
+
from ..settings import Setting, number, one_of, table
|
|
32
|
+
from ..validators import length
|
|
33
|
+
from ..spec import auto_colors, current
|
|
34
|
+
from . import readers, sources
|
|
35
|
+
from .regions import parse_bp
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from ..figure import Figure
|
|
39
|
+
from ..types import SiteHeatmapSettings, SiteProfileSettings
|
|
40
|
+
|
|
41
|
+
__all__ = ["SiteHeatmap", "SiteProfile", "SignalMatrix"]
|
|
42
|
+
|
|
43
|
+
#: The reductions a bin of a window takes.
|
|
44
|
+
_MODES = ("mean", "min", "max", "sum", "count")
|
|
45
|
+
#: The share of the rows a gap between groups takes.
|
|
46
|
+
_GAP = 0.02
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _flank(v: Any) -> tuple[int, int]:
|
|
50
|
+
"""One length on both sides, or ``(upstream, downstream)``."""
|
|
51
|
+
if isinstance(v, (tuple, list)):
|
|
52
|
+
if len(v) != 2:
|
|
53
|
+
raise ValueError(f"expected a length or (upstream, downstream), got {v!r}")
|
|
54
|
+
up, down = (parse_bp(x, "flank") for x in v)
|
|
55
|
+
else:
|
|
56
|
+
up = down = parse_bp(v, "flank")
|
|
57
|
+
if up < 0 or down < 0 or up + down <= 0:
|
|
58
|
+
raise ValueError(f"the flanks must be non-negative and not both 0, got {v!r}")
|
|
59
|
+
return int(up), int(down)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _sort(v: Any) -> Any:
|
|
63
|
+
if v is False:
|
|
64
|
+
return False
|
|
65
|
+
return one_of("mean", "max")(v)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _row_bins(v: Any) -> Any:
|
|
69
|
+
if v is False or v == "auto":
|
|
70
|
+
return v
|
|
71
|
+
return number(positive=True, integer=True)(v)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _stat(v: Any) -> Any:
|
|
75
|
+
return one_of("mean", "median")(v)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _band(v: Any) -> Any:
|
|
79
|
+
if v is False:
|
|
80
|
+
return False
|
|
81
|
+
return one_of("sem", "sd")(v)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _window_settings() -> dict[str, Setting]:
|
|
85
|
+
return dict(
|
|
86
|
+
flank=Setting(_flank, doc="The window around each site's anchor: a length on both "
|
|
87
|
+
"sides (`\"5kb\"`) or `(upstream, downstream)`."),
|
|
88
|
+
bins=Setting(number(positive=True, integer=True), doc="Bins per window."),
|
|
89
|
+
anchor=Setting(one_of("center", "start", "end", "summit"),
|
|
90
|
+
doc="The position the window is centred on: the site's `center`, "
|
|
91
|
+
"`start` or `end` (5′ and 3′ ends on a `-` site), or its "
|
|
92
|
+
"`summit` column (absolute, or a narrowPeak `peak` offset)."),
|
|
93
|
+
bin_mode=Setting(one_of(*_MODES),
|
|
94
|
+
doc="Reduction per bin: `mean`, `min`, `max`, `sum` or `count`."),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# Sites
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def _columns(value: Any, what: str, owner: Any) -> dict[str, list[Any]]:
|
|
103
|
+
"""The columns of a sites table: a mapping of columns, a DataFrame, rows (mappings,
|
|
104
|
+
or tuples in BED order), an interval reader or a path (every entry)."""
|
|
105
|
+
if hasattr(value, "to_dict") and hasattr(value, "columns"): # a DataFrame
|
|
106
|
+
return {str(c): list(value[c]) for c in value.columns}
|
|
107
|
+
if isinstance(value, Mapping):
|
|
108
|
+
return {str(k): list(v.tolist() if hasattr(v, "tolist") else v)
|
|
109
|
+
for k, v in value.items()}
|
|
110
|
+
if isinstance(value, (str, bytes)) or hasattr(value, "__fspath__") \
|
|
111
|
+
or sources.kind_of(value) is not None:
|
|
112
|
+
reader = readers.adapt(value, "intervals", what, owner=owner)
|
|
113
|
+
sizes = getattr(reader, "chr_sizes", None)
|
|
114
|
+
if not isinstance(sizes, Mapping) or not sizes:
|
|
115
|
+
raise ValueError(f"{what}: an interval reader as sites needs its chr_sizes "
|
|
116
|
+
f"(to read every entry); pass a table")
|
|
117
|
+
got = reader.read_entries([(str(c), 0, int(n)) for c, n in sizes.items()])
|
|
118
|
+
return _columns(dict(got.items()), what, owner)
|
|
119
|
+
rows = list(value)
|
|
120
|
+
if rows and isinstance(rows[0], Mapping):
|
|
121
|
+
return {str(k): [r[k] for r in rows] for k in rows[0]}
|
|
122
|
+
names = ("chr", "start", "end", "name", "score", "strand")
|
|
123
|
+
return {n: [r[i] if i < len(r) else None for r in rows]
|
|
124
|
+
for i, n in enumerate(names) if any(len(r) > i for r in rows)}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _find(cols: Mapping[str, list[Any]], *names: str) -> list[Any] | None:
|
|
128
|
+
low = {k.lower(): v for k, v in cols.items()}
|
|
129
|
+
for n in names:
|
|
130
|
+
if n in low:
|
|
131
|
+
return low[n]
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Sites:
|
|
136
|
+
"""The sites of a site matrix: ``chr``, ``start``, ``end``, and when given
|
|
137
|
+
``strand``, ``summit`` (absolute; or ``start`` plus a narrowPeak ``peak`` offset) and
|
|
138
|
+
one group label per site."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, value: Any, group: Any, what: str, owner: Any = None) -> None:
|
|
141
|
+
cols = _columns(value, what, owner)
|
|
142
|
+
chrs = _find(cols, "chr", "chrom", "chromosome", "seqname")
|
|
143
|
+
starts = _find(cols, "start", "chromstart")
|
|
144
|
+
ends = _find(cols, "end", "chromend")
|
|
145
|
+
if chrs is None or starts is None or ends is None:
|
|
146
|
+
raise ValueError(f"{what}: sites need chr, start and end columns")
|
|
147
|
+
n = len(chrs)
|
|
148
|
+
self.chr = [str(c) for c in chrs]
|
|
149
|
+
self.start = [sources._position(s, i, "start") for i, s in enumerate(starts)]
|
|
150
|
+
self.end = [sources._position(e, i, "end") for i, e in enumerate(ends)]
|
|
151
|
+
if len(self.start) != n or len(self.end) != n:
|
|
152
|
+
raise ValueError(f"{what}: the site columns have different lengths")
|
|
153
|
+
strand = _find(cols, "strand")
|
|
154
|
+
#: ``True`` per ``-`` site (``None``: no strand column)
|
|
155
|
+
self.minus = None if strand is None else [s == "-" for s in strand]
|
|
156
|
+
summit = _find(cols, "summit")
|
|
157
|
+
peak = _find(cols, "peak")
|
|
158
|
+
if summit is not None:
|
|
159
|
+
self.summit: list[int] | None = [int(s) for s in summit]
|
|
160
|
+
elif peak is not None:
|
|
161
|
+
self.summit = [s + int(p) if p is not None and int(p) >= 0 else (s + e) // 2
|
|
162
|
+
for s, e, p in zip(self.start, self.end, peak)]
|
|
163
|
+
else:
|
|
164
|
+
self.summit = None
|
|
165
|
+
self.groups: list[Any] | None = None
|
|
166
|
+
if group is not None:
|
|
167
|
+
labels = cols.get(group) if isinstance(group, str) else group
|
|
168
|
+
if labels is None:
|
|
169
|
+
raise ValueError(f"{what}: no column {group!r} in the sites")
|
|
170
|
+
labels = list(labels.tolist() if hasattr(labels, "tolist") else labels)
|
|
171
|
+
if len(labels) != n:
|
|
172
|
+
raise ValueError(f"{what}: group has {len(labels)} labels for {n} sites")
|
|
173
|
+
self.groups = labels
|
|
174
|
+
|
|
175
|
+
def __len__(self) -> int:
|
|
176
|
+
return len(self.chr)
|
|
177
|
+
|
|
178
|
+
def anchors(self, anchor: str, what: str) -> list[int]:
|
|
179
|
+
"""The anchor position of each site."""
|
|
180
|
+
s, e, minus = self.start, self.end, self.minus
|
|
181
|
+
if anchor == "center":
|
|
182
|
+
return [(a + b) // 2 for a, b in zip(s, e)]
|
|
183
|
+
if anchor == "summit":
|
|
184
|
+
if self.summit is None:
|
|
185
|
+
raise ValueError(f"{what}: anchor=\"summit\" needs a summit (or peak) "
|
|
186
|
+
f"column in the sites")
|
|
187
|
+
return list(self.summit)
|
|
188
|
+
first = anchor == "start"
|
|
189
|
+
if minus is None:
|
|
190
|
+
return list(s if first else e)
|
|
191
|
+
return [(b if m else a) if first else (a if m else b)
|
|
192
|
+
for a, b, m in zip(s, e, minus)]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
# The read and the row order (kept by the figure)
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def _ident(v: Any) -> Any:
|
|
200
|
+
if isinstance(v, (str, bytes)) or hasattr(v, "__fspath__"):
|
|
201
|
+
return os.fsdecode(v)
|
|
202
|
+
return id(v)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class Matrix:
|
|
206
|
+
"""The windows of one signal around the sites: ``n`` rows of ``w`` bins (row-major,
|
|
207
|
+
5′ to 3′), the bins from ``-up`` to ``down`` bp."""
|
|
208
|
+
|
|
209
|
+
__slots__ = ("n", "w", "flat", "up", "down", "_rows")
|
|
210
|
+
|
|
211
|
+
def __init__(self, n: int, w: int, flat: array, up: int, down: int) -> None:
|
|
212
|
+
self.n, self.w, self.flat, self.up, self.down = n, w, flat, up, down
|
|
213
|
+
self._rows: Any = None
|
|
214
|
+
|
|
215
|
+
def rows(self) -> Any:
|
|
216
|
+
"""The rows as an ``(n, w)`` buffer."""
|
|
217
|
+
if self._rows is None:
|
|
218
|
+
self._rows = memoryview(self.flat).cast("B").cast("d", (self.n, self.w))
|
|
219
|
+
return self._rows
|
|
220
|
+
|
|
221
|
+
def column(self, c: int, rows: Any) -> array:
|
|
222
|
+
"""Bin ``c`` of ``rows`` (indices)."""
|
|
223
|
+
return kernels.take(self.flat[c::self.w], rows)
|
|
224
|
+
|
|
225
|
+
def edges(self) -> list[float]:
|
|
226
|
+
bw = (self.up + self.down) / self.w
|
|
227
|
+
return [-self.up + bw * j for j in range(self.w + 1)]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _read(series: Any) -> Matrix:
|
|
231
|
+
"""The windows of ``series``' signal around its sites (one read)."""
|
|
232
|
+
sites: Sites = series.sites
|
|
233
|
+
up, down = series.resolve("flank")
|
|
234
|
+
w = int(series.resolve("bins"))
|
|
235
|
+
anchors = sites.anchors(series.resolve("anchor"), series.kind)
|
|
236
|
+
minus = sites.minus or [False] * len(sites)
|
|
237
|
+
windows = [(c, a - down, a + up) if m else (c, a - up, a + down)
|
|
238
|
+
for c, a, m in zip(sites.chr, anchors, minus)]
|
|
239
|
+
reader = series.reader
|
|
240
|
+
mode = series.resolve("bin_mode")
|
|
241
|
+
n = len(windows)
|
|
242
|
+
if not n:
|
|
243
|
+
return Matrix(0, w, array("d"), up, down)
|
|
244
|
+
if callable(reader) and not hasattr(reader, "read_values"):
|
|
245
|
+
rows = [reader(c, s, e, w) for c, s, e in windows]
|
|
246
|
+
else:
|
|
247
|
+
rows = reader.read_values(windows, bin_count=w, bin_mode=mode, def_value=0.0)
|
|
248
|
+
g = kernels.grid(rows)
|
|
249
|
+
if g is not None:
|
|
250
|
+
h, gw, _, flat = g
|
|
251
|
+
else:
|
|
252
|
+
rows = [list(r) for r in rows]
|
|
253
|
+
h, gw = len(rows), len(rows[0]) if rows else 0
|
|
254
|
+
flat = array("d", [float(v) for r in rows for v in r])
|
|
255
|
+
if h != n or gw != w:
|
|
256
|
+
raise ValueError(f"{series.kind}: the reader gave {h} rows of {gw} bins for {n} "
|
|
257
|
+
f"windows of {w}")
|
|
258
|
+
sizes = getattr(reader, "chr_sizes", None)
|
|
259
|
+
sizes = sizes if isinstance(sizes, Mapping) else {}
|
|
260
|
+
bw = (up + down) / w
|
|
261
|
+
nan = math.nan
|
|
262
|
+
for i, (c, lo, hi) in enumerate(windows):
|
|
263
|
+
size = sizes.get(c)
|
|
264
|
+
if lo < 0 or (size is not None and hi > size):
|
|
265
|
+
for j in range(w):
|
|
266
|
+
a = lo + j * bw
|
|
267
|
+
if a + bw <= 0 or (size is not None and a >= size):
|
|
268
|
+
flat[i * w + j] = nan
|
|
269
|
+
if minus[i]:
|
|
270
|
+
flat[i * w:(i + 1) * w] = flat[i * w:(i + 1) * w][::-1]
|
|
271
|
+
return Matrix(n, w, flat, up, down)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _row_stat(m: Matrix, how: str) -> array:
|
|
275
|
+
w, flat = m.w, m.flat
|
|
276
|
+
out = array("d")
|
|
277
|
+
for i in range(m.n):
|
|
278
|
+
row = flat[i * w:(i + 1) * w]
|
|
279
|
+
s = sum(row)
|
|
280
|
+
if s != s:
|
|
281
|
+
row = array("d", [v for v in row if v == v])
|
|
282
|
+
s = sum(row)
|
|
283
|
+
if not row:
|
|
284
|
+
out.append(math.nan)
|
|
285
|
+
elif how == "mean":
|
|
286
|
+
out.append(s / len(row))
|
|
287
|
+
else:
|
|
288
|
+
out.append(max(row))
|
|
289
|
+
return out
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _cache(series: Any) -> dict[Any, Any]:
|
|
293
|
+
fig = getattr(series.plot, "figure", None) if series.plot is not None else None
|
|
294
|
+
return (fig if fig is not None else series).__dict__.setdefault("_site_matrices", {})
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _key(series: Any) -> tuple[Any, ...]:
|
|
298
|
+
return (_ident(series.signal), _ident(series.sites_arg), series.resolve("flank"),
|
|
299
|
+
series.resolve("bins"), series.resolve("anchor"), series.resolve("bin_mode"))
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def matrix(series: Any) -> Matrix:
|
|
303
|
+
"""The read of ``series`` (shared by the series of its figure that read the same)."""
|
|
304
|
+
cache = _cache(series)
|
|
305
|
+
key = ("read", *_key(series))
|
|
306
|
+
got = cache.get(key)
|
|
307
|
+
if got is None:
|
|
308
|
+
try:
|
|
309
|
+
got = _read(series)
|
|
310
|
+
except Exception as e:
|
|
311
|
+
if series.kind in str(e):
|
|
312
|
+
raise
|
|
313
|
+
raise type(e)(f"{series.kind}: {e}") from e
|
|
314
|
+
cache[key] = got
|
|
315
|
+
cache[("held", *key)] = (series.signal, series.sites_arg)
|
|
316
|
+
return got
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def ordered(series: Any, sort: Any) -> tuple[array, list[tuple[Any, int, int]]]:
|
|
320
|
+
"""``(order, blocks)``: the rows in drawing order (the groups in the order they first
|
|
321
|
+
appear in the sites, the rows of each by ``sort``, descending, NaN last) and the
|
|
322
|
+
groups as ``(label, first, end)`` of ``order``."""
|
|
323
|
+
cache = _cache(series)
|
|
324
|
+
key = ("order", sort, _ident(series.group), *_key(series))
|
|
325
|
+
got = cache.get(key)
|
|
326
|
+
if got is not None:
|
|
327
|
+
return got
|
|
328
|
+
m = matrix(series)
|
|
329
|
+
if not sort:
|
|
330
|
+
order = array("q", range(m.n))
|
|
331
|
+
else:
|
|
332
|
+
order = kernels.argsort(_row_stat(m, sort), descending=True)
|
|
333
|
+
blocks: list[tuple[Any, int, int]] = []
|
|
334
|
+
groups = series.sites.groups
|
|
335
|
+
if groups is None:
|
|
336
|
+
blocks.append((None, 0, m.n))
|
|
337
|
+
else:
|
|
338
|
+
by: dict[Any, list[int]] = {g: [] for g in groups} # in the sites' order
|
|
339
|
+
for i in order:
|
|
340
|
+
by[groups[i]].append(i)
|
|
341
|
+
order = array("q")
|
|
342
|
+
for label, idx in by.items():
|
|
343
|
+
blocks.append((label, len(order), len(order) + len(idx)))
|
|
344
|
+
order.extend(idx)
|
|
345
|
+
got = cache[key] = (order, blocks)
|
|
346
|
+
return got
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def averaged(m: Matrix, rows: array, k: int) -> array:
|
|
350
|
+
"""``rows`` (indices, in order) averaged to ``k`` rows, each over ``len(rows) / k``
|
|
351
|
+
consecutive rows (a row on a boundary weighted by its share); NaN left out.
|
|
352
|
+
Row-major."""
|
|
353
|
+
n, w = len(rows), m.w
|
|
354
|
+
if not n:
|
|
355
|
+
return array("d")
|
|
356
|
+
if k >= n:
|
|
357
|
+
return kernels.take(m.rows(), rows)
|
|
358
|
+
out = array("d", bytes(8 * k * w))
|
|
359
|
+
step = n / k
|
|
360
|
+
spans = [] # whole rows [i0, i1), shares of rows i0 - 1 and i1
|
|
361
|
+
for j in range(k):
|
|
362
|
+
a, b = j * step, (float(n) if j == k - 1 else (j + 1) * step)
|
|
363
|
+
i0, i1 = math.ceil(a), min(n, math.floor(b))
|
|
364
|
+
spans.append((j * w, i0, i1, i0 - a, b - i1 if i1 < n else 0.0))
|
|
365
|
+
nan = math.nan
|
|
366
|
+
for c in range(w):
|
|
367
|
+
col = m.column(c, rows)
|
|
368
|
+
for at, i0, i1, fa, fb in spans:
|
|
369
|
+
part = col[i0:i1]
|
|
370
|
+
total, weight = sum(part), float(i1 - i0)
|
|
371
|
+
if total != total:
|
|
372
|
+
part = array("d", [v for v in part if v == v])
|
|
373
|
+
total, weight = sum(part), float(len(part))
|
|
374
|
+
if fa > 0:
|
|
375
|
+
v = col[i0 - 1]
|
|
376
|
+
if v == v:
|
|
377
|
+
total += fa * v
|
|
378
|
+
weight += fa
|
|
379
|
+
if fb > 0:
|
|
380
|
+
v = col[i1]
|
|
381
|
+
if v == v:
|
|
382
|
+
total += fb * v
|
|
383
|
+
weight += fb
|
|
384
|
+
out[at + c] = total / weight if weight > 1e-12 else nan
|
|
385
|
+
return out
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _x_ticks(series: Any, up: int, down: int) -> list[tuple[float, Any]]:
|
|
389
|
+
from .regions import auto_unit
|
|
390
|
+
from .scale import GenomeFormat
|
|
391
|
+
from ..numbers import NumberStyle
|
|
392
|
+
values = [float(-up), 0.0, float(down)] if up and down else \
|
|
393
|
+
[float(-up), 0.0] if up else [0.0, float(down)]
|
|
394
|
+
unit = auto_unit(-up, down)
|
|
395
|
+
labels = GenomeFormat(unit, "ticks").format_all(values, NumberStyle.of(
|
|
396
|
+
series.plot.x_axis))
|
|
397
|
+
return list(zip(values, labels))
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class _SiteSeries(Series):
|
|
401
|
+
"""Base of the site series: the signal, the sites and the group labels."""
|
|
402
|
+
|
|
403
|
+
SPLITS_GROUPS = False
|
|
404
|
+
|
|
405
|
+
def __init__(self, signal: Any, sites: Any, plot: Any = None, *, group: Any = None,
|
|
406
|
+
**settings: Any) -> None:
|
|
407
|
+
what = type(self).__name__
|
|
408
|
+
self.signal = signal
|
|
409
|
+
self.reader = readers.adapt(signal, "signal", what, owner=self)
|
|
410
|
+
self.sites_arg = sites
|
|
411
|
+
self.group = group
|
|
412
|
+
self.sites = Sites(sites, group, what, owner=self)
|
|
413
|
+
super().__init__(plot=plot, **settings)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class SiteHeatmap(_SiteSeries):
|
|
417
|
+
"""A signal around sites, one row per site (a table with ``chr``, ``start``, ``end``
|
|
418
|
+
and optional ``strand``, ``summit`` columns, or an interval reader), x the distance
|
|
419
|
+
from the anchor in bp; ``group`` (a column of the sites, or a label per site) makes a
|
|
420
|
+
block of rows per group.
|
|
421
|
+
|
|
422
|
+
``signal`` is anything a :class:`BigWig` takes (`readers <docs/api.md#readers>`_); ``sites`` is a table (a dict
|
|
423
|
+
of columns, a DataFrame, rows of dicts, or tuples in BED order) with ``chr``, ``start``,
|
|
424
|
+
``end`` and optionally ``strand`` and ``summit`` (absolute; or a narrowPeak ``peak``
|
|
425
|
+
offset from ``start``), or an interval reader or path (every entry, over its
|
|
426
|
+
``chr_sizes``).
|
|
427
|
+
|
|
428
|
+
The window is ``flank`` around the site's ``anchor`` (``center``, ``start``, ``end``: the
|
|
429
|
+
5′ and 3′ ends on a ``-`` site; ``summit``), read as ``bins`` bins in one ``read_values``
|
|
430
|
+
call per signal (no strand passed to the reader). A ``-`` site's row is reversed, so
|
|
431
|
+
rows run 5′ to 3′. Bins wholly past a chromosome end (the reader's ``chr_sizes``; before
|
|
432
|
+
0 always) are NaN. x is labelled in genome units (``-5kb 0 5kb``). A heat map and a
|
|
433
|
+
profile of the same signal, sites, ``group`` and window in one figure share the read and
|
|
434
|
+
the row order; :func:`~plot_io.genome.sites.SignalMatrix` makes the figure of both.
|
|
435
|
+
|
|
436
|
+
Rows are ordered by ``sort`` (each row's ``mean`` or ``max``, descending; ``False``: the
|
|
437
|
+
sites' order). With ``group``, the blocks are in the order the groups first appear, a
|
|
438
|
+
gap between blocks, the group names on the y axis (else no y axis).
|
|
439
|
+
``row_bins="auto"`` averages consecutive rows (a row on a boundary weighted by its
|
|
440
|
+
share, NaN left out) so that at most two rows fall in a px; a number draws that many
|
|
441
|
+
rows; ``False`` every row. The color axis is trained on the rows drawn. Use::
|
|
442
|
+
|
|
443
|
+
p.site_heatmap("atac.bw", tss, flank=(2000, 5000), anchor="start", group="cluster")
|
|
444
|
+
|
|
445
|
+
Example::
|
|
446
|
+
|
|
447
|
+
import math
|
|
448
|
+
rng = random.Random(1)
|
|
449
|
+
sites = {"chr": ["chr1"] * 40, "start": [10_000 + 20_000 * i for i in range(40)],
|
|
450
|
+
"group": ["bound" if i % 3 else "unbound" for i in range(40)]}
|
|
451
|
+
sites["end"] = [s + 1 for s in sites["start"]]
|
|
452
|
+
signal = {"chr": [], "start": [], "end": [], "value": []}
|
|
453
|
+
for s, g in zip(sites["start"], sites["group"]):
|
|
454
|
+
h = rng.uniform(1, 3) if g == "bound" else rng.uniform(0, 0.5)
|
|
455
|
+
for x in range(s - 4000, s + 4000, 400):
|
|
456
|
+
signal["chr"].append("chr1")
|
|
457
|
+
signal["start"].append(x)
|
|
458
|
+
signal["end"].append(x + 400)
|
|
459
|
+
signal["value"].append(0.1 * rng.random() + h * math.exp(-((x - s) / 1000) ** 2))
|
|
460
|
+
p = plt.Plot(width=240, height=160)
|
|
461
|
+
p.site_heatmap(signal, sites, group="group", flank="4kb", bins=20, colorbar=True)
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
SETTINGS = table(
|
|
465
|
+
**_window_settings(),
|
|
466
|
+
sort=Setting(_sort, doc="Row order: `mean` or `max` of each row, descending (within "
|
|
467
|
+
"each group); `False`: the sites' order."),
|
|
468
|
+
row_bins=Setting(_row_bins,
|
|
469
|
+
doc="Rows drawn: `auto` averages consecutive rows so there are at "
|
|
470
|
+
"most two per px; a number of rows; `False`: every row."),
|
|
471
|
+
**common("color_map"),
|
|
472
|
+
nan_color=Setting(color, doc="Color of missing bins (past a chromosome end); "
|
|
473
|
+
"default: the map's `bad` (transparent)."),
|
|
474
|
+
**common("name", "opacity", "z", "rasterize"),
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
def __init__(self, signal: Any, sites: Any, plot: Any = None, *, group: Any = None,
|
|
478
|
+
**settings: Any) -> None:
|
|
479
|
+
super().__init__(signal, sites, plot=plot, group=group, **settings)
|
|
480
|
+
|
|
481
|
+
def set(self, **settings: "Unpack[SiteHeatmapSettings]") -> Self: # type: ignore[override]
|
|
482
|
+
super().set(**settings)
|
|
483
|
+
if getattr(self, "plot", None) is not None:
|
|
484
|
+
self._attach_bar()
|
|
485
|
+
return self
|
|
486
|
+
|
|
487
|
+
def _bar_source(self) -> Any:
|
|
488
|
+
return self
|
|
489
|
+
|
|
490
|
+
def blocks(self) -> tuple[array, list[tuple[Any, int, int, float]], float]:
|
|
491
|
+
"""``(order, blocks, height)``: the blocks as ``(label, first, end, y)`` (``y`` the
|
|
492
|
+
top of the block in rows, gaps between groups included) and the rows' height."""
|
|
493
|
+
order, blocks = ordered(self, self.resolve("sort"))
|
|
494
|
+
n = len(order)
|
|
495
|
+
gap = _GAP * n if len(blocks) > 1 else 0.0
|
|
496
|
+
out = [(label, a, b, a + i * gap) for i, (label, a, b) in enumerate(blocks)]
|
|
497
|
+
return order, out, n + gap * (len(blocks) - 1)
|
|
498
|
+
|
|
499
|
+
def drawn_rows(self, height: float) -> list[int]:
|
|
500
|
+
"""The rows drawn per block on axes ``height`` px high."""
|
|
501
|
+
_, blocks, total = self.blocks()
|
|
502
|
+
rb = self.resolve("row_bins")
|
|
503
|
+
n = sum(b - a for _, a, b, _ in blocks) or 1
|
|
504
|
+
out = []
|
|
505
|
+
for _, a, b, _ in blocks:
|
|
506
|
+
nb = b - a
|
|
507
|
+
if rb is False:
|
|
508
|
+
k = nb
|
|
509
|
+
elif rb == "auto":
|
|
510
|
+
k = math.ceil(2 * height * nb / total) if total else nb
|
|
511
|
+
else:
|
|
512
|
+
k = round(rb * nb / n)
|
|
513
|
+
out.append(max(1, min(nb, k)) if nb else 0)
|
|
514
|
+
return out
|
|
515
|
+
|
|
516
|
+
def _drawn(self, rows: list[int]) -> list[array]:
|
|
517
|
+
"""The drawn values of each block, averaged to ``rows``."""
|
|
518
|
+
order, blocks, _ = self.blocks()
|
|
519
|
+
m = matrix(self)
|
|
520
|
+
cache = _cache(self)
|
|
521
|
+
out = []
|
|
522
|
+
for (_, a, b, _), k in zip(blocks, rows):
|
|
523
|
+
key = ("drawn", self.resolve("sort"), _ident(self.group), a, b, k, *_key(self))
|
|
524
|
+
got = cache.get(key)
|
|
525
|
+
if got is None:
|
|
526
|
+
got = cache[key] = averaged(m, order[a:b], k)
|
|
527
|
+
out.append(got)
|
|
528
|
+
return out
|
|
529
|
+
|
|
530
|
+
def layers(self) -> Call:
|
|
531
|
+
call = Call(self)
|
|
532
|
+
m = matrix(self)
|
|
533
|
+
order, blocks, total = self.blocks()
|
|
534
|
+
axis = color_axis_of(self)
|
|
535
|
+
cmap = axis.color_map()
|
|
536
|
+
|
|
537
|
+
def height(ctx: Any) -> float:
|
|
538
|
+
return abs(ctx.rect[3] - ctx.rect[1])
|
|
539
|
+
|
|
540
|
+
def placed() -> Any:
|
|
541
|
+
r = current()
|
|
542
|
+
return None if r is None or self.plot is None else r.placed.get(id(self.plot))
|
|
543
|
+
|
|
544
|
+
def values() -> Any: # trained once the axes are placed
|
|
545
|
+
ctx = placed()
|
|
546
|
+
if ctx is None:
|
|
547
|
+
return None
|
|
548
|
+
out = array("d")
|
|
549
|
+
for v in self._drawn(self.drawn_rows(height(ctx))):
|
|
550
|
+
out.extend(v)
|
|
551
|
+
return out
|
|
552
|
+
|
|
553
|
+
for k, (label, a, b, y0) in enumerate(blocks):
|
|
554
|
+
if b <= a or not m.w:
|
|
555
|
+
continue
|
|
556
|
+
|
|
557
|
+
call.layers.append(Layer(GEOMS["cells"], Table(),
|
|
558
|
+
paint=Paint(self, "sites.block", k=k, rows=b - a, y0=y0,
|
|
559
|
+
m=m, cmap=cmap),
|
|
560
|
+
stat="site_matrix", occupy="all", key=False))
|
|
561
|
+
name = self.resolve("name")
|
|
562
|
+
if name is not None:
|
|
563
|
+
call.entries.append(gradient_entry(self, name, axis.color_map))
|
|
564
|
+
call.color_sources = [(axis, values)]
|
|
565
|
+
call.resolution = lambda ctx: tuple(self.drawn_rows(height(ctx)))
|
|
566
|
+
call.extra = {"x": array("d", [-m.up, m.down]), "y": array("d", [0.0, total])}
|
|
567
|
+
call.ticks["x"] = _x_ticks(self, m.up, m.down)
|
|
568
|
+
if len(blocks) > 1:
|
|
569
|
+
call.ticks["y"] = [(y0 + (b - a) / 2, str(label)) for label, a, b, y0 in blocks]
|
|
570
|
+
else:
|
|
571
|
+
call.hide = {"y"}
|
|
572
|
+
call.invert = {"y"}
|
|
573
|
+
call.exact = {"x", "y"}
|
|
574
|
+
return call
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class SiteProfile(_SiteSeries):
|
|
578
|
+
"""The column mean (or median) of a signal around sites (see :class:`SiteHeatmap`):
|
|
579
|
+
one line per group, x the distance from the anchor in bp, an optional band.
|
|
580
|
+
|
|
581
|
+
Sites, windows and reads as :class:`SiteHeatmap`'s. Each group is a line in the
|
|
582
|
+
plot's colors (a legend entry each); ``band="sem"`` or ``"sd"`` draws a band about it.
|
|
583
|
+
|
|
584
|
+
Example::
|
|
585
|
+
|
|
586
|
+
import math
|
|
587
|
+
rng = random.Random(1)
|
|
588
|
+
sites = {"chr": ["chr1"] * 40, "start": [10_000 + 20_000 * i for i in range(40)],
|
|
589
|
+
"group": ["bound" if i % 3 else "unbound" for i in range(40)]}
|
|
590
|
+
sites["end"] = [s + 1 for s in sites["start"]]
|
|
591
|
+
signal = {"chr": [], "start": [], "end": [], "value": []}
|
|
592
|
+
for s, g in zip(sites["start"], sites["group"]):
|
|
593
|
+
h = rng.uniform(1, 3) if g == "bound" else rng.uniform(0, 0.5)
|
|
594
|
+
for x in range(s - 4000, s + 4000, 400):
|
|
595
|
+
signal["chr"].append("chr1")
|
|
596
|
+
signal["start"].append(x)
|
|
597
|
+
signal["end"].append(x + 400)
|
|
598
|
+
signal["value"].append(0.1 * rng.random() + h * math.exp(-((x - s) / 1000) ** 2))
|
|
599
|
+
p = plt.Plot(width=240, height=160)
|
|
600
|
+
p.site_profile(signal, sites, group="group", flank="4kb", bins=20, band="sem")
|
|
601
|
+
"""
|
|
602
|
+
|
|
603
|
+
SETTINGS = table(
|
|
604
|
+
**_window_settings(),
|
|
605
|
+
stat=Setting(_stat, doc="The value per bin: the `mean` or `median` of the rows."),
|
|
606
|
+
band=Setting(_band, doc="A band about the line: `sem` (standard error of the mean), "
|
|
607
|
+
"`sd`; `None` or `False`: none."),
|
|
608
|
+
color=Setting(color, doc="Line color; default: the plot's next color (one per "
|
|
609
|
+
"group)."),
|
|
610
|
+
line_width=Setting(length(lo=0), inherited=True, doc="Line width."),
|
|
611
|
+
fill_opacity=Setting(number(lo=0, hi=1), doc="Opacity of the band."),
|
|
612
|
+
**common("name", "opacity", "z", "rasterize", "simplify"),
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
def set(self, **settings: "Unpack[SiteProfileSettings]") -> Self: # type: ignore[override]
|
|
616
|
+
super().set(**settings)
|
|
617
|
+
return self
|
|
618
|
+
|
|
619
|
+
def profiles(self) -> list[tuple[Any, array, array, array | None]]:
|
|
620
|
+
"""``(group label, x, y, band half-width or None)`` per group, x the bin centres."""
|
|
621
|
+
m = matrix(self)
|
|
622
|
+
stat, band = self.resolve("stat"), self.resolve("band")
|
|
623
|
+
key = ("profile", stat, band, _ident(self.group), *_key(self))
|
|
624
|
+
cache = _cache(self)
|
|
625
|
+
got = cache.get(key)
|
|
626
|
+
if got is not None:
|
|
627
|
+
return got
|
|
628
|
+
order = array("q", range(m.n))
|
|
629
|
+
groups = self.sites.groups
|
|
630
|
+
if groups is not None:
|
|
631
|
+
by: dict[Any, list[int]] = {}
|
|
632
|
+
for i, g in enumerate(groups):
|
|
633
|
+
by.setdefault(g, []).append(i)
|
|
634
|
+
parts = [(label, array("q", idx)) for label, idx in by.items()]
|
|
635
|
+
else:
|
|
636
|
+
parts = [(None, order)]
|
|
637
|
+
edges = m.edges()
|
|
638
|
+
xs = array("d", [(a + b) / 2 for a, b in zip(edges, edges[1:])])
|
|
639
|
+
out = []
|
|
640
|
+
for label, rows in parts:
|
|
641
|
+
ys, hw = array("d"), array("d") if band else None
|
|
642
|
+
for c in range(m.w):
|
|
643
|
+
col = m.column(c, rows)
|
|
644
|
+
if sum(col) != sum(col):
|
|
645
|
+
col = array("d", [v for v in col if v == v])
|
|
646
|
+
k = len(col)
|
|
647
|
+
if not k:
|
|
648
|
+
ys.append(math.nan)
|
|
649
|
+
if hw is not None:
|
|
650
|
+
hw.append(math.nan)
|
|
651
|
+
continue
|
|
652
|
+
mean = sum(col) / k
|
|
653
|
+
ys.append(mean if stat == "mean" else _median(col))
|
|
654
|
+
if hw is not None:
|
|
655
|
+
var = max(0.0, sum(map(operator.mul, col, col)) / k - mean * mean) * k / (k - 1) \
|
|
656
|
+
if k > 1 else 0.0
|
|
657
|
+
hw.append(math.sqrt(var / (k if band == "sem" else 1)))
|
|
658
|
+
out.append((label, xs, ys, hw))
|
|
659
|
+
cache[key] = out
|
|
660
|
+
return out
|
|
661
|
+
|
|
662
|
+
def profile_colors(self, ctx: Any) -> list[Any]:
|
|
663
|
+
"""The color of each profile."""
|
|
664
|
+
n = len(self.profiles())
|
|
665
|
+
c = self.resolve("color")
|
|
666
|
+
if c is not None:
|
|
667
|
+
return [ctx.color(c)] * n
|
|
668
|
+
auto = auto_colors(self) or [None]
|
|
669
|
+
return [ctx.color(auto[i % len(auto)]) for i in range(n)]
|
|
670
|
+
|
|
671
|
+
def layers(self) -> Call:
|
|
672
|
+
call = Call(self)
|
|
673
|
+
m = matrix(self)
|
|
674
|
+
got = self.profiles()
|
|
675
|
+
|
|
676
|
+
for i, (label, xs, ys, hw) in enumerate(got):
|
|
677
|
+
mine = []
|
|
678
|
+
if hw is not None:
|
|
679
|
+
lo = array("d", [y - h for y, h in zip(ys, hw)])
|
|
680
|
+
hi = array("d", [y + h for y, h in zip(ys, hw)])
|
|
681
|
+
|
|
682
|
+
mine.append(Layer(GEOMS["band"], Table(x=xs, top=hi, bottom=lo),
|
|
683
|
+
aes={"y1": "top", "y2": "bottom"},
|
|
684
|
+
paint=Paint(self, "band", color="profile_colors()",
|
|
685
|
+
index=i, opacity="fill_opacity"),
|
|
686
|
+
stat="site_profile", occupy=False))
|
|
687
|
+
mine.append(Layer(GEOMS["path"], Table(x=xs, y=ys),
|
|
688
|
+
paint=Paint(self, "stroke", color="profile_colors()", index=i,
|
|
689
|
+
opacity=True),
|
|
690
|
+
stat="site_profile"))
|
|
691
|
+
call.layers += mine
|
|
692
|
+
name = str(label) if label is not None else self.resolve("name")
|
|
693
|
+
if name is not None:
|
|
694
|
+
call.entries.append(entry(self, name, mine, fill="profile_colors()",
|
|
695
|
+
index=i))
|
|
696
|
+
call.color_count = 0 if self.is_set("color") else len(got)
|
|
697
|
+
call.ticks["x"] = _x_ticks(self, m.up, m.down)
|
|
698
|
+
call.extra = {"x": array("d", [-m.up, m.down])}
|
|
699
|
+
call.exact = {"x"}
|
|
700
|
+
return call
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def _median(v: array) -> float:
|
|
704
|
+
s = sorted(v)
|
|
705
|
+
k = len(s)
|
|
706
|
+
return s[k // 2] if k % 2 else (s[k // 2 - 1] + s[k // 2]) / 2
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
#: The settings of :func:`SignalMatrix` that go to both plot types, to the heat maps,
|
|
710
|
+
#: and to the profiles.
|
|
711
|
+
_BOTH = ("flank", "bins", "anchor", "bin_mode", "group")
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def SignalMatrix(signals: Any, sites: Any, **settings: Any) -> "Figure":
|
|
715
|
+
"""A figure of site matrices: per signal (a mapping of names to signals, or one
|
|
716
|
+
signal), a column of a :class:`SiteProfile` above a :class:`SiteHeatmap` of the same
|
|
717
|
+
rows, x shared by column. ``share_color``: one color axis and one colorbar for all
|
|
718
|
+
heat maps (else one per column); ``profile_height``: the profiles' share of the
|
|
719
|
+
height (default ``0.25``). The settings of both plot types go to them; the others are
|
|
720
|
+
the figure's.
|
|
721
|
+
|
|
722
|
+
Use::
|
|
723
|
+
|
|
724
|
+
fig = plt.SignalMatrix({"ATAC": "atac.bw", "H3K27ac": "k27ac.bw"}, "peaks.narrowPeak",
|
|
725
|
+
anchor="summit", flank="5kb", width=180, height=600)
|
|
726
|
+
"""
|
|
727
|
+
from ..figure import Figure
|
|
728
|
+
named = list(signals.items()) if isinstance(signals, Mapping) else [(None, signals)]
|
|
729
|
+
share = bool(settings.pop("share_color", False))
|
|
730
|
+
share_part = float(settings.pop("profile_height", 0.25))
|
|
731
|
+
both = {k: settings.pop(k) for k in _BOTH if k in settings}
|
|
732
|
+
heat = {k: settings.pop(k) for k in list(settings)
|
|
733
|
+
if k in SiteHeatmap.SETTINGS and k not in SiteProfile.SETTINGS}
|
|
734
|
+
prof = {k: settings.pop(k) for k in list(settings)
|
|
735
|
+
if k in SiteProfile.SETTINGS and k not in SiteHeatmap.SETTINGS}
|
|
736
|
+
for k in [k for k in settings if k in SiteHeatmap.SETTINGS]:
|
|
737
|
+
heat[k] = prof[k] = settings.pop(k)
|
|
738
|
+
n = len(named)
|
|
739
|
+
settings.setdefault("heights", [share_part, 1 - share_part])
|
|
740
|
+
settings.setdefault("share_x", "col")
|
|
741
|
+
fig = Figure(layout=[[f"p{i}" for i in range(n)], [f"h{i}" for i in range(n)]],
|
|
742
|
+
**settings)
|
|
743
|
+
if share:
|
|
744
|
+
fig.set(share_color=True)
|
|
745
|
+
for i, (name, signal) in enumerate(named):
|
|
746
|
+
top = fig.plot(f"p{i}", title=name) if name is not None else fig.plot(f"p{i}")
|
|
747
|
+
top.site_profile(signal, sites, **both, **prof)
|
|
748
|
+
bar = heat.get("colorbar", True) if not share or i == n - 1 else False
|
|
749
|
+
fig.plot(f"h{i}").site_heatmap(signal, sites, **both, **{**heat, "colorbar": bar})
|
|
750
|
+
return fig
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
add_layer("site_heatmap", SiteHeatmap)
|
|
754
|
+
add_layer("site_profile", SiteProfile)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
@role("sites.block")
|
|
758
|
+
def _block_paint(s: Any, ctx: Any, glyph: bool, k: int = 0, rows: int = 0, y0: float = 0.0,
|
|
759
|
+
m: Any = None, cmap: Any = None) -> dict[str, Any]:
|
|
760
|
+
"""The cells of block ``k`` (``rows`` sites from ``y0``), averaged to the rows the
|
|
761
|
+
axes' height draws."""
|
|
762
|
+
def make() -> dict[str, Any]:
|
|
763
|
+
drawn = s.drawn_rows(abs(ctx.rect[3] - ctx.rect[1]))
|
|
764
|
+
vals = s._drawn(drawn)[k]
|
|
765
|
+
st = trained(color_axis_of(s))
|
|
766
|
+
assert st.scale is not None
|
|
767
|
+
fn = st.scale.function
|
|
768
|
+
f = kernels.scaled(vals, 1.0, 0.0, fn, **st.scale.options) if fn else vals
|
|
769
|
+
fill, table, _ = cmap.cells(f, st.flo, st.fhi, "image")
|
|
770
|
+
return {"rows": drawn[k], "fill": fill, "table": table}
|
|
771
|
+
g = memo(ctx, (id(s), "block", k), make)
|
|
772
|
+
x0 = ctx.xmap(float(-m.up))
|
|
773
|
+
xb = (ctx.xmap(float(m.down)) - x0) / m.w
|
|
774
|
+
ya = ctx.ymap(y0)
|
|
775
|
+
yb = (ctx.ymap(y0 + rows) - ya) / g["rows"]
|
|
776
|
+
nan = ctx.color(s.resolve("nan_color"))
|
|
777
|
+
opacity = s.resolve("opacity")
|
|
778
|
+
return {"transform": (xb, 0.0, 0.0, yb, x0, ya),
|
|
779
|
+
"x_edges": range(m.w + 1), "y_edges": range(g["rows"] + 1),
|
|
780
|
+
"fill": g["fill"], "palette": g["table"],
|
|
781
|
+
"bad": nan if nan is not None else cmap.bad, "mode": "image",
|
|
782
|
+
"opacity": 1.0 if opacity is None else opacity}
|