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