plot-io 0.12.0__py3-none-win_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. plot_io/__init__.py +73 -0
  2. plot_io/_methods.py +1691 -0
  3. plot_io/_native/__init__.py +199 -0
  4. plot_io/_native/__main__.py +197 -0
  5. plot_io/_native/_api.py +457 -0
  6. plot_io/_native/bin/windows-arm64.dll +0 -0
  7. plot_io/_native/kernels.py +335 -0
  8. plot_io/_native/manifest.json +49 -0
  9. plot_io/axis.py +1347 -0
  10. plot_io/boxes.py +1156 -0
  11. plot_io/canvas.py +389 -0
  12. plot_io/color/__init__.py +3 -0
  13. plot_io/color/data.py +1646 -0
  14. plot_io/color/maps.py +922 -0
  15. plot_io/color/values.py +295 -0
  16. plot_io/color/variants.py +1246 -0
  17. plot_io/colorbar.py +1426 -0
  18. plot_io/columns.py +614 -0
  19. plot_io/decorations.py +920 -0
  20. plot_io/defaults.py +809 -0
  21. plot_io/density.py +113 -0
  22. plot_io/ext.py +68 -0
  23. plot_io/facets.py +240 -0
  24. plot_io/figure.py +788 -0
  25. plot_io/formatters.py +97 -0
  26. plot_io/genome/__init__.py +17 -0
  27. plot_io/genome/adapters.py +356 -0
  28. plot_io/genome/combine.py +123 -0
  29. plot_io/genome/contacts.py +508 -0
  30. plot_io/genome/intervals.py +750 -0
  31. plot_io/genome/lanes.py +191 -0
  32. plot_io/genome/layout.py +184 -0
  33. plot_io/genome/links.py +364 -0
  34. plot_io/genome/pack.py +27 -0
  35. plot_io/genome/readers.py +145 -0
  36. plot_io/genome/regions.py +228 -0
  37. plot_io/genome/scale.py +349 -0
  38. plot_io/genome/series.py +344 -0
  39. plot_io/genome/signal.py +621 -0
  40. plot_io/genome/sites.py +782 -0
  41. plot_io/genome/sources.py +459 -0
  42. plot_io/genome/testing.py +161 -0
  43. plot_io/geometry.py +289 -0
  44. plot_io/groups.py +308 -0
  45. plot_io/guides.py +503 -0
  46. plot_io/helpers/__init__.py +17 -0
  47. plot_io/helpers/clustermap.py +202 -0
  48. plot_io/helpers/dendrogram.py +172 -0
  49. plot_io/helpers/grids.py +304 -0
  50. plot_io/helpers/qq.py +334 -0
  51. plot_io/helpers/raincloud.py +249 -0
  52. plot_io/helpers/tree.py +214 -0
  53. plot_io/helpers/volcano.py +505 -0
  54. plot_io/hints.py +125 -0
  55. plot_io/kernels.py +740 -0
  56. plot_io/labels.py +566 -0
  57. plot_io/layers/__init__.py +100 -0
  58. plot_io/layers/base.py +630 -0
  59. plot_io/layers/context.py +87 -0
  60. plot_io/layers/core.py +446 -0
  61. plot_io/layers/geoms.py +1362 -0
  62. plot_io/layers/grammar.py +586 -0
  63. plot_io/layers/paint.py +614 -0
  64. plot_io/layers/pipeline.py +170 -0
  65. plot_io/layers/positions.py +431 -0
  66. plot_io/layers/stats.py +1088 -0
  67. plot_io/layout.py +1381 -0
  68. plot_io/legend.py +605 -0
  69. plot_io/levels.py +509 -0
  70. plot_io/limits.py +124 -0
  71. plot_io/lines.py +42 -0
  72. plot_io/mapped.py +522 -0
  73. plot_io/markers.py +110 -0
  74. plot_io/markup.py +128 -0
  75. plot_io/numbers.py +354 -0
  76. plot_io/occupancy.py +295 -0
  77. plot_io/plot.py +597 -0
  78. plot_io/presets/__init__.py +51 -0
  79. plot_io/presets/_util.py +153 -0
  80. plot_io/presets/area.py +145 -0
  81. plot_io/presets/bar.py +261 -0
  82. plot_io/presets/box.py +400 -0
  83. plot_io/presets/catpoints.py +177 -0
  84. plot_io/presets/contour.py +353 -0
  85. plot_io/presets/curves.py +30 -0
  86. plot_io/presets/dots.py +355 -0
  87. plot_io/presets/ecdf.py +97 -0
  88. plot_io/presets/errorbar.py +106 -0
  89. plot_io/presets/errors.py +296 -0
  90. plot_io/presets/fill.py +74 -0
  91. plot_io/presets/heatmap.py +322 -0
  92. plot_io/presets/hexbin.py +198 -0
  93. plot_io/presets/hist.py +391 -0
  94. plot_io/presets/hist2d.py +87 -0
  95. plot_io/presets/kde.py +162 -0
  96. plot_io/presets/kde2d.py +147 -0
  97. plot_io/presets/line.py +143 -0
  98. plot_io/presets/pie.py +502 -0
  99. plot_io/presets/regression.py +115 -0
  100. plot_io/presets/scatter.py +194 -0
  101. plot_io/presets/stem.py +100 -0
  102. plot_io/presets/violin.py +323 -0
  103. plot_io/py.typed +0 -0
  104. plot_io/registry.py +82 -0
  105. plot_io/render.py +480 -0
  106. plot_io/rows.py +491 -0
  107. plot_io/scales.py +103 -0
  108. plot_io/seams.py +138 -0
  109. plot_io/settings.py +807 -0
  110. plot_io/shapes.py +66 -0
  111. plot_io/sharing.py +232 -0
  112. plot_io/sizemaps.py +97 -0
  113. plot_io/spec.py +240 -0
  114. plot_io/states.py +216 -0
  115. plot_io/styles/__init__.py +307 -0
  116. plot_io/styles/library.py +397 -0
  117. plot_io/tables.py +138 -0
  118. plot_io/text.py +412 -0
  119. plot_io/ticks.py +400 -0
  120. plot_io/times.py +287 -0
  121. plot_io/titles.py +225 -0
  122. plot_io/transform.py +114 -0
  123. plot_io/types.py +13604 -0
  124. plot_io/units.py +768 -0
  125. plot_io/validators.py +1029 -0
  126. plot_io/values.py +190 -0
  127. plot_io-0.12.0.dist-info/METADATA +215 -0
  128. plot_io-0.12.0.dist-info/RECORD +130 -0
  129. plot_io-0.12.0.dist-info/WHEEL +4 -0
  130. plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
plot_io/markup.py ADDED
@@ -0,0 +1,128 @@
1
+ """Text markup: tags in strings written as ``vec_io.Span`` objects.
2
+
3
+ ``<i>``/``<em>`` (italic), ``<b>``/``<strong>`` (bold), ``<u>`` (underline),
4
+ ``<s>``/``<del>`` (strikethrough), ``<sup>`` and ``<sub>`` (raised and lowered, 0.7 of the
5
+ size), in either case, nested in any order, each closed by its own end tag. Any other
6
+ ``<`` is text; ``&lt;``, ``&gt;`` and ``&amp;`` write a literal ``<``, ``>`` or ``&``. Styles
7
+ add to the text's own (``<b>`` in bold text stays bold).
8
+
9
+ :func:`parse` turns content (a string, or a list of strings and spans: its strings are
10
+ parsed) into rich content; text without tags or entities is returned as given. The result
11
+ parses to itself: literal text that could read as markup is kept in spans.
12
+ """
13
+
14
+ import functools
15
+ import re
16
+ from typing import Any
17
+
18
+ from vec_io import Span
19
+
20
+ __all__ = ["TAGS", "parse", "check", "check_settings"]
21
+
22
+ _SCALE = 0.7
23
+
24
+ #: Tag → the span attributes it sets.
25
+ TAGS: dict[str, dict[str, Any]] = {
26
+ "i": {"font_style": "italic"}, "em": {"font_style": "italic"},
27
+ "b": {"font_weight": "bold"}, "strong": {"font_weight": "bold"},
28
+ "u": {"underline": True},
29
+ "s": {"strikethrough": True}, "del": {"strikethrough": True},
30
+ "sup": {"baseline_shift": "super", "font_scale": _SCALE},
31
+ "sub": {"baseline_shift": "sub", "font_scale": _SCALE},
32
+ }
33
+ _ENTITIES = {"lt": "<", "gt": ">", "amp": "&"}
34
+ _TOKEN = re.compile(r"<(/?)(%s)>|&(lt|gt|amp);" % "|".join(sorted(TAGS, key=len, reverse=True)),
35
+ re.IGNORECASE)
36
+
37
+
38
+ def _piece(text: str, open_: list[str]) -> Any:
39
+ """Text under the open tags: a span, or a string when nothing applies and it holds no
40
+ ``<`` or ``&`` (which a second parse would read)."""
41
+ if not open_ and "<" not in text and "&" not in text:
42
+ return text
43
+ attrs: dict[str, Any] = {}
44
+ for tag in open_:
45
+ attrs.update(TAGS[tag])
46
+ return Span(text, **attrs)
47
+
48
+
49
+ @functools.lru_cache(maxsize=4096)
50
+ def _parse(s: str) -> str | tuple[Any, ...]:
51
+ open_: list[str] = []
52
+ out: list[Any] = []
53
+ buf: list[str] = []
54
+ pos = 0
55
+ found = False
56
+
57
+ def flush() -> None:
58
+ text = "".join(buf)
59
+ buf.clear()
60
+ if text:
61
+ out.append(_piece(text, open_))
62
+
63
+ for m in _TOKEN.finditer(s):
64
+ found = True
65
+ buf.append(s[pos:m.start()])
66
+ pos = m.end()
67
+ if m.group(3) is not None:
68
+ buf.append(_ENTITIES[m.group(3).lower()])
69
+ continue
70
+ tag = m.group(2).lower()
71
+ flush()
72
+ if not m.group(1):
73
+ open_.append(tag)
74
+ elif not open_:
75
+ raise ValueError(f"markup: </{tag}> without <{tag}> in {s!r}")
76
+ elif open_[-1] != tag:
77
+ raise ValueError(f"markup: </{tag}> closes <{open_[-1]}> in {s!r}")
78
+ else:
79
+ open_.pop()
80
+ if not found:
81
+ return s
82
+ if open_:
83
+ raise ValueError(f"markup: unclosed <{open_[-1]}> in {s!r}")
84
+ buf.append(s[pos:])
85
+ flush()
86
+ return tuple(out)
87
+
88
+
89
+ def parse(content: Any) -> Any:
90
+ """``content`` with its markup written as spans: a string without tags or entities as
91
+ given, else rich content (a list); spans are kept as they are. Raises ``ValueError``
92
+ for an unclosed or crossed tag."""
93
+ if isinstance(content, str):
94
+ got = _parse(content)
95
+ return got if isinstance(got, str) else list(got)
96
+ if not isinstance(content, (list, tuple)):
97
+ return content
98
+ if not any(isinstance(item, str) and isinstance(_parse(item), tuple) for item in content):
99
+ return content
100
+ out: list[Any] = []
101
+ for item in content:
102
+ got = _parse(item) if isinstance(item, str) else item
103
+ out.extend(got if isinstance(got, tuple) else (got,))
104
+ return out
105
+
106
+
107
+ def check(content: Any) -> None:
108
+ """Raise ``ValueError`` if the markup of ``content`` is unclosed or crossed."""
109
+ if isinstance(content, str):
110
+ _parse(content)
111
+ elif isinstance(content, (list, tuple)):
112
+ for item in content:
113
+ if isinstance(item, str):
114
+ _parse(item)
115
+
116
+
117
+ def check_settings(obj: Any, values: dict[str, Any]) -> None:
118
+ """Check the markup of the text settings of ``obj`` (resolved ``values``) when its
119
+ ``markup`` is on; the error names the setting."""
120
+ from .settings import text
121
+ if not values.get("markup"):
122
+ return
123
+ for name, s in obj.SETTINGS.items():
124
+ if s.check is text:
125
+ try:
126
+ check(values[name])
127
+ except ValueError as e:
128
+ raise ValueError(f"{obj.kind} setting {name!r}: {e}") from None
plot_io/numbers.py ADDED
@@ -0,0 +1,354 @@
1
+ """Numbers as text: one formatter for tick labels, colorbars and heatmap cell labels.
2
+
3
+ ``fmt`` is ``"auto"``, a format string, a callable, or a preset (``"percent"``,
4
+ ``"sci"``, ``"eng"``, ``"si"``, ``"int"``). A :class:`NumberStyle` carries the decimal
5
+ and thousands separators, the minus sign, a prefix and suffix, and the precision.
6
+ """
7
+
8
+ import decimal
9
+ import math
10
+ import re
11
+ from collections.abc import Sequence
12
+ from typing import Any
13
+
14
+ from vec_io import Span
15
+
16
+ from .validators import typed
17
+
18
+ __all__ = ["NumberStyle", "format_numbers", "format_axis", "PRESETS", "check_format",
19
+ "additive_offset", "offset_label", "apply_format"]
20
+
21
+ PRESETS = ("auto", "percent", "sci", "eng", "si", "int")
22
+ _SI = {-24: "y", -21: "z", -18: "a", -15: "f", -12: "p", -9: "n", -6: "µ", -3: "m",
23
+ 0: "", 3: "k", 6: "M", 9: "G", 12: "T", 15: "P", 18: "E", 21: "Z", 24: "Y"}
24
+ _MINUS = {"hyphen": "-", "minus": "−"}
25
+
26
+
27
+ @typed("str | Callable[..., Any]")
28
+ def check_format(v: Any) -> Any:
29
+ if callable(v):
30
+ return v
31
+ if isinstance(v, str):
32
+ key = v.strip().lower()
33
+ if key in PRESETS:
34
+ return key
35
+ if "%" in v and "{" not in v: # printf (numbers), strftime (time axes)
36
+ return v
37
+ if "{" in v:
38
+ try:
39
+ v.format(1.5)
40
+ except (IndexError, KeyError, ValueError) as e:
41
+ raise ValueError(f"invalid format string {v!r}: {e}") from None
42
+ return v
43
+ if v.strip(): # a format spec: ".2f"
44
+ try:
45
+ format(1.5, v)
46
+ return v
47
+ except ValueError:
48
+ pass
49
+ raise ValueError(f"expected 'auto', a preset ({', '.join(PRESETS[1:])}), a format "
50
+ f"string or a function, got {v!r}")
51
+
52
+
53
+ def apply_format(fmt: str, v: Any, what: str = "tick_format") -> str:
54
+ """``v`` written by a format string: ``"{:.2f}"`` (``str.format``), ``"%.2f"``
55
+ (printf) or a bare format spec (``".2f"``)."""
56
+ try:
57
+ if "{" in fmt:
58
+ return fmt.format(v)
59
+ if "%" in fmt:
60
+ return fmt % v
61
+ return format(v, fmt)
62
+ except (TypeError, ValueError) as e:
63
+ raise ValueError(f"{what} {fmt!r} on {v!r}: {e}") from None
64
+
65
+
66
+ class NumberStyle:
67
+ """Separators, minus sign, affixes and precision of formatted numbers."""
68
+
69
+ __slots__ = ("decimal", "thousands", "minus", "prefix", "suffix", "precision")
70
+
71
+ def __init__(self, decimal_separator: str = ".", thousands_separator: str = "",
72
+ minus: str = "hyphen", prefix: str = "", suffix: str = "",
73
+ precision: int | None = None) -> None:
74
+ self.decimal = decimal_separator
75
+ self.thousands = thousands_separator
76
+ self.minus = _MINUS[minus]
77
+ self.prefix = prefix
78
+ self.suffix = suffix
79
+ self.precision = precision
80
+
81
+ @classmethod
82
+ def of(cls, obj: Any) -> "NumberStyle":
83
+ """The style from an object's (inherited) settings."""
84
+ return cls(obj.resolve("decimal_separator"), obj.resolve("thousands_separator"),
85
+ obj.resolve("minus"), obj.resolve("prefix"), obj.resolve("suffix"),
86
+ obj.resolve("precision"))
87
+
88
+ def digits(self, s: str) -> str:
89
+ """A plain decimal string (``-1234.5``) with this style's signs and separators."""
90
+ neg = s.startswith("-")
91
+ if neg:
92
+ s = s[1:]
93
+ whole, _, frac = s.partition(".")
94
+ if self.thousands and len(whole) > 3:
95
+ groups = []
96
+ while len(whole) > 3:
97
+ groups.append(whole[-3:])
98
+ whole = whole[:-3]
99
+ groups.append(whole)
100
+ whole = self.thousands.join(reversed(groups))
101
+ out = whole + (self.decimal + frac if frac else "")
102
+ return (self.minus if neg else "") + out
103
+
104
+ def exponent(self, k: int) -> str:
105
+ return (self.minus if k < 0 else "") + str(abs(k))
106
+
107
+ def wrap(self, label: Any) -> Any:
108
+ if not (self.prefix or self.suffix):
109
+ return label
110
+ if isinstance(label, str):
111
+ return self.prefix + label + self.suffix
112
+ return [self.prefix] + list(label) + [self.suffix]
113
+
114
+
115
+ def _fixed(v: float, d: int) -> str:
116
+ """``v`` with ``d`` decimals, rounding halves away from zero on the shortest decimal
117
+ form (1234.5 → 1235, 2.675 → 2.68)."""
118
+ if not math.isfinite(v):
119
+ return str(v)
120
+ q = decimal.Decimal(repr(float(v))).quantize(decimal.Decimal(1).scaleb(-d),
121
+ rounding=decimal.ROUND_HALF_UP)
122
+ s = f"{q:f}"
123
+ if s.startswith("-") and float(s) == 0:
124
+ s = s[1:]
125
+ return s
126
+
127
+
128
+ def _decimals(values: Sequence[float], precision: int | None, cap: int = 12,
129
+ distinct: bool = True) -> int:
130
+ """The fewest decimals telling the values apart and representing them (or giving
131
+ ``precision`` significant digits to the largest; ``distinct``: more when they would not
132
+ tell the values apart)."""
133
+ finite = [v for v in values if math.isfinite(v)]
134
+ if not finite:
135
+ return 0
136
+ big = max(abs(v) for v in finite)
137
+ if precision is not None:
138
+ if big == 0:
139
+ return 0
140
+ e = math.floor(math.log10(big))
141
+ d = precision - 1 - e
142
+ if abs(float(_fixed(big, max(0, d)))) >= 10.0 ** (e + 1):
143
+ d -= 1 # rounding reached the next power (9.96 → 10)
144
+ d = max(0, min(cap, d))
145
+ while distinct and d < cap and len({_fixed(v, d) for v in finite}) < len(set(finite)):
146
+ d += 1 # enough decimals to tell the values apart
147
+ return d
148
+ span = max(finite) - min(finite)
149
+ tol = max(1e-12, 1e-9 * big, span * 1e-6)
150
+ for d in range(cap + 1):
151
+ labels = [_fixed(v, d) for v in finite]
152
+ if len(set(labels)) == len(set(finite)) and all(abs(float(s) - v) <= tol
153
+ for s, v in zip(labels, finite)):
154
+ return d
155
+ return cap
156
+
157
+
158
+ def _superscript(text: str) -> Any:
159
+ from .text import superscript
160
+ return superscript(text)
161
+
162
+
163
+ def _exp_form(values: Sequence[float], style: NumberStyle, step: int,
164
+ si: bool = False) -> list[Any]:
165
+ """Mantissa × 10ᵏ labels (``step`` 1: sci, 3: eng) or SI prefixes."""
166
+ parts: list[tuple[float, int] | None] = []
167
+ for v in values:
168
+ if not math.isfinite(v):
169
+ parts.append(None)
170
+ elif v == 0:
171
+ parts.append((0.0, 0))
172
+ else:
173
+ k = math.floor(math.log10(abs(v)) / step) * step
174
+ parts.append((v / 10.0 ** k, k))
175
+ mants = [p[0] for p in parts if p is not None]
176
+ prec = style.precision
177
+ if prec is None:
178
+ d = _decimals(mants, None, 10)
179
+ elif step == 1:
180
+ d = prec - 1
181
+ else:
182
+ bigm = max((abs(m) for m in mants), default=0.0)
183
+ d = max(0, prec - 1 - math.floor(math.log10(bigm))) if bigm else 0
184
+ out: list[Any] = []
185
+ for p in parts:
186
+ if p is None:
187
+ out.append("")
188
+ continue
189
+ m, k = p
190
+ dd = d
191
+ s = _fixed(m, dd)
192
+ if abs(float(s)) >= 10 ** step and m: # rounding reached the next power
193
+ m, k = m / 10 ** step, k + step
194
+ if prec is not None and step == 3:
195
+ dd = prec - 1 # the rounded mantissa is 1
196
+ s = _fixed(m, dd)
197
+ if si and k in _SI:
198
+ out.append(style.wrap(style.digits(s) + _SI[k]))
199
+ elif k == 0 or float(s) == 0:
200
+ out.append(style.wrap(style.digits(s)))
201
+ else:
202
+ out.append(style.wrap([style.digits(s) + "×10", _superscript(style.exponent(k))]))
203
+ return out
204
+
205
+
206
+ _NUMBER = re.compile(r"(-?)(\d{1,3}(?:,\d{3})+|\d+)(?:\.(\d+))?")
207
+
208
+
209
+ def _restyle(text: str, style: NumberStyle) -> str:
210
+ """Separators and minus applied to the numbers inside a formatted string."""
211
+ def sub(m: re.Match) -> str:
212
+ whole = m.group(2)
213
+ commas = "," in whole
214
+ whole = whole.replace(",", "")
215
+ s = (m.group(1) or "") + whole + ("." + m.group(3) if m.group(3) else "")
216
+ if commas and not style.thousands:
217
+ return NumberStyle(style.decimal, ",", "hyphen" if style.minus == "-" else "minus"
218
+ ).digits(s)
219
+ return style.digits(s)
220
+ return _NUMBER.sub(sub, text)
221
+
222
+
223
+ def format_axis(values: Sequence[float], fmt: Any = "auto", style: NumberStyle | None = None,
224
+ sci_limits: tuple[int, int] | None = (-4, 5),
225
+ what: str = "tick_format", distinct: bool = True) -> tuple[list[Any], int]:
226
+ """Labels of a set of values formatted together, and the exponent of a common ×10ᵏ
227
+ factor (``"auto"`` beyond ``10^sci_limits``; 0: none). ``sci_limits=None``: no
228
+ factor, the labels carry their exponent (``"sci"``) instead. ``distinct``: a
229
+ ``precision`` keeps enough decimals to tell the values apart (ticks)."""
230
+ style = style or NumberStyle()
231
+ vals = list(values)
232
+ finite = [v for v in vals if math.isfinite(v)]
233
+ if hasattr(fmt, "format_all"): # formats the values together
234
+ return fmt.format_all(vals, style), 0
235
+ if callable(fmt):
236
+ out: list[Any] = []
237
+ for v in vals:
238
+ if not math.isfinite(v):
239
+ out.append("")
240
+ continue
241
+ try:
242
+ r = fmt(v)
243
+ except (TypeError, ValueError) as e:
244
+ raise ValueError(f"{what} function {fmt!r} on {v!r}: {e}") from None
245
+ if not isinstance(r, (str, list, Span)):
246
+ raise ValueError(f"{what} function {fmt!r} on {v!r} returned {r!r}, "
247
+ f"expected text")
248
+ out.append(style.wrap(_restyle(r, style)) if isinstance(r, str) else r)
249
+ return out, 0
250
+ if fmt not in PRESETS:
251
+ return [style.wrap(_restyle(apply_format(fmt, v, what), style))
252
+ if math.isfinite(v) else "" for v in vals], 0
253
+ if fmt == "int":
254
+ return [style.wrap(style.digits(_fixed(math.copysign(math.floor(abs(v) + 0.5), v), 0)))
255
+ if math.isfinite(v) else "" for v in vals], 0
256
+ if fmt == "sci":
257
+ return _exp_form(vals, style, 1), 0
258
+ if fmt == "eng":
259
+ return _exp_form(vals, style, 3), 0
260
+ if fmt == "si":
261
+ return _exp_form(vals, style, 3, si=True), 0
262
+ scale = 100.0 if fmt == "percent" else 1.0
263
+ exp = 0
264
+ big = max((abs(v) for v in finite), default=0.0) * scale
265
+ if fmt == "auto" and big:
266
+ if sci_limits is None:
267
+ pass
268
+ elif big >= 10.0 ** sci_limits[1] or big < 10.0 ** sci_limits[0]:
269
+ exp = math.floor(math.log10(big))
270
+ half = -exp // 2 # two factors: 10³²⁰ overflows, 10¹⁶⁰ twice does not
271
+ k1, k2 = scale * 10.0 ** half, 10.0 ** (-exp - half)
272
+ scaled = [v * k1 * k2 for v in vals]
273
+ d = _decimals(scaled, style.precision, distinct=distinct)
274
+ pct = "%" if fmt == "percent" else ""
275
+ return [style.wrap(style.digits(_fixed(v, d)) + pct) if math.isfinite(v) else ""
276
+ for v in scaled], exp
277
+
278
+
279
+ def additive_offset(values: Sequence[float]) -> float:
280
+ """The part common to tick values that their labels leave out (written once at the
281
+ axis end), when it saves at least four digits; 0: none. Ticks of both signs, or a
282
+ single value, take none."""
283
+ finite = [v for v in values if math.isfinite(v)]
284
+ if len(finite) < 2:
285
+ return 0.0
286
+ lo, hi = min(finite), max(finite)
287
+ if lo == hi or lo <= 0 <= hi:
288
+ return 0.0
289
+ small, big = sorted((abs(lo), abs(hi)))
290
+ top = math.ceil(math.log10(big))
291
+ k = top
292
+ while k > top - 400 and math.floor(small / 10.0 ** k) == math.floor(big / 10.0 ** k):
293
+ k -= 1
294
+ k += 1
295
+ if (big - small) / 10.0 ** k <= 1e-2: # straddling a multiple of a larger power
296
+ k = top
297
+ while k > top - 400 and big - small < 10.0 ** (k - 2):
298
+ k -= 1
299
+ k += 1
300
+ whole = math.floor(big / 10.0 ** k)
301
+ if whole < 1000:
302
+ return 0.0
303
+ shift = whole * 10.0 ** k
304
+ if k < 0:
305
+ shift = round(shift, -k)
306
+ return math.copysign(shift, lo)
307
+
308
+
309
+ def offset_label(shift: float, style: NumberStyle) -> list[Any]:
310
+ """The text of an additive offset: ``+1×10⁹``, ``−1.25×10⁶``."""
311
+ k = math.floor(math.log10(abs(shift)))
312
+ m = abs(shift) / 10.0 ** k
313
+ s = f"{m:.12g}"
314
+ if float(s) >= 10: # rounding reached the next power
315
+ k += 1
316
+ s = f"{m / 10:.12g}"
317
+ sign = "+" if shift > 0 else style.minus
318
+ return [sign + style.digits(s) + "×10", _superscript(style.exponent(k))]
319
+
320
+
321
+ def format_numbers(values: Sequence[float], fmt: Any = "auto",
322
+ settings: Any = None, what: str = "value_label_format") -> list[Any]:
323
+ """Labels of values formatted together; ``settings`` is a :class:`NumberStyle` or an
324
+ object with the number settings. ``"auto"`` beyond 10⁵ or below 10⁻⁴ writes each
325
+ label in scientific form (there is no axis to carry a common factor)."""
326
+ style = settings if isinstance(settings, NumberStyle) or settings is None \
327
+ else NumberStyle.of(settings)
328
+ style = style or NumberStyle()
329
+ labels, exp = format_axis(values, fmt, style, what=what, distinct=False)
330
+ if exp:
331
+ return _exp_form(list(values), style, 1)
332
+ return labels
333
+
334
+
335
+ @typed("Literal['hyphen', 'minus']")
336
+ def _check_minus(v: Any) -> str:
337
+ if v not in _MINUS:
338
+ raise ValueError(f"expected 'hyphen' or 'minus', got {v!r}")
339
+ return v
340
+
341
+
342
+ @typed("int")
343
+ def _check_precision(v: Any) -> int:
344
+ if isinstance(v, bool) or not isinstance(v, int) or not 1 <= v <= 15:
345
+ raise ValueError(f"expected an integer from 1 to 15, got {v!r}")
346
+ return v
347
+
348
+
349
+ @typed("str")
350
+ def _check_str(v: Any) -> str:
351
+ if not isinstance(v, str):
352
+ raise ValueError(f"expected a string, got {v!r}")
353
+ return v
354
+