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,191 @@
1
+ """Lanes: a plot holding interval series is a vertical stack of lanes in its axes
2
+ rectangle.
3
+
4
+ The *value lane* holds the series with a y scale (signals, ordinary series, the Hi-C
5
+ triangle). Each interval series (a series with a ``lane`` setting; a signal drawn as a
6
+ heat strip) gets a lane of its own, measured in px (its rows × ``row_height``):
7
+ ``lane="below"`` (the default) or ``"above"`` the value lane, each group top to bottom in
8
+ call order, ``lane_gap`` apart; or ``lane="over"``: drawn in the lane of the series called before it (over the value lane, its
9
+ rows span that lane; over an interval lane, the two share rows).
10
+
11
+ The y axis maps only the value lane: :func:`y_span` gives the px its state leaves at each
12
+ end (``AxisState.span``), which its mapper, ticks, grid and spine follow. Interval series
13
+ draw through a y mapper of their lane (``call.lane``, see :func:`lane_mapper`): rows from
14
+ the lane's top, no axis padding. With ``height="auto"`` a track is as high as its lanes
15
+ (:func:`natural_height`); with a fixed height the interval lanes keep their px and the
16
+ value lane takes the rest; without a value lane the lanes are scaled to fill the axes.
17
+ An interval lane's ``name`` is drawn left of the lane, centred on it (:func:`names`).
18
+ """
19
+
20
+ from typing import Any
21
+
22
+ from ..axis import Mapper
23
+ from ..settings import Setting, one_of
24
+
25
+ __all__ = ["lane_setting", "is_lane", "natural_height", "y_span", "value_height",
26
+ "lane_mapper", "names", "LANE_GAP"]
27
+
28
+ #: ``lane_gap`` of a plot without the setting (an ordinary plot), in em.
29
+ LANE_GAP = 0.3
30
+
31
+
32
+ def lane_setting() -> Setting:
33
+ return Setting(one_of("below", "above", "over"),
34
+ doc="Where the series is drawn: in a lane of its own `below` or `above` "
35
+ "the track's value lane (signals, Hi-C; each group top to bottom "
36
+ "in call order), or `over` the lane of the series called before it "
37
+ "(over a signal, its rows span the signal's lane; over intervals, "
38
+ "the two share rows; over a Hi-C triangle, intervals are drawn as "
39
+ "TADs and links as loop marks).")
40
+
41
+
42
+ def is_lane(series: Any) -> bool:
43
+ """Whether a series is drawn in a lane of its own: it has a ``lane`` setting and rows
44
+ (``lane_rows()``, their count; ``drawn_rows()``, the row range its features cover;
45
+ ``row_px()``, the height of a row), and ``in_lane()`` when it has one (a signal is a
46
+ lane as a heat strip only)."""
47
+ if not (hasattr(series, "lane_rows") and "lane" in series.SETTINGS):
48
+ return False
49
+ in_lane = getattr(series, "in_lane", None)
50
+ return in_lane is None or bool(in_lane())
51
+
52
+
53
+ class Lane:
54
+ """A lane: ``where`` (``"above"``, ``"value"`` or ``"below"``), the interval series
55
+ drawn in it, its top and height (px from the axes' top, once placed)."""
56
+
57
+ __slots__ = ("where", "members", "top", "height")
58
+
59
+ def __init__(self, where: str, members: list[Any]) -> None:
60
+ self.where, self.members = where, members
61
+ self.top = self.height = 0.0
62
+
63
+ def rows(self) -> int:
64
+ """The rows the members share."""
65
+ return max((s.lane_rows() for s in self.members), default=1)
66
+
67
+ def px(self) -> float:
68
+ """The lane's own height: its rows × its first member's row height
69
+ (``row_px()``: ``row_height``, half of it for squished rows), or the height a
70
+ member asks (``lane_px()``: links), the larger."""
71
+ rowed = [s for s in self.members if not hasattr(s, "lane_px")]
72
+ own = [s.lane_px() for s in self.members if hasattr(s, "lane_px")]
73
+ if rowed:
74
+ own.append(max(s.lane_rows() for s in rowed) * rowed[0].row_px())
75
+ return max(own)
76
+
77
+
78
+ def _lanes(plot: Any) -> tuple[Lane | None, list[Lane]]:
79
+ """The value lane (``None``: the plot has no series with a y scale) and every lane, top
80
+ to bottom."""
81
+ value = Lane("value", [])
82
+ has_value = any(t.series for t in getattr(plot, "twins", ()))
83
+ above: list[Lane] = []
84
+ below: list[Lane] = []
85
+ prev: Lane | None = None
86
+ for s in plot.series:
87
+ if not is_lane(s):
88
+ has_value = True
89
+ prev = value
90
+ continue
91
+ where = s.resolve("lane")
92
+ if where == "over" and prev is not None:
93
+ prev.members.append(s)
94
+ continue
95
+ lane = Lane("above" if where == "above" else "below", [s])
96
+ (above if where == "above" else below).append(lane)
97
+ prev = lane
98
+ if not has_value:
99
+ return None, above + below
100
+ return value, above + [value] + below
101
+
102
+
103
+ def lane_gap(plot: Any) -> float:
104
+ if "lane_gap" in plot.SETTINGS:
105
+ return float(plot.resolve("lane_gap"))
106
+ return LANE_GAP * float(plot.resolve("font_size"))
107
+
108
+
109
+ def natural_height(plot: Any, value_height: float) -> float:
110
+ """The height of a plot's lanes: the interval lanes', the value lane
111
+ ``value_height`` high (when the plot has one), and the gaps between them."""
112
+ value, lanes = _lanes(plot)
113
+ if not lanes:
114
+ return value_height
115
+ total = sum(value_height if la is value else la.px() for la in lanes)
116
+ return total + lane_gap(plot) * (len(lanes) - 1)
117
+
118
+
119
+ def _placed(plot: Any, h: float) -> tuple[Lane | None, list[Lane]]:
120
+ """The lanes placed in axes ``h`` px high."""
121
+ value, lanes = _lanes(plot)
122
+ if not lanes:
123
+ return value, lanes
124
+ gaps = lane_gap(plot) * (len(lanes) - 1)
125
+ own = [la.px() for la in lanes if la is not value]
126
+ total = sum(own)
127
+ if value is not None:
128
+ rest = h - gaps - total
129
+ k = 1.0 if rest >= 1.0 or not total else max(0.0, h - gaps - 1.0) / total
130
+ rest = max(rest, min(1.0, h))
131
+ else:
132
+ rest = 0.0
133
+ k = max(0.0, h - gaps) / total if total else 1.0
134
+ y = 0.0
135
+ for la in lanes:
136
+ la.top = y
137
+ la.height = rest if la is value else la.px() * k
138
+ y += la.height + lane_gap(plot)
139
+ return value, lanes
140
+
141
+
142
+ def y_span(plot: Any, h: float) -> tuple[float, float]:
143
+ """The px the y axis leaves below and above the value lane of axes ``h`` px high (0 and
144
+ 0: the plot has no interval lane, or no value lane)."""
145
+ value, lanes = _placed(plot, h)
146
+ if value is None or len(lanes) < 2:
147
+ return 0.0, 0.0
148
+ return h - value.top - value.height, value.top
149
+
150
+
151
+ def value_height(plot: Any, h: float) -> float:
152
+ """The height of the value lane of axes ``h`` px high (``h``: no interval lane)."""
153
+ value, lanes = _placed(plot, h)
154
+ return h if value is None or len(lanes) < 2 else value.height
155
+
156
+
157
+ def _lane_of(plot: Any, series: Any, h: float) -> Lane | None:
158
+ _, lanes = _placed(plot, h)
159
+ return next((la for la in lanes if any(s is series for s in la.members)), None)
160
+
161
+
162
+ def lane_mapper(series: Any) -> Any:
163
+ """``call.lane`` of an interval series: the y mapper of its rows in its lane, from the
164
+ plot's drawing context (row 0 at the lane's top)."""
165
+ def mapper(ctx: Any) -> Mapper:
166
+ x0, y0, x1, y1 = ctx.rect
167
+ lane = _lane_of(series.plot, series, y1 - y0)
168
+ if lane is None:
169
+ top, height, lo, hi = y0, y1 - y0, 0.0, float(series.lane_rows())
170
+ elif lane.where == "value": # the features span the value lane
171
+ top, height = y0 + lane.top, lane.height
172
+ lo, hi = series.drawn_rows()
173
+ else:
174
+ top, height, lo, hi = y0 + lane.top, lane.height, 0.0, float(lane.rows())
175
+ return Mapper(None, lo, hi, top, top + height)
176
+ return mapper
177
+
178
+
179
+ def names(plot: Any, h: float) -> list[tuple[Any, float, float]]:
180
+ """The names of the interval lanes of axes ``h`` px high: ``(name, top, bottom)``, px
181
+ from the axes' top (a lane's name: its first member's ``name``)."""
182
+ _, lanes = _placed(plot, h)
183
+ out = []
184
+ for la in lanes:
185
+ if la.where == "value":
186
+ continue
187
+ name = next((s.resolve("name") for s in la.members if s.resolve("name") is not None),
188
+ None)
189
+ if name is not None and name != "":
190
+ out.append((name, la.top, la.top + la.height))
191
+ return out
@@ -0,0 +1,184 @@
1
+ """Genome tracks: a tracks figure (``plt.Tracks(region)``) whose rows are :class:`Track`
2
+ plots over a genome x axis.
3
+
4
+ ``plt.Tracks(region, **settings)`` is a :class:`~plot_io.Figure` with ``layout="tracks"``:
5
+ one column of plots sharing x, each as high as its ``height``; its x axes take the genome
6
+ scale over ``region`` (``figure.x_axis``). A track is a plot preset: a height from its
7
+ content, space above it, the y axis with at most 4 ticks and its label left of the track
8
+ (``label_position="left"``; a ``band`` label longer than the track is drawn as ``left``),
9
+ axis lines left and bottom (Hi-C: bottom only). Its interval series are in lanes of their
10
+ own beside the value lane (:mod:`plot_io.genome.lanes`). Plot types called on the figure
11
+ (``fig.bigwig(…)``) add a track holding them; called on a track, they overlay in it.
12
+ """
13
+
14
+ from typing import Any
15
+
16
+ from .._methods import TrackMixin
17
+ from ..figure import Figure
18
+ from ..plot import Plot
19
+ from ..settings import Setting, either, literal, table
20
+ from ..styles import register_class
21
+ from ..validators import length
22
+ from .contacts import HiC
23
+ from .lanes import is_lane, natural_height
24
+ from .series import GenomeSeries, axes_width
25
+ from .signal import fill_mode
26
+
27
+ __all__ = ["Track", "Tracks"]
28
+
29
+ #: Keywords of :func:`Tracks` that go to the genome x axis.
30
+ AXIS_KEYS = ("unit", "unit_in", "tick_format", "origin", "region_widths", "region_gap",
31
+ "chr_sizes", "scale_bar")
32
+
33
+
34
+ def _hangs(track: "Track") -> bool:
35
+ """A Hi-C triangle hangs from the top (no x axis under it)."""
36
+ return any(isinstance(s, HiC) and s.resolve("direction") == "down" for s in track.series)
37
+
38
+
39
+ def _spines(track: "Track") -> Any:
40
+ if any(isinstance(s, HiC) and not s.square() for s in track.series):
41
+ return "none" if _hangs(track) else ["bottom"]
42
+ return ["left", "bottom"]
43
+
44
+
45
+ def check_height(v: Any) -> Any:
46
+ """A track height: ``"auto"``, a length, or a share ``"2fr"`` (kept as ``("fr", 2.0)``:
47
+ the figure turns it into a row ratio)."""
48
+ if isinstance(v, tuple) and len(v) == 2 and v[0] == "fr":
49
+ return v
50
+ if isinstance(v, str) and v.strip().lower().endswith("fr"):
51
+ try:
52
+ n = float(v.strip()[:-2] or 1.0)
53
+ except ValueError:
54
+ raise ValueError(f"expected a share such as '1fr', got {v!r}") from None
55
+ if not n > 0:
56
+ raise ValueError(f"expected a positive share, got {v!r}")
57
+ return ("fr", n)
58
+ return either(literal("auto"), length(positive=True))(v)
59
+
60
+
61
+ def _auto_space(track: "Track") -> float:
62
+ """``space_above="auto"``: none above the figure's first plot, else
63
+ ``auto_space_above``."""
64
+ plots = track.figure.plots
65
+ if plots and plots[0] is track:
66
+ return 0.0
67
+ return float(track.resolve("auto_space_above"))
68
+
69
+
70
+ def _auto_height(track: "Track") -> float:
71
+ """``height="auto"``: the track's lanes, its value lane as high as the largest height
72
+ a genome series asks (Hi-C from its depth), else ``auto_height``."""
73
+ width = axes_width(track)
74
+ asked = [a for a in (s.track_height(width) for s in track.series
75
+ if isinstance(s, GenomeSeries) and not is_lane(s)) if a is not None]
76
+ value = max(asked) if asked else float(track.resolve("auto_height"))
77
+ return natural_height(track, value)
78
+
79
+
80
+ class Track(TrackMixin, Plot):
81
+ """A row of a tracks figure (:func:`Tracks`): a plot with a height and space above it.
82
+ Add series with its methods (``bigwig``, ``bed``, ``genes``, ``gtf``, ``hic``, or any
83
+ plot type).
84
+
85
+ A track has axis lines left and bottom (Hi-C: bottom only), at most 4 y ticks and its y
86
+ ``label`` left of it. A plot holding interval series is a stack of lanes: the value
87
+ lane (signals, ordinary series, Hi-C) and one lane per interval series (``bed``,
88
+ ``genes``, ``gtf``: rows × ``row_height``; ``links``: its arcs), ``lane="below"``
89
+ (default) or ``"above"`` it, ``lane_gap`` apart; ``lane="over"`` draws a series in the
90
+ lane of the one before it (over Hi-C: intervals as domains, links as loops). An
91
+ interval lane's ``name`` is written left of it. The y axis maps the value lane only;
92
+ interval rows are px. ``height``: a length, a share (``"1fr"``) or ``"auto"`` (the Hi-C
93
+ triangle's, capped by ``max_height``, or ``auto_height`` for signals, plus the lanes).
94
+ ``bins`` of ``bigwig`` and ``hic``: a width on the axes (``"0.5px"``: two per px), a size
95
+ (``"10kb"``, read on the genome-wide grid) or a count. ``share_y=other_track``, or the
96
+ figure's ``share_y="bigwig"`` (a plot type's name), puts tracks on one y scale.
97
+ """
98
+
99
+ SETTINGS = table(**{**Plot.SETTINGS, **dict(
100
+ height=Setting(check_height, auto=_auto_height,
101
+ doc="Height of the track's axes: a length, a share of the room the "
102
+ "fixed heights leave (`\"1fr\"`, with the figure's height given), "
103
+ "or `auto`: the lanes' heights, intervals rows × `row_height`, the "
104
+ "value lane from Hi-C's `depth` (square cells), else "
105
+ "`auto_height`."),
106
+ auto_height=Setting(length(positive=True, percent="em"),
107
+ doc="Height of the value lane (signals) and of empty tracks with "
108
+ "`height=\"auto\"`."),
109
+ space_above=Setting(either(literal("auto"), length(lo=0, percent="em")),
110
+ auto=_auto_space,
111
+ doc="Space above the track; `auto`: none for the first track, "
112
+ "else `auto_space_above`."),
113
+ auto_space_above=Setting(length(lo=0, percent="em"),
114
+ doc="The space `space_above=\"auto\"` leaves."),
115
+ lane_gap=Setting(length(lo=0, percent="em"),
116
+ doc="Space between the track's lanes (the value lane and each "
117
+ "interval series' lane)."),
118
+ fill_mode=Setting(fill_mode,
119
+ doc="Fill between the track's signals (replacing their `fill_to`): "
120
+ "`\"in_between(up)\"` (or `\"in_between\"`), each band between "
121
+ "two neighbouring signals in the fill color of the one above; "
122
+ "`(down)`, of the one below; `(max)`, one band from the lowest "
123
+ "signal to the highest in the color of the top one; `(min)`, of "
124
+ "the bottom one. `None`: each signal's `fill_to`."),
125
+ )})
126
+ RULES = {"spines": _spines}
127
+ #: a ``band`` label longer than the track is drawn as ``left`` (:mod:`plot_io.rows`)
128
+ BAND_FITS = True
129
+
130
+ def __init__(self, figure: Any = None, id: Any = None, **settings: Any) -> None:
131
+ own = {k: settings.pop(k) for k in list(settings)
132
+ if k in Track.SETTINGS and k not in Plot.SETTINGS}
133
+ y_axis = settings.pop("y_axis", None)
134
+ if figure is None:
135
+ figure = Tracks()
136
+ super().__init__(figure=figure, id=id, **settings)
137
+ self.y_axis.set(max_ticks=4, label_position="left", label_pad="0.5em")
138
+ self.set(**own)
139
+ if y_axis is not None:
140
+ self._set_axes({"y_axis": y_axis})
141
+
142
+ def __repr__(self) -> str:
143
+ label = self.y_axis.get("label")
144
+ return f"Track({label!r})" if label else f"Track({self.id!r})"
145
+
146
+
147
+ def Tracks(region: Any = None, **settings: Any) -> Figure:
148
+ """A figure of genome tracks over ``region`` (``"chr2:143,120,001-143,780,000"``,
149
+ ``(chr, start, end)`` or a mapping), or over a list of regions drawn side by side in
150
+ each track (segments of one genome axis: :mod:`plot_io.genome.scale`); a chromosome
151
+ name (``"chr1"``) is the whole chromosome: ``layout="tracks"``, its x axes on the
152
+ genome scale over the region (``unit``, ``unit_in``, ``tick_format``, ``origin``,
153
+ ``region_widths``, ``region_gap``, ``chr_sizes`` and ``scale_bar`` go to
154
+ ``figure.x_axis`` too); the other
155
+ settings are the figure's.
156
+
157
+ A region is a string, 1-based and inclusive (``"chr2:143,120,001-143,780,000"``;
158
+ positions may carry a unit: ``"chr1:1.2M-1.5M"``), a ``(chr, start, end)`` tuple
159
+ (0-based, half-open) or a mapping with ``chr``, ``start``, ``end``; it has no strand. A
160
+ chromosome name takes its size from the x axis' ``chr_sizes``, else the readers'. Data
161
+ are read when the figure is drawn, for the region drawn: ``save``, ``write``, ``draw`` and
162
+ ``size`` take ``region=``.
163
+
164
+ The x axis: ticks under every track, tick labels and the label (``chr2 (Mb)``) under the
165
+ bottom one (``label_outer=False``: every track). ``unit`` (``auto``, ``bp``, ``kb``,
166
+ ``Mb``) and ``unit_in`` (``"label"``, or ``"ticks"``: ``-500kb 0 500kb``) set the labels;
167
+ ``tick_format="chr:pos"`` writes positions; in a ``label`` string ``{chr}``, ``{unit}``,
168
+ ``{start}``, ``{end}`` are filled in. ``origin`` (a position, ``"center"``, ``"start"``,
169
+ ``"end"``) is drawn at 0: limits, ticks and decorations are distances from it.
170
+ ``scale_bar=True`` (or a length) draws a scale bar in place of ticks. Regions side by
171
+ side (a list) are segments of one x axis, ``region_widths`` (``"span"`` or ``"equal"``)
172
+ wide and ``region_gap`` apart, each with its ticks and label; each series reads each
173
+ region, and a track keeps one y scale across them.
174
+ """
175
+ axis = {k: settings.pop(k) for k in AXIS_KEYS if k in settings}
176
+ fig = Figure(layout="tracks", **settings)
177
+ if region is not None:
178
+ axis["region"] = region
179
+ if axis:
180
+ fig.x_axis.set(scale="genome", **axis)
181
+ return fig
182
+
183
+
184
+ register_class(Track)