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
plot_io/genome/signal.py
ADDED
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
"""Binned signals (:class:`BigWig`): a step line along the bins, filled to a baseline or,
|
|
2
|
+
by a track's ``fill_mode``, between neighbouring signals; with ``bin_mode="auto"`` the
|
|
3
|
+
mean line over a band from the bins' minimum to their maximum; with ``kind="heat"`` a
|
|
4
|
+
one-row lane of cells colored by value (the heat signals of a track share one color
|
|
5
|
+
axis)."""
|
|
6
|
+
|
|
7
|
+
import contextlib
|
|
8
|
+
import math
|
|
9
|
+
from array import array
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Self, Unpack
|
|
11
|
+
|
|
12
|
+
from vec_io.arrays import doubles
|
|
13
|
+
|
|
14
|
+
from .. import groups, kernels
|
|
15
|
+
from ..color.values import color, with_alpha
|
|
16
|
+
from ..settings import Setting, one_of, table
|
|
17
|
+
from ..transform import settings_both as coord_settings
|
|
18
|
+
from ..layers import positions, add_layer
|
|
19
|
+
from ..layers.base import Series, common
|
|
20
|
+
from ..layers.core import Call, Layer, Table
|
|
21
|
+
from ..layers.geoms import GEOMS, with_crossings
|
|
22
|
+
from ..layers.grammar import entry
|
|
23
|
+
from ..layers.paint import Paint, base_color, role
|
|
24
|
+
from ..mapped import ColorAxis, own_axis, shared_axis, trained
|
|
25
|
+
from ..validators import length, real
|
|
26
|
+
from . import readers, sources
|
|
27
|
+
from .lanes import lane_mapper, lane_setting
|
|
28
|
+
from .series import GenomeSeries, check_bins, in_segment, region_setting
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from ..types import BigWigSettings
|
|
32
|
+
|
|
33
|
+
__all__ = ["BigWig", "fill_mode", "signal_bands"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_REDUCTIONS = ("mean", "sd", "sem", "sum", "count", "min", "max", "l1norm", "l2norm")
|
|
37
|
+
#: The reductions ``bin_mode="auto"`` reads: the line, the band's low and high ends.
|
|
38
|
+
_AUTO = ("mean", "min", "max")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _def_value(v: Any) -> float:
|
|
42
|
+
x = real(v)
|
|
43
|
+
if x is None:
|
|
44
|
+
raise ValueError(f"expected a number (NaN allowed), got {v!r}")
|
|
45
|
+
return x
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _callable(v: Any) -> Any:
|
|
49
|
+
if not callable(v):
|
|
50
|
+
raise ValueError(f"expected a function, got {v!r}")
|
|
51
|
+
return v
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_FILL_MODES = ("up", "down", "min", "max")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def fill_mode(v: Any) -> str | None:
|
|
58
|
+
"""A track's ``fill_mode``: ``None`` or ``"in_between(up|down|min|max)"`` (stored as the
|
|
59
|
+
word in parentheses; ``"in_between"`` is ``up``)."""
|
|
60
|
+
if v is None:
|
|
61
|
+
return None
|
|
62
|
+
t = str(v).replace(" ", "").lower() if isinstance(v, str) else None
|
|
63
|
+
if t == "in_between":
|
|
64
|
+
return "up"
|
|
65
|
+
if t is not None and t.startswith("in_between(") and t.endswith(")") \
|
|
66
|
+
and t[11:-1] in _FILL_MODES:
|
|
67
|
+
return t[11:-1]
|
|
68
|
+
if t in _FILL_MODES:
|
|
69
|
+
return t
|
|
70
|
+
raise ValueError(f"expected None or \"in_between(up|down|min|max)\", got {v!r}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _fill_to(v: Any) -> Any:
|
|
74
|
+
if v is False:
|
|
75
|
+
return False
|
|
76
|
+
if isinstance(v, Series):
|
|
77
|
+
return v
|
|
78
|
+
x = real(v)
|
|
79
|
+
if x is None or not math.isfinite(x):
|
|
80
|
+
raise ValueError(f"expected a number, a series or False, got {v!r}")
|
|
81
|
+
return x
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class BigWig(GenomeSeries):
|
|
85
|
+
"""A binned signal over the region: a step line along the bins, filled to a baseline.
|
|
86
|
+
``reader`` is a signal reader (bigWig; bigBed, BED and peaks give their coverage;
|
|
87
|
+
:func:`~plot_io.genome.combine.combine` of two), a path (opened with gwseq_io),
|
|
88
|
+
``chr``, ``start``, ``end``, ``value`` records (columns, a DataFrame or rows; binned
|
|
89
|
+
by plot_io), a function ``f(chr, start, end, bins)`` giving values, or ``None`` with
|
|
90
|
+
``y`` (values over the region) and optionally ``x`` (bin centres, or n + 1 edges, in
|
|
91
|
+
bp).
|
|
92
|
+
|
|
93
|
+
Records and paths: see `readers <docs/api.md#readers>`_. The color is black when the series is alone in its
|
|
94
|
+
track, the plot's colors when overlaid. ``fill_to=<series>`` fills between two signals,
|
|
95
|
+
each part in the fill color of the series on top. For more, the track's ``fill_mode``
|
|
96
|
+
fills between all its signals, in place of their ``fill_to``: ``"in_between(up)"`` (or
|
|
97
|
+
``"in_between"``) fills each band between two neighbouring signals in the color of the
|
|
98
|
+
one just above, ``"in_between(down)"`` of the one just below; ``"in_between(max)"`` fills
|
|
99
|
+
from the lowest signal to the highest in the color of the top one, ``"in_between(min)"``
|
|
100
|
+
of the bottom one. The colors follow wherever the order changes (lines get points where
|
|
101
|
+
they cross), and 0 is not kept on the y axis::
|
|
102
|
+
|
|
103
|
+
t = fig.track(y_axis={"label": "Lamin A/C"}, fill_mode="in_between(up)")
|
|
104
|
+
t.bigwig(neo_reader, name="Neo")
|
|
105
|
+
t.bigwig(pax7_reader, name="Pax7")
|
|
106
|
+
|
|
107
|
+
``bin_mode="auto"`` reads each bin's mean, minimum and maximum: the line is the mean, a
|
|
108
|
+
band from the minimum to the maximum (in the fill color at ``fill_opacity``) replaces
|
|
109
|
+
the fill, and the y limits cover the band (narrow peaks stay visible at any zoom; three
|
|
110
|
+
reads). ``kind="heat"`` draws the bins as one row of cells colored through
|
|
111
|
+
``color_map``, in a lane of its own ``row_height`` high, named on the left (``name``);
|
|
112
|
+
the heat strips of a track share one color axis (the first one's ``color_*`` settings,
|
|
113
|
+
one colorbar with ``colorbar=True``), and a track of heat strips alone has no y axis.
|
|
114
|
+
|
|
115
|
+
``plot_io.genome.combine(a, b, op="log2ratio", pseudo_count=0)`` is a signal reader of
|
|
116
|
+
two (readers, paths or records), read with the same arguments and combined bin by bin:
|
|
117
|
+
``"difference"`` (``a − b``), ``"ratio"`` (``(a + p) / (b + p)``), ``"log2ratio"``
|
|
118
|
+
(``log2((a + p) / (b + p))``) or a function ``op(a, b)`` of two numbers; NaN where
|
|
119
|
+
either is NaN or the result is undefined::
|
|
120
|
+
|
|
121
|
+
t = fig.track()
|
|
122
|
+
for name, r in readers.items():
|
|
123
|
+
t.bigwig(r, kind="heat", name=name, colorbar=True)
|
|
124
|
+
fig.bigwig(plt.genome.combine(treat, control, "log2ratio", pseudo_count=1))
|
|
125
|
+
|
|
126
|
+
Example::
|
|
127
|
+
|
|
128
|
+
import math
|
|
129
|
+
fig = plt.Tracks(("chr1", 0, 200_000), width=300)
|
|
130
|
+
fig.bigwig(y=[0.2 + math.exp(-((i - 60) / 6) ** 2) + 0.6 * math.exp(-((i - 140) / 10) ** 2)
|
|
131
|
+
for i in range(200)])
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
SETTINGS = table(
|
|
135
|
+
region=region_setting(),
|
|
136
|
+
bins=Setting(check_bins,
|
|
137
|
+
doc="A bin count over the region (exactly, not on a grid); a bin size "
|
|
138
|
+
"(`\"10kb\"`, `\"5000bp\"`), read on the genome-wide grid of the "
|
|
139
|
+
"size (the row grown to it, then cut to the region); or a bin "
|
|
140
|
+
"width on the axes (`\"0.5px\"`: two bins per px, read as a "
|
|
141
|
+
"count)."),
|
|
142
|
+
bin_mode=Setting(one_of(*_REDUCTIONS, "auto"),
|
|
143
|
+
doc="Reduction per bin: `mean`, `max` (keeps narrow peaks), `min`, "
|
|
144
|
+
"`sum`, … (gwseq_io's); `auto`: the mean line over a band from "
|
|
145
|
+
"each bin's `min` to its `max` (in the fill color at "
|
|
146
|
+
"`fill_opacity`, in place of the fill; three reads)."),
|
|
147
|
+
def_value=Setting(_def_value,
|
|
148
|
+
doc="Value of bases no record covers; NaN breaks the line there."),
|
|
149
|
+
kind=Setting(one_of("step", "line", "bar", "heat"),
|
|
150
|
+
doc="`step` (a step line along the bins), `line` (through the bin "
|
|
151
|
+
"centres), `bar` (one rectangle per bin) or `heat` (a lane of one "
|
|
152
|
+
"row of cells colored through `color_map`, named on the left; "
|
|
153
|
+
"the heat signals of a track share one color axis, set by the "
|
|
154
|
+
"first one's `color_*` settings)."),
|
|
155
|
+
lane=lane_setting(),
|
|
156
|
+
row_height=Setting(length(positive=True, percent="em"),
|
|
157
|
+
doc="`kind=\"heat\"`: the height of the lane."),
|
|
158
|
+
fill_to=Setting(_fill_to,
|
|
159
|
+
doc="Fill between the signal and a value, another series of the "
|
|
160
|
+
"track (colored by whichever is above), or `False`."),
|
|
161
|
+
**groups.FillStyle.embed(docs={"fill": "Fill (to `fill_to`): `True` (the line "
|
|
162
|
+
"color), a color, or `False`.",
|
|
163
|
+
"fill_opacity": "Opacity of the fill, times "
|
|
164
|
+
"`opacity`."}),
|
|
165
|
+
color=Setting(color, doc="Line color; default black alone in its track, the next "
|
|
166
|
+
"plot's color when overlaid."),
|
|
167
|
+
transform=Setting(_callable, doc="A function applied to each value (log, scaling)."),
|
|
168
|
+
**common("name", "color_map", "opacity", "z", "rasterize", "simplify", "line_width",
|
|
169
|
+
"font_size", "font_family", "text_color",
|
|
170
|
+
docs={"colorbar": "`kind=\"heat\"`: a colorbar of the track's heat color "
|
|
171
|
+
"axis attached to the track (one per track): `True`, a "
|
|
172
|
+
"place or a dict of colorbar settings."}),
|
|
173
|
+
**coord_settings(),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def __init__(self, reader: Any = None, plot: Any = None, *, x: Any = None, y: Any = None,
|
|
177
|
+
**settings: Any) -> None:
|
|
178
|
+
if reader is None and y is None:
|
|
179
|
+
raise TypeError("BigWig needs a reader, a function or y values")
|
|
180
|
+
self.reader = None if reader is None else readers.adapt(reader, "signal", "BigWig",
|
|
181
|
+
owner=self)
|
|
182
|
+
self.x_data = None if x is None else doubles(x)
|
|
183
|
+
self.y_data = None if y is None else doubles(y)
|
|
184
|
+
if self.x_data is not None and self.y_data is not None and \
|
|
185
|
+
len(self.x_data) not in (len(self.y_data), len(self.y_data) + 1):
|
|
186
|
+
raise ValueError(f"x has {len(self.x_data)} values for {len(self.y_data)} bins "
|
|
187
|
+
f"(give centres or edges)")
|
|
188
|
+
super().__init__(plot=plot, **settings)
|
|
189
|
+
|
|
190
|
+
def set(self, **settings: "Unpack[BigWigSettings]") -> Self: # type: ignore[override]
|
|
191
|
+
super().set(**settings)
|
|
192
|
+
if getattr(self, "plot", None) is not None:
|
|
193
|
+
self._attach_bar()
|
|
194
|
+
return self
|
|
195
|
+
|
|
196
|
+
# -- heat signals ----------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
def is_heat(self) -> bool:
|
|
199
|
+
"""Whether it is drawn as a heat strip (``kind="heat"``)."""
|
|
200
|
+
return self.resolve("kind") == "heat"
|
|
201
|
+
|
|
202
|
+
def in_lane(self) -> bool:
|
|
203
|
+
"""Whether it is drawn in a lane of its own (:mod:`plot_io.genome.lanes`): a heat
|
|
204
|
+
strip."""
|
|
205
|
+
return self.is_heat()
|
|
206
|
+
|
|
207
|
+
def lane_rows(self) -> int:
|
|
208
|
+
return 1
|
|
209
|
+
|
|
210
|
+
def drawn_rows(self) -> tuple[float, float]:
|
|
211
|
+
return 0.0, 1.0
|
|
212
|
+
|
|
213
|
+
def row_px(self) -> float:
|
|
214
|
+
return float(self.resolve("row_height"))
|
|
215
|
+
|
|
216
|
+
def heat_axis(self) -> ColorAxis:
|
|
217
|
+
"""The color axis of a heat strip: the shared one given as ``color_axis=``, else the
|
|
218
|
+
track's (the own axis of its first heat signal without a shared one)."""
|
|
219
|
+
axis = shared_axis(self, "color")
|
|
220
|
+
if axis is not None:
|
|
221
|
+
return axis
|
|
222
|
+
plot = self.plot
|
|
223
|
+
first = next((s for s in (plot.series if plot is not None else ())
|
|
224
|
+
if isinstance(s, BigWig) and s.is_heat()
|
|
225
|
+
and s.resolve("color_axis") is None), self)
|
|
226
|
+
return own_axis(first, ColorAxis)
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def color_axis(self) -> Any:
|
|
230
|
+
"""The color axis of a heat strip (:meth:`heat_axis`)."""
|
|
231
|
+
return self.heat_axis()
|
|
232
|
+
|
|
233
|
+
def _bar_source(self) -> Any:
|
|
234
|
+
return self.heat_axis() if self.is_heat() else None
|
|
235
|
+
|
|
236
|
+
# -- data ----------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
def bins(self) -> tuple[int | None, int | None]:
|
|
239
|
+
"""``(bin count, bin size)``: one of them."""
|
|
240
|
+
b = self.resolve("bins")
|
|
241
|
+
if b.kind == "count":
|
|
242
|
+
return int(b.value), None
|
|
243
|
+
if b.kind == "bp":
|
|
244
|
+
return None, int(b.value)
|
|
245
|
+
return max(1, round(self.axes_px() / b.value)), None
|
|
246
|
+
|
|
247
|
+
def data(self) -> tuple[array, array]:
|
|
248
|
+
"""``(edges, values)``: n + 1 bin edges (bp) and n values."""
|
|
249
|
+
edges, vals, _ = self.reads()
|
|
250
|
+
return edges, vals
|
|
251
|
+
|
|
252
|
+
def reads(self) -> tuple[array, array, tuple[array, array] | None]:
|
|
253
|
+
"""``(edges, values, band)``: :meth:`data`, and with ``bin_mode="auto"`` the band
|
|
254
|
+
``(low, high)``: each bin's ``min`` and ``max`` (``None`` otherwise, and for plain
|
|
255
|
+
values and functions). The reads of one draw are cached as one."""
|
|
256
|
+
if self.reader is None and self.x_data is not None:
|
|
257
|
+
key: Any = ("plain",)
|
|
258
|
+
else:
|
|
259
|
+
key = (self.region(), self.bins(), self.resolve("bin_mode"),
|
|
260
|
+
self.resolve("def_value"))
|
|
261
|
+
return self.cached(key, lambda: self._guard(self._read), kind="data")
|
|
262
|
+
|
|
263
|
+
def _read(self) -> tuple[array, array, tuple[array, array] | None]:
|
|
264
|
+
band = None
|
|
265
|
+
if self.reader is None:
|
|
266
|
+
assert self.y_data is not None
|
|
267
|
+
vals = array("d", self.y_data)
|
|
268
|
+
n = len(vals)
|
|
269
|
+
if not n:
|
|
270
|
+
edges = array("d")
|
|
271
|
+
elif self.x_data is None:
|
|
272
|
+
r = self.region()
|
|
273
|
+
edges = sources.even_edges(r.start, r.end, n)
|
|
274
|
+
elif len(self.x_data) == n + 1:
|
|
275
|
+
edges = array("d", self.x_data)
|
|
276
|
+
else:
|
|
277
|
+
c = self.x_data
|
|
278
|
+
if n == 1:
|
|
279
|
+
edges = array("d", [c[0] - 0.5, c[0] + 0.5])
|
|
280
|
+
else:
|
|
281
|
+
mids = [(a + b) / 2 for a, b in zip(c, c[1:])]
|
|
282
|
+
edges = array("d", [c[0] - (mids[0] - c[0])] + mids
|
|
283
|
+
+ [c[-1] + (c[-1] - mids[-1])])
|
|
284
|
+
elif sources.kind_of(self.reader) is None: # a function
|
|
285
|
+
r = self.region()
|
|
286
|
+
count, size = self.bins()
|
|
287
|
+
n = count if count is not None else max(1, math.ceil(r.span / (size or 1)))
|
|
288
|
+
vals = doubles(self.reader(r.chr, r.start, r.end, n))
|
|
289
|
+
if len(vals) != n:
|
|
290
|
+
raise ValueError(f"the function gave {len(vals)} values for {n} bins")
|
|
291
|
+
edges = sources.even_edges(r.start, r.end, n)
|
|
292
|
+
else:
|
|
293
|
+
count, size = self.bins()
|
|
294
|
+
mode = self.resolve("bin_mode")
|
|
295
|
+
got = [sources.read_signal(self.reader, self.region(), count, size, m,
|
|
296
|
+
self.resolve("def_value"))
|
|
297
|
+
for m in (_AUTO if mode == "auto" else (mode,))]
|
|
298
|
+
edges, vals = got[0]
|
|
299
|
+
if len(got) > 1:
|
|
300
|
+
band = (got[1][1], got[2][1])
|
|
301
|
+
f = self.resolve("transform")
|
|
302
|
+
if f is not None:
|
|
303
|
+
vals = array("d", [float(f(v)) for v in vals])
|
|
304
|
+
if band is not None:
|
|
305
|
+
band = (array("d", [float(f(v)) for v in band[0]]),
|
|
306
|
+
array("d", [float(f(v)) for v in band[1]]))
|
|
307
|
+
return edges, vals, band
|
|
308
|
+
|
|
309
|
+
def centres_at(self, xs: Any) -> array:
|
|
310
|
+
"""The signal through the bin centres (``kind="line"``) at ``xs``: NaN outside."""
|
|
311
|
+
return centres_at(*self.data(), xs)
|
|
312
|
+
|
|
313
|
+
def values_at(self, edges: Any) -> array:
|
|
314
|
+
"""The values at the centres of the bins ``edges`` (another series' bins)."""
|
|
315
|
+
return values_at(*self.data(), edges)
|
|
316
|
+
|
|
317
|
+
def fill_mode(self) -> str | None:
|
|
318
|
+
"""The track's ``fill_mode`` (``None`` on a plot without one, for bars, heat strips
|
|
319
|
+
and ``bin_mode="auto"``)."""
|
|
320
|
+
p = self.plot
|
|
321
|
+
if p is None or "fill_mode" not in p.SETTINGS or self.resolve("kind") in ("bar", "heat") \
|
|
322
|
+
or self._banded():
|
|
323
|
+
return None
|
|
324
|
+
return p.resolve("fill_mode")
|
|
325
|
+
|
|
326
|
+
def _banded(self) -> bool:
|
|
327
|
+
"""Whether it draws a min–max band (``bin_mode="auto"`` on a reader) in place of
|
|
328
|
+
its fill."""
|
|
329
|
+
return self.resolve("bin_mode") == "auto" and self.reader is not None \
|
|
330
|
+
and sources.kind_of(self.reader) is not None
|
|
331
|
+
|
|
332
|
+
def _fill_to(self) -> Any:
|
|
333
|
+
if self.fill_mode() is not None or self._banded():
|
|
334
|
+
return False
|
|
335
|
+
return self.resolve("fill_to")
|
|
336
|
+
|
|
337
|
+
def _alone(self) -> bool:
|
|
338
|
+
return not any(isinstance(s, BigWig) and s is not self and not s.is_set("color")
|
|
339
|
+
and not s.is_heat() for s in self.plot.series)
|
|
340
|
+
|
|
341
|
+
def line_color(self, ctx: Any) -> Any:
|
|
342
|
+
"""The line color: its own, black alone in its track, else the plot's next."""
|
|
343
|
+
if not self.is_set("color") and self._alone():
|
|
344
|
+
return ctx.color("black")
|
|
345
|
+
return base_color(self, ctx)
|
|
346
|
+
|
|
347
|
+
def fill_of(self, ctx: Any) -> Any:
|
|
348
|
+
c = groups.fill_parts(self)[0]
|
|
349
|
+
return ctx.color(c) if c is not None else self.line_color(ctx)
|
|
350
|
+
|
|
351
|
+
def fill_amount(self) -> float:
|
|
352
|
+
"""The opacity of the fill (0 without one)."""
|
|
353
|
+
return groups.fill_parts(self)[1]
|
|
354
|
+
|
|
355
|
+
def _points(self, edges: Any, vals: Any) -> tuple[array, array, str]:
|
|
356
|
+
style = self.resolve("kind")
|
|
357
|
+
if style == "line":
|
|
358
|
+
return (array("d", [(a + b) / 2 for a, b in zip(edges, edges[1:])]),
|
|
359
|
+
array("d", vals), "linear")
|
|
360
|
+
ys = array("d", vals)
|
|
361
|
+
if len(ys):
|
|
362
|
+
ys.append(ys[-1])
|
|
363
|
+
return array("d", edges), ys, "step-post"
|
|
364
|
+
|
|
365
|
+
# -- the preset (stat region_signal) ----------------------------------------------
|
|
366
|
+
|
|
367
|
+
def layers(self) -> Call:
|
|
368
|
+
edges, vals, band = self.reads()
|
|
369
|
+
call = self.genome_call()
|
|
370
|
+
if self.is_heat():
|
|
371
|
+
return self._heat(call, edges, vals)
|
|
372
|
+
opacity = self.resolve("opacity")
|
|
373
|
+
opacity = 1.0 if opacity is None else opacity
|
|
374
|
+
to = self._fill_to()
|
|
375
|
+
style = self.resolve("kind")
|
|
376
|
+
mode = self.fill_mode()
|
|
377
|
+
if len(vals):
|
|
378
|
+
xs, ys, interp = self._points(edges, vals)
|
|
379
|
+
fill_opacity = self.fill_amount() * opacity
|
|
380
|
+
if band is not None and fill_opacity > 0:
|
|
381
|
+
_, lo, _ = self._points(edges, band[0])
|
|
382
|
+
_, hi, _ = self._points(edges, band[1])
|
|
383
|
+
call.layers.append(Layer(GEOMS["band"], Table(x=xs, y1=lo, y2=hi),
|
|
384
|
+
paint=Paint(self, "signal.fill",
|
|
385
|
+
opacity=fill_opacity, interp=interp),
|
|
386
|
+
stat="region_signal", key=False))
|
|
387
|
+
if style == "bar":
|
|
388
|
+
base = to if isinstance(to, float) else 0.0
|
|
389
|
+
t = Table(x0=edges[:-1], x1=edges[1:], y0=base, y1=vals) # NaN: no bar
|
|
390
|
+
call.layers.append(Layer(GEOMS["rects"], t,
|
|
391
|
+
paint=Paint(self, "signal.bars", opacity=opacity),
|
|
392
|
+
stat="region_signal", key=False))
|
|
393
|
+
else:
|
|
394
|
+
if mode is not None:
|
|
395
|
+
t = Table()
|
|
396
|
+
t.signal = (self, mode) # type: ignore[attr-defined]
|
|
397
|
+
call.tables.append(t)
|
|
398
|
+
call.layers.append(Layer(GEOMS["band"], t, aes={"x": "band_x",
|
|
399
|
+
"y1": "band_y",
|
|
400
|
+
"y2": "band_bound"},
|
|
401
|
+
paint=Paint(self, "signal.fill",
|
|
402
|
+
opacity=fill_opacity,
|
|
403
|
+
interp=interp),
|
|
404
|
+
stat="region_signal", position="signal_band",
|
|
405
|
+
key=False))
|
|
406
|
+
elif isinstance(to, float):
|
|
407
|
+
call.layers.append(Layer(GEOMS["band"], Table(x=xs, y1=ys, y2=to),
|
|
408
|
+
paint=Paint(self, "signal.fill",
|
|
409
|
+
opacity=fill_opacity,
|
|
410
|
+
interp=interp),
|
|
411
|
+
stat="region_signal", key=False))
|
|
412
|
+
elif isinstance(to, BigWig):
|
|
413
|
+
other = to.values_at(edges)
|
|
414
|
+
if style == "line":
|
|
415
|
+
oy = other
|
|
416
|
+
else:
|
|
417
|
+
oy = array("d", other)
|
|
418
|
+
oy.append(oy[-1])
|
|
419
|
+
call.layers.append(Layer(GEOMS["band"], Table(x=xs, y1=ys, y2=oy),
|
|
420
|
+
paint=Paint(self, "signal.fill",
|
|
421
|
+
opacity=fill_opacity,
|
|
422
|
+
interp=interp, below=to,
|
|
423
|
+
below_opacity=to.fill_amount()
|
|
424
|
+
* opacity),
|
|
425
|
+
stat="region_signal", key=False))
|
|
426
|
+
|
|
427
|
+
call.layers.append(Layer(GEOMS["path"], Table(x=xs, y=ys),
|
|
428
|
+
paint=Paint(self, "signal.line", opacity=opacity,
|
|
429
|
+
interp=interp),
|
|
430
|
+
stat="region_signal", key=False))
|
|
431
|
+
for la in call.layers: # the axes see the signal and its baseline
|
|
432
|
+
la.options["bounds"] = False
|
|
433
|
+
name = self.resolve("name")
|
|
434
|
+
if name is not None:
|
|
435
|
+
legend = Layer(GEOMS["path"], Table(),
|
|
436
|
+
paint=Paint(self, "stroke", color="line_color()"))
|
|
437
|
+
call.entries.append(entry(self, name, [legend], fill="line_color()"))
|
|
438
|
+
call.color_count = 0 if (self.is_set("color") or self._alone()) else 1
|
|
439
|
+
y = array("d", [v for v in vals if math.isfinite(v)])
|
|
440
|
+
if band is not None:
|
|
441
|
+
y.extend(v for v in (*band[0], *band[1]) if math.isfinite(v))
|
|
442
|
+
base = self.resolve("fill_to")
|
|
443
|
+
to = base if isinstance(base, float) else to # the baseline stays in view
|
|
444
|
+
if isinstance(to, float):
|
|
445
|
+
y.append(to)
|
|
446
|
+
call.sticky_values["y"] = [to]
|
|
447
|
+
call.extra = {"x": array("d", [edges[0], edges[-1]]) if len(edges) else edges, "y": y}
|
|
448
|
+
if mode is None:
|
|
449
|
+
call.zero = {"y"}
|
|
450
|
+
call.exact = {"x"}
|
|
451
|
+
if len(vals):
|
|
452
|
+
mids = array("d", [(a + b) / 2 for a, b in zip(edges, edges[1:])])
|
|
453
|
+
call.layers.append(Layer(GEOMS["points"], Table(x=mids, y=vals), paint=None,
|
|
454
|
+
key=False, bounds=False))
|
|
455
|
+
for la in call.layers:
|
|
456
|
+
if la.geom is not GEOMS["points"]:
|
|
457
|
+
la.occupy = False
|
|
458
|
+
return call
|
|
459
|
+
|
|
460
|
+
def _heat(self, call: Call, edges: Any, vals: Any) -> Call:
|
|
461
|
+
"""The call of a heat strip: one row of cells in its lane, colored through the
|
|
462
|
+
track's heat color axis."""
|
|
463
|
+
axis = self.heat_axis()
|
|
464
|
+
cmap = axis.color_map()
|
|
465
|
+
keep = [i for i in range(len(vals)) if edges[i + 1] > edges[i]]
|
|
466
|
+
if keep:
|
|
467
|
+
xs = array("d", [edges[keep[0]]] + [edges[i + 1] for i in keep])
|
|
468
|
+
vs = array("d", [vals[i] for i in keep])
|
|
469
|
+
|
|
470
|
+
call.layers.append(Layer(GEOMS["cells"], Table(),
|
|
471
|
+
paint=Paint(self, "signal.heat", axis=axis, cmap=cmap,
|
|
472
|
+
xs=xs, vs=vs),
|
|
473
|
+
stat="region_signal", occupy=False, key=False,
|
|
474
|
+
bounds=False))
|
|
475
|
+
call.color_sources = [(axis, array("d", [v for v in vals if math.isfinite(v)]))]
|
|
476
|
+
call.color_count = 0
|
|
477
|
+
r = self.region()
|
|
478
|
+
call.extra = {"x": array("d", [r.start, r.end])}
|
|
479
|
+
call.hide = {"y"} # one row, in the series' lane
|
|
480
|
+
call.lane = lane_mapper(self)
|
|
481
|
+
call.exact = {"x"}
|
|
482
|
+
return call
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def centres_at(edges: Any, vals: Any, xs: Any) -> array:
|
|
486
|
+
"""A binned signal through its bin centres at ``xs``: NaN outside."""
|
|
487
|
+
cs = [(a + b) / 2 for a, b in zip(edges, edges[1:])]
|
|
488
|
+
out = array("d")
|
|
489
|
+
j = 0
|
|
490
|
+
n = len(cs)
|
|
491
|
+
for x in xs:
|
|
492
|
+
while j < n - 2 and cs[j + 1] <= x:
|
|
493
|
+
j += 1
|
|
494
|
+
if not n or x < cs[0] or x > cs[-1]:
|
|
495
|
+
out.append(math.nan)
|
|
496
|
+
elif n == 1 or x == cs[j]:
|
|
497
|
+
out.append(vals[j])
|
|
498
|
+
else:
|
|
499
|
+
t = (x - cs[j]) / (cs[j + 1] - cs[j])
|
|
500
|
+
out.append(vals[j] + t * (vals[j + 1] - vals[j]))
|
|
501
|
+
return out
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def values_at(mine: Any, vals: Any, edges: Any) -> array:
|
|
505
|
+
"""A binned signal's values at the centres of the bins ``edges``."""
|
|
506
|
+
out = array("d")
|
|
507
|
+
j = 0
|
|
508
|
+
n = len(vals)
|
|
509
|
+
for a, b in zip(edges, edges[1:]):
|
|
510
|
+
c = (a + b) / 2
|
|
511
|
+
while j < n - 1 and mine[j + 1] <= c:
|
|
512
|
+
j += 1
|
|
513
|
+
out.append(vals[j] if n and mine[0] <= c <= mine[-1] else math.nan)
|
|
514
|
+
return out
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def signal_bands(plot: Any, calls: list[Any]) -> None:
|
|
518
|
+
"""Position ``signal_band``: under a track's ``fill_mode``, each signal's band runs to
|
|
519
|
+
the next signal below (``up``), above (``down``), or to the others where it is the
|
|
520
|
+
highest (``max``) or lowest (``min``), on the points of all the track's signals (bin
|
|
521
|
+
edges for steps, the last value repeated; bin centres and their crossings for lines,
|
|
522
|
+
split where they cross with :func:`~plot_io.layers.geoms.with_crossings`). Writes
|
|
523
|
+
``band_x``, ``band_y`` and ``band_bound``. On an x axis over several regions, per
|
|
524
|
+
region."""
|
|
525
|
+
tables = [t for c in calls for t in c.tables if getattr(t, "signal", None) is not None]
|
|
526
|
+
by: dict[Any, list[Any]] = {}
|
|
527
|
+
for t in tables:
|
|
528
|
+
seg = getattr(t, "segment", None)
|
|
529
|
+
by.setdefault(None if seg is None else seg.number, []).append(t)
|
|
530
|
+
for group in by.values():
|
|
531
|
+
seg = getattr(group[0], "segment", None)
|
|
532
|
+
with in_segment(group[0].signal[0], seg) if seg is not None \
|
|
533
|
+
else contextlib.nullcontext():
|
|
534
|
+
_bands(group)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _bands(tables: list[Any]) -> None:
|
|
538
|
+
peers = [t.signal[0] for t in tables]
|
|
539
|
+
for me, t in enumerate(tables):
|
|
540
|
+
s, mode = t.signal
|
|
541
|
+
if s.resolve("kind") == "line":
|
|
542
|
+
xs = sorted({(a + b) / 2 for p in peers
|
|
543
|
+
for a, b in zip(p.data()[0], p.data()[0][1:])})
|
|
544
|
+
curves = [list(p.centres_at(xs)) for p in peers]
|
|
545
|
+
xs, curves = with_crossings(xs, curves)
|
|
546
|
+
else:
|
|
547
|
+
xs = sorted({e for p in peers for e in p.data()[0]})
|
|
548
|
+
curves = []
|
|
549
|
+
for p in peers:
|
|
550
|
+
v = list(p.values_at(xs))
|
|
551
|
+
curves.append(v + v[-1:])
|
|
552
|
+
ys = array("d", curves[me])
|
|
553
|
+
bound = array("d")
|
|
554
|
+
for k, v in enumerate(ys):
|
|
555
|
+
if not math.isfinite(v):
|
|
556
|
+
bound.append(math.nan)
|
|
557
|
+
continue
|
|
558
|
+
# ties ordered by series, so equal signals share their band once
|
|
559
|
+
mine = (v, me)
|
|
560
|
+
others = [(c[k], j) for j, c in enumerate(curves) if j != me
|
|
561
|
+
and math.isfinite(c[k])]
|
|
562
|
+
if mode == "up":
|
|
563
|
+
b = max((o for o in others if o < mine), default=mine)[0]
|
|
564
|
+
elif mode == "down":
|
|
565
|
+
b = min((o for o in others if o > mine), default=mine)[0]
|
|
566
|
+
elif mode == "max":
|
|
567
|
+
b = min(others, default=mine)[0] if all(o < mine for o in others) else v
|
|
568
|
+
else:
|
|
569
|
+
b = max(others, default=mine)[0] if all(o > mine for o in others) else v
|
|
570
|
+
bound.append(b)
|
|
571
|
+
t["band_x"], t["band_y"], t["band_bound"] = array("d", xs), ys, bound
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
add_layer("bigwig", BigWig)
|
|
575
|
+
positions.register(signal_bands, "signal_band")
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
@role("signal.fill")
|
|
579
|
+
def _fill_paint(s: BigWig, ctx: Any, glyph: bool, opacity: float = 1.0, interp: str = "linear",
|
|
580
|
+
below: Any = None, below_opacity: float = 1.0) -> dict[str, Any]:
|
|
581
|
+
"""A signal's fill (``below``: the signal it fills to, whose fill shows below it)."""
|
|
582
|
+
p = {"color": s.fill_of(ctx), "opacity": opacity}
|
|
583
|
+
if below is not None:
|
|
584
|
+
p.update(below=below.fill_of(ctx), below_opacity=below_opacity)
|
|
585
|
+
p["interpolation"] = interp
|
|
586
|
+
return p
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
@role("signal.bars")
|
|
590
|
+
def _bars_paint(s: BigWig, ctx: Any, glyph: bool, opacity: float = 1.0) -> dict[str, Any]:
|
|
591
|
+
return {"fill": with_alpha(s.line_color(ctx), opacity)}
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@role("signal.line")
|
|
595
|
+
def _line_paint(s: BigWig, ctx: Any, glyph: bool, opacity: float = 1.0,
|
|
596
|
+
interp: str = "linear") -> dict[str, Any]:
|
|
597
|
+
p = {"color": s.line_color(ctx), "width": s.resolve("line_width")}
|
|
598
|
+
if not glyph:
|
|
599
|
+
p.update(opacity=opacity, interpolation=interp, simplify=s.resolve("simplify"))
|
|
600
|
+
return p
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
@role("signal.heat")
|
|
604
|
+
def _heat_paint(s: BigWig, ctx: Any, glyph: bool, axis: Any = None, cmap: Any = None,
|
|
605
|
+
xs: Any = None, vs: Any = None) -> dict[str, Any]:
|
|
606
|
+
"""A heat strip's cells through the track's heat color axis."""
|
|
607
|
+
from ..presets.heatmap import _even
|
|
608
|
+
st = trained(axis)
|
|
609
|
+
assert st.scale is not None
|
|
610
|
+
fn = st.scale.function
|
|
611
|
+
f = kernels.scaled(vs, 1.0, 0.0, fn, **st.scale.options) if fn else vs
|
|
612
|
+
xe = [ctx.xmap(x) for x in xs]
|
|
613
|
+
ye = sorted((ctx.ymap(0.0), ctx.ymap(1.0)))
|
|
614
|
+
if xe[0] > xe[-1]:
|
|
615
|
+
xe, f = xe[::-1], array("d", f[::-1])
|
|
616
|
+
mode = "image" if _even(xe) else "cells"
|
|
617
|
+
fill, table, bounds = cmap.cells(f, st.flo, st.fhi, mode)
|
|
618
|
+
opacity = s.resolve("opacity")
|
|
619
|
+
return {"x_edges": xe, "y_edges": ye, "even": mode == "image", "mode": mode,
|
|
620
|
+
"fill": fill, "palette": table, "bounds": bounds, "bad": cmap.bad,
|
|
621
|
+
"opacity": 1.0 if opacity is None else opacity}
|