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,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