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/states.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""The render's states of a figure's areas: each plot resolved (calls, cycle, axes and
|
|
2
|
+
their limits, legend, colorbars), before the layout measures and places them."""
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .axis import AxisState
|
|
7
|
+
from .layers.pipeline import plot_calls, train_colors, z_of
|
|
8
|
+
from .settings import cascade
|
|
9
|
+
|
|
10
|
+
__all__ = ["PlotState", "build", "occupy", "z_key", "label_content", "offset_content"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def z_key(item: Any) -> float:
|
|
14
|
+
"""Drawing order key of a call or a decoration (see :func:`~plot_io.layers.pipeline.z_of`)."""
|
|
15
|
+
return z_of(getattr(item, "series", item))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def occupy(call: Any, ctx: Any, occ: Any) -> None:
|
|
19
|
+
"""Mark the cells a call's layers cover (legend placement)."""
|
|
20
|
+
c = ctx.for_call(call)
|
|
21
|
+
for layer in call.layers:
|
|
22
|
+
if not layer.occupy or layer.overlay:
|
|
23
|
+
continue
|
|
24
|
+
if layer.occupy == "all":
|
|
25
|
+
x0, y0, x1, y1 = c.rect
|
|
26
|
+
occ.rects([x0], [y0], [x1], [y1])
|
|
27
|
+
continue
|
|
28
|
+
seg = layer.options.get("segment")
|
|
29
|
+
if seg is None:
|
|
30
|
+
_occupy(c, layer, occ)
|
|
31
|
+
else:
|
|
32
|
+
with seg.scope(c) as cs:
|
|
33
|
+
_occupy(cs, layer, occ)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _occupy(c: Any, layer: Any, occ: Any) -> None:
|
|
37
|
+
lay = layer.columns(c)
|
|
38
|
+
aes = layer.options.get("occupy_aes")
|
|
39
|
+
if aes:
|
|
40
|
+
lay = _with_aes(lay, aes)
|
|
41
|
+
lay.geom.occupy(c, lay, occ)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _with_aes(layer: Any, aes: dict[str, str]) -> Any:
|
|
45
|
+
from .layers.core import Layer
|
|
46
|
+
out = Layer.__new__(Layer)
|
|
47
|
+
for k in Layer.__slots__:
|
|
48
|
+
setattr(out, k, getattr(layer, k))
|
|
49
|
+
out.aes = {**layer.aes, **aes}
|
|
50
|
+
return out
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def label_content(state: AxisState) -> Any:
|
|
54
|
+
"""The axis title: its label, or the label it takes from a data column (on a genome
|
|
55
|
+
axis, the chromosome and unit); ``None`` for ``""`` and under ``hide_title``."""
|
|
56
|
+
if state.hide_title:
|
|
57
|
+
return None
|
|
58
|
+
label = state.axis.resolve("label")
|
|
59
|
+
if state.genome:
|
|
60
|
+
from .genome.scale import axis_label
|
|
61
|
+
label = axis_label(state, label)
|
|
62
|
+
label = state.auto_label if label is None else label
|
|
63
|
+
return None if label == "" else label
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def offset_content(state: AxisState) -> Any:
|
|
67
|
+
"""What is written at the end of the axis, as its formatter gave it: the common ×10ᵏ
|
|
68
|
+
factor of the tick labels and the value added to them (``+1×10⁹``), or the common date
|
|
69
|
+
part of a time axis; ``None`` when there is none."""
|
|
70
|
+
return state.offset_label
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PlotState:
|
|
74
|
+
"""A plot while its figure is drawn."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, plot: Any, cycle: Any = None) -> None:
|
|
77
|
+
self.plot = plot
|
|
78
|
+
self.x = AxisState(plot.x_axis)
|
|
79
|
+
self.y = AxisState(plot.y_axis)
|
|
80
|
+
calls = plot_calls(plot)
|
|
81
|
+
#: the plot's calls, in drawing order
|
|
82
|
+
self.calls = sorted(calls, key=z_key)
|
|
83
|
+
twins = list(getattr(plot, "twins", []))
|
|
84
|
+
twin_calls = [c for t in twins for c in plot_calls(t)]
|
|
85
|
+
self.cycle = train_colors(plot, calls, twin_calls, cycle)
|
|
86
|
+
cycle = self.cycle
|
|
87
|
+
kinds = {c.axes for c in calls}
|
|
88
|
+
if len(kinds) > 1:
|
|
89
|
+
raise ValueError(f"{plot!r} mixes series drawn without axes (pie) and with axes")
|
|
90
|
+
self.axes_on = plot.resolve("axes") and kinds != {False}
|
|
91
|
+
decos = list(plot.decorations)
|
|
92
|
+
self.decorations = decos
|
|
93
|
+
limit_items = list(calls) + [d for d in decos if d.resolve("extend_limits")]
|
|
94
|
+
self.limit_items, self.padded = limit_items, self.axes_on
|
|
95
|
+
for role, st in (("x", self.x), ("y", self.y)):
|
|
96
|
+
st.auto_label = next((t for t in (c.axis_title(role) for c in calls)
|
|
97
|
+
if t is not None), None)
|
|
98
|
+
self.twins = [PlotState(t, cycle) for t in twins]
|
|
99
|
+
self.role_items = {"x": list(limit_items), "y": list(limit_items)}
|
|
100
|
+
for t in self.twins: # a twin shares the other axis
|
|
101
|
+
shared = "x" if t.plot.twin_of[1] == "y" else "y"
|
|
102
|
+
self.role_items[shared] += t.limit_items
|
|
103
|
+
self.x.resolve(self.role_items["x"], self.padded)
|
|
104
|
+
self.y.resolve(self.role_items["y"], self.padded)
|
|
105
|
+
#: roles whose axis is not drawn: hidden by every call on it, unless set visible
|
|
106
|
+
self.hidden: set[str] = set()
|
|
107
|
+
for role, st in (("x", self.x), ("y", self.y)):
|
|
108
|
+
on = [c for c in calls if c.has_axis(role)]
|
|
109
|
+
st.call_leaves = any(role in c.leaves for c in on)
|
|
110
|
+
if on and all(role in c.ticks for c in on):
|
|
111
|
+
st.call_ticks = [t for c in on for t in c.ticks[role]]
|
|
112
|
+
st.call_thin = any(role in c.thin for c in on)
|
|
113
|
+
if on and all(role in c.hide for c in on) \
|
|
114
|
+
and cascade(st.axis, "visible")[2] not in ("own", "figure"):
|
|
115
|
+
self.hidden.add(role)
|
|
116
|
+
for t in self.twins:
|
|
117
|
+
shared = "x" if t.plot.twin_of[1] == "y" else "y"
|
|
118
|
+
mine, theirs = getattr(self, shared), getattr(t, shared)
|
|
119
|
+
theirs.copy_limits(mine)
|
|
120
|
+
theirs.shared = mine
|
|
121
|
+
from .guides import GuideState
|
|
122
|
+
bars = list(enumerate(GuideState.for_colorbar(cb)
|
|
123
|
+
for cb in [*plot.colorbars, *size_keys(plot)]))
|
|
124
|
+
#: the attached colorbars, each side's from the axes outward (``order``, else
|
|
125
|
+
#: creation order); those a figure colorbar shows instead are left out (layout)
|
|
126
|
+
self.bars = [b for _, b in sorted(bars, key=lambda e: (
|
|
127
|
+
e[0] if e[1].order is None else e[1].order, e[0]))]
|
|
128
|
+
self.legend = GuideState.for_plot(self)
|
|
129
|
+
#: in a figure of given width / height, the plot's box width / height as last
|
|
130
|
+
#: placed (``None``: not given, or not placed yet): an outside legend wraps
|
|
131
|
+
#: within it (layout)
|
|
132
|
+
self.box_room: tuple[float | None, float | None] = (None, None)
|
|
133
|
+
#: the bands last measured (see :func:`plot_io.boxes.measure`), with the row
|
|
134
|
+
#: label and group bands (:func:`plot_io.rows.bands`)
|
|
135
|
+
self.bands: Any = None
|
|
136
|
+
#: the nodes of its box tree with their bands, as last measured (see
|
|
137
|
+
#: :mod:`plot_io.boxes`)
|
|
138
|
+
self.tree: list[Any] = []
|
|
139
|
+
|
|
140
|
+
def shows(self, role: str) -> bool:
|
|
141
|
+
"""Whether the axis ``role`` is drawn (ticks, labels)."""
|
|
142
|
+
return role not in self.hidden and getattr(self.plot, f"{role}_axis").resolve("visible")
|
|
143
|
+
|
|
144
|
+
def aspect_want(self) -> float | None:
|
|
145
|
+
"""Height / width the axes need for the `data_ratio` (1 without axes), else
|
|
146
|
+
``None``."""
|
|
147
|
+
aspect = self.plot.resolve("data_ratio") or (None if self.axes_on else 1.0)
|
|
148
|
+
if not aspect:
|
|
149
|
+
return None
|
|
150
|
+
dx = abs(self.x.fhi - self.x.flo) or 1.0
|
|
151
|
+
dy = abs(self.y.fhi - self.y.flo) or 1.0
|
|
152
|
+
return aspect * dy / dx
|
|
153
|
+
|
|
154
|
+
def aspect_rect(self, rect: tuple[float, float, float, float]) -> tuple:
|
|
155
|
+
want = self.aspect_want() # h / w
|
|
156
|
+
if not want:
|
|
157
|
+
return rect
|
|
158
|
+
x0, y0, x1, y1 = rect
|
|
159
|
+
w, h = x1 - x0, y1 - y0
|
|
160
|
+
if h > w * want:
|
|
161
|
+
nh = w * want
|
|
162
|
+
y0 += (h - nh) / 2
|
|
163
|
+
y1 = y0 + nh
|
|
164
|
+
else:
|
|
165
|
+
nw = h / want
|
|
166
|
+
x0 += (w - nw) / 2
|
|
167
|
+
x1 = x0 + nw
|
|
168
|
+
return (x0, y0, x1, y1)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def build(fig: Any, areas: list[Any]) -> None:
|
|
172
|
+
"""The areas' states: the color and size axes trained first (:func:`plot_io.mapped.
|
|
173
|
+
train`), then each plot, colorbar and size legend, then the shared x and y axes."""
|
|
174
|
+
from .guides import GuideState
|
|
175
|
+
from .legend import SizeLegend
|
|
176
|
+
from .mapped import train
|
|
177
|
+
from .plot import Plot
|
|
178
|
+
train(fig, areas)
|
|
179
|
+
for a in areas:
|
|
180
|
+
if isinstance(a.obj, Plot):
|
|
181
|
+
a.state = PlotState(a.obj)
|
|
182
|
+
elif isinstance(a.obj, SizeLegend):
|
|
183
|
+
a.state = GuideState.for_size_legend(a.obj)
|
|
184
|
+
else:
|
|
185
|
+
a.state = GuideState.for_colorbar(a.obj)
|
|
186
|
+
from .sharing import apply
|
|
187
|
+
apply(fig, areas)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def size_keys(plot: Any) -> list[Any]:
|
|
191
|
+
"""The size keys a plot's series ask for (``size_key="key"`` or a list of values):
|
|
192
|
+
one per size axis of the plot and its twins, unless a colorbar of the plot or a size
|
|
193
|
+
legend area (``figure.size_legend``) shows it."""
|
|
194
|
+
from .colorbar import Colorbar
|
|
195
|
+
from .legend import SizeLegend
|
|
196
|
+
from .mapped import _size_key, size_axis_of
|
|
197
|
+
shown = {id(cb.source) for cb in plot.colorbars if cb.sizes}
|
|
198
|
+
fig = plot.figure
|
|
199
|
+
areas = {_size_key(o.size_axis) for o in (fig._areas.values() if fig is not None else ())
|
|
200
|
+
if isinstance(o, SizeLegend)}
|
|
201
|
+
areas.discard(None)
|
|
202
|
+
out = []
|
|
203
|
+
for p in [plot, *plot.twins]:
|
|
204
|
+
for s in p.series:
|
|
205
|
+
v = s.resolve("size_key") if "size_key" in s.SETTINGS else False
|
|
206
|
+
if v is False or v == "legend":
|
|
207
|
+
continue
|
|
208
|
+
axis = size_axis_of(s)
|
|
209
|
+
if axis is None or id(axis) in shown or (areas and _size_key(axis) in areas):
|
|
210
|
+
continue
|
|
211
|
+
shown.add(id(axis))
|
|
212
|
+
cb = Colorbar(axis, plot=plot)
|
|
213
|
+
if isinstance(v, list):
|
|
214
|
+
cb.marks = v
|
|
215
|
+
out.append(cb)
|
|
216
|
+
return out
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Styles: named presets of settings.
|
|
2
|
+
|
|
3
|
+
A style is a dict of settings: setting names for inherited settings (``"font_size"``),
|
|
4
|
+
``"Class.name"`` for any setting of a class (``"Axis.grid"``) and ``"Group.member"`` for
|
|
5
|
+
a group (``"Marker.size"``). The key ``"base"`` names the styles it builds on, one or a
|
|
6
|
+
list (a later base over an earlier one). Keys starting with ``--`` are custom variables,
|
|
7
|
+
read by ``var(--name)``.
|
|
8
|
+
|
|
9
|
+
A figure's style chain is its style, then that style's bases (depth first, each once),
|
|
10
|
+
then ``"default"`` (:data:`~plot_io.defaults.DEFAULTS`). The nearer style wins; within a
|
|
11
|
+
style, the most specific key wins.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import contextlib
|
|
15
|
+
import contextvars
|
|
16
|
+
import difflib
|
|
17
|
+
from collections.abc import Generator
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from .. import hints
|
|
21
|
+
from ..defaults import DEFAULTS
|
|
22
|
+
from ..units import given
|
|
23
|
+
from ..settings import ALIASES, BASE_THEME, Setting, Theme, check_value, custom_vars
|
|
24
|
+
|
|
25
|
+
__all__ = ["add_style", "names", "settings", "set_style", "use_style",
|
|
26
|
+
"check_style", "effective", "current"]
|
|
27
|
+
|
|
28
|
+
#: ``"default"``: no keys of its own, as ``DEFAULTS`` ends every chain.
|
|
29
|
+
_STYLES: dict[str, dict[str, Any]] = {"default": {}}
|
|
30
|
+
_version = 0
|
|
31
|
+
_session: Any = None
|
|
32
|
+
#: the styles of the enclosing ``use_style`` blocks of this thread or task
|
|
33
|
+
_stack: contextvars.ContextVar[tuple[Any, ...]] = contextvars.ContextVar("styles", default=())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
#: Style classes registered by the modules that define them (the genome track preset).
|
|
37
|
+
_REGISTERED: dict[str, Any] = {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def register_class(cls: Any, name: str | None = None) -> None:
|
|
41
|
+
"""Make ``cls`` (a ``Settings`` class) a style class: ``"Name.setting"`` style keys and
|
|
42
|
+
defaults apply to it. Plot types added with ``registry.layers.add``
|
|
43
|
+
are style classes without this."""
|
|
44
|
+
_REGISTERED[name or cls.__name__] = cls
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _classes() -> dict[str, Any]:
|
|
48
|
+
from ..axis import Axis
|
|
49
|
+
from ..colorbar import Colorbar, FigureColorbar
|
|
50
|
+
from ..figure import Figure, FigureTitle, MainTitle
|
|
51
|
+
from ..plot import FigureLegend, Legend, Plot, Title
|
|
52
|
+
from .. import decorations as d
|
|
53
|
+
from ..layers import Series, layer_types
|
|
54
|
+
out = {"Series": Series, "Decoration": d.Decoration,
|
|
55
|
+
"Figure": Figure, "FigureTitle": FigureTitle, "MainTitle": MainTitle, "Plot": Plot,
|
|
56
|
+
"Title": Title,
|
|
57
|
+
"Legend": Legend, "FigureLegend": FigureLegend, "Axis": Axis, "Colorbar": Colorbar,
|
|
58
|
+
"FigureColorbar": FigureColorbar}
|
|
59
|
+
for f in layer_types().values():
|
|
60
|
+
if isinstance(f, type) and issubclass(f, Series):
|
|
61
|
+
out[f.__name__] = f
|
|
62
|
+
out.update({"HLine": d.HLine, "VLine": d.VLine, "AxLine": d.AxLine, "HSpan": d.HSpan,
|
|
63
|
+
"VSpan": d.VSpan, "Text": d.Text, "Annotate": d.Annotate})
|
|
64
|
+
from .. import rows
|
|
65
|
+
out.update({"FigureVLine": rows.FigureVLine, "FigureVSpan": rows.FigureVSpan,
|
|
66
|
+
"FigureGroup": rows.FigureGroup})
|
|
67
|
+
out.update(_REGISTERED)
|
|
68
|
+
return out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _subclass_setting(cls: Any, name: str) -> Setting | None:
|
|
72
|
+
"""Setting ``name`` of ``cls`` or of a subclass (keys of a base class)."""
|
|
73
|
+
todo = [cls]
|
|
74
|
+
while todo:
|
|
75
|
+
c = todo.pop()
|
|
76
|
+
s = c.SETTINGS.get(name)
|
|
77
|
+
if s is not None:
|
|
78
|
+
return s
|
|
79
|
+
todo.extend(c.__subclasses__())
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _setting(key: str) -> Setting:
|
|
84
|
+
"""The setting a style key refers to: ``"name"`` (inherited), ``"Class.name"`` (of a
|
|
85
|
+
class and its subclasses) or ``"Group.member"``."""
|
|
86
|
+
from ..groups import GROUPS
|
|
87
|
+
if key.rpartition(".")[2] in ("color_map", "cmap"):
|
|
88
|
+
from ..color.maps import COLOR_MAP_HINT
|
|
89
|
+
raise ValueError(f"style key {key!r}: {COLOR_MAP_HINT}")
|
|
90
|
+
classes = _classes()
|
|
91
|
+
if "." in key:
|
|
92
|
+
cls_name, _, name = key.partition(".")
|
|
93
|
+
group = GROUPS.get(cls_name)
|
|
94
|
+
if group is not None and cls_name not in classes:
|
|
95
|
+
if name not in group.MEMBERS:
|
|
96
|
+
near = difflib.get_close_matches(name, group.MEMBERS, n=1)
|
|
97
|
+
hint = f" (did you mean {cls_name}.{near[0]}?)" if near else ""
|
|
98
|
+
raise ValueError(f"unknown member {key!r} in style{hint}")
|
|
99
|
+
return group.member_setting(name)
|
|
100
|
+
cls = classes.get(cls_name)
|
|
101
|
+
if cls_name in ("XAxis", "YAxis"): # the x or y axis of plots only
|
|
102
|
+
cls = classes["Axis"]
|
|
103
|
+
if cls is None:
|
|
104
|
+
near = difflib.get_close_matches(cls_name, list(classes) + list(GROUPS), n=1)
|
|
105
|
+
hint = f" (did you mean {near[0]!r}?)" if near else ""
|
|
106
|
+
raise ValueError(f"unknown class {cls_name!r} in style key {key!r}{hint}")
|
|
107
|
+
s = _subclass_setting(cls, name)
|
|
108
|
+
if s is None and cls_name == "Colorbar": # a setting of colorbar axes only
|
|
109
|
+
s = classes["Axis"].SETTINGS.get(name)
|
|
110
|
+
if s is None:
|
|
111
|
+
use = hints.setting(name, cls.SETTINGS)
|
|
112
|
+
if use is not None:
|
|
113
|
+
raise ValueError(f"unknown setting {key!r} in style: use {use}")
|
|
114
|
+
near = difflib.get_close_matches(name, cls.SETTINGS, n=1)
|
|
115
|
+
hint = f" (did you mean {cls_name}.{near[0]}?)" if near else ""
|
|
116
|
+
raise ValueError(f"unknown setting {key!r} in style{hint}")
|
|
117
|
+
return s
|
|
118
|
+
names: dict[str, Setting] = {}
|
|
119
|
+
for cls in classes.values():
|
|
120
|
+
for n, s in cls.SETTINGS.items():
|
|
121
|
+
if s.inherited:
|
|
122
|
+
names.setdefault(n, s)
|
|
123
|
+
if key not in names:
|
|
124
|
+
use = hints.setting(key, names)
|
|
125
|
+
if use is not None:
|
|
126
|
+
raise ValueError(f"unknown style key {key!r}: use {use}")
|
|
127
|
+
near = difflib.get_close_matches(key, names, n=1)
|
|
128
|
+
hint = f" (did you mean {near[0]!r}?)" if near else ""
|
|
129
|
+
raise ValueError(f"unknown style key {key!r}: not an inherited setting; use "
|
|
130
|
+
f"\"Class.{key}\" for a setting of one class{hint}")
|
|
131
|
+
return names[key]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _bases(v: Any) -> tuple[str, ...]:
|
|
135
|
+
names = (v,) if isinstance(v, str) else tuple(v)
|
|
136
|
+
for n in names:
|
|
137
|
+
if not isinstance(n, str):
|
|
138
|
+
raise ValueError(f"a style base is a style name, got {n!r}")
|
|
139
|
+
_get(n)
|
|
140
|
+
return names
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _validate(style: dict[str, Any]) -> dict[str, Any]:
|
|
144
|
+
out: dict[str, Any] = {}
|
|
145
|
+
given_as: dict[str, str] = {}
|
|
146
|
+
for key, value in style.items():
|
|
147
|
+
if key == "base":
|
|
148
|
+
out["base"] = _bases(value)
|
|
149
|
+
continue
|
|
150
|
+
if key.startswith("--"): # a custom variable, read by var(--name)
|
|
151
|
+
if value is None or not key[2:]:
|
|
152
|
+
raise ValueError(f"style key {key!r}: a custom variable needs a name and "
|
|
153
|
+
f"a value")
|
|
154
|
+
out[key] = value
|
|
155
|
+
continue
|
|
156
|
+
cls_part, dot, name = key.rpartition(".")
|
|
157
|
+
given_key = key
|
|
158
|
+
if name in ALIASES:
|
|
159
|
+
key = f"{cls_part}{dot}{ALIASES[name]}"
|
|
160
|
+
if key in out:
|
|
161
|
+
raise ValueError(f"style keys {given_as[key]!r} and {given_key!r} are the same "
|
|
162
|
+
f"setting; give one")
|
|
163
|
+
given_as[key] = given_key
|
|
164
|
+
s = _setting(key)
|
|
165
|
+
if s.check is custom_vars:
|
|
166
|
+
raise ValueError(f"style key {key!r}: a style sets custom variables as "
|
|
167
|
+
f"\"--name\" keys")
|
|
168
|
+
if s.name == "style" and s is _classes()["Figure"].SETTINGS["style"]:
|
|
169
|
+
raise ValueError(f"style key {key!r}: a style builds on others with \"base\"")
|
|
170
|
+
if value is None:
|
|
171
|
+
raise ValueError(f"style key {key!r}: None is not a value")
|
|
172
|
+
try:
|
|
173
|
+
value = check_value(s, value)
|
|
174
|
+
except (TypeError, ValueError) as e:
|
|
175
|
+
raise ValueError(f"style key {key!r}: {e}") from None
|
|
176
|
+
out[key] = value
|
|
177
|
+
return out
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
_loaded = False
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _load() -> None:
|
|
184
|
+
"""Register the bundled styles (on first use: they refer to every settings class)."""
|
|
185
|
+
global _loaded
|
|
186
|
+
if not _loaded:
|
|
187
|
+
_loaded = True
|
|
188
|
+
from . import library # noqa: F401
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _get(name: str) -> dict[str, Any]:
|
|
192
|
+
_load()
|
|
193
|
+
key = name.strip().lower()
|
|
194
|
+
if key not in _STYLES:
|
|
195
|
+
near = difflib.get_close_matches(key, _STYLES, n=1)
|
|
196
|
+
hint = f" (did you mean {near[0]!r}?)" if near else ""
|
|
197
|
+
known = ", ".join(repr(n) for n in sorted(_STYLES) if n != "solid")
|
|
198
|
+
raise ValueError(f"unknown style {name!r}{hint}; the styles: {known}, or a dict")
|
|
199
|
+
return _STYLES[key]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def add_style(name: str, style: dict[str, Any]) -> None:
|
|
203
|
+
"""Register ``style`` (a dict of settings, with an optional ``"base"``) as ``name``."""
|
|
204
|
+
global _version
|
|
205
|
+
_load()
|
|
206
|
+
if name.strip().lower() == "default":
|
|
207
|
+
raise ValueError('the "default" style is DEFAULTS and cannot be replaced; register '
|
|
208
|
+
'a style under another name and set it with set_style')
|
|
209
|
+
if not isinstance(style, dict):
|
|
210
|
+
raise ValueError("a style is a dict of settings")
|
|
211
|
+
key = name.strip().lower()
|
|
212
|
+
old = _STYLES.get(key)
|
|
213
|
+
_STYLES[key] = _validate(style)
|
|
214
|
+
try:
|
|
215
|
+
chain(key) # refuses a cycle of bases
|
|
216
|
+
except ValueError:
|
|
217
|
+
if old is None:
|
|
218
|
+
del _STYLES[key]
|
|
219
|
+
else:
|
|
220
|
+
_STYLES[key] = old
|
|
221
|
+
raise
|
|
222
|
+
_version += 1
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def names() -> list[str]:
|
|
226
|
+
"""Names of the registered styles."""
|
|
227
|
+
_load()
|
|
228
|
+
return sorted(_STYLES)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def check_style(v: Any) -> Any:
|
|
232
|
+
"""Validator of the ``style`` setting: a registered name or a dict."""
|
|
233
|
+
if isinstance(v, str):
|
|
234
|
+
_get(v)
|
|
235
|
+
return v.strip().lower()
|
|
236
|
+
if isinstance(v, dict):
|
|
237
|
+
return _validate(v)
|
|
238
|
+
raise ValueError(f"a style is a style name or a dict of settings, got {v!r}")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def chain(style: Any) -> list[tuple[str, dict[str, Any]]]:
|
|
242
|
+
"""The styles of ``style``'s chain other than ``"default"``, nearest first, as
|
|
243
|
+
``(label, keys)``."""
|
|
244
|
+
out: list[tuple[str, dict[str, Any]]] = []
|
|
245
|
+
done: set[str] = set()
|
|
246
|
+
|
|
247
|
+
def walk(st: Any, path: tuple[str, ...], of: str | None) -> None:
|
|
248
|
+
if isinstance(st, str):
|
|
249
|
+
key = st.strip().lower()
|
|
250
|
+
if key in path:
|
|
251
|
+
raise ValueError(f"style {key!r} is its own base "
|
|
252
|
+
f"({' → '.join(path + (key,))})")
|
|
253
|
+
if key in done or key == "default":
|
|
254
|
+
return
|
|
255
|
+
done.add(key)
|
|
256
|
+
d, path, name = _get(key), path + (key,), f'"{key}"'
|
|
257
|
+
else:
|
|
258
|
+
d, name = check_style(st), "(a dict)"
|
|
259
|
+
label = name if of is None else f"{name} (base of {of})"
|
|
260
|
+
out.append((label, {k: v for k, v in d.items() if k != "base"}))
|
|
261
|
+
for b in reversed(d.get("base", ())):
|
|
262
|
+
walk(b, path, name)
|
|
263
|
+
|
|
264
|
+
walk(style, (), None)
|
|
265
|
+
return out
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def settings(style: Any) -> dict[str, Any]:
|
|
269
|
+
"""Every key of ``style``'s chain, ``DEFAULTS`` included, as the nearest style gives
|
|
270
|
+
it (values as given), to inspect or copy."""
|
|
271
|
+
out = dict(DEFAULTS)
|
|
272
|
+
for _, keys in reversed(chain(style)):
|
|
273
|
+
out.update(keys)
|
|
274
|
+
return {k: given(v) for k, v in out.items()}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def effective(style: Any) -> Theme:
|
|
278
|
+
"""The theme of a figure with ``style`` (``None``: ``DEFAULTS`` only)."""
|
|
279
|
+
return BASE_THEME if style is None else Theme(chain(style))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def version() -> int:
|
|
283
|
+
"""Changes whenever a style is registered (for caches of effective themes)."""
|
|
284
|
+
_load()
|
|
285
|
+
return _version
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def current() -> Any:
|
|
289
|
+
"""The style new figures take: the innermost ``use_style``, else ``set_style``'s."""
|
|
290
|
+
stack = _stack.get()
|
|
291
|
+
return stack[-1] if stack else _session
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def set_style(style: Any) -> None:
|
|
295
|
+
"""Default style of figures created afterwards (``None``: back to ``"default"``)."""
|
|
296
|
+
global _session
|
|
297
|
+
_session = None if style is None else check_style(style)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@contextlib.contextmanager
|
|
301
|
+
def use_style(style: Any) -> Generator[None]:
|
|
302
|
+
"""Default style of figures created inside the block."""
|
|
303
|
+
token = _stack.set((*_stack.get(), check_style(style)))
|
|
304
|
+
try:
|
|
305
|
+
yield
|
|
306
|
+
finally:
|
|
307
|
+
_stack.reset(token)
|