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/geometry.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Geometry: a figure laid out, in px.
|
|
2
|
+
|
|
3
|
+
The layout (:mod:`plot_io.layout`) measures each plot into :class:`Bands`: the axes
|
|
4
|
+
rectangle surrounded by four stacks of bands (tick marks, tick labels, the ×10ᵏ offset,
|
|
5
|
+
the axis label, twin axes, attached colorbars, an outside legend, the titles, the row
|
|
6
|
+
label and group bands), each with a thickness and an overhang along the axes side. Once the
|
|
7
|
+
grid is solved, every area is placed into a frozen geometry record: rectangles, tick marks,
|
|
8
|
+
text runs (:class:`plot_io.text.Run`), legend entries. Drawing (:mod:`plot_io.render`)
|
|
9
|
+
reads these records and the settings' colors and widths; it neither measures nor places.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import dataclasses
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
__all__ = ["Band", "Bands", "AxisGeometry", "PlotGeometry", "LegendGeometry", "LegendItem",
|
|
16
|
+
"BarGeometry", "ColorKeyGeometry", "KeyGeometry", "FigureColorbarGeometry",
|
|
17
|
+
"TitleGeometry", "FigureLegendGeometry",
|
|
18
|
+
"FigureGeometry", "dump", "GRID"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Segment = tuple[float, float, float, float]
|
|
22
|
+
Rect = tuple[float, float, float, float]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
#: Marks a field left out of a :func:`dump` while it is empty (``None``, ``()``).
|
|
26
|
+
_OPTIONAL = {"optional": True}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
30
|
+
class Band:
|
|
31
|
+
"""One band of a side stack: ``size`` px of the margin, ``pad`` px of gap then
|
|
32
|
+
``extent`` px of content (``size`` is their sum), nominally ``offset`` px from the axes
|
|
33
|
+
edge; ``lo``/``hi``: how far its content passes the start/end of the side (left/right
|
|
34
|
+
of a top or bottom band, above/below a left or right band). A band's offset is the sum
|
|
35
|
+
of the sizes before it on its side, except twin axes: the first on a side drawn at the
|
|
36
|
+
axes edge (offset 0), over the plot's mirrored ticks, a further one past the earlier."""
|
|
37
|
+
|
|
38
|
+
kind: str
|
|
39
|
+
offset: float
|
|
40
|
+
size: float
|
|
41
|
+
lo: float = 0.0
|
|
42
|
+
hi: float = 0.0
|
|
43
|
+
pad: float = 0.0
|
|
44
|
+
extent: float = 0.0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
48
|
+
class Bands:
|
|
49
|
+
"""What surrounds axes of ``size`` (w, h): the four stacks, from the axes outward, and
|
|
50
|
+
the margins (left, right, top, bottom) they take: each side's sizes summed, at least
|
|
51
|
+
the overhangs of the bands across it, plus the plot's ``space_above``; ``twins``:
|
|
52
|
+
the bands of the twin plots over the same axes (their margins are the ``twin`` bands)."""
|
|
53
|
+
|
|
54
|
+
size: tuple[float, float]
|
|
55
|
+
left: tuple[Band, ...] = ()
|
|
56
|
+
right: tuple[Band, ...] = ()
|
|
57
|
+
top: tuple[Band, ...] = ()
|
|
58
|
+
bottom: tuple[Band, ...] = ()
|
|
59
|
+
margins: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0)
|
|
60
|
+
twins: tuple["Bands", ...] = ()
|
|
61
|
+
|
|
62
|
+
def side(self, name: str) -> tuple[Band, ...]:
|
|
63
|
+
return getattr(self, name)
|
|
64
|
+
|
|
65
|
+
def extent(self, side: str, kinds: tuple[str, ...]) -> float:
|
|
66
|
+
"""The sizes of the bands of ``kinds`` on ``side``, summed."""
|
|
67
|
+
return sum(b.size for b in self.side(side) if b.kind in kinds)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
71
|
+
class AxisGeometry:
|
|
72
|
+
"""An axis as drawn: its tick mark groups (each stroked as one path: the ticks, then
|
|
73
|
+
the mirrored ones), tick label runs, title and ×10ᵏ offset runs."""
|
|
74
|
+
|
|
75
|
+
role: str
|
|
76
|
+
shown: bool = False
|
|
77
|
+
marks: tuple[tuple[Segment, ...], ...] = ()
|
|
78
|
+
labels: tuple[Any, ...] = ()
|
|
79
|
+
title: Any = None
|
|
80
|
+
offset: Any = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
84
|
+
class LegendItem:
|
|
85
|
+
"""A legend row: ``kind`` ``"header"`` (a size key's title), ``"size"`` (a size key
|
|
86
|
+
entry) or ``"entry"``; the symbol's box and the label run."""
|
|
87
|
+
|
|
88
|
+
kind: str
|
|
89
|
+
source: Any
|
|
90
|
+
index: int
|
|
91
|
+
glyph: Rect
|
|
92
|
+
label: Any
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
96
|
+
class LegendGeometry:
|
|
97
|
+
"""A legend's box ``(x, y, w, h)``, its title run and its rows."""
|
|
98
|
+
|
|
99
|
+
box: Rect
|
|
100
|
+
title: Any
|
|
101
|
+
items: tuple[LegendItem, ...]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
105
|
+
class BarGeometry:
|
|
106
|
+
"""A colorbar drawn as a bar: the gradient's rectangle, extension blocks ``(rect, color)`` or
|
|
107
|
+
triangles ``(points, color)``, tick mark segments, tick label runs and the label run;
|
|
108
|
+
``boxes``: the layout boxes of the colorbar and of its axis that their settings' ``%``
|
|
109
|
+
referred to when placed."""
|
|
110
|
+
|
|
111
|
+
state: Any
|
|
112
|
+
rect: Rect
|
|
113
|
+
blocks: tuple[tuple[Rect, Any], ...]
|
|
114
|
+
triangles: tuple[tuple[tuple[tuple[float, float], ...], Any], ...]
|
|
115
|
+
marks: tuple[Segment, ...]
|
|
116
|
+
labels: tuple[Any, ...]
|
|
117
|
+
title: Any
|
|
118
|
+
boxes: tuple[dict[str, float] | None, dict[str, float] | None]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
122
|
+
class ColorKeyGeometry:
|
|
123
|
+
"""A colorbar drawn as a key: the bar's rectangle (round ends, the radius half its
|
|
124
|
+
thickness), the key's box (texts included), the end shapes ``(shape, geometry,
|
|
125
|
+
color)`` (``"dot"``: ``(cx, cy, r)``; ``"block"``: a rectangle; ``"triangle"``: its
|
|
126
|
+
points), the end text or tick label runs and the label run; ``boxes`` as a bar's."""
|
|
127
|
+
|
|
128
|
+
state: Any
|
|
129
|
+
rect: Rect
|
|
130
|
+
box: Rect
|
|
131
|
+
ends: tuple[tuple[str, Any, Any], ...]
|
|
132
|
+
values: tuple[Any, ...]
|
|
133
|
+
title: Any
|
|
134
|
+
boxes: tuple[dict[str, float] | None, dict[str, float] | None]
|
|
135
|
+
#: a size key's markers ``(cx, cy, diameter)``
|
|
136
|
+
marks: tuple[tuple[float, float, float], ...] = dataclasses.field(default=(),
|
|
137
|
+
metadata=_OPTIONAL)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
141
|
+
class KeyGeometry:
|
|
142
|
+
"""A size legend in its own area: rows ``(kind, glyph box, label run)``."""
|
|
143
|
+
|
|
144
|
+
state: Any
|
|
145
|
+
rows: tuple[tuple[str, Rect, Any], ...]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
149
|
+
class TitleGeometry:
|
|
150
|
+
"""A title as drawn: its ``side``, its ``lane`` (place in the side's stack, 0 nearest),
|
|
151
|
+
its box ``(x, y, w, h)``, its line runs and its rule's rectangle; a figure title on top
|
|
152
|
+
or bottom is a ``paragraph`` wrapped to its span instead of runs, drawn with its
|
|
153
|
+
top-left corner at ``at``."""
|
|
154
|
+
|
|
155
|
+
obj: Any
|
|
156
|
+
side: str
|
|
157
|
+
lane: int
|
|
158
|
+
box: Rect
|
|
159
|
+
runs: tuple[Any, ...]
|
|
160
|
+
rule: Rect | None = None
|
|
161
|
+
paragraph: Any = dataclasses.field(default=None, metadata=_OPTIONAL)
|
|
162
|
+
at: tuple[float, float] | None = dataclasses.field(default=None, metadata=_OPTIONAL)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
166
|
+
class FigureLegendGeometry:
|
|
167
|
+
"""A figure legend as drawn: its ``side``, its ``lane`` (0 nearest the plots) and the
|
|
168
|
+
legend placed there."""
|
|
169
|
+
|
|
170
|
+
state: Any
|
|
171
|
+
side: str
|
|
172
|
+
lane: int
|
|
173
|
+
legend: LegendGeometry
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
177
|
+
class FigureColorbarGeometry:
|
|
178
|
+
"""A figure colorbar as drawn: its ``side``, its ``lane`` (0 nearest the plots) and the
|
|
179
|
+
colorbar placed there (a key or a bar)."""
|
|
180
|
+
|
|
181
|
+
side: str
|
|
182
|
+
lane: int
|
|
183
|
+
bar: Any
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
187
|
+
class PlotGeometry:
|
|
188
|
+
"""A plot (or a twin over it) as drawn: the axes rectangle, the bands it was measured
|
|
189
|
+
into, grid lines ``(axis, setting prefix, horizontal, positions)``, axis line segments
|
|
190
|
+
``(axis, segment)``, the x and y axes, the twins, the row label runs, the main title's
|
|
191
|
+
run (a tuple of runs for several lines), the legend, the attached colorbars, the main
|
|
192
|
+
title's rule, the plot's other titles and the names of its interval lanes."""
|
|
193
|
+
|
|
194
|
+
state: Any
|
|
195
|
+
rect: Rect
|
|
196
|
+
bands: Bands
|
|
197
|
+
figure_size: tuple[float, float]
|
|
198
|
+
grid: tuple[tuple[Any, str, bool, tuple[float, ...]], ...]
|
|
199
|
+
spines: tuple[tuple[Any, Segment], ...]
|
|
200
|
+
x: AxisGeometry
|
|
201
|
+
y: AxisGeometry
|
|
202
|
+
twins: tuple["PlotGeometry", ...] = ()
|
|
203
|
+
row_label: tuple[Any, ...] = ()
|
|
204
|
+
title: Any = None
|
|
205
|
+
legend: LegendGeometry | None = None
|
|
206
|
+
bars: tuple[BarGeometry, ...] = ()
|
|
207
|
+
title_rule: Rect | None = dataclasses.field(default=None, metadata=_OPTIONAL)
|
|
208
|
+
titles: tuple[TitleGeometry, ...] = dataclasses.field(default=(), metadata=_OPTIONAL)
|
|
209
|
+
lane_names: tuple[Any, ...] = dataclasses.field(default=(), metadata=_OPTIONAL)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
213
|
+
class FigureGeometry:
|
|
214
|
+
"""The figure's own parts: its title (a paragraph laid out ``title_width`` wide, its
|
|
215
|
+
box's top-left corner at ``title_at``), the rule above it, bands under the plots
|
|
216
|
+
``(span, rect)``, lines over them ``(line, segment)``, the groups' runs, and the
|
|
217
|
+
figure's other titles, its legends and its colorbars, in the lanes between the plots."""
|
|
218
|
+
|
|
219
|
+
title: Any = None
|
|
220
|
+
title_at: tuple[float, float] = (0.0, 0.0)
|
|
221
|
+
title_width: float = 0.0
|
|
222
|
+
rule: Rect | None = None
|
|
223
|
+
spans: tuple[tuple[Any, Rect], ...] = ()
|
|
224
|
+
lines: tuple[tuple[Any, Segment], ...] = ()
|
|
225
|
+
groups: tuple[Any, ...] = ()
|
|
226
|
+
titles: tuple[TitleGeometry, ...] = dataclasses.field(default=(), metadata=_OPTIONAL)
|
|
227
|
+
legends: tuple[FigureLegendGeometry, ...] = dataclasses.field(default=(),
|
|
228
|
+
metadata=_OPTIONAL)
|
|
229
|
+
colorbars: tuple[FigureColorbarGeometry, ...] = dataclasses.field(default=(),
|
|
230
|
+
metadata=_OPTIONAL)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
#: Positions in :func:`dump` are rounded to 1/``GRID`` px.
|
|
234
|
+
GRID = 64
|
|
235
|
+
|
|
236
|
+
#: Fields of the records that hold render state (not geometry), left out of a dump; a
|
|
237
|
+
#: run's shaped line (``glyphs``) is its ``content`` with its offsets.
|
|
238
|
+
_STATE = frozenset({"state", "obj", "glyphs"})
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def dump(value: Any, grid: int = GRID) -> Any:
|
|
242
|
+
"""``value`` (a :class:`~plot_io.layout.Geometry` or any of its records) as plain data
|
|
243
|
+
for comparison: dataclass records as dicts with their ``"type"``, numbers rounded to
|
|
244
|
+
1/``grid`` px, text runs with their plain text and font, model objects (axes, series,
|
|
245
|
+
decorations) by their class and role or name; render state left out."""
|
|
246
|
+
from vec_io.text import Paragraphs
|
|
247
|
+
from .text import Font, plain
|
|
248
|
+
if value is None or isinstance(value, (bool, str)):
|
|
249
|
+
return value
|
|
250
|
+
if isinstance(value, int):
|
|
251
|
+
return value
|
|
252
|
+
if isinstance(value, float):
|
|
253
|
+
r = round(value * grid) / grid
|
|
254
|
+
return 0.0 if r == 0 else r
|
|
255
|
+
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
|
256
|
+
out: dict[str, Any] = {"type": type(value).__name__}
|
|
257
|
+
for f in dataclasses.fields(value):
|
|
258
|
+
if f.name in _STATE:
|
|
259
|
+
continue
|
|
260
|
+
v = getattr(value, f.name)
|
|
261
|
+
if f.metadata.get("optional") and (v is None or v == ()):
|
|
262
|
+
continue
|
|
263
|
+
out[f.name] = plain(v) if f.name == "content" else dump(v, grid)
|
|
264
|
+
if type(value).__name__ == "Placed":
|
|
265
|
+
out["area"] = _model(getattr(value, "obj"))
|
|
266
|
+
return out
|
|
267
|
+
if isinstance(value, Font):
|
|
268
|
+
return {"size": dump(value.size, grid), "family": value.family,
|
|
269
|
+
"weight": value.weight, "style": value.style, "color": dump(value.color, grid),
|
|
270
|
+
"stretch": value.stretch}
|
|
271
|
+
if isinstance(value, Paragraphs):
|
|
272
|
+
return {"type": "Paragraphs", "width": dump(value.width, grid),
|
|
273
|
+
"height": dump(value.height, grid),
|
|
274
|
+
"lines": [value.line_text(k) for k in range(value.lines)],
|
|
275
|
+
"baselines": dump(value.baselines, grid)}
|
|
276
|
+
if isinstance(value, (tuple, list)):
|
|
277
|
+
return [dump(v, grid) for v in value]
|
|
278
|
+
if isinstance(value, dict):
|
|
279
|
+
return {str(k): dump(v, grid) for k, v in value.items()}
|
|
280
|
+
return _model(value)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _model(obj: Any) -> Any:
|
|
284
|
+
"""A model object (or another non-geometry value) by what names it."""
|
|
285
|
+
name = type(obj).__name__
|
|
286
|
+
role = getattr(obj, "role", None)
|
|
287
|
+
if isinstance(role, str):
|
|
288
|
+
return f"{name}:{role}"
|
|
289
|
+
return name
|
plot_io/groups.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""Setting groups: settings that travel together, declared once.
|
|
2
|
+
|
|
3
|
+
A group (:class:`Font`, :class:`TextStyle`, :class:`Stroke`, :class:`Marker`,
|
|
4
|
+
:class:`FillStyle`, :class:`NumberFormat`) declares its members once. An owner embeds it with
|
|
5
|
+
:meth:`Group.embed` under a prefix: the primary element of the owner takes the group
|
|
6
|
+
unprefixed (a scatter's markers: ``size``, ``edge_color``), a secondary one prefixed (a
|
|
7
|
+
line's markers: ``marker_size``, ``marker_edge_color``). Each member has its unprefixed
|
|
8
|
+
name and the pattern of its prefixed name (``"{p}size"``).
|
|
9
|
+
|
|
10
|
+
``"Group.member"`` keys (``"Marker.size"``, member names unprefixed) are defaults and style
|
|
11
|
+
keys of every embedding of the member; an owner's own key (``"Line.marker_size"``) comes
|
|
12
|
+
first.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from collections.abc import Callable, Iterable
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from .color.values import color
|
|
19
|
+
from .lines import line_style
|
|
20
|
+
from .markers import MARKERS, check_marker
|
|
21
|
+
from .numbers import _check_minus, _check_precision, _check_str
|
|
22
|
+
from .settings import (MARKER_ALIASES, Setting, boolean, family, font_stretch, font_style,
|
|
23
|
+
font_variations, font_weight, line_height, number, one_of)
|
|
24
|
+
from .validators import Custom, length, typed
|
|
25
|
+
from .values import paint, size_value
|
|
26
|
+
|
|
27
|
+
__all__ = ["Group", "Member", "GROUPS", "series_color", "fill_value", "fill_parts", "Font",
|
|
28
|
+
"TextStyle", "Stroke", "Marker", "FillStyle", "NumberFormat"]
|
|
29
|
+
|
|
30
|
+
#: Every group, by name.
|
|
31
|
+
GROUPS: dict[str, type["Group"]] = {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _constant(check: Any) -> Callable[[str], Any]:
|
|
35
|
+
def make(name: str) -> Any:
|
|
36
|
+
return check
|
|
37
|
+
return make
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Member:
|
|
41
|
+
"""One member of a group: its unprefixed ``name``, the ``pattern`` of its name under a
|
|
42
|
+
prefix ``{p}`` (a pattern without ``{p}`` is never prefixed), ``make(name)`` giving the
|
|
43
|
+
validator of a setting called ``name``, its doc and whether it is inherited."""
|
|
44
|
+
|
|
45
|
+
__slots__ = ("name", "pattern", "make", "doc", "inherited", "expr")
|
|
46
|
+
|
|
47
|
+
def __init__(self, name: str, check: Any, doc: str, pattern: str | None = None,
|
|
48
|
+
inherited: bool = False, per_name: bool = False,
|
|
49
|
+
expr: bool | None = None) -> None:
|
|
50
|
+
self.name = name
|
|
51
|
+
self.pattern = pattern if pattern is not None else "{p}" + name
|
|
52
|
+
self.make: Callable[[str], Any] = check if per_name else _constant(check)
|
|
53
|
+
self.doc = doc
|
|
54
|
+
self.inherited = inherited
|
|
55
|
+
self.expr = expr
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Group:
|
|
59
|
+
"""Base of setting groups."""
|
|
60
|
+
|
|
61
|
+
MEMBERS: dict[str, Member] = {}
|
|
62
|
+
|
|
63
|
+
def __init_subclass__(cls, **kw: Any) -> None:
|
|
64
|
+
super().__init_subclass__(**kw)
|
|
65
|
+
GROUPS[cls.__name__] = cls
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def setting_name(cls, member: str, prefix: str = "") -> str:
|
|
69
|
+
"""The name of ``member`` embedded under ``prefix``."""
|
|
70
|
+
if not prefix:
|
|
71
|
+
return member
|
|
72
|
+
return cls.MEMBERS[member].pattern.format(p=prefix)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def embed(cls, prefix: str = "", *, only: Iterable[str] | None = None,
|
|
76
|
+
docs: dict[str, str] | None = None,
|
|
77
|
+
inherited: Iterable[str] | bool | None = None,
|
|
78
|
+
checks: dict[str, Any] | None = None) -> dict[str, Setting]:
|
|
79
|
+
"""The group's settings for a ``SETTINGS`` table: members ``only`` (default all),
|
|
80
|
+
under ``prefix``; ``docs`` and ``checks`` replace their
|
|
81
|
+
docs and validators, ``inherited`` (member names, or a bool for all) makes them
|
|
82
|
+
inherited or not."""
|
|
83
|
+
members = list(cls.MEMBERS) if only is None else list(only)
|
|
84
|
+
out: dict[str, Setting] = {}
|
|
85
|
+
for m in members:
|
|
86
|
+
mem = cls.MEMBERS[m]
|
|
87
|
+
name = cls.setting_name(m, prefix)
|
|
88
|
+
if inherited is None:
|
|
89
|
+
inh = mem.inherited
|
|
90
|
+
elif isinstance(inherited, bool):
|
|
91
|
+
inh = inherited
|
|
92
|
+
else:
|
|
93
|
+
inh = m in inherited
|
|
94
|
+
check = (checks or {}).get(m, None) or mem.make(name)
|
|
95
|
+
s = Setting(check, inherited=inh, doc=(docs or {}).get(m, mem.doc), expr=mem.expr)
|
|
96
|
+
s.group, s.field, s.prefix = cls, m, prefix
|
|
97
|
+
s.name = name
|
|
98
|
+
out[name] = s
|
|
99
|
+
return out
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def member_setting(cls, member: str) -> Setting:
|
|
103
|
+
"""A setting of ``member`` unprefixed (to check ``"Group.member"`` keys)."""
|
|
104
|
+
return cls.embed(only=[member])[member]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# The groups
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
_FONT_SIZE = length(positive=True, font=True, percent="em")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Font(Group):
|
|
115
|
+
"""The face of text: size, family and variable-font axes (all inherited)."""
|
|
116
|
+
|
|
117
|
+
MEMBERS = {m.name: m for m in (
|
|
118
|
+
Member("font_size", _FONT_SIZE, "Base font size.", inherited=True),
|
|
119
|
+
Member("font_family", family, "Font family.", inherited=True),
|
|
120
|
+
Member("font_stretch", font_stretch,
|
|
121
|
+
"Font width: `normal`, `condensed`, …, or a percentage (`\"87.5%\"`); the "
|
|
122
|
+
"`wdth` axis of variable fonts.", inherited=True),
|
|
123
|
+
Member("font_variations", font_variations,
|
|
124
|
+
"Variable font axes: `{\"GRAD\": 50}` or `'\"GRAD\" 50, \"wght\" 650'`.",
|
|
125
|
+
inherited=True),
|
|
126
|
+
Member("font_optical_sizing", one_of("auto", "none"),
|
|
127
|
+
"`auto`: a variable font's `opsz` axis follows the font size; `none`.",
|
|
128
|
+
inherited=True),
|
|
129
|
+
Member("line_height", line_height,
|
|
130
|
+
"Height of a line of text: a multiple of the font size (`1.2`), a percentage "
|
|
131
|
+
"of it, or `normal` (the font's ascent and descent). Text boxes and the gaps "
|
|
132
|
+
"around them follow it, with the glyphs centered.", inherited=True),
|
|
133
|
+
Member("markup", boolean,
|
|
134
|
+
"Read markup in strings (`<b>`, `<i>`, `<sup>`, …: see "
|
|
135
|
+
"[conventions](#1-conventions)); `False`: strings stay literal.",
|
|
136
|
+
inherited=True),
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
FACE = ("font_family", "font_stretch", "font_variations", "font_optical_sizing",
|
|
140
|
+
"line_height", "markup")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TextStyle(Group):
|
|
145
|
+
"""One text element: size, weight, style and color."""
|
|
146
|
+
|
|
147
|
+
MEMBERS = {m.name: m for m in (
|
|
148
|
+
Member("font_size", _FONT_SIZE, "Font size.", pattern="{p}font_size"),
|
|
149
|
+
Member("font_weight", font_weight, "`normal`, `bold` or a weight 1–1000.",
|
|
150
|
+
pattern="{p}weight"),
|
|
151
|
+
Member("font_style", font_style,
|
|
152
|
+
"`normal`, `italic`, `oblique` or `\"oblique 10deg\"`.", pattern="{p}style"),
|
|
153
|
+
Member("color", color, "Text color."),
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@typed("ColorLike")
|
|
158
|
+
def series_color(v: Any) -> Any:
|
|
159
|
+
"""A color, or ``"series"`` (the series' own color)."""
|
|
160
|
+
return v if v == "series" else color(v)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Stroke(Group):
|
|
164
|
+
"""A stroke: color, width, dash style, caps and joins."""
|
|
165
|
+
|
|
166
|
+
MEMBERS = {m.name: m for m in (
|
|
167
|
+
Member("color", color, "Line color."),
|
|
168
|
+
Member("line_width", length(lo=0), "Line width.", pattern="{p}width"),
|
|
169
|
+
Member("line_style", line_style,
|
|
170
|
+
"`solid`, `dashed`, `dotted`, `dashdot` or a dash list.", pattern="{p}style"),
|
|
171
|
+
Member("cap", one_of("butt", "round", "square"), "Line cap."),
|
|
172
|
+
Member("join", one_of("miter", "round", "bevel"), "Line join."),
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
_PAINT = "ColorLike | Column | Density"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _paint(name: str) -> Any:
|
|
180
|
+
return typed(_PAINT)(lambda v: paint(v, name))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _edge_paint(name: str) -> Any:
|
|
184
|
+
return typed(_PAINT)(lambda v: v if v == "auto" else paint(v, name))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _marker_size(name: str) -> Any:
|
|
188
|
+
"""``size`` maps numbers and densities (scatter, strip, swarm: a size axis); a secondary
|
|
189
|
+
group's ``marker_size`` is sizes in px."""
|
|
190
|
+
return size_value if name == "size" else _PX_SIZES
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@typed("SupportsFloat | str | Column | Density")
|
|
194
|
+
def _px_sizes(v: Any) -> Any:
|
|
195
|
+
from .values import is_computed
|
|
196
|
+
out = size_value(v)
|
|
197
|
+
if is_computed(out):
|
|
198
|
+
raise ValueError("sizes in px: a density maps through a size axis, which only the "
|
|
199
|
+
"`size` of scatter, strip and swarm has")
|
|
200
|
+
return out
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
_PX_SIZES = Custom(_px_sizes, like=size_value) # resolved as a length
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
#: A marker name of the typed API: the built-in names and their shorthands.
|
|
207
|
+
_MARKER_NAME = "Literal[" + ", ".join(map(repr, [*MARKERS, *MARKER_ALIASES])) + "]"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@typed(f"{_MARKER_NAME} | Path | list[{_MARKER_NAME} | Path] | tuple[{_MARKER_NAME} | Path, ...]")
|
|
211
|
+
def _marker_value(v: Any) -> Any:
|
|
212
|
+
if isinstance(v, (list, tuple)):
|
|
213
|
+
return [check_marker(m) for m in v]
|
|
214
|
+
return check_marker(v)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@typed("bool | ColorLike")
|
|
218
|
+
def fill_value(v: Any, colors: bool = True) -> Any:
|
|
219
|
+
"""A ``fill``: ``True``, ``False`` (no fill) or (with ``colors``) a color; its opacity
|
|
220
|
+
is ``fill_opacity``."""
|
|
221
|
+
if v is True or v is False:
|
|
222
|
+
return v
|
|
223
|
+
if isinstance(v, (int, float)):
|
|
224
|
+
raise ValueError(f"expected True, False{' or a color' if colors else ''}, got {v!r} "
|
|
225
|
+
"(an opacity is fill_opacity)")
|
|
226
|
+
if not colors:
|
|
227
|
+
raise ValueError(f"expected True or False, got {v!r}")
|
|
228
|
+
return color(v)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class Marker(Group):
|
|
232
|
+
"""Markers: shape, size (by area), fill and outline."""
|
|
233
|
+
|
|
234
|
+
MEMBERS = {m.name: m for m in (
|
|
235
|
+
Member("marker", _marker_value, "Marker name, `Path` or path data, or one per point.",
|
|
236
|
+
pattern="marker"),
|
|
237
|
+
Member("size", _marker_size,
|
|
238
|
+
"Marker size: the diameter of the circle of equal area; one value or one "
|
|
239
|
+
"per point; numbers per point are values to map when `size_map` is set; or "
|
|
240
|
+
"`\"density\"`.", per_name=True),
|
|
241
|
+
Member("color", _paint, "Fill: a color, colors per point, numbers per point "
|
|
242
|
+
"(mapped through `color_map`) or `\"density\"`.",
|
|
243
|
+
per_name=True),
|
|
244
|
+
Member("edge_color", _edge_paint,
|
|
245
|
+
"Outline: as the fill color, or `\"auto\"` (white around filled markers; "
|
|
246
|
+
"hollow ones outlined in their color).", per_name=True),
|
|
247
|
+
Member("edge_width", length(lo=0), "Outline width; 0: no outline."),
|
|
248
|
+
Member("fill", typed("bool")(lambda v: fill_value(v, colors=False)),
|
|
249
|
+
"`True`: filled with the color; `False`: hollow, outlined in the color."),
|
|
250
|
+
Member("fill_opacity", number(0, 1),
|
|
251
|
+
"Opacity of the fill; below 1 the marker is also outlined in its color.",
|
|
252
|
+
pattern="{p}fill_opacity"),
|
|
253
|
+
Member("size_basis", one_of("area", "box"),
|
|
254
|
+
"`area`: shapes of the same size have the same area; `box`: size is the "
|
|
255
|
+
"width of the shape's box.", pattern="size_basis"),
|
|
256
|
+
)}
|
|
257
|
+
|
|
258
|
+
#: Docs of members under a prefix (a secondary element's markers, without a size axis).
|
|
259
|
+
PREFIXED_DOCS = {"size": "Marker size: the diameter of the circle of equal area, in px; "
|
|
260
|
+
"one value or one per point."}
|
|
261
|
+
|
|
262
|
+
@classmethod
|
|
263
|
+
def embed(cls, prefix: str = "", **kw: Any) -> dict[str, Setting]:
|
|
264
|
+
if prefix:
|
|
265
|
+
kw["docs"] = {**cls.PREFIXED_DOCS, **(kw.get("docs") or {})}
|
|
266
|
+
return super().embed(prefix, **kw)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def fill_parts(owner: Any, prefix: str = "") -> tuple[Any, float]:
|
|
270
|
+
"""``(color, opacity)`` of an owner's ``fill`` and ``fill_opacity``: the color it gives
|
|
271
|
+
(``None``: the owner's) and the opacity it applies (0 without a fill)."""
|
|
272
|
+
v = owner.resolve(f"{prefix}fill")
|
|
273
|
+
if v is False or v is None:
|
|
274
|
+
return None, 0.0
|
|
275
|
+
op = owner.resolve(f"{prefix}fill_opacity")
|
|
276
|
+
return (None if v is True else v), (1.0 if op is None else op)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class FillStyle(Group):
|
|
280
|
+
"""A fill: on or off, or its color, and its opacity."""
|
|
281
|
+
|
|
282
|
+
MEMBERS = {m.name: m for m in (
|
|
283
|
+
Member("fill", fill_value,
|
|
284
|
+
"Fill: `True` (the owner's color), a color, or `False` (no fill)."),
|
|
285
|
+
Member("fill_opacity", number(0, 1), "Opacity of the fill (0–1).",
|
|
286
|
+
pattern="{p}fill_opacity"),
|
|
287
|
+
)}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class NumberFormat(Group):
|
|
291
|
+
"""How numbers are written (all inherited)."""
|
|
292
|
+
|
|
293
|
+
MEMBERS = {m.name: m for m in (
|
|
294
|
+
Member("decimal_separator", _check_str,
|
|
295
|
+
"Decimal separator of numbers (`\".\"`).", inherited=True),
|
|
296
|
+
Member("thousands_separator", _check_str,
|
|
297
|
+
"Thousands separator (`\"\"`: none).", inherited=True),
|
|
298
|
+
Member("minus", _check_minus,
|
|
299
|
+
"`hyphen` (`-`) or `minus` (U+2212, from an installed font).", inherited=True),
|
|
300
|
+
Member("prefix", _check_str,
|
|
301
|
+
"Text before every number.", inherited=True),
|
|
302
|
+
Member("suffix", _check_str,
|
|
303
|
+
"Text after every number.", inherited=True),
|
|
304
|
+
Member("precision", _check_precision,
|
|
305
|
+
"Significant digits of `auto`, `sci`, `eng`, `si` (unset: as needed); "
|
|
306
|
+
"`auto` tick labels keep enough decimals to differ.",
|
|
307
|
+
inherited=True),
|
|
308
|
+
)}
|