plot-io 0.12.0__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plot_io/__init__.py +73 -0
- plot_io/_methods.py +1691 -0
- plot_io/_native/__init__.py +199 -0
- plot_io/_native/__main__.py +197 -0
- plot_io/_native/_api.py +457 -0
- plot_io/_native/bin/windows-x86_64.dll +0 -0
- plot_io/_native/kernels.py +335 -0
- plot_io/_native/manifest.json +49 -0
- plot_io/axis.py +1347 -0
- plot_io/boxes.py +1156 -0
- plot_io/canvas.py +389 -0
- plot_io/color/__init__.py +3 -0
- plot_io/color/data.py +1646 -0
- plot_io/color/maps.py +922 -0
- plot_io/color/values.py +295 -0
- plot_io/color/variants.py +1246 -0
- plot_io/colorbar.py +1426 -0
- plot_io/columns.py +614 -0
- plot_io/decorations.py +920 -0
- plot_io/defaults.py +809 -0
- plot_io/density.py +113 -0
- plot_io/ext.py +68 -0
- plot_io/facets.py +240 -0
- plot_io/figure.py +788 -0
- plot_io/formatters.py +97 -0
- plot_io/genome/__init__.py +17 -0
- plot_io/genome/adapters.py +356 -0
- plot_io/genome/combine.py +123 -0
- plot_io/genome/contacts.py +508 -0
- plot_io/genome/intervals.py +750 -0
- plot_io/genome/lanes.py +191 -0
- plot_io/genome/layout.py +184 -0
- plot_io/genome/links.py +364 -0
- plot_io/genome/pack.py +27 -0
- plot_io/genome/readers.py +145 -0
- plot_io/genome/regions.py +228 -0
- plot_io/genome/scale.py +349 -0
- plot_io/genome/series.py +344 -0
- plot_io/genome/signal.py +621 -0
- plot_io/genome/sites.py +782 -0
- plot_io/genome/sources.py +459 -0
- plot_io/genome/testing.py +161 -0
- plot_io/geometry.py +289 -0
- plot_io/groups.py +308 -0
- plot_io/guides.py +503 -0
- plot_io/helpers/__init__.py +17 -0
- plot_io/helpers/clustermap.py +202 -0
- plot_io/helpers/dendrogram.py +172 -0
- plot_io/helpers/grids.py +304 -0
- plot_io/helpers/qq.py +334 -0
- plot_io/helpers/raincloud.py +249 -0
- plot_io/helpers/tree.py +214 -0
- plot_io/helpers/volcano.py +505 -0
- plot_io/hints.py +125 -0
- plot_io/kernels.py +740 -0
- plot_io/labels.py +566 -0
- plot_io/layers/__init__.py +100 -0
- plot_io/layers/base.py +630 -0
- plot_io/layers/context.py +87 -0
- plot_io/layers/core.py +446 -0
- plot_io/layers/geoms.py +1362 -0
- plot_io/layers/grammar.py +586 -0
- plot_io/layers/paint.py +614 -0
- plot_io/layers/pipeline.py +170 -0
- plot_io/layers/positions.py +431 -0
- plot_io/layers/stats.py +1088 -0
- plot_io/layout.py +1381 -0
- plot_io/legend.py +605 -0
- plot_io/levels.py +509 -0
- plot_io/limits.py +124 -0
- plot_io/lines.py +42 -0
- plot_io/mapped.py +522 -0
- plot_io/markers.py +110 -0
- plot_io/markup.py +128 -0
- plot_io/numbers.py +354 -0
- plot_io/occupancy.py +295 -0
- plot_io/plot.py +597 -0
- plot_io/presets/__init__.py +51 -0
- plot_io/presets/_util.py +153 -0
- plot_io/presets/area.py +145 -0
- plot_io/presets/bar.py +261 -0
- plot_io/presets/box.py +400 -0
- plot_io/presets/catpoints.py +177 -0
- plot_io/presets/contour.py +353 -0
- plot_io/presets/curves.py +30 -0
- plot_io/presets/dots.py +355 -0
- plot_io/presets/ecdf.py +97 -0
- plot_io/presets/errorbar.py +106 -0
- plot_io/presets/errors.py +296 -0
- plot_io/presets/fill.py +74 -0
- plot_io/presets/heatmap.py +322 -0
- plot_io/presets/hexbin.py +198 -0
- plot_io/presets/hist.py +391 -0
- plot_io/presets/hist2d.py +87 -0
- plot_io/presets/kde.py +162 -0
- plot_io/presets/kde2d.py +147 -0
- plot_io/presets/line.py +143 -0
- plot_io/presets/pie.py +502 -0
- plot_io/presets/regression.py +115 -0
- plot_io/presets/scatter.py +194 -0
- plot_io/presets/stem.py +100 -0
- plot_io/presets/violin.py +323 -0
- plot_io/py.typed +0 -0
- plot_io/registry.py +82 -0
- plot_io/render.py +480 -0
- plot_io/rows.py +491 -0
- plot_io/scales.py +103 -0
- plot_io/seams.py +138 -0
- plot_io/settings.py +807 -0
- plot_io/shapes.py +66 -0
- plot_io/sharing.py +232 -0
- plot_io/sizemaps.py +97 -0
- plot_io/spec.py +240 -0
- plot_io/states.py +216 -0
- plot_io/styles/__init__.py +307 -0
- plot_io/styles/library.py +397 -0
- plot_io/tables.py +138 -0
- plot_io/text.py +412 -0
- plot_io/ticks.py +400 -0
- plot_io/times.py +287 -0
- plot_io/titles.py +225 -0
- plot_io/transform.py +114 -0
- plot_io/types.py +13604 -0
- plot_io/units.py +768 -0
- plot_io/validators.py +1029 -0
- plot_io/values.py +190 -0
- plot_io-0.12.0.dist-info/METADATA +215 -0
- plot_io-0.12.0.dist-info/RECORD +130 -0
- plot_io-0.12.0.dist-info/WHEEL +4 -0
- plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
"""Intervals and gene models (:class:`Bed`, :class:`Genes`, :class:`Gtf`): boxes or gene
|
|
2
|
+
models packed on rows, with names, strands and colors from the data, each series in a
|
|
3
|
+
lane of its own (:mod:`plot_io.genome.lanes`)."""
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import warnings
|
|
7
|
+
from array import array
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from .._methods import BedMixin, GenesMixin, GtfMixin
|
|
12
|
+
from .. import groups
|
|
13
|
+
from ..color.values import color, nearest_named, rgba_of, variant_of, with_alpha
|
|
14
|
+
from ..labels import NAME_POSITIONS, row as name_row, spot
|
|
15
|
+
from ..settings import Setting, boolean, number, one_of, table
|
|
16
|
+
from ..transform import settings_both as coord_settings
|
|
17
|
+
from ..layers import add_layer
|
|
18
|
+
from ..layers.base import common
|
|
19
|
+
from ..layers.core import Call, Layer, Table
|
|
20
|
+
from ..layers.geoms import GEOMS
|
|
21
|
+
from ..layers.paint import Paint, role
|
|
22
|
+
from ..mapped import color_axis_of, unit_of
|
|
23
|
+
from ..presets._util import memo, placed_text
|
|
24
|
+
from ..spec import warn
|
|
25
|
+
from ..text import document_db, font_of, measure
|
|
26
|
+
from ..validators import length
|
|
27
|
+
from . import readers, sources
|
|
28
|
+
from .lanes import lane_mapper, lane_setting
|
|
29
|
+
from .pack import pack_rows
|
|
30
|
+
from .regions import Region, parse_region
|
|
31
|
+
from .series import GenomeSeries, region_setting
|
|
32
|
+
|
|
33
|
+
__all__ = ["Bed", "Genes", "Gtf"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_BOX = 8 / 13.2 # height of boxes and coding exons, in rows: 8 px of 13.2
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _label_region(v: Any) -> Region | None:
|
|
40
|
+
return None if v is None else parse_region(v)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _bed_color(v: Any) -> Any:
|
|
44
|
+
if isinstance(v, str) and v.strip().lower() in ("item_rgb", "score"):
|
|
45
|
+
return v.strip().lower()
|
|
46
|
+
return color(v)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _recolor(v: Any) -> Any:
|
|
50
|
+
if isinstance(v, bool):
|
|
51
|
+
return v
|
|
52
|
+
return number(0, 1)(v)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _lightness(c: Any) -> float:
|
|
56
|
+
"""Oklab lightness of ``c`` composited over white (1 when there is no color)."""
|
|
57
|
+
rgba = rgba_of(c)
|
|
58
|
+
if rgba is None:
|
|
59
|
+
return 1.0
|
|
60
|
+
a = rgba[3] if len(rgba) > 3 else 1.0
|
|
61
|
+
lin = [(lambda v: v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4)(
|
|
62
|
+
a * float(v) + 1.0 - a) for v in rgba[:3]]
|
|
63
|
+
lms = [sum(k * v for k, v in zip(row, lin)) ** (1 / 3) for row in _LMS]
|
|
64
|
+
return 0.2104542553 * lms[0] + 0.7936177850 * lms[1] - 0.0040720342 * lms[2]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_LMS = ((0.4122214708, 0.5363325363, 0.0514459929), (0.2119034982, 0.6806995451, 0.1073969566),
|
|
68
|
+
(0.0883024619, 0.2817188376, 0.6299787005))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _bp(v: Any) -> float | None:
|
|
72
|
+
"""A position as a float; ``None`` when missing (``None``, ``"."``, ``""``, NaN)."""
|
|
73
|
+
if v is None or (isinstance(v, str) and v.strip() in ("", ".")) \
|
|
74
|
+
or (isinstance(v, float) and math.isnan(v)):
|
|
75
|
+
return None
|
|
76
|
+
return float(int(v))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _rgb(v: Any, i: int) -> Any:
|
|
80
|
+
"""A BED ``itemRgb`` (``"r,g,b"``) of interval ``i``, or ``None`` (``"0"``, missing);
|
|
81
|
+
components outside 0–255 raise."""
|
|
82
|
+
if v is None:
|
|
83
|
+
return None
|
|
84
|
+
if isinstance(v, (tuple, list)) and len(v) == 3:
|
|
85
|
+
parts: Any = list(v)
|
|
86
|
+
else:
|
|
87
|
+
parts = str(v).replace(" ", "").split(",")
|
|
88
|
+
if len(parts) != 3:
|
|
89
|
+
return None
|
|
90
|
+
try:
|
|
91
|
+
rgb = [int(c) for c in parts]
|
|
92
|
+
except ValueError:
|
|
93
|
+
return None
|
|
94
|
+
if not all(0 <= c <= 255 for c in rgb):
|
|
95
|
+
raise ValueError(f"itemRgb of interval {i + 1} is {v!r}: components are 0 to 255")
|
|
96
|
+
return tuple(c / 255 for c in rgb)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
#: Strand chevrons' height, × the feature box's height: inside a box, on an intron.
|
|
101
|
+
_CHEVRON_BOX, _CHEVRON_INTRON = 0.45, 0.55
|
|
102
|
+
|
|
103
|
+
class Bed(BedMixin, GenomeSeries):
|
|
104
|
+
"""Intervals over the region: boxes or gene models, on one row or packed onto
|
|
105
|
+
rows. ``reader`` is an interval reader (bigBed, BED, peaks), a path (opened with
|
|
106
|
+
gwseq_io) or plain data: a dict of
|
|
107
|
+
columns, a DataFrame, or rows (``start``, ``end`` and optionally ``name``, ``score``,
|
|
108
|
+
``strand``, ``thickStart``, ``thickEnd``, ``itemRgb``, ``blockCount``,
|
|
109
|
+
``blockSizes``, ``blockStarts``).
|
|
110
|
+
|
|
111
|
+
Paths and readers: see `readers <docs/api.md#readers>`_. Rows of other chromosomes than the region's are left
|
|
112
|
+
out (when a ``chr`` column is given). A score that is not a number (``"."``) is missing
|
|
113
|
+
(the map's ``bad`` color); an interval ending before it starts, or blocks not matching
|
|
114
|
+
``blockCount``, are a ``ValueError``. ``itemRgb`` is ``reserved`` in bigBeds.
|
|
115
|
+
|
|
116
|
+
``kind="genes"`` draws each BED12 model as a line, thin boxes for UTRs and thick boxes
|
|
117
|
+
for coding exons, chevrons on introns (``strand``), and its name in italics. A name goes where ``name_position`` says: ``auto`` tries
|
|
118
|
+
left of its feature, then right, then inside a feature wide enough; a feature crossing
|
|
119
|
+
the view's edge is named at that edge, over its visible part when the name leaves some
|
|
120
|
+
of it uncovered (on a background box over a dark feature). A name with no room is
|
|
121
|
+
shortened with an ellipsis; one with no room even shortened is left out, and a warning
|
|
122
|
+
counts them. In one row (``rows="collapse"``) longer features keep theirs first
|
|
123
|
+
(``names=False``: none).
|
|
124
|
+
``name_region`` names only the features overlapping it (a locus within a wider view).
|
|
125
|
+
|
|
126
|
+
``rows="expand"`` packs the features with their names; past ``max_rows`` it packs them
|
|
127
|
+
without names and names those that still fit; past ``max_rows`` again, the features of
|
|
128
|
+
later rows are left out and a "+n more" mark is drawn at the right of the last row.
|
|
129
|
+
``rows="squish"`` packs them on rows half ``row_height`` high, without names, under the
|
|
130
|
+
same cap. Packing and drawing read one result: no row is kept for a name that is not
|
|
131
|
+
drawn.
|
|
132
|
+
|
|
133
|
+
Features are outlined in their color (``outline``, ``line_width``) over their ``fill``
|
|
134
|
+
(``False`` for outlines only) at ``fill_opacity``, the outline inside the feature's
|
|
135
|
+
extent: a feature narrower than two borders is one line, ``line_width`` wide.
|
|
136
|
+
``recolor`` snaps the features' colors (``itemRgb`` ones included) to the named colors
|
|
137
|
+
(``"blue 0.4"``): ``True`` takes the closest of all shades, a level such as ``0.4`` the
|
|
138
|
+
closest color's shade at that level, so every feature has the same lightness.
|
|
139
|
+
|
|
140
|
+
Over a Hi-C triangle (``lane="over"``) each interval is an outlined triangle (a TAD);
|
|
141
|
+
over a square, an outlined square on the diagonal (see :class:`HiC`).
|
|
142
|
+
|
|
143
|
+
Example::
|
|
144
|
+
|
|
145
|
+
fig = plt.Tracks(("chr1", 0, 100_000), width=300)
|
|
146
|
+
fig.bed({"start": [5_000, 22_000, 40_000, 47_000, 70_000],
|
|
147
|
+
"end": [15_000, 30_000, 52_000, 60_000, 92_000],
|
|
148
|
+
"name": ["p1", "p2", "p3", "p4", "p5"]}, rows="expand", names=True)
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
SETTINGS = table(
|
|
152
|
+
region=region_setting(),
|
|
153
|
+
lane=lane_setting(),
|
|
154
|
+
kind=Setting(one_of("boxes", "genes"),
|
|
155
|
+
doc="`boxes` (intervals, 8/13.2 of a row high: 8 px in the default "
|
|
156
|
+
"row) or `genes` (BED12 models: exons as high, UTRs half, "
|
|
157
|
+
"introns)."),
|
|
158
|
+
rows=Setting(one_of("collapse", "expand", "squish"),
|
|
159
|
+
doc="`collapse`: one row; `expand`: overlapping features (and their "
|
|
160
|
+
"names) packed onto rows; `squish`: packed onto rows half "
|
|
161
|
+
"`row_height` high, without names."),
|
|
162
|
+
max_rows=Setting(number(lo=1, integer=True),
|
|
163
|
+
doc="Most rows of `expand` and `squish`: names are given up before "
|
|
164
|
+
"rows are added, then features past the last row are left out "
|
|
165
|
+
"and a \"+n more\" mark is drawn at the lane's lower right."),
|
|
166
|
+
min_width=Setting(length(lo=0), doc="Narrowest drawn feature (1-bp sites stay "
|
|
167
|
+
"visible)."),
|
|
168
|
+
color=Setting(_bed_color,
|
|
169
|
+
doc="A color, `\"item_rgb\"` (the `itemRgb` column) or `\"score\"` "
|
|
170
|
+
"(the `score` column through `color_map`)."),
|
|
171
|
+
recolor=Setting(_recolor,
|
|
172
|
+
doc="Replace each feature's color by the closest named color "
|
|
173
|
+
"(`True`: the closest shade; a level 0–1: the closest color's "
|
|
174
|
+
"shade at that level); `False`: as given. Not with "
|
|
175
|
+
"`color=\"score\"`."),
|
|
176
|
+
names=Setting(boolean, doc="Feature names (`name`) beside the features "
|
|
177
|
+
"(`name_position`), shortened or left out (with a "
|
|
178
|
+
"warning) where they collide; "
|
|
179
|
+
"in one row, longer features keep theirs first."),
|
|
180
|
+
name_position=Setting(one_of(*NAME_POSITIONS),
|
|
181
|
+
doc="Where a name goes: `left` or `right` of its feature, or "
|
|
182
|
+
"`inside` a feature wide enough; `auto`: the first of "
|
|
183
|
+
"left, right, inside with room. A feature crossing the "
|
|
184
|
+
"view's edge is named at that edge, over its visible "
|
|
185
|
+
"part when the name leaves some of it uncovered. A "
|
|
186
|
+
"name with no room is shortened with an ellipsis."),
|
|
187
|
+
name_region=Setting(_label_region,
|
|
188
|
+
doc="Only features overlapping this region (as `region` takes "
|
|
189
|
+
"it) get names; `None`: all."),
|
|
190
|
+
strand=Setting(boolean, doc="Strand chevrons on boxes and introns."),
|
|
191
|
+
outline=Setting(boolean, doc="Outline features in their color (`line_width` wide)."),
|
|
192
|
+
**groups.FillStyle.embed(docs={"fill": "Fill of the features: `True` (their "
|
|
193
|
+
"color), a color, or `False` (outlines only).",
|
|
194
|
+
"fill_opacity": "Opacity of the fill, times "
|
|
195
|
+
"`opacity`."}),
|
|
196
|
+
row_height=Setting(length(positive=True, percent="em"),
|
|
197
|
+
doc="Height of a row (half of it with `rows=\"squish\"`): the "
|
|
198
|
+
"series' lane is rows × it high (scaled to fill a track "
|
|
199
|
+
"without a value lane)."),
|
|
200
|
+
**groups.TextStyle.embed("name_",
|
|
201
|
+
only=("font_size",),
|
|
202
|
+
docs={"font_size": "Font size of feature names."}),
|
|
203
|
+
nan_color=Setting(color, doc="Color of features without a score under "
|
|
204
|
+
"`color=\"score\"`; `None`: the map's `bad` "
|
|
205
|
+
"(transparent)."),
|
|
206
|
+
**common("color_map", "name", "opacity", "z", "line_width", "font_size", "font_family",
|
|
207
|
+
"text_color"),
|
|
208
|
+
**coord_settings(),
|
|
209
|
+
)
|
|
210
|
+
READS = "intervals"
|
|
211
|
+
|
|
212
|
+
def __init__(self, reader: Any, plot: Any = None, **settings: Any) -> None:
|
|
213
|
+
self.reader = readers.adapt(reader, self.READS, type(self).__name__, owner=self)
|
|
214
|
+
super().__init__(plot=plot, **settings)
|
|
215
|
+
|
|
216
|
+
# -- data (stat intervals) ---------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
def entries(self) -> dict[str, list[Any]]:
|
|
219
|
+
"""The BED columns of the entries overlapping the region (positions in bp)."""
|
|
220
|
+
region = self.region()
|
|
221
|
+
return self.cached(region, lambda: self._guard(lambda: self._entries(region)),
|
|
222
|
+
kind="entries")
|
|
223
|
+
|
|
224
|
+
def _entries(self, region: Region) -> dict[str, list[Any]]:
|
|
225
|
+
cols = self._read(region)
|
|
226
|
+
if self.resolve("color") == "item_rgb":
|
|
227
|
+
for i, v in enumerate(cols.get("itemRgb") or ()):
|
|
228
|
+
_rgb(v, i) # components out of 0–255 raise
|
|
229
|
+
chrs = cols.get("chr")
|
|
230
|
+
keep = [i for i, (s, e) in enumerate(zip(cols["start"], cols["end"]))
|
|
231
|
+
if e > region.start and s < region.end and (chrs is None or chrs[i] == region.chr)]
|
|
232
|
+
names = getattr(self.reader, "chr_sizes", None) or getattr(self.reader, "chr_names",
|
|
233
|
+
None) or ()
|
|
234
|
+
if not keep and region.chr not in names:
|
|
235
|
+
other = region.chr[3:] if region.chr.startswith("chr") else "chr" + region.chr
|
|
236
|
+
if other in names:
|
|
237
|
+
warnings.warn(f"{self._what()}: no interval on {region.chr!r}; the data name it "
|
|
238
|
+
f"{other!r}", stacklevel=2)
|
|
239
|
+
out = {k: [v[i] for i in keep] for k, v in cols.items()}
|
|
240
|
+
for k in ("start", "end", "thickStart", "thickEnd"):
|
|
241
|
+
if k in out:
|
|
242
|
+
out[k] = [_bp(v) for v in out[k]]
|
|
243
|
+
return out
|
|
244
|
+
|
|
245
|
+
def _read(self, region: Region) -> dict[str, list[Any]]:
|
|
246
|
+
return sources.read_entries(self.reader, region)
|
|
247
|
+
|
|
248
|
+
def _label_font(self) -> Any:
|
|
249
|
+
return font_of(self, self.resolve("name_font_size"),
|
|
250
|
+
style="italic" if self.resolve("kind") == "genes" else "normal",
|
|
251
|
+
color=self.resolve("text_color"))
|
|
252
|
+
|
|
253
|
+
def _label_text(self, i: int, cols: dict[str, list[Any]], keep: int | None = None) -> Any:
|
|
254
|
+
"""The name of entry ``i`` (``None``: none), its first ``keep`` characters and an
|
|
255
|
+
ellipsis when ``keep`` is given."""
|
|
256
|
+
names = cols.get("name")
|
|
257
|
+
if not names or names[i] is None or str(names[i]) in ("", "."):
|
|
258
|
+
return None
|
|
259
|
+
only = self.resolve("name_region")
|
|
260
|
+
if only is not None:
|
|
261
|
+
if only.chr != self.region().chr or cols["end"][i] <= only.start \
|
|
262
|
+
or cols["start"][i] >= only.end:
|
|
263
|
+
return None
|
|
264
|
+
name = str(names[i])
|
|
265
|
+
if keep is not None:
|
|
266
|
+
name = name[:keep].rstrip() + "…"
|
|
267
|
+
return name
|
|
268
|
+
|
|
269
|
+
def placed(self) -> "Placed":
|
|
270
|
+
"""The rows and names of the entries, for the axes width: packing and drawing
|
|
271
|
+
read this one result."""
|
|
272
|
+
return self.cached((self.region(), self.axes_px()), self._place, kind="rows")
|
|
273
|
+
|
|
274
|
+
def layout_rows(self) -> tuple[list[int], int]:
|
|
275
|
+
"""The row of each entry (-1: left out past ``max_rows``) and the number of
|
|
276
|
+
rows."""
|
|
277
|
+
p = self.placed()
|
|
278
|
+
return p.rows, p.count
|
|
279
|
+
|
|
280
|
+
def _place(self) -> "Placed":
|
|
281
|
+
cols = self.entries()
|
|
282
|
+
n = len(cols["start"])
|
|
283
|
+
if not n:
|
|
284
|
+
return Placed([], 1, [], 0, 0)
|
|
285
|
+
region = self.region()
|
|
286
|
+
ppb = self.axes_px() / region.span # px per bp
|
|
287
|
+
lo, hi = region.start * ppb, region.end * ppb # the view, px
|
|
288
|
+
mw = self.resolve("min_width")
|
|
289
|
+
spans = []
|
|
290
|
+
for i in range(n):
|
|
291
|
+
a, b = cols["start"][i] * ppb, cols["end"][i] * ppb # px, widened as drawn
|
|
292
|
+
if b - a < mw:
|
|
293
|
+
a, b = (a + b - mw) / 2, (a + b + mw) / 2
|
|
294
|
+
spans.append((a, b))
|
|
295
|
+
mode = self.resolve("rows")
|
|
296
|
+
widths: list[float | None] = [None] * n
|
|
297
|
+
heights = [0.0] * n
|
|
298
|
+
gap = 0.0
|
|
299
|
+
font = self._label_font()
|
|
300
|
+
fonts = document_db()
|
|
301
|
+
if self.resolve("names") and mode != "squish":
|
|
302
|
+
gap = 0.3 * font.size
|
|
303
|
+
for i in range(n):
|
|
304
|
+
t = self._label_text(i, cols)
|
|
305
|
+
if t is not None:
|
|
306
|
+
w, a, d = measure(fonts, t, font)
|
|
307
|
+
widths[i], heights[i] = w, a + d
|
|
308
|
+
positions = NAME_POSITIONS[self.resolve("name_position")]
|
|
309
|
+
order = None
|
|
310
|
+
more = 0
|
|
311
|
+
if mode == "collapse":
|
|
312
|
+
rows, count = [0] * n, 1
|
|
313
|
+
order = [b - a for a, b in spans] # longer features first
|
|
314
|
+
else:
|
|
315
|
+
cap = int(self.resolve("max_rows"))
|
|
316
|
+
rows = []
|
|
317
|
+
if any(w is not None for w in widths): # features with their names
|
|
318
|
+
foot = [spans[i] if w is None
|
|
319
|
+
else spot(*spans[i], w, lo, hi, gap, positions) or spans[i]
|
|
320
|
+
for i, w in enumerate(widths)]
|
|
321
|
+
rows = pack_rows([f[0] for f in foot], [f[1] for f in foot], gap=2.0)
|
|
322
|
+
if max(rows) + 1 > cap:
|
|
323
|
+
rows = []
|
|
324
|
+
if not rows: # the features alone
|
|
325
|
+
rows = pack_rows([a for a, _ in spans], [b for _, b in spans], gap=2.0)
|
|
326
|
+
count = max(rows) + 1
|
|
327
|
+
if count > cap:
|
|
328
|
+
more = sum(r >= cap for r in rows)
|
|
329
|
+
rows = [r if r < cap else -1 for r in rows]
|
|
330
|
+
count = cap
|
|
331
|
+
names: list[Any] = [None] * n
|
|
332
|
+
if any(w is not None for w in widths):
|
|
333
|
+
keep = [i for i in range(n) if rows[i] >= 0]
|
|
334
|
+
taken = None
|
|
335
|
+
if more: # the mark's room
|
|
336
|
+
w = measure(document_db(), f"+{more} more", self._more_font())[0]
|
|
337
|
+
taken = {count - 1: (hi - w - gap, hi)} # gap: 2 × its pad
|
|
338
|
+
cut = _Cut(self, cols, keep, font)
|
|
339
|
+
got = name_row([spans[i] for i in keep], [widths[i] for i in keep],
|
|
340
|
+
[rows[i] for i in keep], lo, hi, gap, positions,
|
|
341
|
+
None if order is None else [order[i] for i in keep], taken, cut)
|
|
342
|
+
for j, (i, p) in enumerate(zip(keep, got)):
|
|
343
|
+
if p is not None:
|
|
344
|
+
k = cut.kept.get(j)
|
|
345
|
+
drawn = widths[i] if k is None else cut.width(j, k)
|
|
346
|
+
names[i] = (p[0] / ppb, p[1], p[2], drawn, heights[i],
|
|
347
|
+
None if k is None else cut.text(j, k))
|
|
348
|
+
unnamed = sum(1 for i in range(n)
|
|
349
|
+
if rows[i] >= 0 and widths[i] is not None and names[i] is None)
|
|
350
|
+
return Placed(rows, count, names, more, unnamed)
|
|
351
|
+
|
|
352
|
+
def lane_rows(self) -> int:
|
|
353
|
+
"""The number of rows of the series' lane."""
|
|
354
|
+
return self.layout_rows()[1]
|
|
355
|
+
|
|
356
|
+
def drawn_rows(self) -> tuple[float, float]:
|
|
357
|
+
"""The range of rows its features cover (a box's height in its first and last
|
|
358
|
+
row)."""
|
|
359
|
+
return 0.5 - _BOX / 2, self.lane_rows() - 0.5 + _BOX / 2
|
|
360
|
+
|
|
361
|
+
# -- drawing ---------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
def _colors(self, ctx: Any, cols: dict[str, list[Any]]) -> list[Any]:
|
|
364
|
+
c = self.resolve("color")
|
|
365
|
+
n = len(cols["start"])
|
|
366
|
+
black = ctx.color(self.resolve("text_color"))
|
|
367
|
+
if c == "item_rgb":
|
|
368
|
+
rgb = cols.get("itemRgb") or [None] * n
|
|
369
|
+
return self._recolored([_rgb(v, i) or black for i, v in enumerate(rgb)])
|
|
370
|
+
if c == "score":
|
|
371
|
+
axis = color_axis_of(self)
|
|
372
|
+
m, unit = axis.color_map(), unit_of(axis)
|
|
373
|
+
nan = ctx.color(self.resolve("nan_color"))
|
|
374
|
+
bad = nan if nan is not None else m.bad
|
|
375
|
+
out = []
|
|
376
|
+
for v in cols.get("score") or [None] * n:
|
|
377
|
+
if v is None:
|
|
378
|
+
out.append(bad)
|
|
379
|
+
else:
|
|
380
|
+
t = unit(v)
|
|
381
|
+
out.append(m(min(1.0, max(0.0, t))) if math.isfinite(t) else bad)
|
|
382
|
+
return out
|
|
383
|
+
return self._recolored([ctx.color(c)] * n)
|
|
384
|
+
|
|
385
|
+
def _recolored(self, colors: list[Any]) -> list[Any]:
|
|
386
|
+
r = self.resolve("recolor")
|
|
387
|
+
if r is False:
|
|
388
|
+
return colors
|
|
389
|
+
level = None if r is True else float(r)
|
|
390
|
+
variant = variant_of(self)
|
|
391
|
+
return [nearest_named(c, level, variant) for c in colors]
|
|
392
|
+
|
|
393
|
+
def layers(self) -> Call:
|
|
394
|
+
from .contacts import tads, under_hic
|
|
395
|
+
hic = under_hic(self)
|
|
396
|
+
if hic is not None: # over a Hi-C triangle: TADs
|
|
397
|
+
return tads(self, hic)
|
|
398
|
+
call = self.genome_call()
|
|
399
|
+
cols = self.entries()
|
|
400
|
+
n = len(cols["start"])
|
|
401
|
+
key = id(self)
|
|
402
|
+
placed = self.placed()
|
|
403
|
+
rows, spots = placed.rows, placed.names
|
|
404
|
+
if placed.more: # left out past max_rows
|
|
405
|
+
keep = [i for i in range(n) if rows[i] >= 0]
|
|
406
|
+
cols = {k: [v[i] for i in keep] if isinstance(v, list) and len(v) == n else v
|
|
407
|
+
for k, v in cols.items()}
|
|
408
|
+
rows, spots = [rows[i] for i in keep], [spots[i] for i in keep]
|
|
409
|
+
n = len(keep)
|
|
410
|
+
|
|
411
|
+
def colors(ctx: Any) -> list[Any]:
|
|
412
|
+
opacity = self.resolve("opacity")
|
|
413
|
+
opacity = 1.0 if opacity is None else opacity
|
|
414
|
+
return memo(ctx, (key, "colors"), lambda: [with_alpha(c, opacity)
|
|
415
|
+
for c in self._colors(ctx, cols)])
|
|
416
|
+
|
|
417
|
+
if n:
|
|
418
|
+
if self.resolve("kind") == "genes":
|
|
419
|
+
self._genes(call, cols, rows, colors)
|
|
420
|
+
else:
|
|
421
|
+
self._boxes(call, cols, rows, colors)
|
|
422
|
+
if any(p is not None for p in spots):
|
|
423
|
+
call.layers.append(placed_text(self, key, lambda ctx: self._label_items(
|
|
424
|
+
ctx, cols, rows, spots, colors), lambda ctx: self._label_font(),
|
|
425
|
+
overlay=False, background=self._background))
|
|
426
|
+
if placed.unnamed:
|
|
427
|
+
warn(f"{self._what()}: {placed.unnamed} feature name"
|
|
428
|
+
f"{'s' if placed.unnamed > 1 else ''} left out (no room)",
|
|
429
|
+
key=(key, "unnamed"))
|
|
430
|
+
if placed.more:
|
|
431
|
+
call.layers.append(placed_text(self, (key, "more"), lambda ctx: self._more_item(
|
|
432
|
+
ctx, placed), lambda ctx: self._more_font(), overlay=False,
|
|
433
|
+
background=self._background))
|
|
434
|
+
for la in call.layers:
|
|
435
|
+
la.occupy = False
|
|
436
|
+
la.options["bounds"] = False
|
|
437
|
+
la.key = False
|
|
438
|
+
if self.resolve("color") == "score":
|
|
439
|
+
scores = [v for v in self.entries().get("score", []) if v is not None]
|
|
440
|
+
call.color_sources = [(color_axis_of(self), array("d", scores))]
|
|
441
|
+
r = self.region()
|
|
442
|
+
call.extra = {"x": array("d", [r.start, r.end])}
|
|
443
|
+
call.hide = {"y"} # rows, in the series' lane
|
|
444
|
+
call.lane = lane_mapper(self)
|
|
445
|
+
call.exact = {"x"}
|
|
446
|
+
return call
|
|
447
|
+
|
|
448
|
+
def _bar_source(self) -> Any:
|
|
449
|
+
"""``color="score"`` maps the scores through the color axis, as numbers do."""
|
|
450
|
+
return self if self.resolve("color") == "score" else super()._bar_source()
|
|
451
|
+
|
|
452
|
+
def _rect_paint(self, ctx: Any, colors: Any) -> dict[str, Any]:
|
|
453
|
+
"""Features filled (``fill``) and outlined (``outline``) in their colors."""
|
|
454
|
+
cs = colors(ctx)
|
|
455
|
+
fc, fa = groups.fill_parts(self)
|
|
456
|
+
lw = self.resolve("line_width")
|
|
457
|
+
fills = cs if fc is None else [ctx.color(fc)] * len(cs)
|
|
458
|
+
return {"fill": [with_alpha(c, fa) for c in fills] if fa > 0 else None,
|
|
459
|
+
"stroke": cs if self.resolve("outline") and lw else None,
|
|
460
|
+
"line_width": lw}
|
|
461
|
+
|
|
462
|
+
def _span(self, ctx: Any, s: float, e: float) -> tuple[float, float]:
|
|
463
|
+
"""Page x and width of ``[s, e)``, at least ``min_width`` wide."""
|
|
464
|
+
a, b = ctx.xmap(s), ctx.xmap(e)
|
|
465
|
+
a, b = min(a, b), max(a, b)
|
|
466
|
+
mw = self.resolve("min_width")
|
|
467
|
+
if b - a < mw:
|
|
468
|
+
c = (a + b) / 2
|
|
469
|
+
a, b = c - mw / 2, c + mw / 2
|
|
470
|
+
return a, b - a
|
|
471
|
+
|
|
472
|
+
def _band(self, ctx: Any, row: int, frac: float) -> tuple[float, float]:
|
|
473
|
+
"""Page y (top) and height of the middle ``frac`` of row ``row``."""
|
|
474
|
+
top, bot = ctx.ymap(row + 0.5 - frac / 2), ctx.ymap(row + 0.5 + frac / 2)
|
|
475
|
+
return min(top, bot), abs(bot - top)
|
|
476
|
+
|
|
477
|
+
def _boxes(self, call: Call, cols: dict[str, list[Any]], rows: list[int],
|
|
478
|
+
colors: Any) -> None:
|
|
479
|
+
t = Table(x0=cols["start"], x1=cols["end"], y0=[r + 0.5 - _BOX / 2 for r in rows],
|
|
480
|
+
y1=[r + 0.5 + _BOX / 2 for r in rows])
|
|
481
|
+
call.layers.append(Layer(GEOMS["rects"], t,
|
|
482
|
+
paint=Paint(self, "intervals.rects", colors=colors),
|
|
483
|
+
stat="intervals", min_width=self.resolve("min_width"),
|
|
484
|
+
inset=bool(self.resolve("outline"))))
|
|
485
|
+
if self.resolve("strand"):
|
|
486
|
+
def chevrons(ctx: Any) -> list[Any]:
|
|
487
|
+
out: list[Any] = []
|
|
488
|
+
strands = cols.get("strand") or []
|
|
489
|
+
fills = self._rect_paint(ctx, colors)["fill"]
|
|
490
|
+
cs = colors(ctx)
|
|
491
|
+
for i, s in enumerate(strands):
|
|
492
|
+
if s in ("+", "-"):
|
|
493
|
+
x, w = self._span(ctx, cols["start"][i], cols["end"][i])
|
|
494
|
+
y, h = self._band(ctx, rows[i], _BOX)
|
|
495
|
+
# white on a dark fill, else the feature's color
|
|
496
|
+
ink = cs[i] if fills is None or _lightness(fills[i]) > 0.8 else "white"
|
|
497
|
+
self._chevrons(out, x, x + w, y + h / 2, h * _CHEVRON_BOX / 2, s, ink)
|
|
498
|
+
return out
|
|
499
|
+
lines = Table()
|
|
500
|
+
lines.lazy["page_lines"] = chevrons
|
|
501
|
+
call.layers.append(Layer(GEOMS["path"], lines, paint={}, stat="intervals"))
|
|
502
|
+
|
|
503
|
+
def _chevrons(self, out: list[Any], x0: float, x1: float, y: float, size: float,
|
|
504
|
+
strand: str, col: Any) -> None:
|
|
505
|
+
"""Strand marks along ``[x0, x1]`` at ``y`` (page units), added to ``out`` as one
|
|
506
|
+
polyline."""
|
|
507
|
+
step = max(3 * size, 0.9 * self.resolve("font_size"))
|
|
508
|
+
if x1 - x0 < 1.5 * step:
|
|
509
|
+
return
|
|
510
|
+
k = int((x1 - x0) / step)
|
|
511
|
+
pad = ((x1 - x0) - (k - 1) * step) / 2
|
|
512
|
+
d = size if strand == "+" else -size
|
|
513
|
+
xs, ys = array("d"), array("d")
|
|
514
|
+
for j in range(k):
|
|
515
|
+
cx = x0 + pad + j * step
|
|
516
|
+
xs.extend([cx - d / 2, cx + d / 2, cx - d / 2, math.nan])
|
|
517
|
+
ys.extend([y - size, y, y + size, math.nan])
|
|
518
|
+
out.append((xs, ys, col, max(0.5, self.resolve("line_width"))))
|
|
519
|
+
|
|
520
|
+
def _genes(self, call: Call, cols: dict[str, list[Any]], rows: list[int],
|
|
521
|
+
colors: Any) -> None:
|
|
522
|
+
lw = self.resolve("line_width")
|
|
523
|
+
strands = cols.get("strand") or [None] * len(rows)
|
|
524
|
+
counts = cols.get("blockCount")
|
|
525
|
+
sizes = cols.get("blockSizes")
|
|
526
|
+
starts = cols.get("blockStarts")
|
|
527
|
+
t0s = cols.get("thickStart")
|
|
528
|
+
t1s = cols.get("thickEnd")
|
|
529
|
+
x0, x1, y0, y1, owner = [], [], [], [], []
|
|
530
|
+
blocks = []
|
|
531
|
+
for i in range(len(rows)):
|
|
532
|
+
s, e = cols["start"][i], cols["end"][i]
|
|
533
|
+
if counts and sizes and starts and counts[i]:
|
|
534
|
+
bl = [(s + a, s + a + b) for a, b in zip(sources.ints(starts[i]),
|
|
535
|
+
sources.ints(sizes[i]))]
|
|
536
|
+
else:
|
|
537
|
+
bl = [(s, e)]
|
|
538
|
+
blocks.append(bl)
|
|
539
|
+
t0 = t0s[i] if t0s and t0s[i] is not None else s
|
|
540
|
+
t1 = t1s[i] if t1s and t1s[i] is not None else e
|
|
541
|
+
if t1 <= t0: # non-coding: the whole model is UTR-high
|
|
542
|
+
t0, t1 = s, s
|
|
543
|
+
for a, b in bl: # coding exons as tall as boxes, UTRs half
|
|
544
|
+
parts = [(a, min(b, t0), _BOX / 2), (max(a, t0), min(b, t1), _BOX),
|
|
545
|
+
(max(a, t1), b, _BOX / 2)]
|
|
546
|
+
if t0 == t1 == s:
|
|
547
|
+
parts = [(a, b, 0.75 * _BOX)]
|
|
548
|
+
for pa, pb, frac in parts:
|
|
549
|
+
if pb <= pa:
|
|
550
|
+
continue
|
|
551
|
+
x0.append(pa)
|
|
552
|
+
x1.append(pb)
|
|
553
|
+
y0.append(rows[i] + 0.5 - frac / 2)
|
|
554
|
+
y1.append(rows[i] + 0.5 + frac / 2)
|
|
555
|
+
owner.append(i)
|
|
556
|
+
|
|
557
|
+
def models(ctx: Any) -> list[Any]:
|
|
558
|
+
"""The gene lines and their introns' strand marks (page units), per gene."""
|
|
559
|
+
cs = colors(ctx)
|
|
560
|
+
out: list[Any] = []
|
|
561
|
+
for i in range(len(rows)):
|
|
562
|
+
c = cs[i]
|
|
563
|
+
cy = ctx.ymap(rows[i] + 0.5)
|
|
564
|
+
out.append(([ctx.xmap(cols["start"][i]), ctx.xmap(cols["end"][i])], [cy, cy],
|
|
565
|
+
c, lw))
|
|
566
|
+
if self.resolve("strand") and strands[i] in ("+", "-"):
|
|
567
|
+
_, h = self._band(ctx, rows[i], _BOX)
|
|
568
|
+
bl = blocks[i]
|
|
569
|
+
for (_, b0), (a1, _) in zip(bl, bl[1:]):
|
|
570
|
+
self._chevrons(out, ctx.xmap(b0), ctx.xmap(a1), cy, h * _CHEVRON_INTRON / 2,
|
|
571
|
+
strands[i], c)
|
|
572
|
+
return out
|
|
573
|
+
|
|
574
|
+
lines = Table()
|
|
575
|
+
lines.lazy["page_lines"] = models
|
|
576
|
+
call.layers.append(Layer(GEOMS["path"], lines, paint={}, stat="intervals"))
|
|
577
|
+
call.layers.append(Layer(GEOMS["rects"], Table(x0=x0, x1=x1, y0=y0, y1=y1),
|
|
578
|
+
paint=Paint(self, "intervals.rects", colors=colors,
|
|
579
|
+
owner=owner), stat="intervals", min_width=self.resolve("min_width"),
|
|
580
|
+
inset=bool(self.resolve("outline"))))
|
|
581
|
+
|
|
582
|
+
def _label_items(self, ctx: Any, cols: dict[str, list[Any]], rows: list[int],
|
|
583
|
+
spots: list[Any], colors: Any) -> list[Any]:
|
|
584
|
+
"""Feature names where :meth:`placed` put them; a name over a dark feature on a
|
|
585
|
+
background box."""
|
|
586
|
+
font = self._label_font()
|
|
587
|
+
fills = self._rect_paint(ctx, colors)["fill"]
|
|
588
|
+
pad = 0.15 * font.size
|
|
589
|
+
out: list[Any] = []
|
|
590
|
+
for i, p in enumerate(spots):
|
|
591
|
+
if p is None:
|
|
592
|
+
continue
|
|
593
|
+
x, ha, where, w, h, short = p
|
|
594
|
+
x, y = ctx.xmap(x), ctx.ymap(rows[i] + 0.5)
|
|
595
|
+
text_ = self._label_text(i, cols) if short is None else short
|
|
596
|
+
if where in ("inside", "edge") and fills is not None \
|
|
597
|
+
and _lightness(fills[i]) < 0.6:
|
|
598
|
+
x += w / 2 if ha == "left" else -w / 2 if ha == "right" else 0.0
|
|
599
|
+
out.append((text_, x, y, font.color, "center", "center", 0.0,
|
|
600
|
+
(w, h, pad)))
|
|
601
|
+
else:
|
|
602
|
+
out.append((text_, x, y, font.color, ha, "center"))
|
|
603
|
+
return out
|
|
604
|
+
|
|
605
|
+
def _background(self, ctx: Any) -> Any:
|
|
606
|
+
fig = self.plot.figure
|
|
607
|
+
return next(c for c in (ctx.color(self.plot.resolve("background")),
|
|
608
|
+
ctx.color(fig.resolve("background")), "white")
|
|
609
|
+
if c is not None)
|
|
610
|
+
|
|
611
|
+
def _more_font(self) -> Any:
|
|
612
|
+
return font_of(self, self.resolve("name_font_size"), color=self.resolve("text_color"))
|
|
613
|
+
|
|
614
|
+
def _more_item(self, ctx: Any, placed: "Placed") -> list[Any]:
|
|
615
|
+
"""The "+n more" mark at the right of the series' last row, on a background
|
|
616
|
+
box."""
|
|
617
|
+
font = self._more_font()
|
|
618
|
+
text_ = f"+{placed.more} more"
|
|
619
|
+
w, a, d = measure(ctx.canvas, text_, font)
|
|
620
|
+
pad = 0.15 * font.size
|
|
621
|
+
return [(text_, ctx.rect[2] - w / 2 - pad, ctx.ymap(placed.count - 0.5), font.color,
|
|
622
|
+
"center", "center", 0.0, (w, a + d, pad))]
|
|
623
|
+
|
|
624
|
+
def row_px(self) -> float:
|
|
625
|
+
"""The height of one of its rows (px)."""
|
|
626
|
+
h = float(self.resolve("row_height"))
|
|
627
|
+
return h / 2 if self.resolve("rows") == "squish" else h
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
class Placed:
|
|
631
|
+
"""The rows of a series' entries (``rows``: -1 for an entry left out past
|
|
632
|
+
``max_rows``; ``count`` rows), their names (per entry ``(x, ha, where, w, h, short)``:
|
|
633
|
+
the anchor in bp, its alignment, its position (:data:`plot_io.labels.Spot`), its size
|
|
634
|
+
in px and the shortened name drawn (``None``: the whole name); or ``None``), how many
|
|
635
|
+
entries were left out (``more``) and how many names (``unnamed``)."""
|
|
636
|
+
|
|
637
|
+
__slots__ = ("rows", "count", "names", "more", "unnamed")
|
|
638
|
+
|
|
639
|
+
def __init__(self, rows: list[int], count: int, names: list[Any], more: int,
|
|
640
|
+
unnamed: int) -> None:
|
|
641
|
+
self.rows, self.count, self.names, self.more = rows, count, names, more
|
|
642
|
+
self.unnamed = unnamed
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
class _Cut:
|
|
646
|
+
"""Names shortened with an ellipsis (:func:`plot_io.labels.row`'s ``cut``) of the
|
|
647
|
+
entries ``keep`` of ``cols``: ``k`` characters kept, measured once."""
|
|
648
|
+
|
|
649
|
+
def __init__(self, series: Any, cols: dict[str, list[Any]], keep: list[int],
|
|
650
|
+
font: Any) -> None:
|
|
651
|
+
self.series, self.cols, self.keep, self.font = series, cols, keep, font
|
|
652
|
+
self.kept: dict[int, int] = {}
|
|
653
|
+
self._widths: dict[tuple[int, int], float] = {}
|
|
654
|
+
|
|
655
|
+
def steps(self, j: int) -> int:
|
|
656
|
+
names = self.cols.get("name") or ()
|
|
657
|
+
i = self.keep[j]
|
|
658
|
+
return max(0, len(str(names[i])) - 1) if i < len(names) else 0
|
|
659
|
+
|
|
660
|
+
def text(self, j: int, k: int) -> Any:
|
|
661
|
+
return self.series._label_text(self.keep[j], self.cols, k)
|
|
662
|
+
|
|
663
|
+
def width(self, j: int, k: int) -> float:
|
|
664
|
+
if (j, k) not in self._widths:
|
|
665
|
+
self._widths[(j, k)] = measure(document_db(), self.text(j, k), self.font)[0]
|
|
666
|
+
return self._widths[(j, k)]
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
class Genes(GenesMixin, Bed):
|
|
670
|
+
"""Gene models (BED12): ``Bed`` with ``kind="genes"``, rows expanded and names on.
|
|
671
|
+
|
|
672
|
+
Filled solid without outlines; the settings are :class:`Bed`'s, its own defaults below.
|
|
673
|
+
|
|
674
|
+
Example::
|
|
675
|
+
|
|
676
|
+
fig = plt.Tracks(("chr1", 13_550_000, 13_980_000), width=300)
|
|
677
|
+
fig.genes({"start": [13_600_000], "end": [13_930_000], "name": ["Pcsk2"], "strand": ["+"],
|
|
678
|
+
"thickStart": [13_640_000], "thickEnd": [13_920_000], "blockCount": [5],
|
|
679
|
+
"blockSizes": ["3000,2000,2500,2000,9000"],
|
|
680
|
+
"blockStarts": ["0,90000,170000,240000,321000"]})
|
|
681
|
+
"""
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _types(v: Any) -> Any:
|
|
685
|
+
if v is None or isinstance(v, str):
|
|
686
|
+
return v
|
|
687
|
+
if isinstance(v, (list, tuple)) and v and all(isinstance(t, str) for t in v):
|
|
688
|
+
return list(v)
|
|
689
|
+
raise ValueError(f"expected a type name, a list of them or None, got {v!r}")
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
class Gtf(GtfMixin, Genes):
|
|
693
|
+
"""Gene models of a GTF or GFF3 reader (``read_entries(model=)``) or path: ``Genes``, one
|
|
694
|
+
model per gene (its transcripts' exons merged) or per transcript, or plain BED12 data
|
|
695
|
+
as ``Genes`` takes it.
|
|
696
|
+
|
|
697
|
+
A path is a bgzipped, indexed GTF or GFF3 (``"gencode.gtf.gz"``; ``gwseq_io.sort_gtf``
|
|
698
|
+
makes one from any GTF; `readers <docs/api.md#readers>`_). The transcripts overlapping the region come whole,
|
|
699
|
+
with their exons outside it. With ``model="gene"`` each gene is one model: its
|
|
700
|
+
transcripts' exons merged, coding from the first CDS start to the last CDS end (the stop
|
|
701
|
+
codon included), named by ``gene_name``; ``model="transcript"`` draws every transcript,
|
|
702
|
+
named by ``transcript_name``. An id names what has no name. ``gene_type`` and
|
|
703
|
+
``transcript_type`` keep those biotypes. A :class:`Bed` or :class:`Genes` given a GTF
|
|
704
|
+
reader raises ``TypeError`` pointing here.
|
|
705
|
+
|
|
706
|
+
Example::
|
|
707
|
+
|
|
708
|
+
fig = plt.Tracks(("chr1", 0, 60_000), width=300)
|
|
709
|
+
fig.gtf({"start": [4_000, 30_000], "end": [26_000, 55_000], "name": ["Abc1", "Def2"],
|
|
710
|
+
"strand": ["+", "-"], "thickStart": [6_000, 32_000], "thickEnd": [24_000, 52_000],
|
|
711
|
+
"blockCount": [3, 2], "blockSizes": ["3000,4000,4000", "6000,5000"],
|
|
712
|
+
"blockStarts": ["0,8000,18000", "0,20000"]})
|
|
713
|
+
"""
|
|
714
|
+
|
|
715
|
+
SETTINGS = table(
|
|
716
|
+
**Genes.SETTINGS,
|
|
717
|
+
model=Setting(one_of("gene", "transcript"),
|
|
718
|
+
doc="`gene`: one model per gene, its transcripts' exons merged and "
|
|
719
|
+
"coding from the first CDS start to the last CDS end, named by "
|
|
720
|
+
"`gene_name`; `transcript`: one per transcript, named by "
|
|
721
|
+
"`transcript_name` (ids where there is no name)."),
|
|
722
|
+
gene_type=Setting(_types, doc="Gene biotypes kept (`\"protein_coding\"`, a list); "
|
|
723
|
+
"`None`: all."),
|
|
724
|
+
transcript_type=Setting(_types, doc="Transcript biotypes kept; `None`: all."),
|
|
725
|
+
)
|
|
726
|
+
READS = "annotation"
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _read(self, region: Region) -> dict[str, list[Any]]:
|
|
730
|
+
if sources.kind_of(self.reader) != "annotation": # BED12 data
|
|
731
|
+
return sources.read_entries(self.reader, region)
|
|
732
|
+
return sources.read_models(self.reader, region, self.resolve("model"),
|
|
733
|
+
self.resolve("gene_type"), self.resolve("transcript_type"))
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
add_layer("bed", Bed)
|
|
737
|
+
add_layer("genes", Genes)
|
|
738
|
+
add_layer("gtf", Gtf)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
@role("intervals.rects")
|
|
742
|
+
def _rects_paint(s: Any, ctx: Any, glyph: bool, colors: Any = None,
|
|
743
|
+
owner: Any = None) -> dict[str, Any]:
|
|
744
|
+
"""Features (``owner``: exons, each in its feature's colors)."""
|
|
745
|
+
p = s._rect_paint(ctx, colors)
|
|
746
|
+
if owner is not None:
|
|
747
|
+
for k in ("fill", "stroke"):
|
|
748
|
+
if isinstance(p[k], list):
|
|
749
|
+
p[k] = [p[k][i] for i in owner]
|
|
750
|
+
return p
|