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
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]