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.
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-x86_64.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/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
+ )}