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/units.py ADDED
@@ -0,0 +1,768 @@
1
+ """Setting values: dimensions with units, ``var()`` and CSS math functions (``calc()``,
2
+ ``min()``, …: vec_io's grammar, :func:`vec_io.units.parse_math`).
3
+
4
+ A validator parses a value once, when it is set, into a plain Python value or a
5
+ :class:`Value` (:class:`Dim`, :class:`Var`, :class:`Calc`); :meth:`Settings.get` returns it
6
+ as given and :meth:`Settings.resolve` turns it into px (lengths), degrees (angles) or a
7
+ plain value.
8
+
9
+ Units: absolute ``px pt pc in cm mm Q``; ``em`` (the object's ``font_size``; in a
10
+ ``font_size`` setting, the parent's); ``rem`` (the figure's ``font_size``); ``%`` (of the
11
+ reference each setting declares, :data:`REFERENTS`); ``d`` or ``data`` (data units along an
12
+ axis; on a time axis ``d`` is a day) and durations; ``fr`` (figure widths and heights);
13
+ angles ``deg grad rad turn``. Bare numbers are px (degrees for angles); positions along
14
+ an axis (a decoration's ``min``/``max``) take bare numbers as data values.
15
+ """
16
+
17
+ import re
18
+ import threading
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ from vec_io.units import ANGLE_UNITS, PX_PER_UNIT, parse_math
22
+
23
+ if TYPE_CHECKING:
24
+ from .validators import Validator
25
+
26
+ __all__ = ["Value", "Dim", "Fixed", "Var", "Calc", "parse", "given", "resolve_raw",
27
+ "SettingError", "NeedsContext", "NeedsScale", "explain_tree", "refers_inherit", "FONT_KEYWORDS",
28
+ "DURATIONS", "REFERENTS", "Expr", "var", "calc"]
29
+
30
+ ABSOLUTE = frozenset(PX_PER_UNIT)
31
+ DURATIONS = {"ms": 0.001, "s": 1.0, "min": 60.0, "h": 3600.0, "d": 86400.0, "w": 604800.0}
32
+ #: Font-size keywords, relative to the figure's font size (matplotlib's scale).
33
+ FONT_KEYWORDS = {"xx-small": 0.579, "x-small": 0.694, "small": 0.833, "medium": 1.0,
34
+ "large": 1.2, "x-large": 1.44, "xx-large": 1.728}
35
+ _RELATIVE_FONT = {"smaller": 1 / 1.2, "larger": 1.2}
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Values
40
+ # ---------------------------------------------------------------------------
41
+
42
+ class Value:
43
+ """A setting value that needs resolving; ``source`` is the text as given."""
44
+
45
+ __slots__ = ("source",)
46
+ source: str
47
+
48
+ def __repr__(self) -> str:
49
+ return repr(self.source)
50
+
51
+
52
+ class Dim(Value):
53
+ """A number with a unit (``""``: bare), or a keyword (``number`` is ``None``)."""
54
+
55
+ __slots__ = ("number", "unit")
56
+
57
+ def __init__(self, source: str, number: float | None, unit: str) -> None:
58
+ self.source = source
59
+ self.number = number
60
+ self.unit = unit
61
+
62
+
63
+ class Fixed(Value):
64
+ """A value whose final form is known when given (a named color's hex): ``source`` is
65
+ the text as given, ``value`` the final value."""
66
+
67
+ __slots__ = ("value",)
68
+
69
+ def __init__(self, source: str, value: Any) -> None:
70
+ self.source = source
71
+ self.value = value
72
+
73
+ def in_context(self, obj: Any) -> Any:
74
+ """The final value for a setting of ``obj`` (a named color: in ``obj``'s color
75
+ variant)."""
76
+ return self.value
77
+
78
+ @property
79
+ def varies(self) -> bool:
80
+ """Whether the final value depends on the color variant."""
81
+ return False
82
+
83
+
84
+ def has_fixed(v: Any) -> bool:
85
+ """Whether ``v`` is or holds (one level) a :class:`Fixed` value, whose final value may
86
+ depend on the object resolving it."""
87
+ return isinstance(v, Fixed) or _has_fixed(v)
88
+
89
+
90
+ def _has_fixed(v: Any) -> bool:
91
+ if isinstance(v, dict):
92
+ v = v.values()
93
+ elif not isinstance(v, (list, tuple)):
94
+ return False
95
+ return any(isinstance(x, Fixed) for x in v)
96
+
97
+
98
+ def _fixed_final(v: Any, obj: Any) -> Any:
99
+ """``v`` with its :class:`Fixed` items replaced by their values for ``obj`` (one
100
+ level)."""
101
+ if isinstance(v, dict):
102
+ return {k: x.in_context(obj) if isinstance(x, Fixed) else x for k, x in v.items()}
103
+ out = [x.in_context(obj) if isinstance(x, Fixed) else x for x in v]
104
+ return tuple(out) if isinstance(v, tuple) else out
105
+
106
+
107
+ class Var(Value):
108
+ """``var(name[, fallback])``."""
109
+
110
+ __slots__ = ("name", "fallback")
111
+
112
+ def __init__(self, source: str, name: str, fallback: str | None) -> None:
113
+ self.source = source
114
+ self.name = name
115
+ self.fallback = fallback
116
+
117
+
118
+ class Calc(Value):
119
+ """A CSS math function (``calc()``, ``min()``, ``clamp()``, …) parsed by vec_io's
120
+ :func:`~vec_io.units.parse_math`: ``math`` evaluates it; ``refs``: the dimensions and
121
+ ``var()`` it holds, in order."""
122
+
123
+ __slots__ = ("math", "refs")
124
+
125
+ def __init__(self, source: str, math: Any, refs: tuple[Value, ...]) -> None:
126
+ self.source = source
127
+ self.math = math
128
+ self.refs = refs
129
+
130
+
131
+
132
+ def given(v: Any) -> Any:
133
+ """``v`` as it was given: the text of values, containers mapped (copies)."""
134
+ if isinstance(v, Value):
135
+ return v.source
136
+ if isinstance(v, tuple):
137
+ return tuple(given(x) for x in v)
138
+ if isinstance(v, list):
139
+ return [given(x) for x in v]
140
+ if isinstance(v, dict):
141
+ return {k: given(x) for k, x in v.items()}
142
+ return v
143
+
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # Parsing
147
+ # ---------------------------------------------------------------------------
148
+
149
+ #: Units plot_io resolves inside math functions besides the CSS ones.
150
+ _MATH_UNITS = ("data", "fr", *DURATIONS)
151
+ _FUNCTION = re.compile(r"([A-Za-z][\w-]*)\s*\(")
152
+ _NAME = re.compile(r"\s*(--[\w-]+|[A-Za-z_][\w-]*)\s*")
153
+ _SCAN = re.compile(r"""
154
+ (?P<var>\bvar\s*\(\s*(?P<vname>--[\w-]+|[A-Za-z_][\w-]*))
155
+ | (?P<name>-*[A-Za-z_][\w-]*)
156
+ | (?P<num>(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)(?P<unit>[A-Za-z]+|%)?
157
+ """, re.X)
158
+
159
+
160
+ def _parse_var(s: str, start: int) -> Var:
161
+ """``var(name[, fallback])``, ``s[start:]`` following its ``(``."""
162
+ m = _NAME.match(s, start)
163
+ if m is None:
164
+ raise ValueError(f"{s}: expected a setting name at {start}")
165
+ name, pos, fallback = m.group(1), m.end(), None
166
+ if s.startswith(",", pos):
167
+ depth, i = 0, pos + 1
168
+ while i < len(s):
169
+ if s[i] == "(":
170
+ depth += 1
171
+ elif s[i] == ")":
172
+ if depth == 0:
173
+ break
174
+ depth -= 1
175
+ i += 1
176
+ fallback = s[pos + 1:i].strip()
177
+ if not fallback:
178
+ raise ValueError(f"{s}: expected a fallback at {i}")
179
+ pos = i
180
+ if not s.startswith(")", pos):
181
+ raise ValueError(f"{s}: expected ')' at {pos}")
182
+ if s[pos + 1:].strip():
183
+ raise ValueError(f"{s}: unexpected text after var() at {pos + 1}")
184
+ return Var(s, name, fallback)
185
+
186
+
187
+ #: The longest math function text, and its deepest nesting of parentheses.
188
+ MAX_MATH_LENGTH, MAX_MATH_DEPTH = 1000, 32
189
+
190
+
191
+ def _parse_math(s: str, units: frozenset[str] | None) -> Calc:
192
+ """A math function; ``units``: the units allowed in it (``None``: all)."""
193
+ if len(s) > MAX_MATH_LENGTH:
194
+ raise ValueError(f"a math function of at most {MAX_MATH_LENGTH} characters, "
195
+ f"got {len(s)}")
196
+ depth = 0
197
+ for i, c in enumerate(s):
198
+ depth += (c == "(") - (c == ")")
199
+ if depth > MAX_MATH_DEPTH:
200
+ raise ValueError(f"{s[:20]}…: parentheses nested deeper than {MAX_MATH_DEPTH} "
201
+ f"at {i + 1}")
202
+ math = parse_math(s, units=_MATH_UNITS)
203
+ refs: list[Value] = []
204
+ for m in _SCAN.finditer(s):
205
+ if m["var"]:
206
+ refs.append(Var(f"var({m['vname']})", m["vname"], None))
207
+ elif m["name"]:
208
+ if m["name"].lower() == "inherit":
209
+ refs.append(Var("inherit", "inherit", None))
210
+ elif m["unit"]:
211
+ unit = m["unit"].lower()
212
+ if units is not None and unit not in units:
213
+ raise ValueError(f"{s}: unit {unit!r} not allowed here at {m.start()}")
214
+ refs.append(Dim(m.group(), float(m["num"]), unit))
215
+ return Calc(s, math, tuple(refs))
216
+
217
+
218
+ def parse(text: str, units: frozenset[str] | None = None, calc: bool = True) -> Value | None:
219
+ """``var(…)`` or ``inherit`` (the parent's value of the same setting), or, when
220
+ ``calc``, a CSS math function (``calc()``, ``min()``, …; vec_io's grammar), as a
221
+ :class:`Value`; ``None`` for other text. ``units``: the units allowed in a math
222
+ function. Raises ``ValueError`` naming the position of an error."""
223
+ s = text.strip()
224
+ if s.lower() == "inherit":
225
+ return Var(s, "inherit", None)
226
+ m = _FUNCTION.match(s)
227
+ if m is None:
228
+ return None
229
+ if m.group(1).lower() == "var":
230
+ return _parse_var(s, m.end())
231
+ return _parse_math(s, units) if calc else None
232
+
233
+
234
+
235
+ def refers_inherit(v: Any) -> bool:
236
+ """Whether a stored value refers to ``inherit``."""
237
+ if isinstance(v, Var):
238
+ return v.name == "inherit"
239
+ if isinstance(v, Calc):
240
+ return "inherit" in v.math.variables
241
+ return False
242
+
243
+
244
+ # ---------------------------------------------------------------------------
245
+ # ---------------------------------------------------------------------------
246
+ # What % is of
247
+ # ---------------------------------------------------------------------------
248
+
249
+ #: What ``%`` of a length is of (``length(percent=…)``), besides another setting of the
250
+ #: object: ``{key: (meaning, how)}``. ``how``: ``"em"`` (the font size ``em`` is of),
251
+ #: ``"box"`` (a layout box, known while the figure is laid out) or ``"given"`` (known
252
+ #: where the setting is used, which passes it as ``of=``).
253
+ REFERENTS: dict[str, tuple[str, str]] = {
254
+ "em": ("the font size", "em"),
255
+ "axes_width": ("the axes width", "box"),
256
+ "axes_height": ("the axes height", "box"),
257
+ "across": ("the axes side across the axis", "box"),
258
+ "along": ("the axes side along the axis", "box"),
259
+ "axes": ("the axes width (across) or height (down)", "given"),
260
+ "axes_length": ("the axes length along the axis", "given"),
261
+ "data_range": ("the axis range", "given"),
262
+ "bar_across": ("the axes side across the colorbar", "given"),
263
+ "bar_thickness": ("the span along a key, the axes side across a bar", "given"),
264
+ "bar_along": ("the span along the colorbar (the axes side of an attached one)", "given"),
265
+ "figure": ("the figure width (across) or height (down)", "given"),
266
+ "room": ("the plots' room: the figure less its margins, spacing and lanes (and the "
267
+ "plots' margins with `align=\"axes\"`)", "given"),
268
+ "slot": ("the slot: one category, or the smallest gap between positions", "given"),
269
+ "slot_part": ("each series' part of a shared slot", "given"),
270
+ "box_width": ("the box width", "given"),
271
+ "radius": ("the radius", "given"),
272
+ }
273
+
274
+
275
+ def percent_meaning(ref: Any) -> str | None:
276
+ """What ``%`` of a length with ``percent=ref`` is of, in words."""
277
+ if ref is None:
278
+ return None
279
+ if ref in REFERENTS:
280
+ return REFERENTS[ref][0]
281
+ return f"`{ref}`"
282
+
283
+
284
+ # ---------------------------------------------------------------------------
285
+ # Resolution
286
+ # ---------------------------------------------------------------------------
287
+
288
+ _local = threading.local()
289
+
290
+
291
+ def _stack() -> list[tuple[int, str, str]]:
292
+ s = getattr(_local, "stack", None)
293
+ if s is None:
294
+ s = _local.stack = []
295
+ return s
296
+
297
+
298
+ class guard:
299
+ """Detects reference loops while ``obj.name`` resolves."""
300
+
301
+ __slots__ = ("key",)
302
+
303
+ def __init__(self, obj: Any, name: str) -> None:
304
+ self.key = (id(obj), name, f"{obj.kind}.{name}")
305
+
306
+ def __enter__(self) -> None:
307
+ st = _stack()
308
+ if any(k[:2] == self.key[:2] for k in st):
309
+ chain = [k[2] for k in st[next(i for i, k in enumerate(st)
310
+ if k[:2] == self.key[:2]):]]
311
+ st.clear()
312
+ raise ValueError("reference loop: " + " → ".join(chain + [self.key[2]]))
313
+ st.append(self.key)
314
+
315
+ def __exit__(self, *exc: Any) -> None:
316
+ st = _stack()
317
+ if st and st[-1] == self.key:
318
+ st.pop()
319
+
320
+
321
+ class Expr(str):
322
+ """A setting value written with ``var()`` or ``calc()`` (made by :func:`var` and
323
+ :func:`calc`): the type of such values in the typed API, where a plain string is
324
+ flagged for a number, a boolean or a choice."""
325
+
326
+ __slots__ = ()
327
+
328
+
329
+ def var(name: str, fallback: Any = None) -> Expr:
330
+ """``var(name)``, or ``var(name, fallback)``: the value of setting ``name`` (or custom
331
+ variable ``--name``) of the object or its parents."""
332
+ return Expr(f"var({name})" if fallback is None else f"var({name}, {fallback})")
333
+
334
+
335
+ def calc(expression: Any) -> Expr:
336
+ """``calc(expression)``: CSS math of numbers, lengths, angles, ``var()`` and
337
+ ``inherit``."""
338
+ return Expr(f"calc({expression})")
339
+
340
+
341
+ class SettingError(ValueError):
342
+ """A value a setting does not take, found when it is set or resolved: ``owner``, the
343
+ object whose setting it is; ``name``, the setting; ``value``, the value as given."""
344
+
345
+ def __init__(self, message: str, owner: Any = None, name: str | None = None,
346
+ value: Any = None) -> None:
347
+ super().__init__(message)
348
+ self.owner = owner
349
+ self.name = name
350
+ self.value = value
351
+
352
+
353
+ class NeedsContext(SettingError):
354
+ """A value that needs what only its use provides: the reference of ``%``, a layout
355
+ box, an axis scale (it stays unresolved in a render's spec)."""
356
+
357
+
358
+ class NeedsScale(NeedsContext):
359
+ """A length in px resolved in data units before the axis scale is known."""
360
+
361
+
362
+ class Context:
363
+ """What one resolution needs besides the object: ``of`` (the reference of ``%``),
364
+ ``scale`` (px per data unit), ``time`` (durations accepted), ``unit`` (``"px"`` or
365
+ ``"data"``: the unit of the result)."""
366
+
367
+ __slots__ = ("obj", "name", "check", "of", "scale", "time", "unit", "raw")
368
+
369
+ def __init__(self, obj: Any, name: str, check: "Validator", of: Any, scale: Any,
370
+ time: bool, unit: str, raw: Any = None) -> None:
371
+ self.obj, self.name, self.check = obj, name, check
372
+ self.of, self.scale, self.time, self.unit = of, scale, time, unit
373
+ self.raw = raw
374
+
375
+ def error(self, msg: str, context: bool = False) -> SettingError:
376
+ cls = NeedsContext if context else SettingError
377
+ return cls(f"{self.obj.kind} setting {self.name!r}: {msg}", self.obj, self.name,
378
+ given(self.raw))
379
+
380
+
381
+ def _scaling(ctx: Context) -> float:
382
+ if not ctx.check.scaled:
383
+ return 1.0
384
+ return ctx.obj._scaling()
385
+
386
+
387
+ def _px_to_unit(px: float, ctx: Context) -> float:
388
+ if ctx.unit == "data":
389
+ if not ctx.scale:
390
+ raise NeedsScale(f"{ctx.obj.kind} setting {ctx.name!r}: a length in px needs "
391
+ f"the axis scale here")
392
+ return px / ctx.scale
393
+ return px
394
+
395
+
396
+ def _data_to_unit(d: float, ctx: Context) -> float:
397
+ if ctx.unit == "data":
398
+ return d
399
+ if ctx.scale is None:
400
+ raise ctx.error("data units are only valid along a linear or category axis", True)
401
+ return d * ctx.scale
402
+
403
+
404
+ def _percent_ref(ctx: Context) -> float:
405
+ if ctx.of is not None:
406
+ return float(ctx.of)
407
+ ref = ctx.check.percent
408
+ if ref is None:
409
+ raise ctx.error("% is not accepted")
410
+ if ref in REFERENTS and REFERENTS[ref][1] == "given":
411
+ raise ctx.error("% needs a reference here", True)
412
+ if ref == "em":
413
+ return _px_to_unit(_em(ctx), ctx)
414
+ if ref in REFERENTS and REFERENTS[ref][1] == "box":
415
+ return _px_to_unit(ctx.obj._box(ref, ctx.name), ctx)
416
+ return ctx.obj.resolve(ref)
417
+
418
+
419
+ def _em(ctx: Context) -> float:
420
+ obj = ctx.obj
421
+ basis = ctx.check.em
422
+ if basis is not None:
423
+ return obj.resolve(basis)
424
+ if ctx.name == "font_size":
425
+ return obj._parent_font_size()
426
+ return obj._font_size()
427
+
428
+
429
+ def _dim(d: Dim, ctx: Context) -> float:
430
+ n, u = d.number, d.unit
431
+ if n is None: # font keyword
432
+ if u in _RELATIVE_FONT:
433
+ return _px_to_unit(_em(ctx) * _RELATIVE_FONT[u], ctx)
434
+ return _px_to_unit(ctx.obj._root_font_size() * FONT_KEYWORDS[u], ctx)
435
+ if u in ABSOLUTE:
436
+ return _px_to_unit(n * PX_PER_UNIT[u] * _scaling(ctx), ctx)
437
+ if u == "em":
438
+ return _px_to_unit(n * _em(ctx), ctx)
439
+ if u == "rem":
440
+ return _px_to_unit(n * ctx.obj._root_font_size(), ctx)
441
+ if u == "%":
442
+ return n / 100.0 * _percent_ref(ctx)
443
+ if u == "data":
444
+ return _data_to_unit(n, ctx)
445
+ if u == "d" and not ctx.time: # data units (a day on a time axis)
446
+ return _data_to_unit(n, ctx)
447
+ if u in DURATIONS:
448
+ if not ctx.time:
449
+ raise ctx.error(f"a duration ({d.source!r}) needs a time axis", True)
450
+ return _data_to_unit(n * DURATIONS[u], ctx)
451
+ if u in ANGLE_UNITS:
452
+ return n * ANGLE_UNITS[u]
453
+ if u == "fr":
454
+ raise ctx.error("fr units are only valid in figure widths and heights")
455
+ raise ctx.error(f"unknown unit {u!r}")
456
+
457
+
458
+ def _bare(n: float, ctx: Context) -> float:
459
+ if ctx.check.kind != "length":
460
+ return n
461
+ if ctx.check.bare == "data":
462
+ return _data_to_unit(n, ctx)
463
+ return _px_to_unit(n * _scaling(ctx), ctx)
464
+
465
+
466
+ def _var(v: Var, ctx: Context, checked: bool = True) -> Any:
467
+ """The value ``v`` refers to: as a value of this setting (``checked``), else as the
468
+ referred setting has it (a ``calc()`` operand)."""
469
+ obj = ctx.obj
470
+ if v.name.startswith("--"):
471
+ raw = obj._custom_var(v.name)
472
+ if raw is None:
473
+ if v.fallback is None:
474
+ raise ctx.error(f"unknown variable {v.name!r}")
475
+ raw = v.fallback
476
+ with guard(obj, v.name):
477
+ return _from_text(raw, ctx)
478
+ if v.name == ctx.name or v.name == "inherit":
479
+ if not obj._inherits(ctx.name):
480
+ if v.fallback is None:
481
+ raise ctx.error(f"{v.source}: no parent of {obj.kind} and no style has "
482
+ f"{ctx.name!r}")
483
+ return _from_text(v.fallback, ctx)
484
+ out = obj._parent_value(ctx.name)
485
+ target = obj._check_of(ctx.name, own=False)
486
+ else:
487
+ out = obj._resolve_any(v.name)
488
+ target = obj._check_of(v.name)
489
+ if out is None:
490
+ return None if v.fallback is None else _from_text(v.fallback, ctx)
491
+ return _take(v, out, target, ctx) if checked else out
492
+
493
+
494
+ def _take(v: Var, out: Any, target: "Validator | None", ctx: Context) -> Any:
495
+ """``out``, the final value of the setting ``v`` refers to (checked by ``target``;
496
+ ``None``: a style's bare value), as a value of this setting."""
497
+ check = ctx.check
498
+ ok = None if target is None else check.accepts(target)
499
+ if ok:
500
+ return out if target is check else check.finish(out, ctx)
501
+ if ok is False:
502
+ assert target is not None
503
+ raise ctx.error(f"{v.source} is {target.noun}, expected {check.noun}")
504
+ try:
505
+ raw = check(out)
506
+ except (TypeError, ValueError) as e:
507
+ raise ctx.error(f"{v.source} is {out!r}: {e}") from None
508
+ return resolve_value(raw, ctx)
509
+
510
+
511
+ def _from_text(raw: Any, ctx: Context) -> Any:
512
+ """A custom variable or fallback, checked and resolved as a value of this setting."""
513
+ try:
514
+ raw = ctx.check(raw)
515
+ except (TypeError, ValueError) as e:
516
+ raise ctx.error(str(e)) from None
517
+ return resolve_value(raw, ctx)
518
+
519
+
520
+ def _finite(x: float, ctx: Context) -> float:
521
+ if x - x != 0: # inf or NaN
522
+ raise ctx.error(f"{given(ctx.raw)} gives {x} (not finite)")
523
+ return x
524
+
525
+
526
+ #: The unit of a ``var()`` value inside a math function: the setting's own unit.
527
+ _FINAL = "="
528
+
529
+
530
+ def _calc(c: "Calc", ctx: Context) -> tuple[float, bool]:
531
+ """Value of a math function and whether it is a dimension (length or angle)."""
532
+ per: dict[str, float] = {}
533
+
534
+ def final(unit: str) -> float:
535
+ """One ``unit`` in the setting's unit."""
536
+ if unit not in per:
537
+ per[unit] = 1.0 if unit == _FINAL else _dim(Dim(f"1{unit}", 1.0, unit), ctx)
538
+ return per[unit]
539
+
540
+ def px(unit: str) -> float:
541
+ return final(unit) / final("px")
542
+
543
+ def var(name: str, fallback: str | None) -> Any:
544
+ v = Var(name if name == "inherit" and fallback is None else
545
+ f"var({name})" if fallback is None else f"var({name}, {fallback})",
546
+ name, fallback)
547
+ out = _var(v, ctx, checked=False)
548
+ if isinstance(out, bool) or not isinstance(out, (int, float)):
549
+ raise ctx.error(f"{v.source} is not a number ({out!r})")
550
+ if name.startswith("--") or name in (ctx.name, "inherit"):
551
+ kind = ctx.check.kind
552
+ else:
553
+ target = ctx.obj._check_of(name)
554
+ kind = None if target is None else target.kind
555
+ x = _finite(float(out), ctx)
556
+ return {"deg": x} if kind == "angle" else {_FINAL: x} if kind == "length" else x
557
+
558
+ try:
559
+ value = c.math.evaluate(px, var)
560
+ except SettingError:
561
+ raise
562
+ except (ValueError, ZeroDivisionError) as e:
563
+ raise ctx.error(str(e)) from None
564
+ for x in value.values():
565
+ _finite(x, ctx)
566
+ if "" in value:
567
+ if len(value) > 1:
568
+ raise ctx.error(f"{c.source} adds a dimension and a plain number")
569
+ return value[""], False
570
+ return _finite(sum(x * final(u) for u, x in value.items()), ctx), True
571
+
572
+
573
+ def resolve_value(raw: Any, ctx: Context) -> Any:
574
+ """The final value of ``raw`` (a stored value of ``ctx.name``)."""
575
+ check = ctx.check
576
+ kind = check.kind
577
+ if isinstance(raw, Fixed):
578
+ return raw.in_context(ctx.obj)
579
+ if isinstance(raw, Var):
580
+ return _var(raw, ctx)
581
+ if isinstance(raw, Calc):
582
+ x, is_dim = _calc(raw, ctx)
583
+ if kind == "length" and not is_dim:
584
+ x = _bare(x, ctx)
585
+ return check.finish(x, ctx)
586
+ if isinstance(raw, Dim):
587
+ return check.finish(_dim(raw, ctx), ctx)
588
+ if kind is not None and isinstance(raw, (int, float)) and not isinstance(raw, bool):
589
+ return _bare(float(raw), ctx)
590
+ inner = check.inner
591
+ if inner is not None and isinstance(raw, (tuple, list)):
592
+ sub = Context(ctx.obj, ctx.name, inner, ctx.of, ctx.scale, ctx.time, ctx.unit,
593
+ ctx.raw)
594
+ out = [resolve_value(x, sub) for x in raw]
595
+ return tuple(out) if isinstance(raw, tuple) else out
596
+ if _has_fixed(raw):
597
+ return _fixed_final(raw, ctx.obj)
598
+ return raw
599
+
600
+
601
+ def resolve_raw(obj: Any, name: str, check: "Validator", raw: Any, of: Any = None,
602
+ scale: Any = None, time: bool = False, unit: str = "px") -> Any:
603
+ """The final value of ``raw``, a stored value of setting ``name`` of ``obj``."""
604
+ return resolve_value(raw, Context(obj, name, check, of, scale, time, unit, raw))
605
+
606
+
607
+ def may_refer(raw: Any) -> bool:
608
+ """Whether resolving ``raw`` may read other settings (and so loop back): a ``var()``, a
609
+ ``calc()``, ``%`` or a relative unit."""
610
+ if isinstance(raw, Dim):
611
+ return raw.number is None or raw.unit in ("%", "em", "rem")
612
+ return isinstance(raw, (Var, Calc)) or isinstance(raw, (tuple, list))
613
+
614
+
615
+ def needs_resolving(raw: Any, check: "Validator") -> bool:
616
+ """Whether ``raw`` differs from its final value (fast path for plain values)."""
617
+ if isinstance(raw, Value):
618
+ return True
619
+ if check.kind == "length" and isinstance(raw, (int, float)):
620
+ return True
621
+ if check.inner is not None and isinstance(raw, (tuple, list)):
622
+ return True
623
+ return _has_fixed(raw)
624
+
625
+
626
+ # ---------------------------------------------------------------------------
627
+ # Explaining a value
628
+ # ---------------------------------------------------------------------------
629
+
630
+ def _refs(raw: Any, out: list[Any]) -> list[Any]:
631
+ """The ``var()`` names and dimensions a stored value refers to."""
632
+ if isinstance(raw, (Var, Dim)):
633
+ out.append(raw)
634
+ elif isinstance(raw, Calc):
635
+ out.extend(raw.refs)
636
+ elif isinstance(raw, (tuple, list)):
637
+ for x in raw:
638
+ _refs(x, out)
639
+ return out
640
+
641
+
642
+ def explain_tree(obj: Any, name: str, as_dict: bool) -> Any:
643
+ """See :meth:`Settings.explain`."""
644
+ tree = _explain(obj, name, 0, set())
645
+ return tree if as_dict else "\n".join(_lines(tree, 0))
646
+
647
+
648
+ def _final(obj: Any, name: str) -> Any:
649
+ try:
650
+ return obj.resolve(name)
651
+ except ValueError as e:
652
+ return f"<{e}>"
653
+
654
+
655
+ def _explain(obj: Any, name: str, depth: int, seen: set[Any]) -> dict[str, Any]:
656
+ source, owner = obj._source(name)
657
+ raw = owner._raw(name)
658
+ c = owner.SETTINGS[name].check if name in owner.SETTINGS else None
659
+ kind = None if c is None else c.kind or (c.inner.kind if c.inner is not None else None)
660
+ node: dict[str, Any] = {"name": f"{obj.kind}.{name}", "value": _final(obj, name),
661
+ "given": given(raw), "source": source, "refers": [],
662
+ "unit": {"length": "px", "angle": "°"}.get(kind or "", "")}
663
+ variant = _variant_used(obj, name, raw)
664
+ if variant is not None:
665
+ node["variant"] = variant
666
+ key = (id(owner), name)
667
+ if key in seen or depth > 8:
668
+ return node
669
+ seen = seen | {key}
670
+ check = None if c is None else c.inner or c # pairs, dash lists: their items
671
+ refs, keys = [], set()
672
+ for r in _refs(raw, []): # each reference once
673
+ k = (type(r).__name__, r.name if isinstance(r, Var) else None,
674
+ r.unit if isinstance(r, Dim) else None)
675
+ if k not in keys:
676
+ keys.add(k)
677
+ refs.append(r)
678
+ for r in refs:
679
+ if isinstance(r, Var):
680
+ if r.name.startswith("--"):
681
+ node["refers"].append({"name": r.name, "value": owner._custom_var(r.name),
682
+ "given": owner._custom_var(r.name),
683
+ "source": "custom variable", "refers": []})
684
+ continue
685
+ target = owner if r.name not in (name, "inherit") else None
686
+ if target is None:
687
+ parent = next((p for p in owner._ancestors() if name in p.SETTINGS), None)
688
+ if parent is not None:
689
+ node["refers"].append(_explain(parent, name, depth + 1, seen))
690
+ continue
691
+ holder = next((o for o in [owner] + owner._ancestors() if r.name in o.SETTINGS),
692
+ None)
693
+ if holder is not None:
694
+ node["refers"].append(_explain(holder, r.name, depth + 1, seen))
695
+ elif isinstance(r, Dim):
696
+ u = r.unit
697
+ if u == "%":
698
+ ref = None if check is None else check.percent
699
+ if isinstance(ref, str) and ref in owner.SETTINGS:
700
+ node["refers"].append(_explain(owner, ref, depth + 1, seen))
701
+ elif ref == "em" or (ref is None and name == "font_size"):
702
+ node["refers"].append(_font_node(owner, name, depth, seen, check))
703
+ else:
704
+ node["refers"].append({"name": f"% of {ref}", "value": None,
705
+ "given": None, "source": "layout", "refers": []})
706
+ elif u in ("em", "smaller", "larger") or (r.number is None and u in _RELATIVE_FONT):
707
+ node["refers"].append(_font_node(owner, name, depth, seen, check))
708
+ elif u == "rem" or (r.number is None):
709
+ root = owner._root()
710
+ if "font_size" in root.SETTINGS:
711
+ node["refers"].append(_explain(root, "font_size", depth + 1, seen))
712
+ elif u in ABSOLUTE and (check is None or check.scaled):
713
+ k = owner._scaling()
714
+ if k != 1.0:
715
+ node["refers"].append({"name": "scaling", "value": k, "given": k,
716
+ "source": "Figure.scaling", "refers": []})
717
+ return node
718
+
719
+
720
+ def _font_node(obj: Any, name: str, depth: int, seen: set[Any],
721
+ check: "Validator | None" = None) -> dict[str, Any]:
722
+ basis = None if check is None else check.em
723
+ if basis is not None:
724
+ return _explain(obj, basis, depth + 1, seen)
725
+ holders = obj._ancestors() if name == "font_size" else [obj] + obj._ancestors()
726
+ holder = next((o for o in holders if "font_size" in o.SETTINGS), None)
727
+ if holder is None:
728
+ return {"name": "font_size", "value": obj._root_font_size(), "given": None,
729
+ "source": "theme", "refers": []}
730
+ return _explain(holder, "font_size", depth + 1, seen)
731
+
732
+
733
+ def _show(v: Any, unit: str = "") -> str:
734
+ if isinstance(v, float):
735
+ return f"{v:.6g}{unit}"
736
+ if isinstance(v, tuple) and all(isinstance(x, float) for x in v):
737
+ return "(" + ", ".join(f"{x:.6g}{unit}" for x in v) + ")"
738
+ return repr(v)
739
+
740
+
741
+ def _variant_used(obj: Any, name: str, raw: Any) -> str | None:
742
+ """The color variant a setting's named colors or color maps are resolved in, when not
743
+ base."""
744
+ from .settings import _variable_default
745
+ s = obj.SETTINGS.get(name)
746
+ items = raw.values() if isinstance(raw, dict) else raw if isinstance(raw, (list, tuple)) \
747
+ else (raw,)
748
+ if s is None or not (any(isinstance(x, Fixed) and x.varies for x in items)
749
+ or isinstance(raw, str) and _variable_default(s, raw)):
750
+ return None
751
+ from .color.values import variant_of
752
+ v = variant_of(obj)
753
+ return None if v == "base" else v
754
+
755
+
756
+ def _lines(node: dict[str, Any], depth: int) -> list[str]:
757
+ pad = " " * depth
758
+ given_ = node["given"]
759
+ head = f"{pad}{node['name']} = {_show(node['value'], node.get('unit', ''))}"
760
+ if given_ is not None and given_ != node["value"]:
761
+ head += f" ← {_show(given_)}"
762
+ variant = node.get("variant")
763
+ head += f" ({node['source']}; color_variant {variant})" if variant \
764
+ else f" ({node['source']})"
765
+ out = [head]
766
+ for r in node["refers"]:
767
+ out.extend(_lines(r, depth + 1))
768
+ return out