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,197 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import plotly.graph_objects as go
|
|
7
|
+
|
|
8
|
+
from diffract.viz.data import Entry, FieldRef
|
|
9
|
+
from diffract.viz.styling import (
|
|
10
|
+
CategoricalPropertyResolver,
|
|
11
|
+
ColorResolver,
|
|
12
|
+
ColorSource,
|
|
13
|
+
DashSource,
|
|
14
|
+
DefaultColorPalette,
|
|
15
|
+
DefaultDashPalette,
|
|
16
|
+
NumericPropertyResolver,
|
|
17
|
+
ResolvedColor,
|
|
18
|
+
Theme,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .coloraxis import SupportsColoraxis
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(kw_only=True)
|
|
25
|
+
class SupportsLine(SupportsColoraxis("line")):
|
|
26
|
+
"""Configurator mixin for line properties.
|
|
27
|
+
|
|
28
|
+
Trace data keys used (resolved in _build_traces_data):
|
|
29
|
+
- line_width: resolved width values
|
|
30
|
+
- line_color: resolved color (single color or array)
|
|
31
|
+
- line_color_values: numeric values for coloraxis (if applicable)
|
|
32
|
+
- line_dash: resolved dash pattern
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
line_width: FieldRef | float | None = 2
|
|
36
|
+
line_width_range: tuple[float, float] | None = None
|
|
37
|
+
line_color: ColorSource = None
|
|
38
|
+
line_dash: DashSource = None
|
|
39
|
+
line_shape: str | None = None
|
|
40
|
+
line_smoothing: float | None = None
|
|
41
|
+
|
|
42
|
+
def configure(self, fig: go.Figure) -> None:
|
|
43
|
+
"""Apply resolved line styling to matching traces in the figure.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
fig: The Plotly figure whose line traces are updated in place.
|
|
47
|
+
"""
|
|
48
|
+
super().configure(fig)
|
|
49
|
+
|
|
50
|
+
if self._traces_data is None:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
coloraxis = self.resolve_coloraxis(fig)
|
|
54
|
+
|
|
55
|
+
for trace_id, trace_data in self._traces_data.items():
|
|
56
|
+
kwargs = _build_line_kwargs(self, trace_data, coloraxis)
|
|
57
|
+
|
|
58
|
+
if kwargs:
|
|
59
|
+
fig.update_traces(
|
|
60
|
+
line=kwargs,
|
|
61
|
+
selector=lambda t, tid=trace_id: t.meta
|
|
62
|
+
and t.meta.get("trace_id") == tid,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def _resolve_line_width(
|
|
66
|
+
self,
|
|
67
|
+
entries: dict[str, Entry] | None,
|
|
68
|
+
theme: Theme | None = None,
|
|
69
|
+
) -> float | list[float] | None:
|
|
70
|
+
match self.line_width:
|
|
71
|
+
case None:
|
|
72
|
+
return None
|
|
73
|
+
case int() | float():
|
|
74
|
+
return float(self.line_width)
|
|
75
|
+
case FieldRef():
|
|
76
|
+
resolver = NumericPropertyResolver(self.line_width_range)
|
|
77
|
+
return resolver.resolve(self.line_width, entries)
|
|
78
|
+
|
|
79
|
+
def _add_line_width_to_trace(
|
|
80
|
+
self,
|
|
81
|
+
trace_data: dict[str, Any],
|
|
82
|
+
resolved_line_width: float | list[float] | None,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Add resolved line width to trace data."""
|
|
85
|
+
if resolved_line_width is None:
|
|
86
|
+
return
|
|
87
|
+
trace_data["line_width"] = resolved_line_width
|
|
88
|
+
|
|
89
|
+
def _resolve_line_dash(
|
|
90
|
+
self,
|
|
91
|
+
entries: dict[str, Entry] | None,
|
|
92
|
+
theme: Theme | None = None,
|
|
93
|
+
) -> str | list[str] | None:
|
|
94
|
+
match self.line_dash:
|
|
95
|
+
case None:
|
|
96
|
+
return None
|
|
97
|
+
case str():
|
|
98
|
+
return self.line_dash
|
|
99
|
+
case FieldRef():
|
|
100
|
+
dash_palette = theme.palettes.dashes if theme else DefaultDashPalette()
|
|
101
|
+
resolver = CategoricalPropertyResolver(list(dash_palette.dashes))
|
|
102
|
+
return resolver.resolve(self.line_dash, entries)
|
|
103
|
+
|
|
104
|
+
def _add_line_dash_to_trace(
|
|
105
|
+
self,
|
|
106
|
+
trace_data: dict[str, Any],
|
|
107
|
+
resolved_line_dash: str | list[str] | None,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Add resolved line dash to trace data."""
|
|
110
|
+
if resolved_line_dash is None:
|
|
111
|
+
return
|
|
112
|
+
trace_data["line_dash"] = resolved_line_dash
|
|
113
|
+
|
|
114
|
+
def _resolve_line_color(
|
|
115
|
+
self,
|
|
116
|
+
entries: dict[str, Entry] | None,
|
|
117
|
+
theme: Theme | None = None,
|
|
118
|
+
) -> ResolvedColor:
|
|
119
|
+
color_palette = theme.palettes.color if theme else DefaultColorPalette()
|
|
120
|
+
resolver = ColorResolver(color_palette)
|
|
121
|
+
return resolver.resolve(self.line_color, entries)
|
|
122
|
+
|
|
123
|
+
def _add_line_color_to_trace(
|
|
124
|
+
self,
|
|
125
|
+
trace_data: dict[str, Any],
|
|
126
|
+
resolved_color: ResolvedColor,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Add resolved line color to trace data."""
|
|
129
|
+
if resolved_color.values is not None:
|
|
130
|
+
trace_data["line_color_values"] = resolved_color.values
|
|
131
|
+
elif resolved_color.color is not None:
|
|
132
|
+
trace_data["line_color"] = resolved_color.color
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _build_line_kwargs(
|
|
136
|
+
line: SupportsLine,
|
|
137
|
+
trace_data: dict[str, Any],
|
|
138
|
+
coloraxis: str | None = None,
|
|
139
|
+
) -> dict[str, Any]:
|
|
140
|
+
"""Build line kwargs from pre-resolved trace data.
|
|
141
|
+
|
|
142
|
+
Expected trace_data keys:
|
|
143
|
+
- line_width: resolved width (number or array)
|
|
144
|
+
- line_dash: resolved dash pattern (string or array)
|
|
145
|
+
- line_color: resolved color (string, rgb, or array)
|
|
146
|
+
- line_color_values: numeric values for coloraxis mapping
|
|
147
|
+
"""
|
|
148
|
+
kwargs: dict[str, Any] = {}
|
|
149
|
+
|
|
150
|
+
# Width
|
|
151
|
+
width = trace_data.get("line_width")
|
|
152
|
+
if width is not None:
|
|
153
|
+
kwargs["width"] = width
|
|
154
|
+
|
|
155
|
+
# Dash
|
|
156
|
+
dash = trace_data.get("line_dash")
|
|
157
|
+
if dash is not None:
|
|
158
|
+
kwargs["dash"] = dash
|
|
159
|
+
|
|
160
|
+
# Shape (static, from config)
|
|
161
|
+
if line.line_shape is not None:
|
|
162
|
+
kwargs["shape"] = line.line_shape
|
|
163
|
+
|
|
164
|
+
# Smoothing (static, from config)
|
|
165
|
+
if line.line_smoothing is not None:
|
|
166
|
+
kwargs["smoothing"] = line.line_smoothing
|
|
167
|
+
|
|
168
|
+
# Color
|
|
169
|
+
color_kwargs = _build_line_color_kwargs(trace_data, coloraxis)
|
|
170
|
+
kwargs.update(color_kwargs)
|
|
171
|
+
|
|
172
|
+
return kwargs
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _build_line_color_kwargs(
|
|
176
|
+
trace_data: dict[str, Any], coloraxis: str | None = None
|
|
177
|
+
) -> dict[str, Any]:
|
|
178
|
+
"""Build line color kwargs from pre-resolved trace data.
|
|
179
|
+
|
|
180
|
+
Expected trace_data keys:
|
|
181
|
+
- line_color: resolved color (string or array of colors)
|
|
182
|
+
- line_color_values: numeric values for coloraxis mapping
|
|
183
|
+
"""
|
|
184
|
+
kwargs: dict[str, Any] = {}
|
|
185
|
+
|
|
186
|
+
color = trace_data.get("line_color")
|
|
187
|
+
color_values = trace_data.get("line_color_values")
|
|
188
|
+
|
|
189
|
+
if color_values is not None:
|
|
190
|
+
kwargs["color"] = color_values
|
|
191
|
+
if coloraxis is not None:
|
|
192
|
+
kwargs["coloraxis"] = coloraxis
|
|
193
|
+
|
|
194
|
+
elif color is not None:
|
|
195
|
+
kwargs["color"] = color
|
|
196
|
+
|
|
197
|
+
return kwargs
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import plotly.graph_objects as go
|
|
7
|
+
|
|
8
|
+
from diffract.viz.data import Entry, FieldRef
|
|
9
|
+
from diffract.viz.styling import (
|
|
10
|
+
CategoricalPropertyResolver,
|
|
11
|
+
ColorResolver,
|
|
12
|
+
ColorSource,
|
|
13
|
+
DefaultColorPalette,
|
|
14
|
+
DefaultSymbolPalette,
|
|
15
|
+
NumericPropertyResolver,
|
|
16
|
+
ResolvedColor,
|
|
17
|
+
SymbolSource,
|
|
18
|
+
Theme,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .coloraxis import SupportsColoraxis
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(kw_only=True)
|
|
25
|
+
class SupportsMarker(SupportsColoraxis("marker")):
|
|
26
|
+
"""Configurator mixin for marker properties."""
|
|
27
|
+
|
|
28
|
+
marker_size: FieldRef | float | None = 6
|
|
29
|
+
marker_size_range: tuple[float, float] | None = None
|
|
30
|
+
marker_opacity: FieldRef | float | None = 0.7
|
|
31
|
+
marker_opacity_range: tuple[float, float] | None = None
|
|
32
|
+
marker_color: ColorSource = None
|
|
33
|
+
marker_symbol: SymbolSource = None
|
|
34
|
+
|
|
35
|
+
def configure(self, fig: go.Figure) -> None:
|
|
36
|
+
"""Apply resolved marker styling to matching traces in the figure.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
fig: The Plotly figure whose marker traces are updated in place.
|
|
40
|
+
"""
|
|
41
|
+
super().configure(fig)
|
|
42
|
+
|
|
43
|
+
if self._traces_data is None:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
coloraxis = self.resolve_coloraxis(fig)
|
|
47
|
+
|
|
48
|
+
for trace_id, trace_data in self._traces_data.items():
|
|
49
|
+
base_kwargs, per_point_kwargs = _build_marker_kwargs(
|
|
50
|
+
trace_data,
|
|
51
|
+
coloraxis,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if base_kwargs:
|
|
55
|
+
fig.update_traces(
|
|
56
|
+
marker=base_kwargs,
|
|
57
|
+
selector=lambda t, tid=trace_id: (
|
|
58
|
+
t.meta and t.meta.get("trace_id") == tid
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if per_point_kwargs:
|
|
63
|
+
fig.update_traces(
|
|
64
|
+
marker=per_point_kwargs,
|
|
65
|
+
selector=lambda t, tid=trace_id: (
|
|
66
|
+
t.meta
|
|
67
|
+
and t.meta.get("trace_id") == tid
|
|
68
|
+
and _supports_per_point_marker(t)
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _resolve_marker_size(
|
|
73
|
+
self,
|
|
74
|
+
entries: dict[str, Entry] | None,
|
|
75
|
+
theme: Theme | None = None,
|
|
76
|
+
) -> float | list[float] | None:
|
|
77
|
+
match self.marker_size:
|
|
78
|
+
case None:
|
|
79
|
+
return None
|
|
80
|
+
case int() | float():
|
|
81
|
+
return float(self.marker_size)
|
|
82
|
+
case FieldRef():
|
|
83
|
+
resolver = NumericPropertyResolver(self.marker_size_range)
|
|
84
|
+
return resolver.resolve(self.marker_size, entries)
|
|
85
|
+
|
|
86
|
+
def _add_marker_size_to_trace(
|
|
87
|
+
self,
|
|
88
|
+
trace_data: dict[str, Any],
|
|
89
|
+
resolved_size: float | list[float] | None,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Add resolved marker size to trace data."""
|
|
92
|
+
if resolved_size is None:
|
|
93
|
+
return
|
|
94
|
+
trace_data["marker_size"] = resolved_size
|
|
95
|
+
|
|
96
|
+
def _resolve_marker_opacity(
|
|
97
|
+
self,
|
|
98
|
+
entries: dict[str, Entry] | None,
|
|
99
|
+
theme: Theme | None = None,
|
|
100
|
+
) -> float | list[float] | None:
|
|
101
|
+
match self.marker_opacity:
|
|
102
|
+
case None:
|
|
103
|
+
return None
|
|
104
|
+
case int() | float():
|
|
105
|
+
return float(self.marker_opacity)
|
|
106
|
+
case FieldRef():
|
|
107
|
+
resolver = NumericPropertyResolver(self.marker_opacity_range)
|
|
108
|
+
return resolver.resolve(self.marker_opacity, entries)
|
|
109
|
+
|
|
110
|
+
def _add_marker_opacity_to_trace(
|
|
111
|
+
self,
|
|
112
|
+
trace_data: dict[str, Any],
|
|
113
|
+
resolved_opacity: float | list[float] | None,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Add resolved marker opacity to trace data."""
|
|
116
|
+
if resolved_opacity is None:
|
|
117
|
+
return
|
|
118
|
+
trace_data["marker_opacity"] = resolved_opacity
|
|
119
|
+
|
|
120
|
+
def _resolve_marker_symbol(
|
|
121
|
+
self,
|
|
122
|
+
entries: dict[str, Entry] | None,
|
|
123
|
+
theme: Theme | None = None,
|
|
124
|
+
) -> str | list[str] | None:
|
|
125
|
+
match self.marker_symbol:
|
|
126
|
+
case None:
|
|
127
|
+
return None
|
|
128
|
+
case str():
|
|
129
|
+
return self.marker_symbol
|
|
130
|
+
case FieldRef():
|
|
131
|
+
symbol_palette = (
|
|
132
|
+
theme.palettes.symbols if theme else DefaultSymbolPalette()
|
|
133
|
+
)
|
|
134
|
+
resolver = CategoricalPropertyResolver(symbol_palette.symbols)
|
|
135
|
+
return resolver.resolve(self.marker_symbol, entries)
|
|
136
|
+
|
|
137
|
+
def _add_marker_symbol_to_trace(
|
|
138
|
+
self,
|
|
139
|
+
trace_data: dict[str, Any],
|
|
140
|
+
resolved_symbol: str | list[str] | None,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Add resolved marker symbol to trace data."""
|
|
143
|
+
if resolved_symbol is None:
|
|
144
|
+
return
|
|
145
|
+
trace_data["marker_symbol"] = resolved_symbol
|
|
146
|
+
|
|
147
|
+
def _resolve_marker_color(
|
|
148
|
+
self,
|
|
149
|
+
entries: dict[str, Entry] | None,
|
|
150
|
+
theme: Theme | None = None,
|
|
151
|
+
) -> ResolvedColor:
|
|
152
|
+
color_palette = theme.palettes.color if theme else DefaultColorPalette()
|
|
153
|
+
resolver = ColorResolver(color_palette)
|
|
154
|
+
return resolver.resolve(self.marker_color, entries)
|
|
155
|
+
|
|
156
|
+
def _add_marker_color_to_trace(
|
|
157
|
+
self,
|
|
158
|
+
trace_data: dict[str, Any],
|
|
159
|
+
resolved_color: ResolvedColor,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Add resolved marker color to trace data."""
|
|
162
|
+
if resolved_color.values is not None:
|
|
163
|
+
trace_data["marker_color_values"] = resolved_color.values
|
|
164
|
+
elif resolved_color.color is not None:
|
|
165
|
+
trace_data["marker_color"] = resolved_color.color
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _build_marker_kwargs(
|
|
169
|
+
trace_data: dict[str, Any],
|
|
170
|
+
coloraxis: str | None = None,
|
|
171
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
172
|
+
"""Build marker kwargs, split into universal and per-point.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Tuple of ``(base_kwargs, per_point_kwargs)``.
|
|
176
|
+
|
|
177
|
+
*base_kwargs* contains properties supported by all trace types
|
|
178
|
+
(scalar size, scalar opacity, symbol, scalar/single color, line).
|
|
179
|
+
|
|
180
|
+
*per_point_kwargs* contains list-valued properties and the
|
|
181
|
+
``coloraxis`` reference, which are only supported by
|
|
182
|
+
scatter-type traces (not ``go.Box``, ``go.Violin``, etc.).
|
|
183
|
+
"""
|
|
184
|
+
base_kwargs: dict[str, Any] = {}
|
|
185
|
+
per_point_kwargs: dict[str, Any] = {}
|
|
186
|
+
|
|
187
|
+
# Size — scalar goes to base, list goes to per-point
|
|
188
|
+
size = trace_data.get("marker_size")
|
|
189
|
+
if size is not None:
|
|
190
|
+
if isinstance(size, list):
|
|
191
|
+
per_point_kwargs["size"] = size
|
|
192
|
+
else:
|
|
193
|
+
base_kwargs["size"] = size
|
|
194
|
+
|
|
195
|
+
# Opacity — scalar goes to base, list goes to per-point
|
|
196
|
+
opacity = trace_data.get("marker_opacity")
|
|
197
|
+
if opacity is not None:
|
|
198
|
+
if isinstance(opacity, list):
|
|
199
|
+
per_point_kwargs["opacity"] = opacity
|
|
200
|
+
else:
|
|
201
|
+
base_kwargs["opacity"] = opacity
|
|
202
|
+
|
|
203
|
+
# Symbol
|
|
204
|
+
symbol = trace_data.get("marker_symbol")
|
|
205
|
+
if symbol is not None:
|
|
206
|
+
base_kwargs["symbol"] = symbol
|
|
207
|
+
|
|
208
|
+
# Color (split into base color value and coloraxis reference)
|
|
209
|
+
color_base, coloraxis_kwargs = _build_marker_color_kwargs(trace_data, coloraxis)
|
|
210
|
+
base_kwargs.update(color_base)
|
|
211
|
+
per_point_kwargs.update(coloraxis_kwargs)
|
|
212
|
+
|
|
213
|
+
return base_kwargs, per_point_kwargs
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _build_marker_color_kwargs(
|
|
217
|
+
trace_data: dict[str, Any],
|
|
218
|
+
coloraxis: str | None = None,
|
|
219
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
220
|
+
"""Build marker color kwargs, split into base and per-point.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Tuple of ``(base_kwargs, per_point_kwargs)``.
|
|
224
|
+
|
|
225
|
+
Numeric ``color_values`` (continuous colorscale mapping) and
|
|
226
|
+
list-valued categorical colors are routed to *per_point_kwargs*
|
|
227
|
+
together with the ``coloraxis`` reference, since these are only
|
|
228
|
+
supported by scatter-type traces.
|
|
229
|
+
|
|
230
|
+
A single color string is placed in *base_kwargs* (safe for all
|
|
231
|
+
trace types).
|
|
232
|
+
"""
|
|
233
|
+
base_kwargs: dict[str, Any] = {}
|
|
234
|
+
per_point_kwargs: dict[str, Any] = {}
|
|
235
|
+
|
|
236
|
+
color = trace_data.get("marker_color")
|
|
237
|
+
color_values = trace_data.get("marker_color_values")
|
|
238
|
+
|
|
239
|
+
if color_values is not None:
|
|
240
|
+
per_point_kwargs["color"] = color_values
|
|
241
|
+
if coloraxis is not None:
|
|
242
|
+
per_point_kwargs["coloraxis"] = coloraxis
|
|
243
|
+
elif color is not None:
|
|
244
|
+
if isinstance(color, list):
|
|
245
|
+
per_point_kwargs["color"] = color
|
|
246
|
+
else:
|
|
247
|
+
base_kwargs["color"] = color
|
|
248
|
+
|
|
249
|
+
return base_kwargs, per_point_kwargs
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# --- Trace-type support ---
|
|
253
|
+
|
|
254
|
+
_PER_POINT_MARKER_TRACE_TYPES = frozenset(
|
|
255
|
+
{
|
|
256
|
+
"scatter",
|
|
257
|
+
"scattergl",
|
|
258
|
+
"scatter3d",
|
|
259
|
+
"scattergeo",
|
|
260
|
+
"scattermapbox",
|
|
261
|
+
"scatterpolar",
|
|
262
|
+
"scatterpolargl",
|
|
263
|
+
"scatterternary",
|
|
264
|
+
"splom",
|
|
265
|
+
"barpolar",
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _supports_per_point_marker(trace: Any) -> bool:
|
|
271
|
+
"""Check if a Plotly trace type supports per-point marker properties.
|
|
272
|
+
|
|
273
|
+
Scatter-type traces accept list-valued ``marker.size``,
|
|
274
|
+
``marker.opacity``, ``marker.color``, and the ``marker.coloraxis``
|
|
275
|
+
reference. Aggregation traces like ``go.Box`` and ``go.Violin``
|
|
276
|
+
only accept scalar values for these properties.
|
|
277
|
+
"""
|
|
278
|
+
return getattr(trace, "type", None) in _PER_POINT_MARKER_TRACE_TYPES
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
import plotly.graph_objects as go
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Overlay(ABC):
|
|
9
|
+
"""A mixin that can add overlay traces to a Plotly figure."""
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def add_overlay_traces(self, fig: go.Figure) -> None:
|
|
13
|
+
"""Add overlay traces to the given figure."""
|
|
14
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass, field, fields
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import plotly.graph_objects as go
|
|
8
|
+
|
|
9
|
+
from diffract.session import Session
|
|
10
|
+
from diffract.viz.data import DataProvider, Entry, FieldRef
|
|
11
|
+
from diffract.viz.plots.base.overlay import Overlay
|
|
12
|
+
from diffract.viz.styling import apply_theme
|
|
13
|
+
|
|
14
|
+
from .configurator import Configurator
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from diffract.viz.styling import Theme
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(kw_only=True)
|
|
21
|
+
class Plot(ABC):
|
|
22
|
+
"""A configurable plot that can render itself given a Session."""
|
|
23
|
+
|
|
24
|
+
title: str | None = None
|
|
25
|
+
value_filter: dict[str, tuple[str, Any]] | None = None
|
|
26
|
+
|
|
27
|
+
_traces_data: dict[str, dict[str, Any]] | None = field(
|
|
28
|
+
default=None, repr=False, compare=False
|
|
29
|
+
)
|
|
30
|
+
_theme: Theme | None = field(default=None, repr=False, compare=False)
|
|
31
|
+
|
|
32
|
+
def render(self, session: Session, theme: Theme | None = None) -> go.Figure:
|
|
33
|
+
"""Render the plot using data from the session.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
session: The diffract session containing data to plot.
|
|
37
|
+
theme: Optional theme for styling. If provided, palettes from theme
|
|
38
|
+
are used for colors/symbols/dashes, and figure-level styling
|
|
39
|
+
is applied after configurators.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A Plotly Figure object.
|
|
43
|
+
"""
|
|
44
|
+
self._theme = theme
|
|
45
|
+
data_provider = DataProvider(session)
|
|
46
|
+
entries = self._collect_entries(data_provider)
|
|
47
|
+
self._traces_data = self._build_traces_data(entries)
|
|
48
|
+
fig: go.Figure = self._build_figure()
|
|
49
|
+
self._add_overlay_traces_by_mro(fig)
|
|
50
|
+
self._configure_by_mro(fig)
|
|
51
|
+
|
|
52
|
+
if theme is not None:
|
|
53
|
+
apply_theme(fig, theme)
|
|
54
|
+
|
|
55
|
+
return fig
|
|
56
|
+
|
|
57
|
+
def _collect_entries(self, data_provider: DataProvider) -> dict[str, Entry]:
|
|
58
|
+
"""Collect all entries needed for the plot."""
|
|
59
|
+
fields_to_fetch = self._collect_fields_to_fetch()
|
|
60
|
+
return data_provider.fetch(fields_to_fetch, value_filter=self.value_filter)
|
|
61
|
+
|
|
62
|
+
def _collect_fields_to_fetch(self) -> list[str]:
|
|
63
|
+
"""Collect all field names needed for the plot.
|
|
64
|
+
|
|
65
|
+
Walks the MRO and inspects dataclass fields, collecting the ``field``
|
|
66
|
+
attribute from every value that is a :class:`FieldRef`.
|
|
67
|
+
"""
|
|
68
|
+
result: list[str] = []
|
|
69
|
+
seen: set[str] = set()
|
|
70
|
+
|
|
71
|
+
for cls in type(self).mro():
|
|
72
|
+
if not hasattr(cls, "__dataclass_fields__"):
|
|
73
|
+
continue
|
|
74
|
+
for f in fields(cls):
|
|
75
|
+
value = getattr(self, f.name, None)
|
|
76
|
+
if isinstance(value, FieldRef) and value.field not in seen:
|
|
77
|
+
seen.add(value.field)
|
|
78
|
+
result.append(value.field)
|
|
79
|
+
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def _build_traces_data(
|
|
84
|
+
self, entries: dict[str, Entry]
|
|
85
|
+
) -> dict[str, dict[str, Any]]:
|
|
86
|
+
raise NotImplementedError
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def _build_figure(self) -> go.Figure:
|
|
90
|
+
raise NotImplementedError
|
|
91
|
+
|
|
92
|
+
def _add_overlay_traces_by_mro(self, fig: go.Figure) -> None:
|
|
93
|
+
"""Hook for mixins to add overlay traces (jitter, annotations, etc.).
|
|
94
|
+
|
|
95
|
+
Override in mixins to add additional traces after the main figure is built.
|
|
96
|
+
Always call super()._add_overlay_traces(fig) to ensure proper MRO chaining.
|
|
97
|
+
"""
|
|
98
|
+
for cls in type(self).mro():
|
|
99
|
+
if cls is Overlay:
|
|
100
|
+
continue
|
|
101
|
+
if issubclass(cls, Overlay):
|
|
102
|
+
cls.add_overlay_traces(self, fig)
|
|
103
|
+
|
|
104
|
+
def _configure_by_mro(self, fig: go.Figure) -> None:
|
|
105
|
+
"""Apply configurator mixins in MRO order."""
|
|
106
|
+
for cls in type(self).mro():
|
|
107
|
+
if cls is Configurator:
|
|
108
|
+
continue
|
|
109
|
+
if issubclass(cls, Configurator):
|
|
110
|
+
cls.configure(self, fig)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from diffract.viz.renderer import Plot
|
|
7
|
+
from diffract.viz.styling import apply_theme
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
10
|
+
import plotly.graph_objects as go # type: ignore[import-not-found]
|
|
11
|
+
|
|
12
|
+
from diffract.session import Session
|
|
13
|
+
from diffract.viz.styling import Theme
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(kw_only=True)
|
|
17
|
+
class UpdateFigure:
|
|
18
|
+
"""Wrap another plot and apply Plotly update calls."""
|
|
19
|
+
|
|
20
|
+
plot: Plot
|
|
21
|
+
|
|
22
|
+
# Back-compat: older configs used `config:` meaning `Figure.update(config)`.
|
|
23
|
+
config: dict[str, Any] | None = None
|
|
24
|
+
update: dict[str, Any] | None = None
|
|
25
|
+
layout: dict[str, Any] | None = None
|
|
26
|
+
traces: dict[str, Any] | None = None
|
|
27
|
+
xaxes: dict[str, Any] | None = None
|
|
28
|
+
yaxes: dict[str, Any] | None = None
|
|
29
|
+
|
|
30
|
+
def render(self, session: Session, theme: Theme | None = None) -> go.Figure:
|
|
31
|
+
"""Render the wrapped plot and apply configuration overrides."""
|
|
32
|
+
fig = self.plot.render(session, theme)
|
|
33
|
+
|
|
34
|
+
if self.config is not None:
|
|
35
|
+
fig.update(self.config)
|
|
36
|
+
if self.update is not None:
|
|
37
|
+
fig.update(self.update)
|
|
38
|
+
if self.layout is not None:
|
|
39
|
+
fig.update_layout(**self.layout)
|
|
40
|
+
if self.traces is not None:
|
|
41
|
+
fig.update_traces(**self.traces)
|
|
42
|
+
if self.xaxes is not None:
|
|
43
|
+
fig.update_xaxes(**self.xaxes)
|
|
44
|
+
if self.yaxes is not None:
|
|
45
|
+
fig.update_yaxes(**self.yaxes)
|
|
46
|
+
|
|
47
|
+
if theme is not None:
|
|
48
|
+
apply_theme(fig, theme)
|
|
49
|
+
|
|
50
|
+
return fig
|