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.
- plot_io/__init__.py +73 -0
- plot_io/_methods.py +1691 -0
- plot_io/_native/__init__.py +199 -0
- plot_io/_native/__main__.py +197 -0
- plot_io/_native/_api.py +457 -0
- plot_io/_native/bin/windows-arm64.dll +0 -0
- plot_io/_native/kernels.py +335 -0
- plot_io/_native/manifest.json +49 -0
- plot_io/axis.py +1347 -0
- plot_io/boxes.py +1156 -0
- plot_io/canvas.py +389 -0
- plot_io/color/__init__.py +3 -0
- plot_io/color/data.py +1646 -0
- plot_io/color/maps.py +922 -0
- plot_io/color/values.py +295 -0
- plot_io/color/variants.py +1246 -0
- plot_io/colorbar.py +1426 -0
- plot_io/columns.py +614 -0
- plot_io/decorations.py +920 -0
- plot_io/defaults.py +809 -0
- plot_io/density.py +113 -0
- plot_io/ext.py +68 -0
- plot_io/facets.py +240 -0
- plot_io/figure.py +788 -0
- plot_io/formatters.py +97 -0
- plot_io/genome/__init__.py +17 -0
- plot_io/genome/adapters.py +356 -0
- plot_io/genome/combine.py +123 -0
- plot_io/genome/contacts.py +508 -0
- plot_io/genome/intervals.py +750 -0
- plot_io/genome/lanes.py +191 -0
- plot_io/genome/layout.py +184 -0
- plot_io/genome/links.py +364 -0
- plot_io/genome/pack.py +27 -0
- plot_io/genome/readers.py +145 -0
- plot_io/genome/regions.py +228 -0
- plot_io/genome/scale.py +349 -0
- plot_io/genome/series.py +344 -0
- plot_io/genome/signal.py +621 -0
- plot_io/genome/sites.py +782 -0
- plot_io/genome/sources.py +459 -0
- plot_io/genome/testing.py +161 -0
- plot_io/geometry.py +289 -0
- plot_io/groups.py +308 -0
- plot_io/guides.py +503 -0
- plot_io/helpers/__init__.py +17 -0
- plot_io/helpers/clustermap.py +202 -0
- plot_io/helpers/dendrogram.py +172 -0
- plot_io/helpers/grids.py +304 -0
- plot_io/helpers/qq.py +334 -0
- plot_io/helpers/raincloud.py +249 -0
- plot_io/helpers/tree.py +214 -0
- plot_io/helpers/volcano.py +505 -0
- plot_io/hints.py +125 -0
- plot_io/kernels.py +740 -0
- plot_io/labels.py +566 -0
- plot_io/layers/__init__.py +100 -0
- plot_io/layers/base.py +630 -0
- plot_io/layers/context.py +87 -0
- plot_io/layers/core.py +446 -0
- plot_io/layers/geoms.py +1362 -0
- plot_io/layers/grammar.py +586 -0
- plot_io/layers/paint.py +614 -0
- plot_io/layers/pipeline.py +170 -0
- plot_io/layers/positions.py +431 -0
- plot_io/layers/stats.py +1088 -0
- plot_io/layout.py +1381 -0
- plot_io/legend.py +605 -0
- plot_io/levels.py +509 -0
- plot_io/limits.py +124 -0
- plot_io/lines.py +42 -0
- plot_io/mapped.py +522 -0
- plot_io/markers.py +110 -0
- plot_io/markup.py +128 -0
- plot_io/numbers.py +354 -0
- plot_io/occupancy.py +295 -0
- plot_io/plot.py +597 -0
- plot_io/presets/__init__.py +51 -0
- plot_io/presets/_util.py +153 -0
- plot_io/presets/area.py +145 -0
- plot_io/presets/bar.py +261 -0
- plot_io/presets/box.py +400 -0
- plot_io/presets/catpoints.py +177 -0
- plot_io/presets/contour.py +353 -0
- plot_io/presets/curves.py +30 -0
- plot_io/presets/dots.py +355 -0
- plot_io/presets/ecdf.py +97 -0
- plot_io/presets/errorbar.py +106 -0
- plot_io/presets/errors.py +296 -0
- plot_io/presets/fill.py +74 -0
- plot_io/presets/heatmap.py +322 -0
- plot_io/presets/hexbin.py +198 -0
- plot_io/presets/hist.py +391 -0
- plot_io/presets/hist2d.py +87 -0
- plot_io/presets/kde.py +162 -0
- plot_io/presets/kde2d.py +147 -0
- plot_io/presets/line.py +143 -0
- plot_io/presets/pie.py +502 -0
- plot_io/presets/regression.py +115 -0
- plot_io/presets/scatter.py +194 -0
- plot_io/presets/stem.py +100 -0
- plot_io/presets/violin.py +323 -0
- plot_io/py.typed +0 -0
- plot_io/registry.py +82 -0
- plot_io/render.py +480 -0
- plot_io/rows.py +491 -0
- plot_io/scales.py +103 -0
- plot_io/seams.py +138 -0
- plot_io/settings.py +807 -0
- plot_io/shapes.py +66 -0
- plot_io/sharing.py +232 -0
- plot_io/sizemaps.py +97 -0
- plot_io/spec.py +240 -0
- plot_io/states.py +216 -0
- plot_io/styles/__init__.py +307 -0
- plot_io/styles/library.py +397 -0
- plot_io/tables.py +138 -0
- plot_io/text.py +412 -0
- plot_io/ticks.py +400 -0
- plot_io/times.py +287 -0
- plot_io/titles.py +225 -0
- plot_io/transform.py +114 -0
- plot_io/types.py +13604 -0
- plot_io/units.py +768 -0
- plot_io/validators.py +1029 -0
- plot_io/values.py +190 -0
- plot_io-0.12.0.dist-info/METADATA +215 -0
- plot_io-0.12.0.dist-info/RECORD +130 -0
- plot_io-0.12.0.dist-info/WHEEL +4 -0
- plot_io-0.12.0.dist-info/licenses/LICENSE +28 -0
plot_io/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; ``<``, ``>`` and ``&`` 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
|
+
|