plot-io 0.1.0__py3-none-any.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 +66 -0
- plot_io/_backend.py +114 -0
- plot_io/axis.py +574 -0
- plot_io/colorbar.py +342 -0
- plot_io/decorations.py +496 -0
- plot_io/defaults.py +766 -0
- plot_io/density.py +97 -0
- plot_io/enums.py +63 -0
- plot_io/facets.py +184 -0
- plot_io/figure.py +331 -0
- plot_io/labels.py +63 -0
- plot_io/legend.py +498 -0
- plot_io/limits.py +92 -0
- plot_io/maps/__init__.py +365 -0
- plot_io/maps/data.py +333 -0
- plot_io/markers.py +117 -0
- plot_io/numbers.py +289 -0
- plot_io/occupancy.py +219 -0
- plot_io/palettes.py +142 -0
- plot_io/plot.py +435 -0
- plot_io/render.py +1122 -0
- plot_io/scales.py +93 -0
- plot_io/series/__init__.py +498 -0
- plot_io/series/area.py +181 -0
- plot_io/series/bar.py +294 -0
- plot_io/series/box.py +401 -0
- plot_io/series/catdata.py +23 -0
- plot_io/series/catpoints.py +182 -0
- plot_io/series/contour.py +294 -0
- plot_io/series/curves.py +128 -0
- plot_io/series/ecdf.py +143 -0
- plot_io/series/errorbar.py +111 -0
- plot_io/series/errors.py +100 -0
- plot_io/series/fill.py +168 -0
- plot_io/series/heatmap.py +304 -0
- plot_io/series/hexbin.py +216 -0
- plot_io/series/hist.py +379 -0
- plot_io/series/hist2d.py +163 -0
- plot_io/series/kde.py +228 -0
- plot_io/series/kde2d.py +153 -0
- plot_io/series/line.py +173 -0
- plot_io/series/pie.py +304 -0
- plot_io/series/points.py +341 -0
- plot_io/series/regression.py +250 -0
- plot_io/series/scatter.py +103 -0
- plot_io/series/stem.py +112 -0
- plot_io/series/violin.py +321 -0
- plot_io/settings.py +281 -0
- plot_io/shapes.py +57 -0
- plot_io/sharing.py +160 -0
- plot_io/slots.py +52 -0
- plot_io/stacking.py +63 -0
- plot_io/stats.py +140 -0
- plot_io/style.py +71 -0
- plot_io/styles/__init__.py +234 -0
- plot_io/styles/library.py +732 -0
- plot_io/tables.py +141 -0
- plot_io/text.py +109 -0
- plot_io/ticks.py +284 -0
- plot_io/times.py +199 -0
- plot_io/values.py +161 -0
- plot_io-0.1.0.dist-info/METADATA +102 -0
- plot_io-0.1.0.dist-info/RECORD +65 -0
- plot_io-0.1.0.dist-info/WHEEL +4 -0
- plot_io-0.1.0.dist-info/licenses/LICENSE +28 -0
plot_io/stats.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Statistics and geometry of the derived plot types: bins, densities, contours, swarms.
|
|
2
|
+
|
|
3
|
+
The work is done in C by pdf_io's array helpers; these wrappers adapt their results to
|
|
4
|
+
plot_io's needs (matplotlib's hexagon lattice, rows of 2-D histograms, sorted LOWESS
|
|
5
|
+
fits, count rules for bins).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
from array import array
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pdf_io import arrays as _arrays
|
|
13
|
+
|
|
14
|
+
from . import _backend
|
|
15
|
+
|
|
16
|
+
__all__ = ["bin_edges", "check_bins", "histogram", "histogram2d", "hexbin", "ecdf",
|
|
17
|
+
"bandwidth", "kde", "contour_lines", "contour_bands", "lowess", "swarm"]
|
|
18
|
+
|
|
19
|
+
_RULES = ("auto", "fd", "sturges", "scott", "sqrt", "rice", "doane")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_bins(v: Any) -> Any:
|
|
23
|
+
"""A bin count, a rule name, or increasing edges."""
|
|
24
|
+
if isinstance(v, str):
|
|
25
|
+
if v not in _RULES:
|
|
26
|
+
raise ValueError(f"unknown bin rule {v!r} (use a count, edges or "
|
|
27
|
+
f"{', '.join(_RULES)})")
|
|
28
|
+
return v
|
|
29
|
+
if isinstance(v, int) and not isinstance(v, bool):
|
|
30
|
+
if v < 1:
|
|
31
|
+
raise ValueError("bins must be at least 1")
|
|
32
|
+
return v
|
|
33
|
+
e = _backend.doubles(v)
|
|
34
|
+
if len(e) < 2 or any(not b > a for a, b in zip(e, e[1:])):
|
|
35
|
+
raise ValueError("bin edges must increase")
|
|
36
|
+
return e
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def bin_edges(values: Any, rule: Any = "auto", range: Any = None) -> array:
|
|
40
|
+
"""Edges of even bins: ``rule`` is a count or one of numpy's rules."""
|
|
41
|
+
if not isinstance(rule, int):
|
|
42
|
+
return array("d", _arrays.bin_edges(values, rule, range))
|
|
43
|
+
if range is not None:
|
|
44
|
+
lo, hi = float(range[0]), float(range[1])
|
|
45
|
+
else:
|
|
46
|
+
n, lo, hi = _backend.extent(values, None)
|
|
47
|
+
if not n:
|
|
48
|
+
lo, hi = 0.0, 1.0
|
|
49
|
+
if lo == hi:
|
|
50
|
+
lo, hi = lo - 0.5, hi + 0.5
|
|
51
|
+
nb = max(1, rule)
|
|
52
|
+
edges = array("d", (lo + (hi - lo) * i / nb for i in builtins_range(nb + 1)))
|
|
53
|
+
edges[-1] = hi
|
|
54
|
+
return edges
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
builtins_range = range
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def histogram(values: Any, edges: Any, weights: Any = None) -> array:
|
|
61
|
+
"""Sum of weights (or counts) per bin; the last bin includes its right edge."""
|
|
62
|
+
return array("d", _arrays.histogram(values, edges, weights))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def histogram2d(x: Any, y: Any, x_edges: Any, y_edges: Any, weights: Any = None) -> list[array]:
|
|
66
|
+
"""Counts per ``(y bin, x bin)``: rows are y bins from the first edge."""
|
|
67
|
+
h, w, flat = _arrays.histogram2d(x, y, x_edges, y_edges, weights)
|
|
68
|
+
return [array("d", flat[r * w:(r + 1) * w]) for r in builtins_range(h)]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_S3 = math.sqrt(3.0)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def hexbin(x: Any, y: Any, x0: float, y0: float, dx: float, dy: float, nx: int, ny: int,
|
|
75
|
+
weights: Any = None) -> tuple[array, array, array]:
|
|
76
|
+
"""matplotlib's hexagonal bins: lattices ``(x0 + i dx, y0 + j dy)`` (``0 ≤ i ≤ nx``,
|
|
77
|
+
``0 ≤ j ≤ ny``) and ``(x0 + (i + ½) dx, y0 + (j + ½) dy)``, each point to the nearest
|
|
78
|
+
centre with y distances weighted by 3. Returns the non-empty cells' centres and
|
|
79
|
+
sums."""
|
|
80
|
+
# In units (dx, dy / √3), matplotlib's metric is the plain distance pdf_io uses.
|
|
81
|
+
u = _backend.scaled(x, 1.0 / dx, -x0 / dx)
|
|
82
|
+
v = _backend.scaled(y, _S3 / dy, -_S3 * y0 / dy)
|
|
83
|
+
cx, cy, cs = _arrays.hexbin(u, v, 0.0, 0.0, 1.0, _S3, nx + 1, ny + 1, weights)
|
|
84
|
+
return (_backend.scaled(cx, dx, x0), _backend.scaled(cy, dy / _S3, y0), array("d", cs))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def ecdf(values: Any, weights: Any = None) -> tuple[array, array]:
|
|
88
|
+
"""Sorted finite values and the cumulative weight up to each."""
|
|
89
|
+
xs, cs = _arrays.ecdf(values, weights)
|
|
90
|
+
return array("d", xs), array("d", cs)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def bandwidth(values: Any, rule: Any = "scott", weights: Any = None) -> float:
|
|
94
|
+
"""Gaussian kernel bandwidth: Scott's or Silverman's factor times the (weighted) sample
|
|
95
|
+
standard deviation, as scipy's ``gaussian_kde``; a number is returned as is."""
|
|
96
|
+
if not isinstance(rule, str):
|
|
97
|
+
return float(rule)
|
|
98
|
+
return float(_arrays.bandwidth(values, rule, weights))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def kde(values: Any, grid_min: float, grid_max: float, n: int, bw: float, weights: Any = None,
|
|
102
|
+
cumulative: bool = False) -> array:
|
|
103
|
+
"""Gaussian kernel density (integral 1 over the real line) at ``n`` points from
|
|
104
|
+
``grid_min`` to ``grid_max``; ``cumulative``: its integral."""
|
|
105
|
+
return array("d", _arrays.kde(values, grid_min, grid_max, n, bw, weights, cumulative))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def contour_lines(grid: Any, w: int, h: int, xs: Any, ys: Any,
|
|
109
|
+
level: float) -> list[tuple[array, array, bool]]:
|
|
110
|
+
"""Iso-lines at ``level`` as ``(x, y, closed)`` polylines (node coordinates ``xs``,
|
|
111
|
+
``ys``; a closed line repeats its first point)."""
|
|
112
|
+
return [(array("d", a), array("d", b), bool(c))
|
|
113
|
+
for a, b, c in _arrays.contour_lines(grid, w, h, xs, ys, level)]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def contour_bands(grid: Any, w: int, h: int, xs: Any, ys: Any, lo: float,
|
|
117
|
+
hi: float) -> tuple[array, array, array]:
|
|
118
|
+
"""The region ``lo <= v < hi`` as rings ``(xs, ys, ring_starts)`` that fill it exactly
|
|
119
|
+
together, in one path with the nonzero rule (pdf_io's form)."""
|
|
120
|
+
a, b, starts = _arrays.contour_bands(grid, w, h, xs, ys, lo, hi)
|
|
121
|
+
return array("d", a), array("d", b), array("q", (int(s) for s in starts))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def lowess(x: Any, y: Any, frac: float = 2.0 / 3.0, iterations: int = 3) -> tuple[array, array]:
|
|
125
|
+
"""Locally weighted linear regression: the sorted x values and the fitted y values."""
|
|
126
|
+
xs0, ys0 = _backend.doubles(x), _backend.doubles(y)
|
|
127
|
+
pairs = sorted((a, b) for a, b in zip(xs0, ys0) if math.isfinite(a) and math.isfinite(b))
|
|
128
|
+
xs = array("d", (p[0] for p in pairs))
|
|
129
|
+
ys = array("d", (p[1] for p in pairs))
|
|
130
|
+
if len(xs) < 2:
|
|
131
|
+
return xs, ys
|
|
132
|
+
return xs, array("d", _arrays.lowess(xs, ys, frac, iterations))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def swarm(positions: Any, size: Any, slot_width: float) -> tuple[array, int]:
|
|
136
|
+
"""Offsets across the slot for markers at ``positions`` (page units along the value
|
|
137
|
+
axis) of diameter ``size`` so that they do not overlap; points that do not fit are
|
|
138
|
+
put at the slot edge. Returns the offsets (in input order) and how many did not fit."""
|
|
139
|
+
offs, gutter = _arrays.swarm(positions, size, slot_width)
|
|
140
|
+
return array("d", offs), int(gutter)
|
plot_io/style.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Palette, palette references (``"C0"``…) and line dash styles."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .defaults import OKABE_ITO
|
|
6
|
+
|
|
7
|
+
__all__ = ["PALETTE", "resolve_color", "dash_of", "with_alpha", "check_palette", "desaturate"]
|
|
8
|
+
|
|
9
|
+
PALETTE = OKABE_ITO
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def check_palette(v: Any) -> Any:
|
|
13
|
+
"""A palette name, a function ``n -> colors``, or a list of colors."""
|
|
14
|
+
from .palettes import check_palette as check
|
|
15
|
+
return check(v)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_color(c: Any, palette: tuple[Any, ...]) -> Any:
|
|
19
|
+
"""A color with ``"C<n>"`` replaced by the palette entry."""
|
|
20
|
+
if isinstance(c, str) and len(c) >= 2 and c[0] == "C" and c[1:].isdigit():
|
|
21
|
+
return palette[int(c[1:]) % len(palette)]
|
|
22
|
+
if isinstance(c, str) and c.strip().lower() == "none":
|
|
23
|
+
return None
|
|
24
|
+
return c
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def with_alpha(c: Any, alpha: float) -> Any:
|
|
28
|
+
"""``c`` with its alpha multiplied by ``alpha`` (as an RGBA tuple)."""
|
|
29
|
+
if c is None:
|
|
30
|
+
return None
|
|
31
|
+
if alpha >= 1:
|
|
32
|
+
return c
|
|
33
|
+
from pdf_io.colors import CMYK, to_rgba
|
|
34
|
+
|
|
35
|
+
if isinstance(c, CMYK):
|
|
36
|
+
return c.with_alpha(c[3] * alpha)
|
|
37
|
+
rgba = to_rgba(c)
|
|
38
|
+
if rgba is None:
|
|
39
|
+
return None
|
|
40
|
+
a = rgba[3] if len(rgba) > 3 else 1.0
|
|
41
|
+
return (rgba[0], rgba[1], rgba[2], a * alpha)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def desaturate(c: Any, k: float) -> Any:
|
|
45
|
+
"""``c`` with its HLS saturation multiplied by ``k`` (as an RGBA tuple)."""
|
|
46
|
+
import colorsys
|
|
47
|
+
from pdf_io.colors import to_rgba
|
|
48
|
+
|
|
49
|
+
rgba = to_rgba(c)
|
|
50
|
+
if rgba is None:
|
|
51
|
+
return None
|
|
52
|
+
h, light, s = colorsys.rgb_to_hls(rgba[0], rgba[1], rgba[2])
|
|
53
|
+
r, g, b = colorsys.hls_to_rgb(h, light, s * k)
|
|
54
|
+
return (r, g, b, rgba[3] if len(rgba) > 3 else 1.0)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_DASHES = {"solid": None, "dashed": (3.7, 1.6), "dotted": (1.0, 1.65),
|
|
58
|
+
"dashdot": (6.4, 1.6, 1.0, 1.6)}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def dash_of(style: Any, width: float) -> list[float] | None:
|
|
62
|
+
"""Dash lengths (pt) of a line style, scaled by the line width."""
|
|
63
|
+
if style is None:
|
|
64
|
+
return None
|
|
65
|
+
if isinstance(style, (list, tuple)):
|
|
66
|
+
return [float(x) for x in style] or None
|
|
67
|
+
pattern = _DASHES[style]
|
|
68
|
+
if pattern is None:
|
|
69
|
+
return None
|
|
70
|
+
w = max(width, 0.5)
|
|
71
|
+
return [x * w for x in pattern]
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Styles: named presets of settings.
|
|
2
|
+
|
|
3
|
+
A style is a dict of settings: setting names for inherited settings (``"font_size"``) and
|
|
4
|
+
``"Class.name"`` for any setting of a class (``"Axis.grid"``). The key ``"base"`` names
|
|
5
|
+
the styles it builds on, one or a list, applied left to right before its own keys. A
|
|
6
|
+
figure's ``style`` is a registered name or such a dict; its effective theme is the base
|
|
7
|
+
theme with the expanded style merged over it, lengths multiplied by ``scaling``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import contextlib
|
|
11
|
+
import difflib
|
|
12
|
+
from collections.abc import Iterator
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from ..defaults import DEFAULTS, class_default
|
|
16
|
+
from ..settings import ALIASES, THEME, Setting
|
|
17
|
+
|
|
18
|
+
__all__ = ["register_style", "styles", "style_settings", "set_style", "use_style",
|
|
19
|
+
"check_style", "effective", "current"]
|
|
20
|
+
|
|
21
|
+
_STYLES: dict[str, dict[str, Any]] = {}
|
|
22
|
+
_version = 0
|
|
23
|
+
_session: Any = None
|
|
24
|
+
_stack: list[Any] = []
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _classes() -> dict[str, Any]:
|
|
28
|
+
from ..axis import Axis
|
|
29
|
+
from ..colorbar import Colorbar
|
|
30
|
+
from ..figure import Figure, FigureTitle
|
|
31
|
+
from ..plot import Legend, Plot, Title
|
|
32
|
+
from ..series.box import Box
|
|
33
|
+
from ..series.fill import Fill
|
|
34
|
+
from ..series.heatmap import Heatmap
|
|
35
|
+
from ..series.line import Line
|
|
36
|
+
from ..series.scatter import Scatter
|
|
37
|
+
from .. import decorations as d
|
|
38
|
+
from ..series import (area, bar, catpoints, contour, ecdf, errorbar, hexbin, hist, hist2d,
|
|
39
|
+
kde2d,
|
|
40
|
+
kde, pie, regression, stem, violin)
|
|
41
|
+
return {"Figure": Figure, "FigureTitle": FigureTitle, "Plot": Plot, "Title": Title,
|
|
42
|
+
"Legend": Legend, "Axis": Axis, "Colorbar": Colorbar, "Scatter": Scatter,
|
|
43
|
+
"Line": Line, "Fill": Fill, "Box": Box, "Heatmap": Heatmap, "Bar": bar.Bar,
|
|
44
|
+
"Hist": hist.Hist, "Kde": kde.Kde, "Kde2d": kde2d.Kde2d, "Ecdf": ecdf.Ecdf, "Violin": violin.Violin,
|
|
45
|
+
"Strip": catpoints.Strip, "Swarm": catpoints.Swarm, "Area": area.Area,
|
|
46
|
+
"Errorbar": errorbar.Errorbar, "Stem": stem.Stem,
|
|
47
|
+
"Regression": regression.Regression, "Hist2d": hist2d.Hist2d,
|
|
48
|
+
"Hexbin": hexbin.Hexbin, "Contour": contour.Contour, "Pie": pie.Pie,
|
|
49
|
+
"HLine": d.HLine, "VLine": d.VLine, "AxLine": d.AxLine, "HSpan": d.HSpan,
|
|
50
|
+
"VSpan": d.VSpan, "Text": d.Text, "Annotate": d.Annotate}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _setting(key: str) -> Setting:
|
|
54
|
+
"""The setting a style key refers to."""
|
|
55
|
+
classes = _classes()
|
|
56
|
+
if "." in key:
|
|
57
|
+
cls_name, _, name = key.partition(".")
|
|
58
|
+
cls = classes.get(cls_name)
|
|
59
|
+
if cls_name in ("XAxis", "YAxis"): # the x or y axis of plots only
|
|
60
|
+
cls = classes["Axis"]
|
|
61
|
+
if cls is None:
|
|
62
|
+
near = difflib.get_close_matches(cls_name, classes, n=1)
|
|
63
|
+
hint = f" (did you mean {near[0]!r}?)" if near else ""
|
|
64
|
+
raise ValueError(f"unknown class {cls_name!r} in style key {key!r}{hint}")
|
|
65
|
+
s = cls.SETTINGS.get(name)
|
|
66
|
+
if s is None and cls_name == "Colorbar": # a setting of colorbar axes only
|
|
67
|
+
s = classes["Axis"].SETTINGS.get(name)
|
|
68
|
+
if s is None:
|
|
69
|
+
near = difflib.get_close_matches(name, cls.SETTINGS, n=1)
|
|
70
|
+
hint = f" (did you mean {cls_name}.{near[0]}?)" if near else ""
|
|
71
|
+
raise ValueError(f"unknown setting {key!r} in style{hint}")
|
|
72
|
+
return s
|
|
73
|
+
names: dict[str, Setting] = {}
|
|
74
|
+
for cls in classes.values():
|
|
75
|
+
for n, s in cls.SETTINGS.items():
|
|
76
|
+
if s.inherited:
|
|
77
|
+
names.setdefault(n, s)
|
|
78
|
+
if key not in names:
|
|
79
|
+
near = difflib.get_close_matches(key, names, n=1)
|
|
80
|
+
hint = f" (did you mean {near[0]!r}?)" if near else ""
|
|
81
|
+
raise ValueError(f"unknown style key {key!r}: not an inherited setting; use "
|
|
82
|
+
f"\"Class.{key}\" for a setting of one class{hint}")
|
|
83
|
+
return names[key]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _bases(v: Any) -> tuple[str, ...]:
|
|
87
|
+
names = (v,) if isinstance(v, str) else tuple(v)
|
|
88
|
+
for n in names:
|
|
89
|
+
if not isinstance(n, str):
|
|
90
|
+
raise ValueError(f"a style base is a style name, got {n!r}")
|
|
91
|
+
_get(n)
|
|
92
|
+
return names
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _validate(style: dict[str, Any]) -> dict[str, Any]:
|
|
96
|
+
out: dict[str, Any] = {}
|
|
97
|
+
for key, value in style.items():
|
|
98
|
+
if key == "base":
|
|
99
|
+
out["base"] = _bases(value)
|
|
100
|
+
continue
|
|
101
|
+
cls_part, dot, name = key.rpartition(".")
|
|
102
|
+
if name in ALIASES:
|
|
103
|
+
key = f"{cls_part}{dot}{ALIASES[name]}"
|
|
104
|
+
s = _setting(key)
|
|
105
|
+
if value is None:
|
|
106
|
+
raise ValueError(f"style key {key!r}: None is not a value")
|
|
107
|
+
if s.check is not None:
|
|
108
|
+
try:
|
|
109
|
+
value = s.check(value)
|
|
110
|
+
except (TypeError, ValueError) as e:
|
|
111
|
+
raise ValueError(f"style key {key!r}: {e}") from None
|
|
112
|
+
out[key] = value
|
|
113
|
+
return out
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
_loaded = False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _load() -> None:
|
|
120
|
+
"""Register the bundled styles (on first use: they refer to every settings class)."""
|
|
121
|
+
global _loaded
|
|
122
|
+
if not _loaded:
|
|
123
|
+
_loaded = True
|
|
124
|
+
from . import library # noqa: F401
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _get(name: str) -> dict[str, Any]:
|
|
128
|
+
_load()
|
|
129
|
+
key = name.strip().lower()
|
|
130
|
+
if key not in _STYLES:
|
|
131
|
+
near = difflib.get_close_matches(key, _STYLES, n=1)
|
|
132
|
+
hint = f" (did you mean {near[0]!r}?)" if near else ""
|
|
133
|
+
raise ValueError(f"unknown style {name!r}{hint}")
|
|
134
|
+
return _STYLES[key]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def register_style(name: str, style: dict[str, Any]) -> None:
|
|
138
|
+
"""Register ``style`` (a dict of settings, with an optional ``"base"``) as ``name``."""
|
|
139
|
+
global _version
|
|
140
|
+
_load()
|
|
141
|
+
if not isinstance(style, dict):
|
|
142
|
+
raise ValueError("a style is a dict of settings")
|
|
143
|
+
_STYLES[name.strip().lower()] = _validate(style)
|
|
144
|
+
_version += 1
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def styles() -> list[str]:
|
|
148
|
+
"""Names of the registered styles."""
|
|
149
|
+
_load()
|
|
150
|
+
return sorted(_STYLES)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def check_style(v: Any) -> Any:
|
|
154
|
+
"""Validator of the ``style`` setting: a registered name or a dict."""
|
|
155
|
+
if isinstance(v, str):
|
|
156
|
+
_get(v)
|
|
157
|
+
return v.strip().lower()
|
|
158
|
+
if isinstance(v, dict):
|
|
159
|
+
return _validate(v)
|
|
160
|
+
raise ValueError(f"a style is a style name or a dict of settings, got {v!r}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def expand(style: Any, _seen: tuple[str, ...] = ()) -> dict[str, Any]:
|
|
164
|
+
"""The settings of ``style`` with its bases applied first."""
|
|
165
|
+
if isinstance(style, str):
|
|
166
|
+
key = style.strip().lower()
|
|
167
|
+
if key in _seen:
|
|
168
|
+
raise ValueError(f"style {key!r} is its own base ({' → '.join(_seen + (key,))})")
|
|
169
|
+
d, seen = _get(key), _seen + (key,)
|
|
170
|
+
else:
|
|
171
|
+
d, seen = check_style(style), _seen
|
|
172
|
+
out: dict[str, Any] = {}
|
|
173
|
+
for b in d.get("base", ()):
|
|
174
|
+
out.update(expand(b, seen))
|
|
175
|
+
out.update((k, v) for k, v in d.items() if k != "base")
|
|
176
|
+
return out
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def style_settings(style: Any) -> dict[str, Any]:
|
|
180
|
+
"""The base theme with ``style`` merged over it (unscaled), to inspect or copy."""
|
|
181
|
+
return {**THEME, **expand(style)}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _scaled(theme: dict[str, Any], s: float) -> dict[str, Any]:
|
|
185
|
+
def scale(v: Any) -> Any:
|
|
186
|
+
return v * s if isinstance(v, (int, float)) and not isinstance(v, bool) else v
|
|
187
|
+
|
|
188
|
+
out = dict(theme)
|
|
189
|
+
for key, value in theme.items():
|
|
190
|
+
if _setting(key).length:
|
|
191
|
+
out[key] = scale(value)
|
|
192
|
+
for cls_name, cls in _classes().items():
|
|
193
|
+
for name, setting in cls.SETTINGS.items():
|
|
194
|
+
key = f"{cls_name}.{name}"
|
|
195
|
+
if setting.length and not setting.inherited and key not in out:
|
|
196
|
+
d = class_default(cls, name)
|
|
197
|
+
if not callable(d) and scale(d) is not d:
|
|
198
|
+
out[key] = scale(d)
|
|
199
|
+
return out
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def effective(style: Any, scaling: float | None) -> dict[str, Any]:
|
|
203
|
+
"""The effective theme of a figure with ``style`` (``None``: the base theme)."""
|
|
204
|
+
theme = {**THEME, **expand(style)} if style is not None else dict(THEME)
|
|
205
|
+
s = scaling if scaling is not None else theme.get("Figure.scaling", DEFAULTS["Figure.scaling"])
|
|
206
|
+
return _scaled(theme, s) if s != 1.0 else theme
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def version() -> int:
|
|
210
|
+
"""Changes whenever a style is registered (for caches of effective themes)."""
|
|
211
|
+
_load()
|
|
212
|
+
return _version
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def current() -> Any:
|
|
216
|
+
"""The style new figures take: the innermost ``use_style``, else ``set_style``'s."""
|
|
217
|
+
return _stack[-1] if _stack else _session
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def set_style(style: Any) -> None:
|
|
221
|
+
"""Default style of figures created afterwards (``None``: back to ``"default"``)."""
|
|
222
|
+
global _session
|
|
223
|
+
_session = None if style is None else check_style(style)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@contextlib.contextmanager
|
|
227
|
+
def use_style(style: Any) -> Iterator[None]:
|
|
228
|
+
"""Default style of figures created inside the block."""
|
|
229
|
+
_stack.append(check_style(style))
|
|
230
|
+
try:
|
|
231
|
+
yield
|
|
232
|
+
finally:
|
|
233
|
+
_stack.pop()
|
|
234
|
+
|