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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Grid plot composition for `viz` Plot objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from plotly.subplots import make_subplots
|
|
10
|
+
|
|
11
|
+
from diffract.viz.plots.base.plot import Plot
|
|
12
|
+
from diffract.viz.styling import Theme, apply_theme
|
|
13
|
+
|
|
14
|
+
from .coloraxis import (
|
|
15
|
+
ColoraxisRegistry,
|
|
16
|
+
apply_coloraxis_configs,
|
|
17
|
+
distribute_colorbars,
|
|
18
|
+
extract_coloraxis_share_keys,
|
|
19
|
+
extract_legacy_coloraxis_name,
|
|
20
|
+
remap_coloraxis,
|
|
21
|
+
)
|
|
22
|
+
from .layout import add_figure_to_subplot
|
|
23
|
+
from .spec import VALID_SESSION_FILTER_KEYS, VALUE_FILTER_SENTINEL, SubplotSpec
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
import plotly.graph_objects as go
|
|
27
|
+
|
|
28
|
+
from diffract.session import Session
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(kw_only=True)
|
|
32
|
+
class GridPlot:
|
|
33
|
+
"""Compose multiple plots into a grid layout."""
|
|
34
|
+
|
|
35
|
+
subplots: list[SubplotSpec]
|
|
36
|
+
make_subplots_kwargs: dict[str, Any]
|
|
37
|
+
|
|
38
|
+
def render(self, session: Session, theme: Theme | None = None) -> go.Figure:
|
|
39
|
+
"""Render all subplot specs into a single Plotly grid figure."""
|
|
40
|
+
_validate_subplots(self.subplots)
|
|
41
|
+
|
|
42
|
+
total_rows = max((spec.row for spec in self.subplots), default=1)
|
|
43
|
+
total_cols = max((spec.col for spec in self.subplots), default=1)
|
|
44
|
+
sorted_specs = sorted(self.subplots, key=lambda spec: (spec.row, spec.col))
|
|
45
|
+
titles = _build_subplot_titles(sorted_specs, total_rows, total_cols)
|
|
46
|
+
|
|
47
|
+
kwargs = dict(self.make_subplots_kwargs or {})
|
|
48
|
+
for forbidden_key in ("rows", "cols", "subplot_titles"):
|
|
49
|
+
kwargs.pop(forbidden_key, None)
|
|
50
|
+
|
|
51
|
+
grid = make_subplots(
|
|
52
|
+
rows=total_rows,
|
|
53
|
+
cols=total_cols,
|
|
54
|
+
subplot_titles=titles,
|
|
55
|
+
**kwargs,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
seen_legend_names: set[str] = set()
|
|
59
|
+
coloraxis_registry = ColoraxisRegistry()
|
|
60
|
+
|
|
61
|
+
for spec in sorted_specs:
|
|
62
|
+
render_session = session
|
|
63
|
+
resolved_session_filter = _resolve_session_filter(spec)
|
|
64
|
+
if resolved_session_filter:
|
|
65
|
+
render_session = _apply_session_filter(session, resolved_session_filter)
|
|
66
|
+
|
|
67
|
+
child_figure = _render_subplot_plot(
|
|
68
|
+
plot=spec.plot,
|
|
69
|
+
session=render_session,
|
|
70
|
+
theme=theme,
|
|
71
|
+
extra_value_filter=spec.value_filter,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
remap_coloraxis(
|
|
75
|
+
child_figure,
|
|
76
|
+
coloraxis_registry,
|
|
77
|
+
explicit_share_keys=extract_coloraxis_share_keys(spec.plot),
|
|
78
|
+
legacy_share_name=extract_legacy_coloraxis_name(spec.plot),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
add_figure_to_subplot(
|
|
82
|
+
grid,
|
|
83
|
+
child_figure,
|
|
84
|
+
row=spec.row,
|
|
85
|
+
col=spec.col,
|
|
86
|
+
transfer_layout=True,
|
|
87
|
+
seen_legend_names=seen_legend_names,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
apply_coloraxis_configs(grid, coloraxis_registry)
|
|
91
|
+
|
|
92
|
+
if theme is not None:
|
|
93
|
+
apply_theme(grid, theme)
|
|
94
|
+
|
|
95
|
+
distribute_colorbars(grid)
|
|
96
|
+
|
|
97
|
+
return grid
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _build_subplot_titles(specs: list[SubplotSpec], rows: int, cols: int) -> list[str]:
|
|
101
|
+
titles = [""] * (rows * cols)
|
|
102
|
+
for spec in specs:
|
|
103
|
+
idx = (spec.row - 1) * cols + (spec.col - 1)
|
|
104
|
+
existing = titles[idx]
|
|
105
|
+
if existing and spec.title and existing != spec.title:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
"Conflicting subplot titles for the same cell "
|
|
108
|
+
f"(row={spec.row}, col={spec.col}): '{existing}' vs '{spec.title}'."
|
|
109
|
+
)
|
|
110
|
+
if not existing and spec.title:
|
|
111
|
+
titles[idx] = spec.title
|
|
112
|
+
return titles
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _validate_subplots(subplots: list[SubplotSpec]) -> None:
|
|
116
|
+
for spec in subplots:
|
|
117
|
+
if spec.row < 1 or spec.col < 1:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
"Subplot row/col must be 1-indexed positive integers; "
|
|
120
|
+
f"got row={spec.row}, col={spec.col}."
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
filter_dict = _resolve_session_filter(spec)
|
|
124
|
+
if filter_dict:
|
|
125
|
+
invalid_keys = sorted(set(filter_dict) - VALID_SESSION_FILTER_KEYS)
|
|
126
|
+
if invalid_keys:
|
|
127
|
+
valid_keys = sorted(VALID_SESSION_FILTER_KEYS)
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"Invalid filter keys {invalid_keys} in subplot filter. "
|
|
130
|
+
f"Valid keys are: {valid_keys}"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _resolve_session_filter(spec: SubplotSpec) -> dict[str, Any] | None:
|
|
135
|
+
if spec.session_filter is None:
|
|
136
|
+
return spec.filter
|
|
137
|
+
if spec.filter is None:
|
|
138
|
+
return spec.session_filter
|
|
139
|
+
|
|
140
|
+
merged = dict(spec.filter)
|
|
141
|
+
merged.update(spec.session_filter)
|
|
142
|
+
return merged
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _render_subplot_plot(
|
|
146
|
+
*,
|
|
147
|
+
plot: Plot,
|
|
148
|
+
session: Session,
|
|
149
|
+
theme: Theme | None,
|
|
150
|
+
extra_value_filter: dict[str, tuple[str, Any]] | None,
|
|
151
|
+
) -> go.Figure:
|
|
152
|
+
original_value_filter = getattr(plot, "value_filter", VALUE_FILTER_SENTINEL)
|
|
153
|
+
|
|
154
|
+
if original_value_filter is not VALUE_FILTER_SENTINEL:
|
|
155
|
+
merged = _merge_value_filters(original_value_filter, extra_value_filter)
|
|
156
|
+
plot.value_filter = merged
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
return _render_plot_with_optional_theme(plot, session, theme)
|
|
160
|
+
finally:
|
|
161
|
+
if original_value_filter is not VALUE_FILTER_SENTINEL:
|
|
162
|
+
plot.value_filter = original_value_filter
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _render_plot_with_optional_theme(
|
|
166
|
+
plot: Plot,
|
|
167
|
+
session: Session,
|
|
168
|
+
theme: Theme | None,
|
|
169
|
+
) -> go.Figure:
|
|
170
|
+
if theme is None:
|
|
171
|
+
return plot.render(session)
|
|
172
|
+
|
|
173
|
+
if _plot_supports_theme_kwarg(plot):
|
|
174
|
+
return plot.render(session, theme=theme)
|
|
175
|
+
return plot.render(session)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _plot_supports_theme_kwarg(plot: Plot) -> bool:
|
|
179
|
+
try:
|
|
180
|
+
signature = inspect.signature(plot.render)
|
|
181
|
+
except (TypeError, ValueError):
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
for parameter in signature.parameters.values():
|
|
185
|
+
if parameter.kind == inspect.Parameter.VAR_KEYWORD:
|
|
186
|
+
return True
|
|
187
|
+
if parameter.name == "theme":
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _merge_value_filters(
|
|
194
|
+
base: dict[str, tuple[str, Any]] | None,
|
|
195
|
+
extra: dict[str, tuple[str, Any]] | None,
|
|
196
|
+
) -> dict[str, tuple[str, Any]] | None:
|
|
197
|
+
if base is None and extra is None:
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
merged: dict[str, tuple[str, Any]] = {}
|
|
201
|
+
if base:
|
|
202
|
+
merged.update(base)
|
|
203
|
+
if extra:
|
|
204
|
+
merged.update(extra)
|
|
205
|
+
return merged
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _apply_session_filter(session: Session, filter_dict: dict[str, Any]) -> Session:
|
|
209
|
+
return session.filter(
|
|
210
|
+
param_ids=filter_dict.get("param_ids"),
|
|
211
|
+
param_names=filter_dict.get("param_names"),
|
|
212
|
+
param_types=filter_dict.get("param_types"),
|
|
213
|
+
model_ids=filter_dict.get("model_ids"),
|
|
214
|
+
)
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""Layout transfer helpers for subplot composition."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
import plotly.graph_objects as go
|
|
10
|
+
|
|
11
|
+
_AXIS_REF_RE = re.compile(r"^(?P<axis>[xy])(?P<id>\d+)?(?P<domain>\s+domain)?$")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def add_figure_to_subplot(
|
|
15
|
+
fig_parent: go.Figure,
|
|
16
|
+
child_fig: go.Figure,
|
|
17
|
+
*,
|
|
18
|
+
row: int,
|
|
19
|
+
col: int,
|
|
20
|
+
transfer_layout: bool,
|
|
21
|
+
seen_legend_names: set[str] | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Add traces from a child figure into a specific subplot cell."""
|
|
24
|
+
if seen_legend_names is None:
|
|
25
|
+
seen_legend_names = set()
|
|
26
|
+
|
|
27
|
+
axis_refs = _get_subplot_axis_refs(fig_parent, row=row, col=col)
|
|
28
|
+
overlay_xaxis_map: dict[str, str] = {}
|
|
29
|
+
if axis_refs is not None:
|
|
30
|
+
x_ref, y_ref = axis_refs
|
|
31
|
+
overlay_xaxis_map = _register_overlay_xaxes(
|
|
32
|
+
fig_parent,
|
|
33
|
+
child_fig,
|
|
34
|
+
x_ref=x_ref,
|
|
35
|
+
y_ref=y_ref,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
for trace in child_fig.data:
|
|
39
|
+
source_xaxis = getattr(trace, "xaxis", None)
|
|
40
|
+
source_yaxis = getattr(trace, "yaxis", None)
|
|
41
|
+
|
|
42
|
+
name = getattr(trace, "name", None)
|
|
43
|
+
if name and name in seen_legend_names:
|
|
44
|
+
trace.showlegend = False
|
|
45
|
+
if not getattr(trace, "legendgroup", None):
|
|
46
|
+
trace.legendgroup = name
|
|
47
|
+
elif name:
|
|
48
|
+
seen_legend_names.add(name)
|
|
49
|
+
if not getattr(trace, "legendgroup", None):
|
|
50
|
+
trace.legendgroup = name
|
|
51
|
+
|
|
52
|
+
fig_parent.add_trace(trace, row=row, col=col)
|
|
53
|
+
|
|
54
|
+
if axis_refs is None:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
mapped_xaxis = _remap_trace_axis_reference(
|
|
58
|
+
source_xaxis,
|
|
59
|
+
axis="x",
|
|
60
|
+
primary_axis=x_ref,
|
|
61
|
+
overlay_map=overlay_xaxis_map,
|
|
62
|
+
)
|
|
63
|
+
mapped_yaxis = _remap_trace_axis_reference(
|
|
64
|
+
source_yaxis,
|
|
65
|
+
axis="y",
|
|
66
|
+
primary_axis=y_ref,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
fig_parent.data[-1].update(xaxis=mapped_xaxis, yaxis=mapped_yaxis)
|
|
70
|
+
|
|
71
|
+
if transfer_layout:
|
|
72
|
+
transfer_layout_to_subplot(fig_parent, child_fig, row=row, col=col)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def transfer_layout_to_subplot(
|
|
76
|
+
fig_parent: go.Figure,
|
|
77
|
+
child_fig: go.Figure,
|
|
78
|
+
*,
|
|
79
|
+
row: int,
|
|
80
|
+
col: int,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Transfer axis/layout overlays from child to the target subplot cell."""
|
|
83
|
+
axis_refs = _get_subplot_axis_refs(fig_parent, row=row, col=col)
|
|
84
|
+
if axis_refs is None:
|
|
85
|
+
return
|
|
86
|
+
x_ref, y_ref = axis_refs
|
|
87
|
+
|
|
88
|
+
_transfer_axis_props(fig_parent, child_fig, row=row, col=col)
|
|
89
|
+
_transfer_shapes(fig_parent, child_fig, x_ref=x_ref, y_ref=y_ref)
|
|
90
|
+
_transfer_annotations(fig_parent, child_fig, x_ref=x_ref, y_ref=y_ref)
|
|
91
|
+
_transfer_images(fig_parent, child_fig, x_ref=x_ref, y_ref=y_ref)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _transfer_axis_props(
|
|
95
|
+
fig_parent: go.Figure,
|
|
96
|
+
child_fig: go.Figure,
|
|
97
|
+
*,
|
|
98
|
+
row: int,
|
|
99
|
+
col: int,
|
|
100
|
+
) -> None:
|
|
101
|
+
axis_props = [
|
|
102
|
+
"tickmode",
|
|
103
|
+
"tickvals",
|
|
104
|
+
"ticktext",
|
|
105
|
+
"tickangle",
|
|
106
|
+
"tickformat",
|
|
107
|
+
"tickprefix",
|
|
108
|
+
"ticksuffix",
|
|
109
|
+
"showticklabels",
|
|
110
|
+
"ticks",
|
|
111
|
+
"tickfont",
|
|
112
|
+
"tickcolor",
|
|
113
|
+
"ticklen",
|
|
114
|
+
"tickwidth",
|
|
115
|
+
"showgrid",
|
|
116
|
+
"gridcolor",
|
|
117
|
+
"gridwidth",
|
|
118
|
+
"zeroline",
|
|
119
|
+
"zerolinecolor",
|
|
120
|
+
"zerolinewidth",
|
|
121
|
+
"type",
|
|
122
|
+
"range",
|
|
123
|
+
"autorange",
|
|
124
|
+
"categoryorder",
|
|
125
|
+
"categoryarray",
|
|
126
|
+
"title",
|
|
127
|
+
"titlefont",
|
|
128
|
+
"showline",
|
|
129
|
+
"linecolor",
|
|
130
|
+
"linewidth",
|
|
131
|
+
"mirror",
|
|
132
|
+
"dtick",
|
|
133
|
+
"tick0",
|
|
134
|
+
"rangemode",
|
|
135
|
+
"fixedrange",
|
|
136
|
+
"automargin",
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
for axis_type, child_axis in (
|
|
140
|
+
("x", child_fig.layout.xaxis),
|
|
141
|
+
("y", child_fig.layout.yaxis),
|
|
142
|
+
):
|
|
143
|
+
if child_axis is None:
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
props = {
|
|
147
|
+
key: getattr(child_axis, key)
|
|
148
|
+
for key in axis_props
|
|
149
|
+
if getattr(child_axis, key, None) is not None
|
|
150
|
+
}
|
|
151
|
+
if not props:
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
if axis_type == "x":
|
|
155
|
+
fig_parent.update_xaxes(row=row, col=col, **props)
|
|
156
|
+
else:
|
|
157
|
+
fig_parent.update_yaxes(row=row, col=col, **props)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _transfer_shapes(
|
|
161
|
+
fig_parent: go.Figure,
|
|
162
|
+
child_fig: go.Figure,
|
|
163
|
+
*,
|
|
164
|
+
x_ref: str,
|
|
165
|
+
y_ref: str,
|
|
166
|
+
) -> None:
|
|
167
|
+
if not child_fig.layout.shapes:
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
new_shapes: list[dict[str, Any]] = []
|
|
171
|
+
for shape in child_fig.layout.shapes:
|
|
172
|
+
shape_dict = shape.to_plotly_json()
|
|
173
|
+
_remap_xy_refs(shape_dict, x_ref=x_ref, y_ref=y_ref)
|
|
174
|
+
new_shapes.append(shape_dict)
|
|
175
|
+
|
|
176
|
+
current_shapes = list(fig_parent.layout.shapes or [])
|
|
177
|
+
fig_parent.update_layout(shapes=current_shapes + new_shapes)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _transfer_annotations(
|
|
181
|
+
fig_parent: go.Figure,
|
|
182
|
+
child_fig: go.Figure,
|
|
183
|
+
*,
|
|
184
|
+
x_ref: str,
|
|
185
|
+
y_ref: str,
|
|
186
|
+
) -> None:
|
|
187
|
+
if not child_fig.layout.annotations:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
new_annotations: list[dict[str, Any]] = []
|
|
191
|
+
for annotation in child_fig.layout.annotations:
|
|
192
|
+
annotation_dict = annotation.to_plotly_json()
|
|
193
|
+
_remap_xy_refs(annotation_dict, x_ref=x_ref, y_ref=y_ref)
|
|
194
|
+
new_annotations.append(annotation_dict)
|
|
195
|
+
|
|
196
|
+
current_annotations = list(fig_parent.layout.annotations or [])
|
|
197
|
+
fig_parent.update_layout(annotations=current_annotations + new_annotations)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _transfer_images(
|
|
201
|
+
fig_parent: go.Figure,
|
|
202
|
+
child_fig: go.Figure,
|
|
203
|
+
*,
|
|
204
|
+
x_ref: str,
|
|
205
|
+
y_ref: str,
|
|
206
|
+
) -> None:
|
|
207
|
+
if not child_fig.layout.images:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
new_images: list[dict[str, Any]] = []
|
|
211
|
+
for image in child_fig.layout.images:
|
|
212
|
+
image_dict = image.to_plotly_json()
|
|
213
|
+
_remap_xy_refs(image_dict, x_ref=x_ref, y_ref=y_ref)
|
|
214
|
+
new_images.append(image_dict)
|
|
215
|
+
|
|
216
|
+
current_images = list(fig_parent.layout.images or [])
|
|
217
|
+
fig_parent.update_layout(images=current_images + new_images)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _remap_xy_refs(item: dict[str, Any], *, x_ref: str, y_ref: str) -> None:
|
|
221
|
+
x_value = item.get("xref")
|
|
222
|
+
y_value = item.get("yref")
|
|
223
|
+
ax_value = item.get("axref")
|
|
224
|
+
ay_value = item.get("ayref")
|
|
225
|
+
|
|
226
|
+
remapped_x = _remap_axis_reference(x_value, axis="x", target_axis=x_ref)
|
|
227
|
+
remapped_y = _remap_axis_reference(y_value, axis="y", target_axis=y_ref)
|
|
228
|
+
remapped_ax = _remap_axis_reference(ax_value, axis="x", target_axis=x_ref)
|
|
229
|
+
remapped_ay = _remap_axis_reference(ay_value, axis="y", target_axis=y_ref)
|
|
230
|
+
|
|
231
|
+
if remapped_x is not None:
|
|
232
|
+
item["xref"] = remapped_x
|
|
233
|
+
if remapped_y is not None:
|
|
234
|
+
item["yref"] = remapped_y
|
|
235
|
+
if remapped_ax is not None:
|
|
236
|
+
item["axref"] = remapped_ax
|
|
237
|
+
if remapped_ay is not None:
|
|
238
|
+
item["ayref"] = remapped_ay
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _remap_axis_reference(
|
|
242
|
+
value: Any,
|
|
243
|
+
*,
|
|
244
|
+
axis: str,
|
|
245
|
+
target_axis: str,
|
|
246
|
+
) -> str | None:
|
|
247
|
+
if not isinstance(value, str):
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
match = _AXIS_REF_RE.fullmatch(value.strip())
|
|
251
|
+
if match is None or match.group("axis") != axis:
|
|
252
|
+
return value
|
|
253
|
+
|
|
254
|
+
domain = match.group("domain") or ""
|
|
255
|
+
return f"{target_axis}{domain}"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _get_subplot_axis_refs(
|
|
259
|
+
fig_parent: go.Figure,
|
|
260
|
+
*,
|
|
261
|
+
row: int,
|
|
262
|
+
col: int,
|
|
263
|
+
) -> tuple[str, str] | None:
|
|
264
|
+
"""Get parent subplot axis references (for example ``('x3', 'y3')``)."""
|
|
265
|
+
grid_refs = fig_parent._grid_ref[row - 1][col - 1]
|
|
266
|
+
if not grid_refs:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
grid_ref = grid_refs[0]
|
|
270
|
+
remap_ref = getattr(grid_ref, "trace_kwargs", None)
|
|
271
|
+
if not isinstance(remap_ref, dict):
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
x_ref = remap_ref.get("xaxis")
|
|
275
|
+
y_ref = remap_ref.get("yaxis")
|
|
276
|
+
if not isinstance(x_ref, str) or not isinstance(y_ref, str):
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
return x_ref, y_ref
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _register_overlay_xaxes(
|
|
283
|
+
fig_parent: go.Figure,
|
|
284
|
+
child_fig: go.Figure,
|
|
285
|
+
*,
|
|
286
|
+
x_ref: str,
|
|
287
|
+
y_ref: str,
|
|
288
|
+
) -> dict[str, str]:
|
|
289
|
+
"""Create parent overlay x-axes for child overlay axes (x2, x3, ...)."""
|
|
290
|
+
layout_dict = child_fig.layout.to_plotly_json()
|
|
291
|
+
overlay_map: dict[str, str] = {}
|
|
292
|
+
|
|
293
|
+
for layout_key, axis_cfg in layout_dict.items():
|
|
294
|
+
if not layout_key.startswith("xaxis") or layout_key == "xaxis":
|
|
295
|
+
continue
|
|
296
|
+
if not isinstance(axis_cfg, dict):
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
suffix = layout_key[5:]
|
|
300
|
+
child_axis = "x" if not suffix else f"x{suffix}"
|
|
301
|
+
overlaying = axis_cfg.get("overlaying")
|
|
302
|
+
if overlaying not in {"x", "x1"}:
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
parent_axis = _next_axis_ref(fig_parent, axis="x")
|
|
306
|
+
parent_layout_key = _axis_layout_key(parent_axis)
|
|
307
|
+
parent_cfg = dict(axis_cfg)
|
|
308
|
+
parent_cfg["overlaying"] = x_ref
|
|
309
|
+
parent_cfg["anchor"] = y_ref
|
|
310
|
+
parent_cfg.pop("domain", None)
|
|
311
|
+
|
|
312
|
+
fig_parent.update_layout(**{parent_layout_key: parent_cfg})
|
|
313
|
+
overlay_map[child_axis] = parent_axis
|
|
314
|
+
|
|
315
|
+
return overlay_map
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _remap_trace_axis_reference(
|
|
319
|
+
value: Any,
|
|
320
|
+
*,
|
|
321
|
+
axis: str,
|
|
322
|
+
primary_axis: str,
|
|
323
|
+
overlay_map: dict[str, str] | None = None,
|
|
324
|
+
) -> str:
|
|
325
|
+
"""Map a child trace axis reference to parent subplot axis reference."""
|
|
326
|
+
if not isinstance(value, str):
|
|
327
|
+
return primary_axis
|
|
328
|
+
|
|
329
|
+
match = _AXIS_REF_RE.fullmatch(value.strip())
|
|
330
|
+
if match is None or match.group("axis") != axis:
|
|
331
|
+
return primary_axis
|
|
332
|
+
|
|
333
|
+
axis_id = match.group("id") or ""
|
|
334
|
+
child_axis = f"{axis}{axis_id}"
|
|
335
|
+
|
|
336
|
+
if overlay_map is not None and child_axis in overlay_map:
|
|
337
|
+
return overlay_map[child_axis]
|
|
338
|
+
|
|
339
|
+
if child_axis in {axis, f"{axis}1"}:
|
|
340
|
+
return primary_axis
|
|
341
|
+
|
|
342
|
+
return child_axis
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _next_axis_ref(fig_parent: go.Figure, *, axis: str) -> str:
|
|
346
|
+
"""Get next available axis reference in parent layout."""
|
|
347
|
+
max_id = 0
|
|
348
|
+
layout_dict = fig_parent.layout.to_plotly_json()
|
|
349
|
+
axis_prefix = f"{axis}axis"
|
|
350
|
+
|
|
351
|
+
for layout_key in layout_dict:
|
|
352
|
+
if layout_key == axis_prefix:
|
|
353
|
+
max_id = max(max_id, 1)
|
|
354
|
+
continue
|
|
355
|
+
if not layout_key.startswith(axis_prefix):
|
|
356
|
+
continue
|
|
357
|
+
|
|
358
|
+
suffix = layout_key[len(axis_prefix) :]
|
|
359
|
+
if suffix.isdigit():
|
|
360
|
+
max_id = max(max_id, int(suffix))
|
|
361
|
+
|
|
362
|
+
next_id = max_id + 1
|
|
363
|
+
return axis if next_id == 1 else f"{axis}{next_id}"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _axis_layout_key(axis_ref: str) -> str:
|
|
367
|
+
"""Convert axis ref like ``x2`` to layout key like ``xaxis2``."""
|
|
368
|
+
axis = axis_ref[0]
|
|
369
|
+
suffix = axis_ref[1:]
|
|
370
|
+
return f"{axis}axis{suffix}"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Subplot specifications and shared constants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from diffract.viz.plots.base.plot import Plot
|
|
9
|
+
|
|
10
|
+
VALID_SESSION_FILTER_KEYS = frozenset(
|
|
11
|
+
{"param_ids", "param_names", "param_types", "model_ids"}
|
|
12
|
+
)
|
|
13
|
+
VALUE_FILTER_SENTINEL = object()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True)
|
|
17
|
+
class SubplotSpec:
|
|
18
|
+
"""Specification for a single subplot in a `GridPlot`.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
row: Row position (1-indexed).
|
|
22
|
+
col: Column position (1-indexed).
|
|
23
|
+
title: Title for this subplot.
|
|
24
|
+
plot: Plot object to render.
|
|
25
|
+
session_filter: Optional dict forwarded to `Session.filter(...)`.
|
|
26
|
+
Valid keys: param_ids, param_names, param_types, model_ids.
|
|
27
|
+
value_filter: Optional value filter merged into `plot.value_filter` for
|
|
28
|
+
this subplot only: {field: (op, threshold)}.
|
|
29
|
+
filter: Backward-compatible alias for `session_filter`.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
row: int
|
|
33
|
+
col: int
|
|
34
|
+
title: str
|
|
35
|
+
plot: Plot
|
|
36
|
+
|
|
37
|
+
session_filter: dict[str, Any] | None = None
|
|
38
|
+
value_filter: dict[str, tuple[str, Any]] | None = None
|
|
39
|
+
filter: dict[str, Any] | None = None
|