plot-io 0.12.0__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plot_io/__init__.py +73 -0
- plot_io/_methods.py +1691 -0
- plot_io/_native/__init__.py +199 -0
- plot_io/_native/__main__.py +197 -0
- plot_io/_native/_api.py +457 -0
- plot_io/_native/bin/windows-x86_64.dll +0 -0
- plot_io/_native/kernels.py +335 -0
- plot_io/_native/manifest.json +49 -0
- plot_io/axis.py +1347 -0
- plot_io/boxes.py +1156 -0
- plot_io/canvas.py +389 -0
- plot_io/color/__init__.py +3 -0
- plot_io/color/data.py +1646 -0
- plot_io/color/maps.py +922 -0
- plot_io/color/values.py +295 -0
- plot_io/color/variants.py +1246 -0
- plot_io/colorbar.py +1426 -0
- plot_io/columns.py +614 -0
- plot_io/decorations.py +920 -0
- plot_io/defaults.py +809 -0
- plot_io/density.py +113 -0
- plot_io/ext.py +68 -0
- plot_io/facets.py +240 -0
- plot_io/figure.py +788 -0
- plot_io/formatters.py +97 -0
- plot_io/genome/__init__.py +17 -0
- plot_io/genome/adapters.py +356 -0
- plot_io/genome/combine.py +123 -0
- plot_io/genome/contacts.py +508 -0
- plot_io/genome/intervals.py +750 -0
- plot_io/genome/lanes.py +191 -0
- plot_io/genome/layout.py +184 -0
- plot_io/genome/links.py +364 -0
- plot_io/genome/pack.py +27 -0
- plot_io/genome/readers.py +145 -0
- plot_io/genome/regions.py +228 -0
- plot_io/genome/scale.py +349 -0
- plot_io/genome/series.py +344 -0
- plot_io/genome/signal.py +621 -0
- plot_io/genome/sites.py +782 -0
- plot_io/genome/sources.py +459 -0
- plot_io/genome/testing.py +161 -0
- plot_io/geometry.py +289 -0
- plot_io/groups.py +308 -0
- plot_io/guides.py +503 -0
- plot_io/helpers/__init__.py +17 -0
- plot_io/helpers/clustermap.py +202 -0
- plot_io/helpers/dendrogram.py +172 -0
- plot_io/helpers/grids.py +304 -0
- plot_io/helpers/qq.py +334 -0
- plot_io/helpers/raincloud.py +249 -0
- plot_io/helpers/tree.py +214 -0
- plot_io/helpers/volcano.py +505 -0
- plot_io/hints.py +125 -0
- plot_io/kernels.py +740 -0
- plot_io/labels.py +566 -0
- plot_io/layers/__init__.py +100 -0
- plot_io/layers/base.py +630 -0
- plot_io/layers/context.py +87 -0
- plot_io/layers/core.py +446 -0
- plot_io/layers/geoms.py +1362 -0
- plot_io/layers/grammar.py +586 -0
- plot_io/layers/paint.py +614 -0
- plot_io/layers/pipeline.py +170 -0
- plot_io/layers/positions.py +431 -0
- plot_io/layers/stats.py +1088 -0
- plot_io/layout.py +1381 -0
- plot_io/legend.py +605 -0
- plot_io/levels.py +509 -0
- plot_io/limits.py +124 -0
- plot_io/lines.py +42 -0
- plot_io/mapped.py +522 -0
- plot_io/markers.py +110 -0
- plot_io/markup.py +128 -0
- plot_io/numbers.py +354 -0
- plot_io/occupancy.py +295 -0
- plot_io/plot.py +597 -0
- plot_io/presets/__init__.py +51 -0
- plot_io/presets/_util.py +153 -0
- plot_io/presets/area.py +145 -0
- plot_io/presets/bar.py +261 -0
- plot_io/presets/box.py +400 -0
- plot_io/presets/catpoints.py +177 -0
- plot_io/presets/contour.py +353 -0
- plot_io/presets/curves.py +30 -0
- plot_io/presets/dots.py +355 -0
- plot_io/presets/ecdf.py +97 -0
- plot_io/presets/errorbar.py +106 -0
- plot_io/presets/errors.py +296 -0
- plot_io/presets/fill.py +74 -0
- plot_io/presets/heatmap.py +322 -0
- plot_io/presets/hexbin.py +198 -0
- plot_io/presets/hist.py +391 -0
- plot_io/presets/hist2d.py +87 -0
- plot_io/presets/kde.py +162 -0
- plot_io/presets/kde2d.py +147 -0
- plot_io/presets/line.py +143 -0
- plot_io/presets/pie.py +502 -0
- plot_io/presets/regression.py +115 -0
- plot_io/presets/scatter.py +194 -0
- plot_io/presets/stem.py +100 -0
- plot_io/presets/violin.py +323 -0
- plot_io/py.typed +0 -0
- plot_io/registry.py +82 -0
- plot_io/render.py +480 -0
- plot_io/rows.py +491 -0
- plot_io/scales.py +103 -0
- plot_io/seams.py +138 -0
- plot_io/settings.py +807 -0
- plot_io/shapes.py +66 -0
- plot_io/sharing.py +232 -0
- plot_io/sizemaps.py +97 -0
- plot_io/spec.py +240 -0
- plot_io/states.py +216 -0
- plot_io/styles/__init__.py +307 -0
- plot_io/styles/library.py +397 -0
- plot_io/tables.py +138 -0
- plot_io/text.py +412 -0
- plot_io/ticks.py +400 -0
- plot_io/times.py +287 -0
- plot_io/titles.py +225 -0
- plot_io/transform.py +114 -0
- plot_io/types.py +13604 -0
- plot_io/units.py +768 -0
- plot_io/validators.py +1029 -0
- plot_io/values.py +190 -0
- plot_io-0.12.0.dist-info/METADATA +215 -0
- plot_io-0.12.0.dist-info/RECORD +130 -0
- plot_io-0.12.0.dist-info/WHEEL +4 -0
- plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
plot_io/titles.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Titles: the settings plot titles (:class:`~plot_io.plot.Title`) and figure titles
|
|
2
|
+
(:class:`~plot_io.figure.FigureTitle`) share, and the measure of a title's text block.
|
|
3
|
+
|
|
4
|
+
A title sits on a ``side`` of what it covers (``top``, ``bottom``, ``left``, ``right``),
|
|
5
|
+
aligned along that side (``align``: ``start``, ``center``, ``end``), rotated by
|
|
6
|
+
``rotation`` (``auto``: 0 on top and bottom, 90 on the left, −90 on the right), ``pad``
|
|
7
|
+
from it, with an optional ``rule`` along its span. The titles of one side stack from what
|
|
8
|
+
they cover outward (``order``).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import itertools
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from . import groups
|
|
15
|
+
from .color.values import color
|
|
16
|
+
from .settings import Setting, Settings, boolean, either, number, one_of, table, text
|
|
17
|
+
from .validators import length, real, typed
|
|
18
|
+
|
|
19
|
+
__all__ = ["TitleBase", "SIDES", "ALIGNS", "align_of", "rotation_of", "along", "rule_extent",
|
|
20
|
+
"rule_rect", "Block", "block", "CREATED", "document_title"]
|
|
21
|
+
|
|
22
|
+
SIDES = ("top", "bottom", "left", "right")
|
|
23
|
+
#: ``align`` of titles, figure legends and colorbars: ``start``, ``center``, ``end``, or an
|
|
24
|
+
#: end by name (``left`` and ``right`` along the top and bottom, ``top`` and ``bottom``
|
|
25
|
+
#: along the left and right).
|
|
26
|
+
ALIGNS = ("start", "center", "end", "left", "right", "top", "bottom")
|
|
27
|
+
_ENDS = {"left": "start", "top": "start", "right": "end", "bottom": "end"}
|
|
28
|
+
#: Creation numbers of figure titles and legends: their creation order in the lanes.
|
|
29
|
+
CREATED = itertools.count()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@typed("Literal['auto'] | SupportsFloat")
|
|
33
|
+
def _rotation(v: Any) -> Any:
|
|
34
|
+
"""``"auto"``, or 0, 90 or −90 degrees."""
|
|
35
|
+
if isinstance(v, str) and v.strip().lower() == "auto":
|
|
36
|
+
return "auto"
|
|
37
|
+
x = real(v)
|
|
38
|
+
if x is not None and x in (0, 90, -90):
|
|
39
|
+
return int(x)
|
|
40
|
+
raise ValueError(f"expected 'auto', 0, 90 or -90, got {v!r}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@typed("bool | Literal['inside', 'outside']")
|
|
44
|
+
def _rule(v: Any) -> Any:
|
|
45
|
+
"""``False``, ``True`` (between the title and what it covers, as ``"inside"``) or
|
|
46
|
+
``"outside"`` (beyond the title)."""
|
|
47
|
+
return either(boolean, one_of("inside", "outside"))(v)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_UNSET = object()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TitleBase(Settings):
|
|
54
|
+
"""Settings of every title: its text, side, alignment, rotation, place in its side's
|
|
55
|
+
stack, rule, and text style."""
|
|
56
|
+
|
|
57
|
+
SETTINGS = table(
|
|
58
|
+
text=Setting(text, doc="Title text: a string or rich content; `\\n` starts a line."),
|
|
59
|
+
side=Setting(one_of(*SIDES), doc="`top`, `bottom`, `left` or `right`."),
|
|
60
|
+
align=Setting(one_of(*ALIGNS),
|
|
61
|
+
doc="Alignment along the side: `start`, `center`, `end`, or an end by "
|
|
62
|
+
"name: `left`, `right` on top and bottom, `top`, `bottom` on the "
|
|
63
|
+
"left and right; there `start` is where rotated text begins (the "
|
|
64
|
+
"bottom at 90, the top at −90 and 0)."),
|
|
65
|
+
over=Setting(one_of("axes", "plot"),
|
|
66
|
+
doc="Span the title is aligned within: the axes, or the axes and the "
|
|
67
|
+
"axis labels beside them (the y axis' on top and bottom, the x "
|
|
68
|
+
"axis' on the left and right)."),
|
|
69
|
+
rotation=Setting(_rotation,
|
|
70
|
+
doc="`auto`: 0 on top and bottom, 90 on the left (reads upward), "
|
|
71
|
+
"−90 on the right (reads downward); or 0, 90, −90."),
|
|
72
|
+
pad=Setting(length(lo=0, percent="axes_height"),
|
|
73
|
+
doc="Gap between the title and what it covers."),
|
|
74
|
+
order=Setting(number(), doc="Place in its side's stack: lower is nearer; unset: "
|
|
75
|
+
"the main title nearest, then the others in creation "
|
|
76
|
+
"order."),
|
|
77
|
+
rule=Setting(_rule, doc="A line along the span: `True` or `\"inside\"` between the "
|
|
78
|
+
"title and what it covers, `\"outside\"` beyond the title, "
|
|
79
|
+
"`False` none."),
|
|
80
|
+
rule_color=Setting(color, doc="Color of the rule."),
|
|
81
|
+
rule_width=Setting(length(lo=0), doc="Thickness of the rule."),
|
|
82
|
+
rule_gap=Setting(length(lo=0, percent="em"), doc="Gap between the rule and the title."),
|
|
83
|
+
**groups.TextStyle.embed(docs={"font_size": "Font size; default the base font size."}),
|
|
84
|
+
**groups.Font.embed(only=groups.Font.FACE),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def __call__(self, text: Any = _UNSET, **settings: Any) -> Any:
|
|
88
|
+
"""Set the title (``None`` removes it) and its settings."""
|
|
89
|
+
if text is not _UNSET:
|
|
90
|
+
settings["text"] = text
|
|
91
|
+
return self.set(**settings)
|
|
92
|
+
|
|
93
|
+
def __repr__(self) -> str:
|
|
94
|
+
return f"{type(self).__name__}({self.get('text')!r})"
|
|
95
|
+
|
|
96
|
+
def rule_place(self) -> str | None:
|
|
97
|
+
"""``"inside"``, ``"outside"`` or ``None`` (no rule, or one of zero width)."""
|
|
98
|
+
r = self.resolve("rule")
|
|
99
|
+
if r is False or r is None or not self.resolve("rule_width"):
|
|
100
|
+
return None
|
|
101
|
+
return "outside" if r == "outside" else "inside"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def rotation_of(title: Any) -> int:
|
|
105
|
+
"""The rotation of ``title`` in degrees (``auto`` resolved by its side)."""
|
|
106
|
+
r = title.resolve("rotation")
|
|
107
|
+
if r != "auto":
|
|
108
|
+
return int(r)
|
|
109
|
+
return {"left": 90, "right": -90}.get(title.resolve("side"), 0)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def align_of(value: str) -> str:
|
|
113
|
+
"""An ``align`` value as ``start``, ``center`` or ``end`` along a side's coordinate
|
|
114
|
+
(``left`` and ``top``: ``start``)."""
|
|
115
|
+
return _ENDS.get(value, value)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def along(title: Any) -> str:
|
|
119
|
+
"""A title's ``align`` as ``start``, ``center`` or ``end`` from where its text begins:
|
|
120
|
+
``top`` and ``bottom`` beside text reading upward (90 on the left or right) are its end
|
|
121
|
+
and start."""
|
|
122
|
+
a = title.resolve("align")
|
|
123
|
+
if a in ("top", "bottom") and title.resolve("side") in ("left", "right") \
|
|
124
|
+
and rotation_of(title) == 90:
|
|
125
|
+
return "end" if a == "top" else "start"
|
|
126
|
+
return align_of(a)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def rule_extent(title: Any) -> float:
|
|
130
|
+
"""What a title's rule adds across its side: its width and its gap (0 without one)."""
|
|
131
|
+
if title.rule_place() is None:
|
|
132
|
+
return 0.0
|
|
133
|
+
return title.resolve("rule_width") + title.resolve("rule_gap")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def rule_inside(title: Any) -> float:
|
|
137
|
+
"""What an ``"inside"`` rule puts between a title and what it covers (else 0)."""
|
|
138
|
+
return rule_extent(title) if title.rule_place() == "inside" else 0.0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def rule_rect(title: Any, side: str, near: float, across: float, s0: float, s1: float
|
|
142
|
+
) -> tuple[float, float, float, float] | None:
|
|
143
|
+
"""The rectangle of a title's rule (``None`` without one): along its span ``s0``–``s1``,
|
|
144
|
+
for text whose edge facing what it covers is at ``near`` and ``across`` thick across
|
|
145
|
+
the side; ``inside`` between that edge and what it covers, ``outside`` beyond the
|
|
146
|
+
text."""
|
|
147
|
+
place = title.rule_place()
|
|
148
|
+
if place is None:
|
|
149
|
+
return None
|
|
150
|
+
width, gap = title.resolve("rule_width"), title.resolve("rule_gap")
|
|
151
|
+
sign = -1.0 if side in ("top", "left") else 1.0 # away from what it covers
|
|
152
|
+
start = near - sign * (width + gap) if place == "inside" else near + sign * (across + gap)
|
|
153
|
+
end = start + sign * width
|
|
154
|
+
lo, hi = min(start, end), max(start, end)
|
|
155
|
+
if side in ("top", "bottom"):
|
|
156
|
+
return (s0, lo, s1 - s0, hi - lo)
|
|
157
|
+
return (lo, s0, hi - lo, s1 - s0)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class Block:
|
|
161
|
+
"""A title's text measured: its ``lines``, each line's width, ascent and descent, the
|
|
162
|
+
distance between baselines (``pitch``), and the block's ``width`` and ``height`` in the
|
|
163
|
+
text's own frame (first line's ascent, the pitches, last line's descent)."""
|
|
164
|
+
|
|
165
|
+
__slots__ = ("lines", "widths", "ascents", "descents", "pitch", "width", "height")
|
|
166
|
+
|
|
167
|
+
def __init__(self, lines: list[Any], metrics: list[tuple[float, float, float]],
|
|
168
|
+
pitch: float) -> None:
|
|
169
|
+
self.lines = lines
|
|
170
|
+
self.widths = [m[0] for m in metrics]
|
|
171
|
+
self.ascents = [m[1] for m in metrics]
|
|
172
|
+
self.descents = [m[2] for m in metrics]
|
|
173
|
+
self.pitch = pitch
|
|
174
|
+
self.width = max(self.widths)
|
|
175
|
+
self.height = self.ascents[0] + (len(lines) - 1) * pitch + self.descents[-1]
|
|
176
|
+
|
|
177
|
+
def top(self, k: int) -> float:
|
|
178
|
+
"""Distance from the block's top to the top of line ``k`` (its frame)."""
|
|
179
|
+
return self.ascents[0] + k * self.pitch - self.ascents[k]
|
|
180
|
+
|
|
181
|
+
def bottom(self, k: int) -> float:
|
|
182
|
+
"""Distance from the bottom of line ``k`` to the block's bottom (its frame)."""
|
|
183
|
+
n = len(self.lines) - 1
|
|
184
|
+
return self.descents[-1] + (n - k) * self.pitch - self.descents[k]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def block(fonts: Any, content: Any, font: Any) -> Block:
|
|
188
|
+
"""The text block of a title's content (split at ``\\n``, its markup read) in ``font``."""
|
|
189
|
+
from .rows import lines
|
|
190
|
+
from .text import line_pitch, measure
|
|
191
|
+
ls = lines(content, font)
|
|
192
|
+
if len(ls) == 1:
|
|
193
|
+
ls = [content] # one line: as given (its markup read when shaped)
|
|
194
|
+
return Block(ls, [measure(fonts, ln, font) for ln in ls],
|
|
195
|
+
line_pitch(fonts, font) if len(ls) > 1 else 0.0)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _plain_title(title: Any) -> str | None:
|
|
199
|
+
"""The plain text of ``title`` as drawn (its markup read when ``markup`` is on), line
|
|
200
|
+
breaks as spaces, runs of spaces collapsed; ``None`` when empty or unset, or when its
|
|
201
|
+
markup is malformed (drawing it raises the error, naming the setting)."""
|
|
202
|
+
from . import markup
|
|
203
|
+
from .text import plain
|
|
204
|
+
content = title.resolve("text")
|
|
205
|
+
if content is None:
|
|
206
|
+
return None
|
|
207
|
+
if title.resolve("markup"):
|
|
208
|
+
try:
|
|
209
|
+
content = markup.parse(content)
|
|
210
|
+
except ValueError:
|
|
211
|
+
return None
|
|
212
|
+
words = plain(content).split()
|
|
213
|
+
return " ".join(words) or None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def document_title(fig: Any, only: Any = None) -> str | None:
|
|
217
|
+
"""The document title of an output of ``fig`` (``only``: one plot of it, alone): the
|
|
218
|
+
plain text of the figure's main title; for one plot alone, or a figure of one plot
|
|
219
|
+
without a main title, that plot's title; ``None`` without one."""
|
|
220
|
+
if only is not None:
|
|
221
|
+
return _plain_title(only.title)
|
|
222
|
+
got = _plain_title(fig.title)
|
|
223
|
+
if got is None and len(fig.plots) == 1:
|
|
224
|
+
got = _plain_title(fig.plots[0].title)
|
|
225
|
+
return got
|
plot_io/transform.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Shifting and rescaling a series' coordinates: ``x_origin``, ``x_scale`` and ``x_extent``
|
|
2
|
+
(and ``y_…``) of a series, and the ``origin`` of its axis (for every series on it). A
|
|
3
|
+
series' values ``v`` on an axis are drawn at ``(v - origin) * scale``; ``x_extent=(lo, hi)``
|
|
4
|
+
sets both so that the series' smallest x is drawn at ``lo`` and its largest at ``hi``.
|
|
5
|
+
Limits, ticks and decorations are in the drawn units."""
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .settings import Setting, number
|
|
11
|
+
from .validators import typed
|
|
12
|
+
|
|
13
|
+
__all__ = ["settings", "settings_both", "affine", "apply", "TRANSFORM_KEYS", "check_origin"]
|
|
14
|
+
|
|
15
|
+
_ANCHORS = ("center", "start", "end")
|
|
16
|
+
TRANSFORM_KEYS = tuple(f"{r}_{k}" for r in "xy" for k in ("origin", "scale", "extent"))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@typed("Literal['center', 'start', 'end'] | SupportsFloat")
|
|
20
|
+
def check_origin(v: Any) -> Any:
|
|
21
|
+
"""A number, or ``"center"``, ``"start"`` or ``"end"``."""
|
|
22
|
+
if isinstance(v, str) and v.strip().lower() in _ANCHORS:
|
|
23
|
+
return v.strip().lower()
|
|
24
|
+
return number()(v)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@typed("SupportsFloat")
|
|
28
|
+
def _scale(v: Any) -> float:
|
|
29
|
+
s = number()(v)
|
|
30
|
+
if isinstance(s, (int, float)) and s == 0:
|
|
31
|
+
raise ValueError("expected a non-zero number")
|
|
32
|
+
return s
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@typed("str | Sequence[SupportsFloat | str]")
|
|
36
|
+
def _extent(v: Any) -> tuple[float, float]:
|
|
37
|
+
if isinstance(v, str):
|
|
38
|
+
parts = v.split("/")
|
|
39
|
+
if len(parts) != 2:
|
|
40
|
+
raise ValueError(f"expected 'lo/hi' or [lo, hi], got {v!r}")
|
|
41
|
+
v = [p.strip() for p in parts]
|
|
42
|
+
if not isinstance(v, (list, tuple)) or len(v) != 2:
|
|
43
|
+
raise ValueError(f"expected 'lo/hi' or [lo, hi], got {v!r}")
|
|
44
|
+
lo, hi = (float(number()(float(x) if isinstance(x, str) else x)) for x in v)
|
|
45
|
+
if lo == hi:
|
|
46
|
+
raise ValueError("an extent needs two different values")
|
|
47
|
+
return lo, hi
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def settings(role: str) -> dict[str, Setting]:
|
|
51
|
+
"""The three settings of axis ``role`` of a series."""
|
|
52
|
+
return {
|
|
53
|
+
f"{role}_origin": Setting(
|
|
54
|
+
check_origin,
|
|
55
|
+
doc=f"The {role} value drawn at 0: a number, `\"center\"` (the middle of the "
|
|
56
|
+
f"series' {role} range), `\"start\"` or `\"end\"` (its smallest or largest "
|
|
57
|
+
f"{role}); unset: the {role} axis' `origin`. Values are drawn at "
|
|
58
|
+
f"`({role} − {role}_origin) × {role}_scale`."),
|
|
59
|
+
f"{role}_scale": Setting(
|
|
60
|
+
_scale,
|
|
61
|
+
doc=f"Factor on {role} values after `{role}_origin` (e.g. bp per bin); default 1."),
|
|
62
|
+
f"{role}_extent": Setting(
|
|
63
|
+
_extent,
|
|
64
|
+
doc=f"The span the series' {role} range is drawn over, `\"lo/hi\"` or `[lo, hi]`: "
|
|
65
|
+
f"its smallest {role} at `lo`, its largest at `hi`; replaces `{role}_origin` "
|
|
66
|
+
f"and `{role}_scale`."),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def settings_both() -> dict[str, Setting]:
|
|
71
|
+
"""The settings of both axes."""
|
|
72
|
+
return {**settings("x"), **settings("y")}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def affine(item: Any, role: str) -> tuple[float, float] | None:
|
|
76
|
+
"""``(origin, scale)`` of a call (or a decoration) on axis ``role`` (values drawn at
|
|
77
|
+
``(v - origin) * scale``), or ``None``: no transform, or a category axis."""
|
|
78
|
+
series = getattr(item, "series", item)
|
|
79
|
+
if f"{role}_origin" not in series.SETTINGS or item.categories(role) is not None:
|
|
80
|
+
return None
|
|
81
|
+
origin = series.resolve(f"{role}_origin")
|
|
82
|
+
scale = series.resolve(f"{role}_scale")
|
|
83
|
+
extent = series.resolve(f"{role}_extent")
|
|
84
|
+
if origin is None:
|
|
85
|
+
axis = getattr(series.plot, f"{role}_axis")
|
|
86
|
+
origin = axis.resolve("origin")
|
|
87
|
+
if origin in _ANCHORS: # of the region, on a genome axis
|
|
88
|
+
from .genome.series import axis_region
|
|
89
|
+
region = axis_region(axis) or (item.region_of(role) if hasattr(
|
|
90
|
+
item, "region_of") else getattr(item, "region", None))
|
|
91
|
+
if region is not None:
|
|
92
|
+
from .genome.scale import origin_of
|
|
93
|
+
origin = origin_of(origin, region)
|
|
94
|
+
if extent is None and origin is None and scale in (None, 1.0):
|
|
95
|
+
return None
|
|
96
|
+
lo = hi = 0.0
|
|
97
|
+
if extent is not None or origin in _ANCHORS:
|
|
98
|
+
from .scales import get_scale
|
|
99
|
+
n, lo, hi = item.extent(role, get_scale("linear"))
|
|
100
|
+
if not n or not (math.isfinite(lo) and math.isfinite(hi)):
|
|
101
|
+
return None
|
|
102
|
+
if extent is not None:
|
|
103
|
+
e0, e1 = extent
|
|
104
|
+
if hi == lo: # one value: drawn mid-extent
|
|
105
|
+
return lo - (e0 + e1) / 2, 1.0
|
|
106
|
+
s = (e1 - e0) / (hi - lo)
|
|
107
|
+
return lo - e0 / s, s
|
|
108
|
+
o = {"center": (lo + hi) / 2, "start": lo, "end": hi}.get(origin, origin)
|
|
109
|
+
return (0.0 if o is None else float(o)), (1.0 if scale is None else float(scale))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def apply(t: tuple[float, float], v: float) -> float:
|
|
113
|
+
"""A value through a transform from :func:`affine`."""
|
|
114
|
+
return (v - t[0]) * t[1]
|