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,405 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, replace
4
+ from statistics import mean, stdev
5
+ from typing import Any, Literal
6
+
7
+ import plotly.graph_objects as go
8
+
9
+ from diffract.viz.data import (
10
+ DataType,
11
+ Entry,
12
+ FieldRef,
13
+ get_field_data,
14
+ get_field_values,
15
+ )
16
+ from diffract.viz.plots.base.axis import AxisType, SupportsAxis
17
+ from diffract.viz.plots.base.line import SupportsLine
18
+ from diffract.viz.plots.base.marker import SupportsMarker
19
+ from diffract.viz.plots.base.plot import Plot
20
+ from diffract.viz.styling import (
21
+ CategoricalPropertyResolver,
22
+ NumericPropertyResolver,
23
+ ResolvedColor,
24
+ )
25
+
26
+
27
+ @dataclass(kw_only=True)
28
+ class SparklinePlot(
29
+ Plot,
30
+ SupportsAxis("x", AxisType.NUMERIC | AxisType.CATEGORICAL),
31
+ SupportsAxis("y", AxisType.NUMERIC),
32
+ SupportsLine,
33
+ SupportsMarker,
34
+ ):
35
+ """Line/sparkline plot for a scalar field as a function of another field."""
36
+
37
+ y: FieldRef
38
+ y_rescale_range: tuple[float, float] | None = None
39
+ y_rescale_traces_separately: bool = False
40
+
41
+ x: FieldRef
42
+ x_rescale_range: tuple[float, float] | None = None
43
+ x_rescale_traces_separately: bool = False
44
+
45
+ group_by: FieldRef | None = None
46
+
47
+ mode: Literal["lines", "markers", "lines+markers"] = "lines"
48
+ show_bands: bool = True
49
+ band_opacity: float = 0.3
50
+ band_line_width: float = 0.5
51
+
52
+ def _build_traces_data(
53
+ self, entries: dict[str, Entry]
54
+ ) -> dict[str, dict[str, Any]]:
55
+ """Build trace data dict for each group (series)."""
56
+ x_resolver = NumericPropertyResolver(self.x_rescale_range)
57
+ y_resolver = NumericPropertyResolver(self.y_rescale_range)
58
+
59
+ x_values = self._resolve_x_values(entries, x_resolver)
60
+ y_values = y_resolver.resolve(self.y, entries)
61
+
62
+ groups, group_indices = self._group_entries(entries, x_values, y_values)
63
+
64
+ # 1) Resolve globally for all entries
65
+ resolved_line_color = self._resolve_line_color(entries, self._theme)
66
+ resolved_marker_color = self._resolve_marker_color(entries, self._theme)
67
+ resolved_symbol = self._resolve_marker_symbol(entries, self._theme)
68
+ resolved_dash = self._resolve_line_dash(entries, self._theme)
69
+ resolved_size = self._resolve_marker_size(entries, self._theme)
70
+ resolved_opacity = self._resolve_marker_opacity(entries, self._theme)
71
+ resolved_width = self._resolve_line_width(entries, self._theme)
72
+
73
+ group_keys = self._get_sorted_group_keys(groups)
74
+
75
+ # 2) Build per-group traces
76
+ traces_data: dict[str, dict[str, Any]] = {}
77
+
78
+ for idx, group in enumerate(group_keys):
79
+ points = groups[group]
80
+ indices = group_indices[group]
81
+
82
+ x_keys, means, stds = self._compute_stats(points)
83
+ sort_idx = self.x.ordering.argsort(x_keys)
84
+ x_keys, means, stds = zip(
85
+ *[(x_keys[i], means[i], stds[i]) for i in sort_idx], strict=False
86
+ )
87
+
88
+ if self.x_rescale_traces_separately:
89
+ x_keys = x_resolver._normalize(x_keys)
90
+
91
+ if self.y_rescale_traces_separately:
92
+ means_stds_normalized = y_resolver._normalize(means + stds)
93
+ means = means_stds_normalized[: len(means)]
94
+ stds = means_stds_normalized[len(means) :]
95
+
96
+ trace_id = f"sparkline_{group}"
97
+ trace_data: dict[str, Any] = {
98
+ "group": group,
99
+ "group_idx": idx,
100
+ "x_keys": x_keys,
101
+ "means": means,
102
+ "stds": stds,
103
+ }
104
+
105
+ # 3) Pick per-group values and add to trace via mixin methods
106
+
107
+ # --- line properties ---
108
+ self._add_line_color_to_trace(
109
+ trace_data,
110
+ self._pick_group_color(indices, resolved_line_color),
111
+ )
112
+ self._add_line_dash_to_trace(
113
+ trace_data,
114
+ self._pick_group_scalar(indices, resolved_dash),
115
+ )
116
+ self._add_line_width_to_trace(
117
+ trace_data,
118
+ self._pick_group_numeric(indices, resolved_width),
119
+ )
120
+
121
+ # --- marker properties ---
122
+ self._add_marker_color_to_trace(
123
+ trace_data,
124
+ self._pick_group_color(indices, resolved_marker_color),
125
+ )
126
+ self._add_marker_symbol_to_trace(
127
+ trace_data,
128
+ self._pick_group_scalar(indices, resolved_symbol),
129
+ )
130
+ self._add_marker_size_to_trace(
131
+ trace_data,
132
+ self._pick_group_numeric(indices, resolved_size),
133
+ )
134
+ self._add_marker_opacity_to_trace(
135
+ trace_data,
136
+ self._pick_group_numeric(indices, resolved_opacity),
137
+ )
138
+
139
+ traces_data[trace_id] = trace_data
140
+
141
+ return traces_data
142
+
143
+ def _resolve_x_values(
144
+ self,
145
+ entries: dict[str, Entry],
146
+ x_resolver: NumericPropertyResolver,
147
+ ) -> list[Any] | None:
148
+ """Resolve x values with the resolver matching the effective axis mode."""
149
+ mode = self.x_axis_mode
150
+ if mode is None:
151
+ data_type = self.x.data_type
152
+ if data_type is None:
153
+ _, data_type, _ = get_field_data(entries, self.x.field)
154
+ mode = "categorical" if data_type == DataType.CATEGORICAL else "numeric"
155
+ self._x_resolved_mode = mode
156
+
157
+ if mode == "numeric":
158
+ x_ref = replace(self.x, data_type=DataType.NUMERIC)
159
+ return x_resolver.resolve(x_ref, entries)
160
+
161
+ if self.x_rescale_range is not None or self.x_rescale_traces_separately:
162
+ raise ValueError(
163
+ f"x rescaling requires a numeric x axis, "
164
+ f"but '{self.x.field}' resolved as categorical"
165
+ )
166
+ x_ref = replace(self.x, data_type=DataType.CATEGORICAL)
167
+ return CategoricalPropertyResolver().resolve(x_ref, entries)
168
+
169
+ def _group_entries(
170
+ self,
171
+ entries: dict[str, Entry],
172
+ x_values: list[Any],
173
+ y_values: list[Any],
174
+ ) -> tuple[dict[str, list[tuple[Any, float]]], dict[str, list[int]]]:
175
+ """Group (x, y) pairs by the group_by field.
176
+
177
+ Returns:
178
+ groups: mapping from group key to list of (x, y) pairs.
179
+ group_indices: mapping from group key to original entry indices
180
+ (only entries with non-None y).
181
+ """
182
+ if self.group_by is not None:
183
+ group_values = get_field_values(entries, self.group_by.field)
184
+ else:
185
+ group_values = ["all"] * len(x_values)
186
+
187
+ groups: dict[str, list[tuple[Any, float]]] = {}
188
+ group_indices: dict[str, list[int]] = {}
189
+ for i, (g_val, x_val, y_val) in enumerate(
190
+ zip(group_values, x_values, y_values, strict=False)
191
+ ):
192
+ if y_val is None:
193
+ continue
194
+ key = str(g_val) if g_val is not None else "null"
195
+ groups.setdefault(key, []).append((x_val, float(y_val)))
196
+ group_indices.setdefault(key, []).append(i)
197
+ return groups, group_indices
198
+
199
+ def _get_sorted_group_keys(self, groups: dict[str, list[Any]]) -> list[str]:
200
+ """Return group keys sorted lexicographically."""
201
+ groups_keys = list(groups.keys())
202
+ if self.group_by is None:
203
+ return groups_keys
204
+ order_indices = self.group_by.ordering.argsort(groups_keys)
205
+ return [str(groups_keys[i]) for i in order_indices]
206
+
207
+ @staticmethod
208
+ def _compute_stats(
209
+ points: list[tuple[Any, float]],
210
+ ) -> tuple[list[Any], list[float], list[float]]:
211
+ """Compute mean and std per unique x-value, preserving x order.
212
+
213
+ Returns (x_keys, means, stds).
214
+ """
215
+ subgroups: dict[Any, list[float]] = {}
216
+ for x_val, y_val in points:
217
+ subgroups.setdefault(x_val, []).append(y_val)
218
+
219
+ x_keys: list[Any] = list(subgroups.keys())
220
+ means: list[float] = []
221
+ stds: list[float] = []
222
+ for x_val in x_keys:
223
+ vals = subgroups[x_val]
224
+ means.append(mean(vals) if len(vals) > 1 else vals[0])
225
+ stds.append(stdev(vals) if len(vals) > 1 else 0.0)
226
+ return x_keys, means, stds
227
+
228
+ @staticmethod
229
+ def _pick_group_color(
230
+ indices: list[int],
231
+ resolved_color: ResolvedColor,
232
+ ) -> ResolvedColor:
233
+ """Build a per-group ResolvedColor from a global ResolvedColor.
234
+
235
+ For continuous values, slices to entries belonging to this group.
236
+ For categorical per-entry colors, picks the color of the first
237
+ entry in this group (within a group, all entries share the same
238
+ categorical color).
239
+ For a single color string, passes through as-is.
240
+ """
241
+ if resolved_color.values is not None:
242
+ filtered = [resolved_color.values[i] for i in indices]
243
+ return ResolvedColor(values=filtered)
244
+
245
+ if resolved_color.color is not None:
246
+ if isinstance(resolved_color.color, list):
247
+ if not resolved_color.color or not indices:
248
+ return ResolvedColor()
249
+ return ResolvedColor(color=resolved_color.color[indices[0]])
250
+ return resolved_color
251
+
252
+ return resolved_color
253
+
254
+ @staticmethod
255
+ def _pick_group_scalar(
256
+ indices: list[int],
257
+ resolved: str | list[str] | None,
258
+ ) -> str | None:
259
+ """Pick a single categorical value for a group.
260
+
261
+ If resolved is scalar or None — return as-is.
262
+ If resolved is a per-entry list — return the value of the first
263
+ entry belonging to this group.
264
+ """
265
+ if resolved is None or not isinstance(resolved, list):
266
+ return resolved
267
+ if not resolved or not indices:
268
+ return None
269
+ return resolved[indices[0]]
270
+
271
+ @staticmethod
272
+ def _pick_group_numeric(
273
+ indices: list[int],
274
+ resolved: float | list[float] | None,
275
+ ) -> float | None:
276
+ """Pick a single numeric value for a group.
277
+
278
+ If resolved is scalar or None — return as-is.
279
+ If resolved is a per-entry list — return the value of the first
280
+ entry belonging to this group (line/sparkline properties like size,
281
+ opacity, and width apply uniformly to the whole trace).
282
+ """
283
+ if resolved is None or not isinstance(resolved, list):
284
+ return resolved
285
+ if not resolved or not indices:
286
+ return None
287
+ return resolved[indices[0]]
288
+
289
+ def _build_figure(self) -> go.Figure:
290
+ """Build the Plotly figure with scatter traces and optional bands."""
291
+ fig = go.Figure()
292
+
293
+ if self._traces_data is None:
294
+ return fig
295
+
296
+ for trace_id, trace_data in self._traces_data.items():
297
+ group = trace_data["group"]
298
+ x_keys = trace_data["x_keys"]
299
+ means = trace_data["means"]
300
+ stds = trace_data["stds"]
301
+
302
+ if not x_keys:
303
+ continue
304
+
305
+ # Determine effective mode (add markers if symbol is set)
306
+ effective_mode = self.mode
307
+ if trace_data.get("marker_symbol") and "markers" not in effective_mode:
308
+ effective_mode = (
309
+ "lines+markers" if "lines" in effective_mode else "markers"
310
+ )
311
+
312
+ # --- main trace ---
313
+ fig.add_trace(
314
+ go.Scatter(
315
+ x=x_keys,
316
+ y=means,
317
+ name=group,
318
+ mode=effective_mode,
319
+ legendgroup=group,
320
+ meta={"trace_id": trace_id},
321
+ )
322
+ )
323
+
324
+ # --- std-band traces ---
325
+ if self.show_bands and any(s > 0 for s in stds):
326
+ self._add_band_traces(
327
+ fig,
328
+ trace_id,
329
+ group,
330
+ x_keys,
331
+ means,
332
+ stds,
333
+ trace_data,
334
+ )
335
+
336
+ # Title
337
+ if self.title:
338
+ fig.update_layout(title=self.title)
339
+ else:
340
+ fig.update_layout(title=f"{self.y.field} by {self.x.field}")
341
+
342
+ return fig
343
+
344
+ def _add_band_traces(
345
+ self,
346
+ fig: go.Figure,
347
+ trace_id: str,
348
+ group: str,
349
+ x_keys: list[Any],
350
+ means: list[float],
351
+ stds: list[float],
352
+ trace_data: dict[str, Any],
353
+ ) -> None:
354
+ """Add upper and lower std-band traces for a group."""
355
+ band_trace_id_upper = f"{trace_id}_band_upper"
356
+ band_trace_id_lower = f"{trace_id}_band_lower"
357
+
358
+ line_color = trace_data.get("line_color")
359
+ fill_color = self._compute_fill_color(line_color)
360
+
361
+ upper_y = [m + s for m, s in zip(means, stds, strict=False)]
362
+ lower_y = [m - s for m, s in zip(means, stds, strict=False)]
363
+
364
+ fig.add_trace(
365
+ go.Scatter(
366
+ x=x_keys,
367
+ y=upper_y,
368
+ name=group,
369
+ showlegend=False,
370
+ mode="lines",
371
+ line={"width": self.band_line_width, "color": line_color},
372
+ legendgroup=group,
373
+ meta={"trace_id": band_trace_id_upper},
374
+ )
375
+ )
376
+
377
+ fig.add_trace(
378
+ go.Scatter(
379
+ x=x_keys,
380
+ y=lower_y,
381
+ name=group,
382
+ showlegend=False,
383
+ mode="lines",
384
+ fill="tonexty",
385
+ fillcolor=fill_color,
386
+ line={"width": self.band_line_width, "color": line_color},
387
+ legendgroup=group,
388
+ meta={"trace_id": band_trace_id_lower},
389
+ )
390
+ )
391
+
392
+ def _compute_fill_color(self, band_color: str | None) -> str:
393
+ """Compute RGBA fill color with transparency for the std band."""
394
+ if band_color is None:
395
+ return f"rgba(128,128,128,{self.band_opacity})"
396
+ try:
397
+ from matplotlib.colors import to_rgba
398
+
399
+ r_, g_, b_, _ = to_rgba(band_color, alpha=self.band_opacity)
400
+ return (
401
+ f"rgba({round(r_ * 255)},{round(g_ * 255)},"
402
+ f"{round(b_ * 255)},{self.band_opacity})"
403
+ )
404
+ except (ImportError, ValueError):
405
+ return f"rgba(128,128,128,{self.band_opacity})"
@@ -0,0 +1,32 @@
1
+ """Subplot composition utilities for `viz` plots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .coloraxis import ColoraxisRegistry
6
+ from .factory import (
7
+ BindTarget,
8
+ CellSelector,
9
+ CellWhere,
10
+ GridAxisBind,
11
+ GridCellRule,
12
+ ValueFilterOperator,
13
+ build_bound_grid,
14
+ )
15
+ from .grid import GridPlot
16
+ from .layout import add_figure_to_subplot, transfer_layout_to_subplot
17
+ from .spec import SubplotSpec
18
+
19
+ __all__ = [
20
+ "BindTarget",
21
+ "CellSelector",
22
+ "CellWhere",
23
+ "ColoraxisRegistry",
24
+ "GridAxisBind",
25
+ "GridCellRule",
26
+ "GridPlot",
27
+ "SubplotSpec",
28
+ "ValueFilterOperator",
29
+ "add_figure_to_subplot",
30
+ "build_bound_grid",
31
+ "transfer_layout_to_subplot",
32
+ ]