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/settings.py ADDED
@@ -0,0 +1,807 @@
1
+ """Settings: declared, validated, inherited and resolved through one cascade.
2
+
3
+ Every object of the model (figure, plot, axis, series, …) declares its settings in a
4
+ ``SETTINGS`` table of :class:`Setting`. Values are checked when they are set.
5
+ :func:`cascade` finds where a value comes from, in this order:
6
+
7
+ 1. the value set on the object, then (plots' x and y axes) the value set on the figure's
8
+ axis of that role (``figure.x_axis``), then a rule of values given elsewhere
9
+ (:func:`given_rule`: a series' own color axis settings);
10
+ 2. each style of the figure's chain (:class:`Theme`), nearest first: the key of the
11
+ object's class, then of its base classes (a base class's key is skipped once a more
12
+ specific class has a default for the setting), then the key of the setting's group
13
+ (``"Marker.size"``); a class rule (``RULES``: a default the class computes from the
14
+ model) comes after the keys of its class and of more specific ones, before the
15
+ others. For an inherited setting, the first style with its bare name is the last
16
+ one read here (the nearer style wins);
17
+ 3. for inherited settings, the parent's value (series → plot → figure);
18
+ 4. the default (:data:`plot_io.defaults.DEFAULTS`): the class's, along the classes as
19
+ style keys, then the group's; first when it refers to the parent's value
20
+ (``"calc(1.4 * inherit)"``);
21
+ 5. for inherited settings, the bare name: the nearest style's, else ``DEFAULTS``'.
22
+
23
+ :meth:`Settings.get` returns the value as given, :meth:`Settings.resolve` the final value
24
+ (units, ``var()``, ``calc()`` and ``"auto"`` rules resolved; see :mod:`plot_io.units`),
25
+ :meth:`Settings.explain` where it comes from. While a figure is rendered, plain
26
+ resolutions read the render's :mod:`~plot_io.spec`.
27
+ """
28
+
29
+ import contextlib
30
+ import contextvars
31
+ import difflib
32
+ from collections.abc import Callable, Generator, Iterable
33
+ from typing import Any, Self
34
+
35
+ from .defaults import DEFAULTS, default_of, inherited
36
+ from .units import (SettingError, Value, Var, guard, has_fixed, may_refer, needs_resolving, parse, refers_inherit, resolve_raw,
37
+ given)
38
+ from .validators import Validator, anything, as_validator, family, text
39
+ # The validators, imported from here by the modules that declare settings.
40
+ from .validators import ( # noqa: F401
41
+ boolean, custom_vars, dpi, dpi_or_bool, either, font_stretch, font_style, font_variations,
42
+ font_weight, line_height, literal, number, one_of, pair, simplify_value, time_zone)
43
+ from . import hints
44
+ from . import spec as _spec
45
+
46
+ _MISS = object()
47
+
48
+ #: Values in force for the output in progress in this thread or task (``write(simplify=…)``,
49
+ #: ``region=``): ``id(obj)`` → ``(obj, {name: value})``, read as if set on ``obj``.
50
+ _OVERRIDES: contextvars.ContextVar[dict[int, tuple[Any, dict[str, Any]]]] = \
51
+ contextvars.ContextVar("plot_io_overrides", default={})
52
+
53
+
54
+ def _own(obj: "Settings", name: str) -> Any:
55
+ """``obj``'s value of ``name``: an override in force, else its own (``_MISS``: none)."""
56
+ o = _OVERRIDES.get()
57
+ if o:
58
+ hit = o.get(id(obj))
59
+ if hit is not None and name in hit[1]:
60
+ return hit[1][name]
61
+ return obj._values.get(name, _MISS)
62
+
63
+
64
+ @contextlib.contextmanager
65
+ def overriding(obj: "Settings", **settings: Any) -> Generator[None]:
66
+ """Settings of ``obj`` in force inside the block, in this thread or task only, as if
67
+ set on ``obj``; the object is not changed. ``None`` values are left out."""
68
+ settings = {k: v for k, v in canonical_kwargs(settings, obj.SETTINGS, obj.kind).items()
69
+ if v is not None}
70
+ if not settings:
71
+ yield
72
+ return
73
+ checked = {}
74
+ for name, value in settings.items():
75
+ s = obj.SETTINGS.get(name)
76
+ if s is None:
77
+ raise SettingError(unknown_message(name, obj.SETTINGS, obj.kind), obj, name, value)
78
+ checked[name] = obj._checked(s, name, value)
79
+ current = _OVERRIDES.get()
80
+ mine = dict(current[id(obj)][1]) if id(obj) in current else {}
81
+ mine.update(checked)
82
+ token = _OVERRIDES.set({**current, id(obj): (obj, mine)})
83
+ try:
84
+ yield
85
+ finally:
86
+ _OVERRIDES.reset(token)
87
+ #: Returned by a class rule that does not apply: the cascade goes on.
88
+ DECLINE = object()
89
+
90
+ __all__ = ["Setting", "Settings", "Theme", "BASE_THEME", "ALIASES", "MARKER_ALIASES", "DECLINE",
91
+ "route", "unknown_message", "canonical", "cascade", "overriding"]
92
+
93
+ #: The shorthands: of setting names (on every class that has the full setting; a setting
94
+ #: has at most one) and of marker names. docs/api.md's table of shorthands is made from them.
95
+ ALIASES = {"c": "color", "s": "size", "cmap": "color_map", "smap": "size_map"}
96
+ MARKER_ALIASES = {"o": "circle", "s": "square", "d": "diamond", "^": "triangle_up",
97
+ "v": "triangle_down", "<": "triangle_left", ">": "triangle_right",
98
+ "+": "plus", "x": "cross", "triangle": "triangle_up"}
99
+
100
+
101
+ def canonical(name: str, table: Any) -> str:
102
+ """``name``, or the setting it is an alias of when ``table`` has that setting."""
103
+ if name not in table:
104
+ target = ALIASES.get(name)
105
+ if target is not None and target in table:
106
+ return target
107
+ return name
108
+
109
+
110
+ def canonical_kwargs(kwargs: dict[str, Any], table: Any, what: str) -> dict[str, Any]:
111
+ """``kwargs`` with aliases replaced; an alias given with its setting is an error."""
112
+ out: dict[str, Any] = {}
113
+ given_as: dict[str, str] = {}
114
+ for name, value in kwargs.items():
115
+ key = canonical(name, table)
116
+ if key in out:
117
+ raise ValueError(f"{what}: {given_as[key]!r} and {name!r} are the same setting; "
118
+ f"give one")
119
+ out[key] = value
120
+ given_as[key] = name
121
+ return out
122
+
123
+
124
+ class Theme:
125
+ """A figure's style chain: ``layers``, the styles other than ``"default"`` as
126
+ ``(label, keys)``, nearest first; ``DEFAULTS`` comes after them. As a mapping, the
127
+ bare names (inherited settings) and custom variables (``--name``): the nearest
128
+ style's, else ``DEFAULTS``'."""
129
+
130
+ __slots__ = ("layers", "_flat")
131
+
132
+ def __init__(self, layers: Iterable[tuple[str, dict[str, Any]]] = ()) -> None:
133
+ self.layers = tuple(layers)
134
+ flat = inherited()
135
+ for _, keys in reversed(self.layers):
136
+ flat.update((k, v) for k, v in keys.items() if "." not in k)
137
+ self._flat = flat
138
+
139
+ def get(self, key: str, default: Any = None) -> Any:
140
+ return self._flat.get(key, default)
141
+
142
+ def __getitem__(self, key: str) -> Any:
143
+ return self._flat[key]
144
+
145
+ def __contains__(self, key: object) -> bool:
146
+ return key in self._flat
147
+
148
+ def label(self, key: str) -> str:
149
+ """``style "name" key`` for the nearest style that has ``key``, else
150
+ ``default key``."""
151
+ for label, keys in self.layers:
152
+ if key in keys:
153
+ return f"style {label} {key}"
154
+ return f"default {key}"
155
+
156
+
157
+ #: The theme of objects outside a figure, and of figures without a style.
158
+ BASE_THEME = Theme()
159
+
160
+
161
+ class Setting:
162
+ """One declared setting: ``check``, its :class:`~plot_io.validators.Validator` (a
163
+ function is wrapped in a :class:`~plot_io.validators.Custom`), validates and normalizes
164
+ a value (raising ``ValueError``). ``expr``: ``var()`` is accepted (default: unless the
165
+ setting is text). ``auto(obj)``: the final value of ``"auto"``, computed from the model
166
+ when the setting is resolved. ``group``, ``field``: the setting group it belongs to and
167
+ its member name there. Defaults are in :data:`plot_io.defaults.DEFAULTS`; ``default``
168
+ is the default of a class that has no ``DEFAULTS`` key for the setting (a plot type
169
+ added by a user)."""
170
+
171
+ __slots__ = ("name", "check", "inherited", "doc", "expr", "auto", "default", "group",
172
+ "field", "prefix")
173
+
174
+ def __init__(self, check: Validator | Callable[[Any], Any] | None = None, *,
175
+ inherited: bool = False, doc: str = "", expr: bool | None = None,
176
+ auto: Callable[[Any], Any] | None = None, default: Any = _MISS) -> None:
177
+ self.name = ""
178
+ self.check: Validator = as_validator(check)
179
+ self.inherited = inherited
180
+ self.doc = doc
181
+ self.expr = expr if expr is not None else check not in (text, str, family, anything,
182
+ None)
183
+ self.auto = auto
184
+ #: the default of a class outside ``DEFAULTS`` (a plot type a user adds), used when
185
+ #: no ``DEFAULTS`` key applies
186
+ self.default = default
187
+ self.group: Any = None
188
+ self.field: str | None = None
189
+ self.prefix = ""
190
+
191
+ @property
192
+ def kind(self) -> str | None:
193
+ """``"length"``, ``"angle"``, ``"number"`` or ``None``."""
194
+ return self.check.kind
195
+
196
+ @property
197
+ def group_key(self) -> str | None:
198
+ """The ``"Group.member"`` key of this setting's group member."""
199
+ g = self.group
200
+ return None if g is None else f"{g.__name__}.{self.field}"
201
+
202
+
203
+ def table(**settings: Setting) -> dict[str, Setting]:
204
+ """A ``SETTINGS`` table, with each setting's name filled in."""
205
+ for name, s in settings.items():
206
+ s.name = name
207
+ return settings
208
+
209
+
210
+ #: Setting names suggested for a name of the same idea that a class lacks (Hist's
211
+ #: ``shape`` for ``kind``, Bar's ``y_error`` for ``error``, Area's ``y`` for ``ys``).
212
+ SAME_IDEA = {"kind": "shape", "error": "y_error", "ys": "y"}
213
+
214
+
215
+ def unknown_message(name: str, candidates: Iterable[str], what: str,
216
+ own: Iterable[str] = ()) -> str:
217
+ """The error of an unknown setting ``name`` of ``what`` among ``candidates``; ``own``:
218
+ the candidates of the class itself, preferred for a matplotlib name's hint."""
219
+ if name == "theme":
220
+ return ("unknown setting 'theme': use style= (a style name, or a dict of settings "
221
+ "with an optional \"base\")")
222
+ candidates = list(candidates)
223
+ if name == "label" and "name" in candidates:
224
+ return f"unknown {what} setting 'label': use name= (the legend entry)"
225
+ if name.endswith("alpha"):
226
+ want = name[:-5] + "opacity"
227
+ if want in candidates or name == "alpha":
228
+ return (f"unknown {what} setting {name!r}: use {want}= (an opacity; alpha is "
229
+ "the alpha channel of a color)")
230
+ if name in ("color_map", "cmap") and "color_map" not in candidates \
231
+ and "sequential_map" in candidates:
232
+ from .color.maps import COLOR_MAP_HINT
233
+ return f"unknown {what} setting {name!r}: {COLOR_MAP_HINT}"
234
+ use = hints.setting(name, own) or hints.setting(name, candidates)
235
+ if use is not None:
236
+ return f"unknown {what} setting {name!r}: use {use}"
237
+ axis = hints.axis_setting(name)
238
+ if axis is not None:
239
+ return f"unknown {what} setting {name!r}: use {axis}"
240
+ near = [SAME_IDEA[name]] if SAME_IDEA.get(name) in candidates else \
241
+ difflib.get_close_matches(name, candidates, n=1, cutoff=0.6)
242
+ hint = f" (did you mean {near[0]!r}?)" if near else ""
243
+ return f"unknown {what} setting {name!r}{hint}"
244
+
245
+
246
+ class Plan:
247
+ """Where a setting of one class is looked up (see :func:`cascade`), computed once per
248
+ class: its owner style keys, its default and the key holding it, its group style key,
249
+ whether it is inherited and whether its default refers to the inherited value, and the
250
+ class rule."""
251
+
252
+ __slots__ = ("setting", "style_keys", "default", "default_key", "group_key",
253
+ "inherited", "inherit_default", "rule", "rule_key", "rule_first", "rule_at",
254
+ "shared")
255
+
256
+ def __init__(self, obj: "Settings", name: str) -> None:
257
+ s = obj.SETTINGS[name]
258
+ self.setting = s
259
+ self.inherited = s.inherited
260
+ keys: list[str] = []
261
+ self.default: Any = _MISS
262
+ self.default_key: str | None = None
263
+ for level in obj._levels(name):
264
+ keys.extend(f"{c}.{name}" for c in level)
265
+ hit = next((f"{c}.{name}" for c in level if f"{c}.{name}" in DEFAULTS), None)
266
+ if hit is not None:
267
+ self.default_key = hit
268
+ break
269
+ self.style_keys = tuple(keys)
270
+ self.group_key = s.group_key
271
+ if self.default_key is None and self.group_key is not None \
272
+ and self.group_key in DEFAULTS:
273
+ self.default_key = self.group_key
274
+ if self.default_key is None and s.default is not _MISS:
275
+ self.default_key = f"{type(obj).__name__}.{name}" # declared: not in DEFAULTS
276
+ if self.default_key is not None:
277
+ self.default = default_of(s, self.default_key)
278
+ self.inherit_default = self.inherited and self.default is not _MISS \
279
+ and refers_inherit(self.default)
280
+ #: whether values set on another object (``obj._shared()``) come before the rest
281
+ self.shared = type(obj)._shared is not Settings._shared
282
+ self.rule = None
283
+ self.rule_key: str | None = None
284
+ #: a rule of given values runs before the styles; another after the style keys of
285
+ #: its class and of more specific ones (``style_keys[:rule_at]``)
286
+ self.rule_first = False
287
+ self.rule_at = 0
288
+ for c in type(obj).__mro__:
289
+ r = c.__dict__.get("RULES", {}).get(name)
290
+ if r is not None:
291
+ self.rule, self.rule_key = r, f"{c.__name__}.{name}"
292
+ self.rule_first = isinstance(r, _GivenRule)
293
+ keys_ = self.style_keys
294
+ self.rule_at = keys_.index(self.rule_key) + 1 if self.rule_key in keys_ \
295
+ else len(keys_)
296
+ break
297
+
298
+
299
+ _PLANS: dict[Any, dict[str, Plan]] = {}
300
+
301
+
302
+ def class_default(cls: Any, name: str, keys: tuple[str, ...] = (), role: Any = None) -> Any:
303
+ """The ``DEFAULTS`` entry (as written) of setting ``name`` of objects of ``cls``, where
304
+ the cascade's :class:`Plan` finds it: the first of ``keys`` present, else along the
305
+ class's levels (``role``: an axis' ``"x"``, ``"y"`` or ``"c"``), else its group's.
306
+ Raises ``KeyError``."""
307
+ for k in keys:
308
+ if k in DEFAULTS:
309
+ return DEFAULTS[k]
310
+ obj: Any = object.__new__(cls) # the class's levels, without a model
311
+ if role is not None:
312
+ obj.role = role
313
+ obj.__dict__["_plans"] = {}
314
+ try:
315
+ plan = Plan(obj, name)
316
+ except AttributeError: # levels that need an instance's state
317
+ obj.__dict__["_levels"] = lambda _: _mro_levels(cls)
318
+ plan = Plan(obj, name)
319
+ if plan.default_key is None:
320
+ raise KeyError(f"no default for {cls.__name__}.{name}")
321
+ if plan.default_key not in DEFAULTS:
322
+ return plan.setting.default
323
+ return DEFAULTS[plan.default_key]
324
+
325
+
326
+ def cascade(obj: "Settings", name: str,
327
+ theme: "Theme | None" = None) -> tuple[Any, "Settings", str, str | None]:
328
+ """Where the value of setting ``name`` of ``obj`` comes from: ``(raw, holder, how,
329
+ key)``. ``how`` is ``"own"``, ``"figure"`` (set on ``holder``, the figure's axis of the
330
+ role), ``"rule"``, ``"style"`` (``key``: the style key),
331
+ ``"default"`` (``key``: the ``DEFAULTS`` key), ``"parent"`` (``holder``: the ancestor
332
+ whose value it is; ``raw`` unused), ``"theme"`` (the bare inherited name) or
333
+ ``"unset"``. ``raw`` is the stored value, as checked. ``theme``: ``obj._theme()``,
334
+ when the caller has it."""
335
+ v = _own(obj, name)
336
+ if v is not _MISS:
337
+ return v, obj, "own", None
338
+ plan = obj._plan(name)
339
+ if plan.shared:
340
+ src = obj._shared()
341
+ if src is not None:
342
+ v = _own(src, name)
343
+ if v is not _MISS:
344
+ return v, src, "figure", None
345
+ rule = plan.rule
346
+ if rule is not None and plan.rule_first:
347
+ v = _rule_value(obj, plan)
348
+ if v is not DECLINE:
349
+ return v, obj, "rule", plan.rule_key
350
+ rule = None
351
+ if theme is None:
352
+ theme = obj._theme()
353
+ group = None if plan.inherited else plan.group_key
354
+ keys_ = plan.style_keys
355
+ layers = theme.layers
356
+ if plan.inherited: # a style's bare name ends the farther styles
357
+ for i, (_, keys) in enumerate(layers):
358
+ if name in keys:
359
+ layers = layers[:i + 1]
360
+ break
361
+ if rule is not None:
362
+ near = keys_[:plan.rule_at]
363
+ for _, keys in layers:
364
+ for k in near:
365
+ v = keys.get(k, _MISS)
366
+ if v is not _MISS:
367
+ return v, obj, "style", k
368
+ v = _rule_value(obj, plan)
369
+ if v is not DECLINE:
370
+ return v, obj, "rule", plan.rule_key
371
+ keys_ = keys_[plan.rule_at:]
372
+ for _, keys in layers:
373
+ for k in keys_:
374
+ v = keys.get(k, _MISS)
375
+ if v is not _MISS:
376
+ return v, obj, "style", k
377
+ if group is not None:
378
+ v = keys.get(group, _MISS)
379
+ if v is not _MISS:
380
+ return v, obj, "style", group
381
+ if plan.inherited:
382
+ if plan.inherit_default:
383
+ return plan.default, obj, "default", plan.default_key
384
+ p = obj._parent()
385
+ while p is not None:
386
+ if name in p.SETTINGS:
387
+ return None, p, "parent", None
388
+ p = p._parent()
389
+ if plan.default is not _MISS:
390
+ return plan.default, obj, "default", plan.default_key
391
+ if plan.inherited:
392
+ return theme.get(name), obj, "theme", name
393
+ return None, obj, "unset", None
394
+
395
+
396
+ #: ``DEFAULTS``' bare values as checked, by (setting, value).
397
+ _CHECKED_DEFAULTS: dict[tuple[int, str], Any] = {}
398
+
399
+
400
+ def _checked_default(s: Setting, raw: str) -> Any:
401
+ key = (id(s), raw)
402
+ checked = _CHECKED_DEFAULTS.get(key, _MISS)
403
+ if checked is _MISS:
404
+ try:
405
+ checked = check_value(s, raw)
406
+ except (TypeError, ValueError):
407
+ checked = raw
408
+ _CHECKED_DEFAULTS[key] = checked
409
+ return checked
410
+
411
+
412
+ def _variable_default(s: Setting, raw: str) -> bool:
413
+ """Whether a bare default of ``DEFAULTS`` given as text (a named color, a color map
414
+ name) depends on the color variant."""
415
+ return has_fixed(_checked_default(s, raw))
416
+
417
+
418
+ def _varied_default(obj: "Settings", s: Setting, raw: str) -> Any:
419
+ """A bare default of ``DEFAULTS`` given as text in ``obj``'s color variant; ``None``
420
+ in the base variant (the text as given)."""
421
+ from .color.values import variant_of
422
+ if variant_of(obj) == "base":
423
+ return None
424
+ return _checked_default(s, raw).in_context(obj)
425
+
426
+
427
+ def _rule_value(obj: "Settings", plan: Plan) -> Any:
428
+ assert plan.rule is not None
429
+ v = plan.rule(obj)
430
+ if v is not DECLINE and v is not None and not isinstance(v, Value):
431
+ v = check_value(plan.setting, v)
432
+ return v
433
+
434
+
435
+ def given_rule(rule: Callable[[Any], Any]) -> Callable[[Any], Any]:
436
+ """Marks a class rule whose value the user gave elsewhere (a series' own setting): it
437
+ comes before the styles."""
438
+ return _GivenRule(rule)
439
+
440
+
441
+ class _GivenRule:
442
+ """A class rule of values given elsewhere (see :func:`given_rule`)."""
443
+
444
+ __slots__ = ("rule",)
445
+
446
+ def __init__(self, rule: Callable[[Any], Any]) -> None:
447
+ self.rule = rule
448
+
449
+ def __call__(self, obj: Any) -> Any:
450
+ return self.rule(obj)
451
+
452
+
453
+ class Settings:
454
+ """Base of every object with settings."""
455
+
456
+ SETTINGS: dict[str, Setting] = {}
457
+ #: Class rules: ``{name: rule(obj)}`` giving the value of a setting (as given) from
458
+ #: the model, a default after the style keys of the class (and of more specific ones);
459
+ #: ``DECLINE`` when the rule does not apply. A :func:`given_rule` comes before them.
460
+ RULES: dict[str, Callable[[Any], Any]] = {}
461
+
462
+ def __init__(self, **settings: Any) -> None:
463
+ self._values: dict[str, Any] = {}
464
+ self.set(**settings)
465
+
466
+ def _parent(self) -> "Settings | None":
467
+ return None
468
+
469
+ def _shared(self) -> "Settings | None":
470
+ """An object whose own values come right after this object's (see
471
+ :func:`cascade`); classes that have one override this."""
472
+ return None
473
+
474
+ def _theme(self) -> Theme:
475
+ p = self._parent()
476
+ return p._theme() if p is not None else BASE_THEME
477
+
478
+ @property
479
+ def kind(self) -> str:
480
+ return type(self).__name__
481
+
482
+ def set(self, **settings: Any) -> Self:
483
+ """Set settings; ``None`` goes back to the inherited or default value."""
484
+ settings = canonical_kwargs(settings, self.SETTINGS, self.kind)
485
+ checked = {}
486
+ for name, value in settings.items():
487
+ s = self.SETTINGS.get(name)
488
+ if s is None:
489
+ raise SettingError(unknown_message(name, self.SETTINGS, self.kind), self, name,
490
+ value)
491
+ checked[name] = None if value is None else self._checked(s, name, value)
492
+ for name, value in checked.items():
493
+ if value is None:
494
+ self._values.pop(name, None)
495
+ else:
496
+ self._values[name] = value
497
+ r = _spec.current()
498
+ if r is not None:
499
+ r.changed(self)
500
+ return self
501
+
502
+ def _checked(self, s: Setting, name: str, value: Any) -> Any:
503
+ """``value`` checked for setting ``s`` (``name``) of this object; a ``var()`` of
504
+ another of its settings is checked against it."""
505
+ try:
506
+ out = check_value(s, value)
507
+ except (TypeError, ValueError) as e:
508
+ raise SettingError(f"{self.kind} setting {name!r}: {e}", self, name,
509
+ value) from None
510
+ if isinstance(out, Var) and out.name in (name, "inherit") and out.fallback is None \
511
+ and not self._inherits(name):
512
+ raise SettingError(f"{self.kind} setting {name!r}: {out.source}: no parent of "
513
+ f"{self.kind} and no style has {name!r}", self, name, value)
514
+ if isinstance(out, Var) and out.name in self.SETTINGS \
515
+ and out.name not in (name, "inherit"):
516
+ target = self.SETTINGS[out.name].check
517
+ if s.check.accepts(target) is False:
518
+ raise SettingError(f"{self.kind} setting {name!r}: {out.source} is "
519
+ f"{target.noun}, expected {s.check.noun}", self, name, value)
520
+ return out
521
+
522
+ def _levels(self, name: str) -> tuple[tuple[str, ...], ...]:
523
+ """The classes whose style keys and defaults apply to setting ``name``, most
524
+ specific first, in levels: a level with a default ends the search."""
525
+ return _mro_levels(type(self))
526
+
527
+ def _level_key(self) -> Any:
528
+ """What the levels depend on besides the class (an axis: its role)."""
529
+ return None
530
+
531
+ def _plan(self, name: str) -> Plan:
532
+ table_ = self.__dict__.get("_plans")
533
+ if table_ is None:
534
+ key = (type(self), self._level_key())
535
+ table_ = _PLANS.get(key)
536
+ if table_ is None:
537
+ table_ = _PLANS[key] = {}
538
+ self.__dict__["_plans"] = table_
539
+ plan = table_.get(name)
540
+ if plan is None:
541
+ plan = table_[name] = Plan(self, name)
542
+ return plan
543
+
544
+ def _name(self, name: str) -> str:
545
+ """The setting ``name`` refers to (a shorthand's full name); ``ValueError`` for an
546
+ unknown name."""
547
+ if name in self.SETTINGS:
548
+ return name
549
+ key = canonical(name, self.SETTINGS)
550
+ if key not in self.SETTINGS:
551
+ raise SettingError(unknown_message(name, self.SETTINGS, self.kind), self, name)
552
+ return key
553
+
554
+ def is_set(self, name: str) -> bool:
555
+ """Whether setting ``name`` is set on this object."""
556
+ return _own(self, self._name(name)) is not _MISS
557
+
558
+ def _raw(self, name: str) -> Any:
559
+ """The stored value of setting ``name`` (as checked, before resolving), wherever
560
+ the cascade finds it."""
561
+ raw, holder, how, _ = cascade(self, name)
562
+ return holder._raw(name) if how == "parent" else raw
563
+
564
+ def get(self, name: str) -> Any:
565
+ """The value of a setting as given (``"4.5pt"``, ``"var(color)"``, ``6``): its own,
566
+ the style's, inherited or the default; see :meth:`resolve` for the final value."""
567
+ return given(self._raw(self._name(name)))
568
+
569
+ def resolve(self, name: str, *, of: Any = None, scale: Any = None, time: bool = False,
570
+ unit: str = "px") -> Any:
571
+ """The final value of a setting: lengths in px (``unit="data"``: in data units),
572
+ angles in degrees, ``var()`` and ``calc()`` evaluated, ``"auto"`` resolved by the
573
+ setting's rule, scaling applied.
574
+
575
+ ``of``: what ``%`` is of, for settings whose reference the caller knows; ``scale``:
576
+ px per data unit, for ``data`` units; ``time``: durations are accepted."""
577
+ if name not in self.SETTINGS:
578
+ name = self._name(name)
579
+ if of is None and scale is None and not time and unit == "px":
580
+ r = _spec._CURRENT.get()
581
+ if r is not None: # the value kept by the render, else resolved
582
+ d = r.values.get(id(self))
583
+ v = _MISS if d is None else d.get(name, _MISS)
584
+ if v is _MISS:
585
+ v = r.lookup(self, name)
586
+ if v is not _spec.LAZY:
587
+ return v
588
+ return self._resolve(name, of, scale, time, unit)
589
+
590
+ def _resolve(self, name: str, of: Any = None, scale: Any = None, time: bool = False,
591
+ unit: str = "px", theme: "Theme | None" = None) -> Any:
592
+ """:meth:`resolve` through the cascade, without the render's values."""
593
+ raw, holder, how, _ = cascade(self, name, theme)
594
+ s = self.SETTINGS[name]
595
+ if how == "parent":
596
+ # named colors and color maps: in this object's color variant
597
+ raw = holder._raw(name)
598
+ if has_fixed(raw):
599
+ return resolve_raw(self, name, s.check, raw, of, scale, time, unit)
600
+ if not (isinstance(raw, str) and _variable_default(s, raw)):
601
+ return holder.resolve(name)
602
+ how = "theme"
603
+ if how == "theme" and isinstance(raw, str) and _variable_default(s, raw):
604
+ varied = _varied_default(self, s, raw)
605
+ return raw if varied is None else varied
606
+ if raw is None or not needs_resolving(raw, s.check):
607
+ out = raw
608
+ elif may_refer(raw):
609
+ with guard(self, name):
610
+ out = resolve_raw(self, name, s.check, raw, of, scale, time, unit)
611
+ else:
612
+ out = resolve_raw(self, name, s.check, raw, of, scale, time, unit)
613
+ if s.auto is not None and isinstance(out, str) and out == "auto":
614
+ out = s.auto(self)
615
+ return out
616
+
617
+ def resolve_linear(self, name: str, **options: Any) -> tuple[float, float]:
618
+ """``(a, b)`` such that the setting resolves to ``a + b × of`` (``%`` of ``of``):
619
+ for settings whose reference varies item by item."""
620
+ a = self.resolve(name, of=0.0, **options)
621
+ return a, self.resolve(name, of=1.0, **options) - a
622
+
623
+ def explain(self, name: str, as_dict: bool = False) -> Any:
624
+ """Where the value of setting ``name`` comes from: its source (set here, a style
625
+ key, inherited, the default), the value as given, what it refers to (``var()``,
626
+ ``%``, ``em``) and the final value. Text, or a dict with ``as_dict``."""
627
+ from .units import explain_tree
628
+ return explain_tree(self, self._name(name), as_dict)
629
+
630
+ def _source(self, name: str) -> tuple[str, "Settings"]:
631
+ """Where ``name``'s stored value comes from, and the object holding it."""
632
+ raw, holder, how, key = cascade(self, name)
633
+ if how == "own":
634
+ return f"set on {self.kind}", self
635
+ if how == "figure":
636
+ return f"set on the figure's {getattr(self, 'role', '')} axis", holder
637
+ if how == "rule":
638
+ return f"rule {key}", self
639
+ if how == "style":
640
+ return self._theme().label(key or name), self
641
+ if how == "default":
642
+ return f"default {key}", self
643
+ if how == "parent":
644
+ src, owner = holder._source(name)
645
+ if src.startswith("inherited from "):
646
+ return src, owner
647
+ return f"inherited from {holder.kind}: {src}", owner
648
+ if how == "theme":
649
+ return self._theme().label(name) if name in self._theme() else "unset", self
650
+ return "unset", self
651
+
652
+ def settings(self) -> dict[str, Any]:
653
+ """The settings set explicitly on this object, as given."""
654
+ return {k: given(v) for k, v in self._values.items()}
655
+
656
+ def _children(self) -> "list[Settings]":
657
+ """The model objects below this one."""
658
+ return []
659
+
660
+ # -- what resolution needs ------------------------------------------------------
661
+
662
+ def _ancestors(self) -> "list[Settings]":
663
+ out = []
664
+ p = self._parent()
665
+ while p is not None:
666
+ out.append(p)
667
+ p = p._parent()
668
+ return out
669
+
670
+ def _root(self) -> "Settings":
671
+ r: Settings = self
672
+ p = r._parent()
673
+ while p is not None:
674
+ r, p = p, p._parent()
675
+ return r
676
+
677
+ def _scaling(self) -> float:
678
+ root = self._root()
679
+ if "scaling" in root.SETTINGS:
680
+ return root.resolve("scaling")
681
+ return 1.0
682
+
683
+ def _font_size(self) -> float:
684
+ """The font size ``em`` is of: this object's ``font_size``, else its parent's."""
685
+ if "font_size" in self.SETTINGS:
686
+ return self.resolve("font_size")
687
+ return self._parent_font_size()
688
+
689
+ def _parent_font_size(self) -> float:
690
+ for p in self._ancestors():
691
+ if "font_size" in p.SETTINGS:
692
+ return p.resolve("font_size")
693
+ return self._root_font_size()
694
+
695
+ def _root_font_size(self) -> float:
696
+ root = self._root()
697
+ if root is not self and "font_size" in root.SETTINGS:
698
+ return root.resolve("font_size")
699
+ v = self._theme().get("font_size", 11.0)
700
+ return float(v) * self._scaling() if isinstance(v, (int, float)) else 11.0
701
+
702
+ def _box(self, kind: str, name: str) -> float:
703
+ """The size (px) of a layout box, while the figure is laid out."""
704
+ r = _spec.current()
705
+ if r is not None:
706
+ for o in [self] + self._ancestors():
707
+ boxes = r.boxes.get(id(o))
708
+ if boxes and kind in boxes:
709
+ return boxes[kind]
710
+ raise _spec.NeedsBox(f"{self.kind} setting {name!r}: % of the "
711
+ f"{kind.replace('_', ' ')} is known only while the figure is "
712
+ f"drawn")
713
+
714
+ def _custom_var(self, key: str) -> Any:
715
+ """Custom variable ``--name``: from the nearest ``vars`` that has it, else the
716
+ style's."""
717
+ for o in [self] + self._ancestors():
718
+ if "vars" in o.SETTINGS and o.is_set("vars"):
719
+ d = o._values["vars"]
720
+ if key[2:] in d:
721
+ return d[key[2:]]
722
+ return self._theme().get(key)
723
+
724
+ def _inherits(self, name: str) -> bool:
725
+ """Whether an ancestor or the style has ``name`` (what ``inherit`` reads)."""
726
+ return any(name in p.SETTINGS for p in self._ancestors()) or name in self._theme()
727
+
728
+ def _parent_value(self, name: str) -> Any:
729
+ """``name`` as inherited, skipping this object (``inherit``, or ``var()`` of a
730
+ setting's own name)."""
731
+ for p in self._ancestors():
732
+ if name in p.SETTINGS:
733
+ return p.resolve(name)
734
+ theme = self._theme()
735
+ if name in theme:
736
+ s = self.SETTINGS.get(name)
737
+ raw = theme[name]
738
+ if s is not None and needs_resolving(raw, s.check):
739
+ return resolve_raw(self, name, s.check, raw)
740
+ return raw
741
+ return None
742
+
743
+ def _resolve_any(self, name: str) -> Any:
744
+ """``name`` of this object, or of the nearest ancestor that has it."""
745
+ for o in [self] + self._ancestors():
746
+ if name in o.SETTINGS:
747
+ return o.resolve(name)
748
+ if name in self._theme():
749
+ return self._theme()[name]
750
+ raise ValueError(f"var({name}): no setting {name!r} on {self.kind} or its parents")
751
+
752
+ def _check_of(self, name: str, own: bool = True) -> Validator | None:
753
+ """The validator of setting ``name`` of this object (``own``) or of the nearest
754
+ ancestor that has it; without ``own``, this object's when no ancestor has it (the
755
+ style's bare value is resolved by it); ``None``: no such setting."""
756
+ for o in ([self] if own else []) + self._ancestors():
757
+ if name in o.SETTINGS:
758
+ return o.SETTINGS[name].check
759
+ if not own and name in self.SETTINGS:
760
+ return self.SETTINGS[name].check
761
+ return None
762
+
763
+
764
+ _MRO_LEVELS: dict[type, tuple[tuple[str, ...], ...]] = {}
765
+
766
+
767
+ def _mro_levels(cls: type) -> tuple[tuple[str, ...], ...]:
768
+ out = _MRO_LEVELS.get(cls)
769
+ if out is None:
770
+ out = _MRO_LEVELS[cls] = tuple((c.__name__,) for c in cls.__mro__
771
+ if issubclass(c, Settings) and c is not Settings)
772
+ return out
773
+
774
+
775
+ def check_value(s: Setting, value: Any) -> Any:
776
+ """``value`` checked for setting ``s``: ``var()`` text as a :class:`Var`, else through
777
+ the setting's validator."""
778
+ if isinstance(value, Value): # already checked (a style built on another)
779
+ return value
780
+ if s.expr and isinstance(value, str):
781
+ v = parse(value, calc=False)
782
+ if v is not None:
783
+ return v
784
+ return s.check(value)
785
+
786
+
787
+ def route(kwargs: dict[str, Any], levels: list[type[Settings]],
788
+ extra: dict[int, Iterable[str]] | None = None) -> list[dict[str, Any]]:
789
+ """Split ``kwargs`` among ``levels`` (lowest first): each name goes to the lowest
790
+ level declaring it (``extra`` adds names a level accepts besides its settings)."""
791
+ out: list[dict[str, Any]] = [{} for _ in levels]
792
+ given_as: dict[tuple[int, str], str] = {}
793
+ for name, value in kwargs.items():
794
+ for i, cls in enumerate(levels):
795
+ key = canonical(name, cls.SETTINGS)
796
+ if key in cls.SETTINGS or (extra and name in extra.get(i, ())):
797
+ if key in out[i]:
798
+ raise ValueError(f"{cls.__name__}: {given_as[i, key]!r} and {name!r} are "
799
+ f"the same setting; give one")
800
+ out[i][key] = value
801
+ given_as[i, key] = name
802
+ break
803
+ else:
804
+ names = set().union(*(cls.SETTINGS for cls in levels))
805
+ raise ValueError(unknown_message(name, names, levels[0].__name__,
806
+ levels[0].SETTINGS))
807
+ return out