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