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.
Files changed (193) hide show
  1. diffract/__init__.py +52 -0
  2. diffract/configs/fast_speed_without_disk.ini +48 -0
  3. diffract/configs/hybrid.ini +74 -0
  4. diffract/configs/sqlite.ini +62 -0
  5. diffract/containers.py +405 -0
  6. diffract/core/__init__.py +23 -0
  7. diffract/core/cache/__init__.py +24 -0
  8. diffract/core/cache/containers.py +93 -0
  9. diffract/core/cache/interface.py +117 -0
  10. diffract/core/cache/redis_manager.py +464 -0
  11. diffract/core/cache/simple_manager.py +478 -0
  12. diffract/core/compute/__init__.py +46 -0
  13. diffract/core/compute/config.py +48 -0
  14. diffract/core/compute/containers.py +81 -0
  15. diffract/core/compute/decorator.py +125 -0
  16. diffract/core/compute/exceptions.py +31 -0
  17. diffract/core/compute/execution/__init__.py +14 -0
  18. diffract/core/compute/execution/_types.py +70 -0
  19. diffract/core/compute/execution/aggregation.py +74 -0
  20. diffract/core/compute/execution/aggregation_runner.py +362 -0
  21. diffract/core/compute/execution/enums.py +38 -0
  22. diffract/core/compute/execution/executor.py +125 -0
  23. diffract/core/compute/execution/parameter_runner.py +125 -0
  24. diffract/core/compute/execution/restrictions.py +61 -0
  25. diffract/core/compute/execution/strategy.py +118 -0
  26. diffract/core/compute/execution/utils.py +112 -0
  27. diffract/core/compute/extensions/__init__.py +0 -0
  28. diffract/core/compute/extensions/power_law.py +1051 -0
  29. diffract/core/compute/extensions/rmt.py +150 -0
  30. diffract/core/compute/extensions/utils.py +36 -0
  31. diffract/core/compute/field_signature.py +183 -0
  32. diffract/core/compute/kernels/__init__.py +48 -0
  33. diffract/core/compute/kernels/alignment.py +106 -0
  34. diffract/core/compute/kernels/heavy_tailed.py +394 -0
  35. diffract/core/compute/kernels/marchenko_pastur.py +99 -0
  36. diffract/core/compute/kernels/mat_decomposition.py +101 -0
  37. diffract/core/compute/kernels/mat_properties.py +53 -0
  38. diffract/core/compute/kernels/model_quality.py +27 -0
  39. diffract/core/compute/kernels/norms.py +88 -0
  40. diffract/core/compute/kernels/ranks.py +35 -0
  41. diffract/core/compute/kernels/tracy_widom.py +36 -0
  42. diffract/core/compute/metadata.py +67 -0
  43. diffract/core/compute/registry.py +485 -0
  44. diffract/core/constants.py +147 -0
  45. diffract/core/data/__init__.py +32 -0
  46. diffract/core/data/interface.py +304 -0
  47. diffract/core/data/metadata/__init__.py +15 -0
  48. diffract/core/data/metadata/containers.py +60 -0
  49. diffract/core/data/metadata/interface.py +208 -0
  50. diffract/core/data/metadata/sqlite_index.py +604 -0
  51. diffract/core/data/nn/__init__.py +43 -0
  52. diffract/core/data/nn/aggregates/__init__.py +35 -0
  53. diffract/core/data/nn/aggregates/metadata.py +96 -0
  54. diffract/core/data/nn/aggregates/proxy.py +57 -0
  55. diffract/core/data/nn/aggregates/repository.py +87 -0
  56. diffract/core/data/nn/aggregates/view.py +307 -0
  57. diffract/core/data/nn/containers.py +86 -0
  58. diffract/core/data/nn/extractors/__init__.py +49 -0
  59. diffract/core/data/nn/extractors/base.py +300 -0
  60. diffract/core/data/nn/extractors/factory.py +234 -0
  61. diffract/core/data/nn/extractors/flax.py +112 -0
  62. diffract/core/data/nn/extractors/handlers/__init__.py +39 -0
  63. diffract/core/data/nn/extractors/handlers/base.py +110 -0
  64. diffract/core/data/nn/extractors/handlers/flax_handlers.py +71 -0
  65. diffract/core/data/nn/extractors/handlers/numpy_handlers.py +63 -0
  66. diffract/core/data/nn/extractors/handlers/onnx_handlers.py +67 -0
  67. diffract/core/data/nn/extractors/handlers/tensorflow_handlers.py +82 -0
  68. diffract/core/data/nn/extractors/handlers/torch_handlers.py +124 -0
  69. diffract/core/data/nn/extractors/interface.py +56 -0
  70. diffract/core/data/nn/extractors/numpy.py +94 -0
  71. diffract/core/data/nn/extractors/onnx.py +108 -0
  72. diffract/core/data/nn/extractors/tensorflow.py +99 -0
  73. diffract/core/data/nn/extractors/torch.py +204 -0
  74. diffract/core/data/nn/params/__init__.py +27 -0
  75. diffract/core/data/nn/params/interface.py +402 -0
  76. diffract/core/data/nn/params/metadata.py +110 -0
  77. diffract/core/data/nn/params/proxy.py +93 -0
  78. diffract/core/data/nn/params/repository.py +45 -0
  79. diffract/core/data/nn/params/schema.py +82 -0
  80. diffract/core/data/nn/params/view.py +393 -0
  81. diffract/core/data/proxy.py +246 -0
  82. diffract/core/data/repository.py +227 -0
  83. diffract/core/data/utils.py +33 -0
  84. diffract/core/data/view.py +389 -0
  85. diffract/core/export/__init__.py +27 -0
  86. diffract/core/export/containers.py +47 -0
  87. diffract/core/export/exporters.py +191 -0
  88. diffract/core/export/formatters/__init__.py +23 -0
  89. diffract/core/export/formatters/base.py +68 -0
  90. diffract/core/export/formatters/dict_formatter.py +38 -0
  91. diffract/core/export/formatters/json_formatter.py +58 -0
  92. diffract/core/export/formatters/list_formatter.py +41 -0
  93. diffract/core/export/formatters/pandas_formatter.py +86 -0
  94. diffract/core/export/formatters/polars_formatter.py +86 -0
  95. diffract/core/export/formatters/registry.py +84 -0
  96. diffract/core/export/interface.py +105 -0
  97. diffract/core/parallel/__init__.py +21 -0
  98. diffract/core/parallel/containers.py +57 -0
  99. diffract/core/parallel/execution.py +91 -0
  100. diffract/core/parallel/runtime.py +91 -0
  101. diffract/core/storage/__init__.py +35 -0
  102. diffract/core/storage/base_manager.py +330 -0
  103. diffract/core/storage/containers.py +122 -0
  104. diffract/core/storage/hdf5_manager.py +856 -0
  105. diffract/core/storage/hybrid_manager.py +218 -0
  106. diffract/core/storage/interface.py +222 -0
  107. diffract/core/storage/metadata.py +89 -0
  108. diffract/core/storage/ram_manager.py +159 -0
  109. diffract/core/storage/sqlite_manager.py +1062 -0
  110. diffract/core/storage/zarr_manager.py +992 -0
  111. diffract/core/utils/__init__.py +45 -0
  112. diffract/core/utils/build.py +46 -0
  113. diffract/core/utils/exceptions.py +11 -0
  114. diffract/core/utils/hashing.py +90 -0
  115. diffract/core/utils/imports.py +292 -0
  116. diffract/core/utils/math.py +15 -0
  117. diffract/py.typed +0 -0
  118. diffract/session/__init__.py +24 -0
  119. diffract/session/errors.py +17 -0
  120. diffract/session/field_cache.py +216 -0
  121. diffract/session/namespaces/__init__.py +15 -0
  122. diffract/session/namespaces/compute/__init__.py +219 -0
  123. diffract/session/namespaces/models/__init__.py +282 -0
  124. diffract/session/namespaces/models/parameters/__init__.py +133 -0
  125. diffract/session/namespaces/models/parameters/meta_patcher.py +167 -0
  126. diffract/session/namespaces/results/__init__.py +378 -0
  127. diffract/session/namespaces/results/eraser.py +129 -0
  128. diffract/session/namespaces/results/injester.py +249 -0
  129. diffract/session/namespaces/utils/__init__.py +141 -0
  130. diffract/session/namespaces/utils/merger.py +295 -0
  131. diffract/session/namespaces/viz/__init__.py +91 -0
  132. diffract/session/namespaces/viz/_utils.py +14 -0
  133. diffract/session/namespaces/viz/box.py +207 -0
  134. diffract/session/namespaces/viz/grid.py +160 -0
  135. diffract/session/namespaces/viz/heatmap.py +164 -0
  136. diffract/session/namespaces/viz/scatter.py +202 -0
  137. diffract/session/namespaces/viz/sparkline.py +270 -0
  138. diffract/session/namespaces/viz/violin.py +212 -0
  139. diffract/session/session.py +260 -0
  140. diffract/session/utils.py +81 -0
  141. diffract/viz/__init__.py +42 -0
  142. diffract/viz/data/__init__.py +34 -0
  143. diffract/viz/data/detection.py +57 -0
  144. diffract/viz/data/extraction.py +117 -0
  145. diffract/viz/data/filtering.py +57 -0
  146. diffract/viz/data/ordering.py +129 -0
  147. diffract/viz/data/provider.py +99 -0
  148. diffract/viz/data/types.py +98 -0
  149. diffract/viz/plots/__init__.py +37 -0
  150. diffract/viz/plots/base/__init__.py +20 -0
  151. diffract/viz/plots/base/axis.py +219 -0
  152. diffract/viz/plots/base/coloraxis.py +133 -0
  153. diffract/viz/plots/base/configurator.py +18 -0
  154. diffract/viz/plots/base/jitter.py +368 -0
  155. diffract/viz/plots/base/line.py +197 -0
  156. diffract/viz/plots/base/marker.py +278 -0
  157. diffract/viz/plots/base/overlay.py +14 -0
  158. diffract/viz/plots/base/plot.py +110 -0
  159. diffract/viz/plots/base/update.py +50 -0
  160. diffract/viz/plots/boxplot.py +283 -0
  161. diffract/viz/plots/cluster.py +374 -0
  162. diffract/viz/plots/heatmap.py +168 -0
  163. diffract/viz/plots/scatter.py +233 -0
  164. diffract/viz/plots/sparkline.py +405 -0
  165. diffract/viz/plots/subplots/__init__.py +32 -0
  166. diffract/viz/plots/subplots/coloraxis.py +339 -0
  167. diffract/viz/plots/subplots/factory.py +713 -0
  168. diffract/viz/plots/subplots/grid.py +214 -0
  169. diffract/viz/plots/subplots/layout.py +370 -0
  170. diffract/viz/plots/subplots/spec.py +39 -0
  171. diffract/viz/plots/violin.py +298 -0
  172. diffract/viz/renderer.py +513 -0
  173. diffract/viz/styling/__init__.py +78 -0
  174. diffract/viz/styling/palettes/__init__.py +20 -0
  175. diffract/viz/styling/palettes/bundle.py +16 -0
  176. diffract/viz/styling/palettes/color.py +83 -0
  177. diffract/viz/styling/palettes/dashes.py +27 -0
  178. diffract/viz/styling/palettes/symbols.py +42 -0
  179. diffract/viz/styling/resolvers/__init__.py +11 -0
  180. diffract/viz/styling/resolvers/categorical.py +68 -0
  181. diffract/viz/styling/resolvers/color.py +77 -0
  182. diffract/viz/styling/resolvers/numeric.py +108 -0
  183. diffract/viz/styling/sources.py +27 -0
  184. diffract/viz/styling/theme/__init__.py +29 -0
  185. diffract/viz/styling/theme/applier.py +114 -0
  186. diffract/viz/styling/theme/components.py +66 -0
  187. diffract/viz/styling/theme/presets.py +58 -0
  188. diffract/viz/styling/theme/theme.py +36 -0
  189. diffract_core-0.2.1.dist-info/METADATA +530 -0
  190. diffract_core-0.2.1.dist-info/RECORD +193 -0
  191. diffract_core-0.2.1.dist-info/WHEEL +4 -0
  192. diffract_core-0.2.1.dist-info/licenses/LICENSE +201 -0
  193. diffract_core-0.2.1.dist-info/licenses/NOTICE +2 -0
@@ -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)