diffract-core 0.2.1__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.
- diffract/__init__.py +52 -0
- diffract/configs/fast_speed_without_disk.ini +48 -0
- diffract/configs/hybrid.ini +74 -0
- diffract/configs/sqlite.ini +62 -0
- diffract/containers.py +405 -0
- diffract/core/__init__.py +23 -0
- diffract/core/cache/__init__.py +24 -0
- diffract/core/cache/containers.py +93 -0
- diffract/core/cache/interface.py +117 -0
- diffract/core/cache/redis_manager.py +464 -0
- diffract/core/cache/simple_manager.py +478 -0
- diffract/core/compute/__init__.py +46 -0
- diffract/core/compute/config.py +48 -0
- diffract/core/compute/containers.py +81 -0
- diffract/core/compute/decorator.py +125 -0
- diffract/core/compute/exceptions.py +31 -0
- diffract/core/compute/execution/__init__.py +14 -0
- diffract/core/compute/execution/_types.py +70 -0
- diffract/core/compute/execution/aggregation.py +74 -0
- diffract/core/compute/execution/aggregation_runner.py +362 -0
- diffract/core/compute/execution/enums.py +38 -0
- diffract/core/compute/execution/executor.py +125 -0
- diffract/core/compute/execution/parameter_runner.py +125 -0
- diffract/core/compute/execution/restrictions.py +61 -0
- diffract/core/compute/execution/strategy.py +118 -0
- diffract/core/compute/execution/utils.py +112 -0
- diffract/core/compute/extensions/__init__.py +0 -0
- diffract/core/compute/extensions/power_law.py +1051 -0
- diffract/core/compute/extensions/rmt.py +150 -0
- diffract/core/compute/extensions/utils.py +36 -0
- diffract/core/compute/field_signature.py +183 -0
- diffract/core/compute/kernels/__init__.py +48 -0
- diffract/core/compute/kernels/alignment.py +106 -0
- diffract/core/compute/kernels/heavy_tailed.py +394 -0
- diffract/core/compute/kernels/marchenko_pastur.py +99 -0
- diffract/core/compute/kernels/mat_decomposition.py +101 -0
- diffract/core/compute/kernels/mat_properties.py +53 -0
- diffract/core/compute/kernels/model_quality.py +27 -0
- diffract/core/compute/kernels/norms.py +88 -0
- diffract/core/compute/kernels/ranks.py +35 -0
- diffract/core/compute/kernels/tracy_widom.py +36 -0
- diffract/core/compute/metadata.py +67 -0
- diffract/core/compute/registry.py +485 -0
- diffract/core/constants.py +147 -0
- diffract/core/data/__init__.py +32 -0
- diffract/core/data/interface.py +304 -0
- diffract/core/data/metadata/__init__.py +15 -0
- diffract/core/data/metadata/containers.py +60 -0
- diffract/core/data/metadata/interface.py +208 -0
- diffract/core/data/metadata/sqlite_index.py +604 -0
- diffract/core/data/nn/__init__.py +43 -0
- diffract/core/data/nn/aggregates/__init__.py +35 -0
- diffract/core/data/nn/aggregates/metadata.py +96 -0
- diffract/core/data/nn/aggregates/proxy.py +57 -0
- diffract/core/data/nn/aggregates/repository.py +87 -0
- diffract/core/data/nn/aggregates/view.py +307 -0
- diffract/core/data/nn/containers.py +86 -0
- diffract/core/data/nn/extractors/__init__.py +49 -0
- diffract/core/data/nn/extractors/base.py +300 -0
- diffract/core/data/nn/extractors/factory.py +234 -0
- diffract/core/data/nn/extractors/flax.py +112 -0
- diffract/core/data/nn/extractors/handlers/__init__.py +39 -0
- diffract/core/data/nn/extractors/handlers/base.py +110 -0
- diffract/core/data/nn/extractors/handlers/flax_handlers.py +71 -0
- diffract/core/data/nn/extractors/handlers/numpy_handlers.py +63 -0
- diffract/core/data/nn/extractors/handlers/onnx_handlers.py +67 -0
- diffract/core/data/nn/extractors/handlers/tensorflow_handlers.py +82 -0
- diffract/core/data/nn/extractors/handlers/torch_handlers.py +124 -0
- diffract/core/data/nn/extractors/interface.py +56 -0
- diffract/core/data/nn/extractors/numpy.py +94 -0
- diffract/core/data/nn/extractors/onnx.py +108 -0
- diffract/core/data/nn/extractors/tensorflow.py +99 -0
- diffract/core/data/nn/extractors/torch.py +204 -0
- diffract/core/data/nn/params/__init__.py +27 -0
- diffract/core/data/nn/params/interface.py +402 -0
- diffract/core/data/nn/params/metadata.py +110 -0
- diffract/core/data/nn/params/proxy.py +93 -0
- diffract/core/data/nn/params/repository.py +45 -0
- diffract/core/data/nn/params/schema.py +82 -0
- diffract/core/data/nn/params/view.py +393 -0
- diffract/core/data/proxy.py +246 -0
- diffract/core/data/repository.py +227 -0
- diffract/core/data/utils.py +33 -0
- diffract/core/data/view.py +389 -0
- diffract/core/export/__init__.py +27 -0
- diffract/core/export/containers.py +47 -0
- diffract/core/export/exporters.py +191 -0
- diffract/core/export/formatters/__init__.py +23 -0
- diffract/core/export/formatters/base.py +68 -0
- diffract/core/export/formatters/dict_formatter.py +38 -0
- diffract/core/export/formatters/json_formatter.py +58 -0
- diffract/core/export/formatters/list_formatter.py +41 -0
- diffract/core/export/formatters/pandas_formatter.py +86 -0
- diffract/core/export/formatters/polars_formatter.py +86 -0
- diffract/core/export/formatters/registry.py +84 -0
- diffract/core/export/interface.py +105 -0
- diffract/core/parallel/__init__.py +21 -0
- diffract/core/parallel/containers.py +57 -0
- diffract/core/parallel/execution.py +91 -0
- diffract/core/parallel/runtime.py +91 -0
- diffract/core/storage/__init__.py +35 -0
- diffract/core/storage/base_manager.py +330 -0
- diffract/core/storage/containers.py +122 -0
- diffract/core/storage/hdf5_manager.py +856 -0
- diffract/core/storage/hybrid_manager.py +218 -0
- diffract/core/storage/interface.py +222 -0
- diffract/core/storage/metadata.py +89 -0
- diffract/core/storage/ram_manager.py +159 -0
- diffract/core/storage/sqlite_manager.py +1062 -0
- diffract/core/storage/zarr_manager.py +992 -0
- diffract/core/utils/__init__.py +45 -0
- diffract/core/utils/build.py +46 -0
- diffract/core/utils/exceptions.py +11 -0
- diffract/core/utils/hashing.py +90 -0
- diffract/core/utils/imports.py +292 -0
- diffract/core/utils/math.py +15 -0
- diffract/py.typed +0 -0
- diffract/session/__init__.py +24 -0
- diffract/session/errors.py +17 -0
- diffract/session/field_cache.py +216 -0
- diffract/session/namespaces/__init__.py +15 -0
- diffract/session/namespaces/compute/__init__.py +219 -0
- diffract/session/namespaces/models/__init__.py +282 -0
- diffract/session/namespaces/models/parameters/__init__.py +133 -0
- diffract/session/namespaces/models/parameters/meta_patcher.py +167 -0
- diffract/session/namespaces/results/__init__.py +378 -0
- diffract/session/namespaces/results/eraser.py +129 -0
- diffract/session/namespaces/results/injester.py +249 -0
- diffract/session/namespaces/utils/__init__.py +141 -0
- diffract/session/namespaces/utils/merger.py +295 -0
- diffract/session/namespaces/viz/__init__.py +91 -0
- diffract/session/namespaces/viz/_utils.py +14 -0
- diffract/session/namespaces/viz/box.py +207 -0
- diffract/session/namespaces/viz/grid.py +160 -0
- diffract/session/namespaces/viz/heatmap.py +164 -0
- diffract/session/namespaces/viz/scatter.py +202 -0
- diffract/session/namespaces/viz/sparkline.py +270 -0
- diffract/session/namespaces/viz/violin.py +212 -0
- diffract/session/session.py +260 -0
- diffract/session/utils.py +81 -0
- diffract/viz/__init__.py +42 -0
- diffract/viz/data/__init__.py +34 -0
- diffract/viz/data/detection.py +57 -0
- diffract/viz/data/extraction.py +117 -0
- diffract/viz/data/filtering.py +57 -0
- diffract/viz/data/ordering.py +129 -0
- diffract/viz/data/provider.py +99 -0
- diffract/viz/data/types.py +98 -0
- diffract/viz/plots/__init__.py +37 -0
- diffract/viz/plots/base/__init__.py +20 -0
- diffract/viz/plots/base/axis.py +219 -0
- diffract/viz/plots/base/coloraxis.py +133 -0
- diffract/viz/plots/base/configurator.py +18 -0
- diffract/viz/plots/base/jitter.py +368 -0
- diffract/viz/plots/base/line.py +197 -0
- diffract/viz/plots/base/marker.py +278 -0
- diffract/viz/plots/base/overlay.py +14 -0
- diffract/viz/plots/base/plot.py +110 -0
- diffract/viz/plots/base/update.py +50 -0
- diffract/viz/plots/boxplot.py +283 -0
- diffract/viz/plots/cluster.py +374 -0
- diffract/viz/plots/heatmap.py +168 -0
- diffract/viz/plots/scatter.py +233 -0
- diffract/viz/plots/sparkline.py +405 -0
- diffract/viz/plots/subplots/__init__.py +32 -0
- diffract/viz/plots/subplots/coloraxis.py +339 -0
- diffract/viz/plots/subplots/factory.py +713 -0
- diffract/viz/plots/subplots/grid.py +214 -0
- diffract/viz/plots/subplots/layout.py +370 -0
- diffract/viz/plots/subplots/spec.py +39 -0
- diffract/viz/plots/violin.py +298 -0
- diffract/viz/renderer.py +513 -0
- diffract/viz/styling/__init__.py +78 -0
- diffract/viz/styling/palettes/__init__.py +20 -0
- diffract/viz/styling/palettes/bundle.py +16 -0
- diffract/viz/styling/palettes/color.py +83 -0
- diffract/viz/styling/palettes/dashes.py +27 -0
- diffract/viz/styling/palettes/symbols.py +42 -0
- diffract/viz/styling/resolvers/__init__.py +11 -0
- diffract/viz/styling/resolvers/categorical.py +68 -0
- diffract/viz/styling/resolvers/color.py +77 -0
- diffract/viz/styling/resolvers/numeric.py +108 -0
- diffract/viz/styling/sources.py +27 -0
- diffract/viz/styling/theme/__init__.py +29 -0
- diffract/viz/styling/theme/applier.py +114 -0
- diffract/viz/styling/theme/components.py +66 -0
- diffract/viz/styling/theme/presets.py +58 -0
- diffract/viz/styling/theme/theme.py +36 -0
- diffract_core-0.2.1.dist-info/METADATA +530 -0
- diffract_core-0.2.1.dist-info/RECORD +193 -0
- diffract_core-0.2.1.dist-info/WHEEL +4 -0
- diffract_core-0.2.1.dist-info/licenses/LICENSE +201 -0
- diffract_core-0.2.1.dist-info/licenses/NOTICE +2 -0
diffract/viz/renderer.py
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""Hydra-configurable rendering utilities for `diffract.viz`.
|
|
2
|
+
|
|
3
|
+
This module keeps rendering orchestration separate from individual plot classes.
|
|
4
|
+
It supports two rendering modes:
|
|
5
|
+
|
|
6
|
+
1. Already-constructed plot object via ``render(...)``.
|
|
7
|
+
2. Hydra YAML config via ``render_from_config(...)``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import inspect
|
|
13
|
+
from dataclasses import fields, is_dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from types import UnionType
|
|
16
|
+
from typing import (
|
|
17
|
+
TYPE_CHECKING,
|
|
18
|
+
Annotated,
|
|
19
|
+
Any,
|
|
20
|
+
Protocol,
|
|
21
|
+
Union,
|
|
22
|
+
get_args,
|
|
23
|
+
get_origin,
|
|
24
|
+
get_type_hints,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from diffract.core.utils import imports as import_utils
|
|
28
|
+
from diffract.viz.styling.sources import StyleLiteralKind
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
31
|
+
import plotly.graph_objects as go # type: ignore[import-not-found]
|
|
32
|
+
|
|
33
|
+
from diffract.session import Session
|
|
34
|
+
from diffract.viz.styling import Theme
|
|
35
|
+
|
|
36
|
+
_PLOTLY_GO = "plotly.graph_objects"
|
|
37
|
+
_VIZ_EXTRA_HINT = (
|
|
38
|
+
"diffract.viz requires the viz extra. "
|
|
39
|
+
'Install it with: pip install "diffract-core[viz]"'
|
|
40
|
+
)
|
|
41
|
+
_THEME_STRUCTURED_KEYS = frozenset(
|
|
42
|
+
{"layout", "typography", "background", "axes", "legend", "colorbar", "palettes"}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def require_plotly_go() -> Any:
|
|
47
|
+
"""Import and return ``plotly.graph_objects``."""
|
|
48
|
+
return import_utils.require(_PLOTLY_GO)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Plot(Protocol):
|
|
52
|
+
"""Protocol for renderable plot objects."""
|
|
53
|
+
|
|
54
|
+
def render(self, session: Session, theme: Theme | None = None) -> go.Figure:
|
|
55
|
+
"""Render a figure from a session."""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def render(
|
|
60
|
+
plot: Plot,
|
|
61
|
+
*,
|
|
62
|
+
session: Session,
|
|
63
|
+
theme: Theme | None = None,
|
|
64
|
+
) -> go.Figure:
|
|
65
|
+
"""Render a Plotly figure from a plot object."""
|
|
66
|
+
go = require_plotly_go()
|
|
67
|
+
|
|
68
|
+
if _plot_supports_theme_kwarg(plot):
|
|
69
|
+
fig = plot.render(session, theme=theme)
|
|
70
|
+
else:
|
|
71
|
+
fig = plot.render(session)
|
|
72
|
+
if theme is not None:
|
|
73
|
+
from diffract.viz.styling import apply_theme
|
|
74
|
+
|
|
75
|
+
fig = apply_theme(fig, theme)
|
|
76
|
+
|
|
77
|
+
if not isinstance(fig, go.Figure):
|
|
78
|
+
raise TypeError("Plot.render() must return plotly.graph_objects.Figure")
|
|
79
|
+
|
|
80
|
+
return fig
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def load_theme(theme_path: str | Path) -> Theme:
|
|
84
|
+
"""Load a `viz` Theme from YAML."""
|
|
85
|
+
try:
|
|
86
|
+
yaml = import_utils.require("yaml")
|
|
87
|
+
except ImportError as e:
|
|
88
|
+
raise ImportError(_VIZ_EXTRA_HINT) from e
|
|
89
|
+
|
|
90
|
+
path = Path(theme_path).expanduser().resolve()
|
|
91
|
+
with path.open(encoding="utf-8") as handle:
|
|
92
|
+
data = yaml.safe_load(handle)
|
|
93
|
+
|
|
94
|
+
if not isinstance(data, dict):
|
|
95
|
+
raise TypeError(f"Theme YAML must be a mapping, got {type(data).__name__}")
|
|
96
|
+
|
|
97
|
+
return _theme_from_dict(data)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def render_from_config(
|
|
101
|
+
*,
|
|
102
|
+
session: Session,
|
|
103
|
+
config_path: str | Path,
|
|
104
|
+
overrides: list[str] | None = None,
|
|
105
|
+
theme: Theme | None = None,
|
|
106
|
+
theme_path: str | Path | None = None,
|
|
107
|
+
) -> go.Figure:
|
|
108
|
+
"""Load a Hydra YAML config and render a figure."""
|
|
109
|
+
try:
|
|
110
|
+
hydra = import_utils.require("hydra")
|
|
111
|
+
instantiate = import_utils.require("hydra.utils").instantiate
|
|
112
|
+
omega_conf = import_utils.require("omegaconf").OmegaConf
|
|
113
|
+
except ImportError as e:
|
|
114
|
+
raise ImportError(_VIZ_EXTRA_HINT) from e
|
|
115
|
+
compose = hydra.compose
|
|
116
|
+
initialize_config_dir = hydra.initialize_config_dir
|
|
117
|
+
|
|
118
|
+
cfg_path = Path(config_path).expanduser().resolve()
|
|
119
|
+
config_dir = str(cfg_path.parent)
|
|
120
|
+
config_name = cfg_path.stem
|
|
121
|
+
|
|
122
|
+
with initialize_config_dir(config_dir=config_dir, version_base="1.3"):
|
|
123
|
+
cfg = compose(config_name=config_name, overrides=overrides or [])
|
|
124
|
+
omega_conf.resolve(cfg)
|
|
125
|
+
|
|
126
|
+
plot_cfg = cfg.get("plot")
|
|
127
|
+
if plot_cfg is None:
|
|
128
|
+
raise ValueError("Config must define a top-level 'plot' node")
|
|
129
|
+
|
|
130
|
+
plot = instantiate(plot_cfg, _convert_="object")
|
|
131
|
+
_coerce_field_refs(plot)
|
|
132
|
+
|
|
133
|
+
resolved_theme = theme
|
|
134
|
+
if resolved_theme is None and theme_path is not None:
|
|
135
|
+
resolved_theme = load_theme(theme_path)
|
|
136
|
+
|
|
137
|
+
fig = render(plot, session=session, theme=resolved_theme)
|
|
138
|
+
|
|
139
|
+
fig_cfg = cfg.get("config")
|
|
140
|
+
if fig_cfg is not None:
|
|
141
|
+
fig_cfg_plain = omega_conf.to_container(fig_cfg, resolve=True)
|
|
142
|
+
if not isinstance(fig_cfg_plain, dict):
|
|
143
|
+
raise TypeError("Config node 'config' must be a dict-like mapping")
|
|
144
|
+
fig.update(fig_cfg_plain)
|
|
145
|
+
|
|
146
|
+
return fig
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _coerce_field_refs(value: Any) -> None:
|
|
150
|
+
"""Recursively coerce string values to `FieldRef` where annotated.
|
|
151
|
+
|
|
152
|
+
Two deterministic, config-time rules:
|
|
153
|
+
|
|
154
|
+
- Annotations that contain ``FieldRef`` and do not legitimately accept
|
|
155
|
+
a plain string always coerce strings to ``FieldRef``.
|
|
156
|
+
- Style properties annotated with a :class:`StyleLiteralKind` (colors,
|
|
157
|
+
symbols, dashes) keep strings that are valid plotly literals of that
|
|
158
|
+
kind and coerce everything else to ``FieldRef``. Literals win over
|
|
159
|
+
identically named fields; an explicit ``FieldRef`` (e.g. refs-style
|
|
160
|
+
configs) is the escape hatch.
|
|
161
|
+
"""
|
|
162
|
+
if is_dataclass(value):
|
|
163
|
+
cls = type(value)
|
|
164
|
+
try:
|
|
165
|
+
type_hints = get_type_hints(cls, include_extras=True)
|
|
166
|
+
except (AttributeError, NameError, TypeError):
|
|
167
|
+
type_hints = {}
|
|
168
|
+
|
|
169
|
+
for f in fields(value):
|
|
170
|
+
item = getattr(value, f.name)
|
|
171
|
+
annotation = type_hints.get(f.name, f.type)
|
|
172
|
+
literal_kind = _style_literal_kind(annotation)
|
|
173
|
+
if isinstance(item, str) and literal_kind is not None:
|
|
174
|
+
if not _is_style_literal(item, literal_kind):
|
|
175
|
+
from diffract.viz.data import FieldRef
|
|
176
|
+
|
|
177
|
+
setattr(value, f.name, FieldRef(field=item))
|
|
178
|
+
continue
|
|
179
|
+
if isinstance(item, str) and _should_coerce_string_to_field_ref(annotation):
|
|
180
|
+
from diffract.viz.data import FieldRef
|
|
181
|
+
|
|
182
|
+
setattr(value, f.name, FieldRef(field=item))
|
|
183
|
+
continue
|
|
184
|
+
_coerce_field_refs(item)
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
if isinstance(value, list):
|
|
188
|
+
for item in value:
|
|
189
|
+
_coerce_field_refs(item)
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
if isinstance(value, tuple):
|
|
193
|
+
for item in value:
|
|
194
|
+
_coerce_field_refs(item)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
if isinstance(value, dict):
|
|
198
|
+
for item in value.values():
|
|
199
|
+
_coerce_field_refs(item)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _contains_field_ref(annotation: Any) -> bool:
|
|
203
|
+
if annotation is None:
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
if isinstance(annotation, str):
|
|
207
|
+
return "FieldRef" in annotation
|
|
208
|
+
|
|
209
|
+
from diffract.viz.data import FieldRef
|
|
210
|
+
|
|
211
|
+
if annotation is FieldRef:
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
origin = get_origin(annotation)
|
|
215
|
+
if origin in (UnionType, Union):
|
|
216
|
+
return any(_contains_field_ref(arg) for arg in get_args(annotation))
|
|
217
|
+
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _annotation_allows_plain_string(annotation: Any) -> bool:
|
|
222
|
+
if annotation is None:
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
if isinstance(annotation, str):
|
|
226
|
+
return "str" in annotation
|
|
227
|
+
|
|
228
|
+
if annotation is str:
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
origin = get_origin(annotation)
|
|
232
|
+
if origin in (UnionType, Union):
|
|
233
|
+
return any(_annotation_allows_plain_string(arg) for arg in get_args(annotation))
|
|
234
|
+
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _should_coerce_string_to_field_ref(annotation: Any) -> bool:
|
|
239
|
+
return _contains_field_ref(annotation) and not _annotation_allows_plain_string(
|
|
240
|
+
annotation
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _style_literal_kind(annotation: Any) -> StyleLiteralKind | None:
|
|
245
|
+
if annotation is None:
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
if isinstance(annotation, str):
|
|
249
|
+
# Annotations are often unresolvable strings here (TYPE_CHECKING-only
|
|
250
|
+
# names in the class hierarchy defeat get_type_hints), so match the
|
|
251
|
+
# source aliases by name.
|
|
252
|
+
by_alias = {
|
|
253
|
+
"ColorSource": StyleLiteralKind.COLOR,
|
|
254
|
+
"SymbolSource": StyleLiteralKind.SYMBOL,
|
|
255
|
+
"DashSource": StyleLiteralKind.DASH,
|
|
256
|
+
}
|
|
257
|
+
return next(
|
|
258
|
+
(kind for alias, kind in by_alias.items() if alias in annotation),
|
|
259
|
+
None,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if get_origin(annotation) is Annotated:
|
|
263
|
+
args = get_args(annotation)
|
|
264
|
+
for metadata in args[1:]:
|
|
265
|
+
if isinstance(metadata, StyleLiteralKind):
|
|
266
|
+
return metadata
|
|
267
|
+
return _style_literal_kind(args[0])
|
|
268
|
+
|
|
269
|
+
if get_origin(annotation) in (UnionType, Union):
|
|
270
|
+
for arg in get_args(annotation):
|
|
271
|
+
kind = _style_literal_kind(arg)
|
|
272
|
+
if kind is not None:
|
|
273
|
+
return kind
|
|
274
|
+
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _is_style_literal(value: str, kind: StyleLiteralKind) -> bool:
|
|
279
|
+
"""True when the string is a valid plotly literal of the given kind."""
|
|
280
|
+
go = require_plotly_go()
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
if kind is StyleLiteralKind.COLOR:
|
|
284
|
+
go.scatter.Marker(color=value)
|
|
285
|
+
elif kind is StyleLiteralKind.SYMBOL:
|
|
286
|
+
go.scatter.Marker(symbol=value)
|
|
287
|
+
else:
|
|
288
|
+
go.scatter.Line(dash=value)
|
|
289
|
+
except ValueError:
|
|
290
|
+
return False
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _plot_supports_theme_kwarg(plot: Plot) -> bool:
|
|
295
|
+
try:
|
|
296
|
+
signature = inspect.signature(plot.render)
|
|
297
|
+
except (TypeError, ValueError):
|
|
298
|
+
# If signature introspection fails, keep permissive behavior.
|
|
299
|
+
return True
|
|
300
|
+
|
|
301
|
+
for parameter in signature.parameters.values():
|
|
302
|
+
if parameter.kind == inspect.Parameter.VAR_KEYWORD:
|
|
303
|
+
return True
|
|
304
|
+
if parameter.name == "theme":
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _theme_from_dict(data: dict[str, Any]) -> Theme:
|
|
311
|
+
from diffract.viz.styling.palettes import (
|
|
312
|
+
DefaultColorPalette,
|
|
313
|
+
DefaultDashPalette,
|
|
314
|
+
DefaultSymbolPalette,
|
|
315
|
+
PaletteBundle,
|
|
316
|
+
)
|
|
317
|
+
from diffract.viz.styling.theme import (
|
|
318
|
+
AxesStyle,
|
|
319
|
+
BackgroundStyle,
|
|
320
|
+
ColorbarStyle,
|
|
321
|
+
LayoutStyle,
|
|
322
|
+
LegendStyle,
|
|
323
|
+
Theme,
|
|
324
|
+
TypographyStyle,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
kwargs: dict[str, Any] = {}
|
|
328
|
+
|
|
329
|
+
if any(key in data for key in _THEME_STRUCTURED_KEYS):
|
|
330
|
+
if "layout" in data:
|
|
331
|
+
kwargs["layout"] = LayoutStyle(**_require_mapping(data["layout"], "layout"))
|
|
332
|
+
if "typography" in data:
|
|
333
|
+
kwargs["typography"] = TypographyStyle(
|
|
334
|
+
**_require_mapping(data["typography"], "typography")
|
|
335
|
+
)
|
|
336
|
+
if "background" in data:
|
|
337
|
+
kwargs["background"] = BackgroundStyle(
|
|
338
|
+
**_require_mapping(data["background"], "background")
|
|
339
|
+
)
|
|
340
|
+
if "axes" in data:
|
|
341
|
+
kwargs["axes"] = AxesStyle(**_require_mapping(data["axes"], "axes"))
|
|
342
|
+
if "legend" in data:
|
|
343
|
+
kwargs["legend"] = LegendStyle(**_require_mapping(data["legend"], "legend"))
|
|
344
|
+
if "colorbar" in data:
|
|
345
|
+
kwargs["colorbar"] = ColorbarStyle(
|
|
346
|
+
**_require_mapping(data["colorbar"], "colorbar")
|
|
347
|
+
)
|
|
348
|
+
if "palettes" in data:
|
|
349
|
+
palettes_dict = _require_mapping(data["palettes"], "palettes")
|
|
350
|
+
palette_kwargs: dict[str, Any] = {}
|
|
351
|
+
|
|
352
|
+
color_cfg = palettes_dict.get("color")
|
|
353
|
+
if color_cfg is not None:
|
|
354
|
+
if isinstance(color_cfg, list):
|
|
355
|
+
palette_kwargs["color"] = DefaultColorPalette(
|
|
356
|
+
_colors=list(color_cfg)
|
|
357
|
+
)
|
|
358
|
+
else:
|
|
359
|
+
color_dict = _require_mapping(color_cfg, "palettes.color")
|
|
360
|
+
colors = color_dict.get("colors", color_dict.get("_colors"))
|
|
361
|
+
if colors is not None:
|
|
362
|
+
palette_kwargs["color"] = DefaultColorPalette(
|
|
363
|
+
_colors=list(colors)
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
symbols_cfg = palettes_dict.get("symbols")
|
|
367
|
+
if symbols_cfg is not None:
|
|
368
|
+
if isinstance(symbols_cfg, list):
|
|
369
|
+
palette_kwargs["symbols"] = DefaultSymbolPalette(
|
|
370
|
+
_symbols=list(symbols_cfg)
|
|
371
|
+
)
|
|
372
|
+
else:
|
|
373
|
+
symbols_dict = _require_mapping(symbols_cfg, "palettes.symbols")
|
|
374
|
+
symbols = symbols_dict.get("symbols", symbols_dict.get("_symbols"))
|
|
375
|
+
if symbols is not None:
|
|
376
|
+
palette_kwargs["symbols"] = DefaultSymbolPalette(
|
|
377
|
+
_symbols=list(symbols)
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
dashes_cfg = palettes_dict.get("dashes")
|
|
381
|
+
if dashes_cfg is not None:
|
|
382
|
+
if isinstance(dashes_cfg, list):
|
|
383
|
+
palette_kwargs["dashes"] = DefaultDashPalette(
|
|
384
|
+
_dashes=list(dashes_cfg)
|
|
385
|
+
)
|
|
386
|
+
else:
|
|
387
|
+
dashes_dict = _require_mapping(dashes_cfg, "palettes.dashes")
|
|
388
|
+
dashes = dashes_dict.get("dashes", dashes_dict.get("_dashes"))
|
|
389
|
+
if dashes is not None:
|
|
390
|
+
palette_kwargs["dashes"] = DefaultDashPalette(
|
|
391
|
+
_dashes=list(dashes)
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
kwargs["palettes"] = PaletteBundle(**palette_kwargs)
|
|
395
|
+
|
|
396
|
+
return Theme(**kwargs)
|
|
397
|
+
|
|
398
|
+
# Flat legacy-like format for convenience.
|
|
399
|
+
layout_kwargs: dict[str, Any] = {}
|
|
400
|
+
typography_kwargs: dict[str, Any] = {}
|
|
401
|
+
background_kwargs: dict[str, Any] = {}
|
|
402
|
+
axes_kwargs: dict[str, Any] = {}
|
|
403
|
+
legend_kwargs: dict[str, Any] = {}
|
|
404
|
+
colorbar_kwargs: dict[str, Any] = {}
|
|
405
|
+
palettes_kwargs: dict[str, Any] = {}
|
|
406
|
+
|
|
407
|
+
if "width" in data:
|
|
408
|
+
layout_kwargs["width"] = data["width"]
|
|
409
|
+
if "height" in data:
|
|
410
|
+
layout_kwargs["height"] = data["height"]
|
|
411
|
+
if "margin" in data:
|
|
412
|
+
layout_kwargs["margin"] = _require_mapping(data["margin"], "margin")
|
|
413
|
+
|
|
414
|
+
if "font_family" in data:
|
|
415
|
+
typography_kwargs["font_family"] = data["font_family"]
|
|
416
|
+
if "title_font_size" in data:
|
|
417
|
+
typography_kwargs["title_font_size"] = data["title_font_size"]
|
|
418
|
+
if "label_font_size" in data:
|
|
419
|
+
typography_kwargs["label_font_size"] = data["label_font_size"]
|
|
420
|
+
if "tick_font_size" in data:
|
|
421
|
+
typography_kwargs["tick_font_size"] = data["tick_font_size"]
|
|
422
|
+
|
|
423
|
+
if "background_color" in data:
|
|
424
|
+
background_kwargs["plot_bgcolor"] = data["background_color"]
|
|
425
|
+
if "paper_bgcolor" in data:
|
|
426
|
+
background_kwargs["paper_bgcolor"] = data["paper_bgcolor"]
|
|
427
|
+
|
|
428
|
+
if "grid_color" in data:
|
|
429
|
+
axes_kwargs["grid_color"] = data["grid_color"]
|
|
430
|
+
if "border_color" in data:
|
|
431
|
+
axes_kwargs["line_color"] = data["border_color"]
|
|
432
|
+
if "show_borders" in data:
|
|
433
|
+
axes_kwargs["show_line"] = bool(data["show_borders"])
|
|
434
|
+
if "mirror_axes" in data:
|
|
435
|
+
axes_kwargs["mirror"] = bool(data["mirror_axes"])
|
|
436
|
+
|
|
437
|
+
show_x_grid = data.get("show_x_grid")
|
|
438
|
+
show_y_grid = data.get("show_y_grid")
|
|
439
|
+
if show_x_grid is not None and show_y_grid is not None:
|
|
440
|
+
sx = bool(show_x_grid)
|
|
441
|
+
sy = bool(show_y_grid)
|
|
442
|
+
if sx != sy:
|
|
443
|
+
raise ValueError(
|
|
444
|
+
"Flat theme format does not support different values for "
|
|
445
|
+
"'show_x_grid' and 'show_y_grid'. Use structured 'axes' theme format."
|
|
446
|
+
)
|
|
447
|
+
axes_kwargs["show_grid"] = sx
|
|
448
|
+
elif show_x_grid is not None:
|
|
449
|
+
axes_kwargs["show_grid"] = bool(show_x_grid)
|
|
450
|
+
elif show_y_grid is not None:
|
|
451
|
+
axes_kwargs["show_grid"] = bool(show_y_grid)
|
|
452
|
+
|
|
453
|
+
if "legend_bgcolor" in data:
|
|
454
|
+
legend_kwargs["bgcolor"] = data["legend_bgcolor"]
|
|
455
|
+
if "legend_border_color" in data:
|
|
456
|
+
legend_kwargs["border_color"] = data["legend_border_color"]
|
|
457
|
+
if "legend_border_width" in data:
|
|
458
|
+
legend_kwargs["border_width"] = data["legend_border_width"]
|
|
459
|
+
if "legend_font_size" in data:
|
|
460
|
+
legend_kwargs["font_size"] = data["legend_font_size"]
|
|
461
|
+
|
|
462
|
+
if "colorbar_orientation" in data:
|
|
463
|
+
colorbar_kwargs["orientation"] = data["colorbar_orientation"]
|
|
464
|
+
if "colorbar_x" in data:
|
|
465
|
+
colorbar_kwargs["x"] = data["colorbar_x"]
|
|
466
|
+
if "colorbar_y" in data:
|
|
467
|
+
colorbar_kwargs["y"] = data["colorbar_y"]
|
|
468
|
+
if "colorbar_xanchor" in data:
|
|
469
|
+
colorbar_kwargs["xanchor"] = data["colorbar_xanchor"]
|
|
470
|
+
if "colorbar_yanchor" in data:
|
|
471
|
+
colorbar_kwargs["yanchor"] = data["colorbar_yanchor"]
|
|
472
|
+
if "colorbar_thickness" in data:
|
|
473
|
+
colorbar_kwargs["thickness"] = data["colorbar_thickness"]
|
|
474
|
+
if "colorbar_len" in data:
|
|
475
|
+
colorbar_kwargs["len"] = data["colorbar_len"]
|
|
476
|
+
|
|
477
|
+
if "discrete_colormap" in data:
|
|
478
|
+
palettes_kwargs["color"] = DefaultColorPalette(
|
|
479
|
+
_colors=list(data["discrete_colormap"])
|
|
480
|
+
)
|
|
481
|
+
if "marker_symbols" in data:
|
|
482
|
+
palettes_kwargs["symbols"] = DefaultSymbolPalette(
|
|
483
|
+
_symbols=list(data["marker_symbols"])
|
|
484
|
+
)
|
|
485
|
+
if "line_dashes" in data:
|
|
486
|
+
palettes_kwargs["dashes"] = DefaultDashPalette(
|
|
487
|
+
_dashes=list(data["line_dashes"])
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
if layout_kwargs:
|
|
491
|
+
kwargs["layout"] = LayoutStyle(**layout_kwargs)
|
|
492
|
+
if typography_kwargs:
|
|
493
|
+
kwargs["typography"] = TypographyStyle(**typography_kwargs)
|
|
494
|
+
if background_kwargs:
|
|
495
|
+
kwargs["background"] = BackgroundStyle(**background_kwargs)
|
|
496
|
+
if axes_kwargs:
|
|
497
|
+
kwargs["axes"] = AxesStyle(**axes_kwargs)
|
|
498
|
+
if legend_kwargs:
|
|
499
|
+
kwargs["legend"] = LegendStyle(**legend_kwargs)
|
|
500
|
+
if colorbar_kwargs:
|
|
501
|
+
kwargs["colorbar"] = ColorbarStyle(**colorbar_kwargs)
|
|
502
|
+
if palettes_kwargs:
|
|
503
|
+
kwargs["palettes"] = PaletteBundle(**palettes_kwargs)
|
|
504
|
+
|
|
505
|
+
return Theme(**kwargs)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _require_mapping(value: Any, field_name: str) -> dict[str, Any]:
|
|
509
|
+
if not isinstance(value, dict):
|
|
510
|
+
raise TypeError(
|
|
511
|
+
f"Theme field '{field_name}' must be a mapping, got {type(value).__name__}"
|
|
512
|
+
)
|
|
513
|
+
return dict(value)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Palettes
|
|
2
|
+
from .palettes import (
|
|
3
|
+
LINE_DASHES,
|
|
4
|
+
MARKER_SYMBOLS,
|
|
5
|
+
ColorPalette,
|
|
6
|
+
DashPalette,
|
|
7
|
+
DefaultColorPalette,
|
|
8
|
+
DefaultDashPalette,
|
|
9
|
+
DefaultSymbolPalette,
|
|
10
|
+
PaletteBundle,
|
|
11
|
+
SymbolPalette,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Resolvers
|
|
15
|
+
from .resolvers import (
|
|
16
|
+
CategoricalPropertyResolver,
|
|
17
|
+
ColorResolver,
|
|
18
|
+
ColorSource,
|
|
19
|
+
NumericPropertyResolver,
|
|
20
|
+
ResolvedColor,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Style property sources
|
|
24
|
+
from .sources import (
|
|
25
|
+
DashSource,
|
|
26
|
+
StyleLiteralKind,
|
|
27
|
+
SymbolSource,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Theme
|
|
31
|
+
from .theme import (
|
|
32
|
+
DARK_THEME,
|
|
33
|
+
DEFAULT_THEME,
|
|
34
|
+
MINIMAL_THEME,
|
|
35
|
+
AxesStyle,
|
|
36
|
+
BackgroundStyle,
|
|
37
|
+
ColorbarStyle,
|
|
38
|
+
LayoutStyle,
|
|
39
|
+
LegendStyle,
|
|
40
|
+
Theme,
|
|
41
|
+
TypographyStyle,
|
|
42
|
+
apply_theme,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"DARK_THEME",
|
|
47
|
+
"DEFAULT_THEME",
|
|
48
|
+
"LINE_DASHES",
|
|
49
|
+
"MARKER_SYMBOLS",
|
|
50
|
+
"MINIMAL_THEME",
|
|
51
|
+
"AxesStyle",
|
|
52
|
+
"BackgroundStyle",
|
|
53
|
+
"CategoricalPropertyResolver",
|
|
54
|
+
# Palettes
|
|
55
|
+
"ColorPalette",
|
|
56
|
+
"ColorResolver",
|
|
57
|
+
# Resolvers
|
|
58
|
+
"ColorSource",
|
|
59
|
+
"ColorbarStyle",
|
|
60
|
+
"DashPalette",
|
|
61
|
+
"DashSource",
|
|
62
|
+
"DefaultColorPalette",
|
|
63
|
+
"DefaultDashPalette",
|
|
64
|
+
"DefaultSymbolPalette",
|
|
65
|
+
# Theme components
|
|
66
|
+
"LayoutStyle",
|
|
67
|
+
"LegendStyle",
|
|
68
|
+
"NumericPropertyResolver",
|
|
69
|
+
"PaletteBundle",
|
|
70
|
+
"ResolvedColor",
|
|
71
|
+
"StyleLiteralKind",
|
|
72
|
+
"SymbolPalette",
|
|
73
|
+
"SymbolSource",
|
|
74
|
+
# Theme
|
|
75
|
+
"Theme",
|
|
76
|
+
"TypographyStyle",
|
|
77
|
+
"apply_theme",
|
|
78
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .bundle import PaletteBundle
|
|
2
|
+
from .color import ColorPalette, DefaultColorPalette
|
|
3
|
+
from .dashes import LINE_DASHES, DashPalette, DefaultDashPalette
|
|
4
|
+
from .symbols import MARKER_SYMBOLS, DefaultSymbolPalette, SymbolPalette
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"LINE_DASHES",
|
|
8
|
+
"MARKER_SYMBOLS",
|
|
9
|
+
# Color
|
|
10
|
+
"ColorPalette",
|
|
11
|
+
# Dashes
|
|
12
|
+
"DashPalette",
|
|
13
|
+
"DefaultColorPalette",
|
|
14
|
+
"DefaultDashPalette",
|
|
15
|
+
"DefaultSymbolPalette",
|
|
16
|
+
# Bundle
|
|
17
|
+
"PaletteBundle",
|
|
18
|
+
# Symbols
|
|
19
|
+
"SymbolPalette",
|
|
20
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from .color import ColorPalette, DefaultColorPalette
|
|
6
|
+
from .dashes import DashPalette, DefaultDashPalette
|
|
7
|
+
from .symbols import DefaultSymbolPalette, SymbolPalette
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class PaletteBundle:
|
|
12
|
+
"""Collection of all palettes used for styling."""
|
|
13
|
+
|
|
14
|
+
color: ColorPalette = field(default_factory=DefaultColorPalette)
|
|
15
|
+
symbols: SymbolPalette = field(default_factory=DefaultSymbolPalette)
|
|
16
|
+
dashes: DashPalette = field(default_factory=DefaultDashPalette)
|