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,168 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, replace
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import plotly.graph_objects as go
|
|
8
|
+
|
|
9
|
+
from diffract.viz.data import (
|
|
10
|
+
DataType,
|
|
11
|
+
Entry,
|
|
12
|
+
FieldRef,
|
|
13
|
+
)
|
|
14
|
+
from diffract.viz.plots.base.axis import AxisType, SupportsAxis
|
|
15
|
+
from diffract.viz.plots.base.coloraxis import SupportsColoraxis
|
|
16
|
+
from diffract.viz.plots.base.plot import Plot
|
|
17
|
+
from diffract.viz.styling import (
|
|
18
|
+
CategoricalPropertyResolver,
|
|
19
|
+
NumericPropertyResolver,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(kw_only=True)
|
|
24
|
+
class HeatmapPlot(
|
|
25
|
+
Plot,
|
|
26
|
+
SupportsAxis("x", AxisType.CATEGORICAL),
|
|
27
|
+
SupportsAxis("y", AxisType.CATEGORICAL),
|
|
28
|
+
SupportsColoraxis("heatmap"),
|
|
29
|
+
):
|
|
30
|
+
"""Heatmap plot that pivots a scalar field by two categorical dimensions.
|
|
31
|
+
|
|
32
|
+
Each cell in the heatmap represents a value of ``z`` at the intersection
|
|
33
|
+
of a ``y`` row and an ``x`` column. When multiple entries map to the
|
|
34
|
+
same (x, y) pair the **last** value wins (no aggregation).
|
|
35
|
+
|
|
36
|
+
Coloraxis settings (colorscale, cmin/cmax, colorbar) are inherited from
|
|
37
|
+
the ``SupportsColoraxis("heatmap")`` mixin and can be controlled via
|
|
38
|
+
``heatmap_colorscale``, ``heatmap_cmin``, ``heatmap_cmax``, etc.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
z: FieldRef
|
|
42
|
+
z_rescale_range: tuple[float, float] | None = None
|
|
43
|
+
|
|
44
|
+
y: FieldRef
|
|
45
|
+
x: FieldRef
|
|
46
|
+
|
|
47
|
+
fill_value: float = float("nan")
|
|
48
|
+
|
|
49
|
+
show_text: bool = False
|
|
50
|
+
text_format: str = ".2f"
|
|
51
|
+
text_font_size: int = 10
|
|
52
|
+
|
|
53
|
+
reverse_y: bool = True
|
|
54
|
+
|
|
55
|
+
def _build_traces_data(
|
|
56
|
+
self, entries: dict[str, Entry]
|
|
57
|
+
) -> dict[str, dict[str, Any]]:
|
|
58
|
+
"""Build a single trace-data dict containing the full z-matrix."""
|
|
59
|
+
x_resolver = y_resolver = CategoricalPropertyResolver()
|
|
60
|
+
z_resolver = NumericPropertyResolver(self.z_rescale_range)
|
|
61
|
+
|
|
62
|
+
x_ref = replace(self.x, data_type=self.x.data_type or DataType.CATEGORICAL)
|
|
63
|
+
y_ref = replace(self.y, data_type=self.y.data_type or DataType.CATEGORICAL)
|
|
64
|
+
x_values = x_resolver.resolve(x_ref, entries)
|
|
65
|
+
y_values = y_resolver.resolve(y_ref, entries)
|
|
66
|
+
z_values = z_resolver.resolve(self.z, entries)
|
|
67
|
+
|
|
68
|
+
sorted_x = self._get_sorted_unique(x_values, self.x)
|
|
69
|
+
sorted_y = self._get_sorted_unique(y_values, self.y)
|
|
70
|
+
|
|
71
|
+
cell_values = self._collect_cell_values(x_values, y_values, z_values)
|
|
72
|
+
z_matrix = self._build_matrix(sorted_x, sorted_y, cell_values)
|
|
73
|
+
|
|
74
|
+
traces_data: dict[str, dict[str, Any]] = {
|
|
75
|
+
"heatmap": {
|
|
76
|
+
"x_labels": sorted_x,
|
|
77
|
+
"y_labels": sorted_y,
|
|
78
|
+
"z_matrix": z_matrix,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
return traces_data
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _get_sorted_unique(values: list[Any], ref: FieldRef) -> list[str]:
|
|
85
|
+
"""Return unique values sorted according to the field's ordering."""
|
|
86
|
+
unique = list(dict.fromkeys(values))
|
|
87
|
+
order_indices = ref.ordering.argsort(unique)
|
|
88
|
+
return [str(unique[i]) for i in order_indices]
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def _collect_cell_values(
|
|
92
|
+
x_values: list[Any],
|
|
93
|
+
y_values: list[Any],
|
|
94
|
+
z_values: list[Any],
|
|
95
|
+
) -> dict[tuple[str, str], float]:
|
|
96
|
+
"""Map (x, y) string pairs to scalar z-values."""
|
|
97
|
+
cell_values: dict[tuple[str, str], float] = {}
|
|
98
|
+
for x_val, y_val, z_val in zip(x_values, y_values, z_values, strict=False):
|
|
99
|
+
if z_val is None:
|
|
100
|
+
continue
|
|
101
|
+
cell_values[(str(x_val), str(y_val))] = float(z_val)
|
|
102
|
+
return cell_values
|
|
103
|
+
|
|
104
|
+
def _build_matrix(
|
|
105
|
+
self,
|
|
106
|
+
sorted_x: list[str],
|
|
107
|
+
sorted_y: list[str],
|
|
108
|
+
cell_values: dict[tuple[str, str], float],
|
|
109
|
+
) -> np.ndarray:
|
|
110
|
+
"""Build a 2-D z-matrix (rows=y, cols=x) from cell values."""
|
|
111
|
+
z_matrix = np.full(
|
|
112
|
+
(len(sorted_y), len(sorted_x)),
|
|
113
|
+
self.fill_value,
|
|
114
|
+
dtype=np.float64,
|
|
115
|
+
)
|
|
116
|
+
x_idx = {label: j for j, label in enumerate(sorted_x)}
|
|
117
|
+
y_idx = {label: i for i, label in enumerate(sorted_y)}
|
|
118
|
+
|
|
119
|
+
for (x_label, y_label), value in cell_values.items():
|
|
120
|
+
j = x_idx.get(x_label)
|
|
121
|
+
i = y_idx.get(y_label)
|
|
122
|
+
if i is not None and j is not None:
|
|
123
|
+
z_matrix[i, j] = value
|
|
124
|
+
|
|
125
|
+
return z_matrix
|
|
126
|
+
|
|
127
|
+
def _build_figure(self) -> go.Figure:
|
|
128
|
+
"""Build the Plotly figure with a single Heatmap trace."""
|
|
129
|
+
fig = go.Figure()
|
|
130
|
+
|
|
131
|
+
if self._traces_data is None:
|
|
132
|
+
return fig
|
|
133
|
+
|
|
134
|
+
trace_data = self._traces_data["heatmap"]
|
|
135
|
+
x_labels: list[str] = trace_data["x_labels"]
|
|
136
|
+
y_labels: list[str] = trace_data["y_labels"]
|
|
137
|
+
z_matrix: np.ndarray = trace_data["z_matrix"]
|
|
138
|
+
|
|
139
|
+
heatmap_kwargs: dict[str, Any] = {
|
|
140
|
+
"z": z_matrix,
|
|
141
|
+
"x": x_labels,
|
|
142
|
+
"y": y_labels,
|
|
143
|
+
"coloraxis": self.resolve_coloraxis(fig),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if self.show_text:
|
|
147
|
+
text = np.vectorize(
|
|
148
|
+
lambda val: "" if np.isnan(val) else format(val, self.text_format),
|
|
149
|
+
)(z_matrix)
|
|
150
|
+
heatmap_kwargs["text"] = text
|
|
151
|
+
heatmap_kwargs["texttemplate"] = "%{text}"
|
|
152
|
+
heatmap_kwargs["textfont"] = {"size": self.text_font_size}
|
|
153
|
+
|
|
154
|
+
fig.add_trace(go.Heatmap(**heatmap_kwargs))
|
|
155
|
+
|
|
156
|
+
# Title
|
|
157
|
+
if self.title:
|
|
158
|
+
fig.update_layout(title=self.title)
|
|
159
|
+
else:
|
|
160
|
+
fig.update_layout(
|
|
161
|
+
title=f"{self.z.field} by {self.y.field} x {self.x.field}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Reverse y-axis so the first row is at the top (matrix convention)
|
|
165
|
+
if self.reverse_y:
|
|
166
|
+
fig.update_yaxes(autorange="reversed")
|
|
167
|
+
|
|
168
|
+
return fig
|
|
@@ -0,0 +1,233 @@
|
|
|
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 (
|
|
9
|
+
Entry,
|
|
10
|
+
FieldRef,
|
|
11
|
+
get_field_values,
|
|
12
|
+
)
|
|
13
|
+
from diffract.viz.plots.base.axis import AxisType, SupportsAxis
|
|
14
|
+
from diffract.viz.plots.base.marker import SupportsMarker
|
|
15
|
+
from diffract.viz.plots.base.plot import Plot
|
|
16
|
+
from diffract.viz.styling import (
|
|
17
|
+
NumericPropertyResolver,
|
|
18
|
+
ResolvedColor,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(kw_only=True)
|
|
23
|
+
class ScatterPlot(
|
|
24
|
+
Plot,
|
|
25
|
+
SupportsAxis("x", AxisType.NUMERIC),
|
|
26
|
+
SupportsAxis("y", AxisType.NUMERIC),
|
|
27
|
+
SupportsMarker,
|
|
28
|
+
):
|
|
29
|
+
"""Scatter plot for two numeric fields."""
|
|
30
|
+
|
|
31
|
+
y: FieldRef
|
|
32
|
+
y_rescale_range: tuple[float, float] | None = None
|
|
33
|
+
y_rescale_traces_separately: bool = False
|
|
34
|
+
|
|
35
|
+
x: FieldRef
|
|
36
|
+
x_rescale_range: tuple[float, float] | None = None
|
|
37
|
+
x_rescale_traces_separately: bool = False
|
|
38
|
+
|
|
39
|
+
group_by: FieldRef | None = None
|
|
40
|
+
|
|
41
|
+
def _build_traces_data(
|
|
42
|
+
self, entries: dict[str, Entry]
|
|
43
|
+
) -> dict[str, dict[str, Any]]:
|
|
44
|
+
"""Build per-group trace data dicts."""
|
|
45
|
+
x_resolver = NumericPropertyResolver(self.x_rescale_range)
|
|
46
|
+
y_resolver = NumericPropertyResolver(self.y_rescale_range)
|
|
47
|
+
|
|
48
|
+
x_values = x_resolver.resolve(self.x, entries)
|
|
49
|
+
y_values = y_resolver.resolve(self.y, entries)
|
|
50
|
+
|
|
51
|
+
groups, group_indices = self._group_entries(entries, x_values, y_values)
|
|
52
|
+
|
|
53
|
+
# 1) Resolve marker properties globally for all entries
|
|
54
|
+
resolved_color = self._resolve_marker_color(entries, self._theme)
|
|
55
|
+
resolved_symbol = self._resolve_marker_symbol(entries, self._theme)
|
|
56
|
+
resolved_size = self._resolve_marker_size(entries, self._theme)
|
|
57
|
+
resolved_opacity = self._resolve_marker_opacity(entries, self._theme)
|
|
58
|
+
|
|
59
|
+
group_keys = self._get_sorted_group_keys(groups)
|
|
60
|
+
|
|
61
|
+
# 2) Build per-group traces
|
|
62
|
+
traces_data: dict[str, dict[str, Any]] = {}
|
|
63
|
+
|
|
64
|
+
for idx, group in enumerate(group_keys):
|
|
65
|
+
points = groups[group]
|
|
66
|
+
indices = group_indices[group]
|
|
67
|
+
|
|
68
|
+
trace_x = [p[0] for p in points]
|
|
69
|
+
if self.x_rescale_traces_separately:
|
|
70
|
+
trace_x = x_resolver._normalize(trace_x)
|
|
71
|
+
|
|
72
|
+
trace_y = [p[1] for p in points]
|
|
73
|
+
if self.y_rescale_traces_separately:
|
|
74
|
+
trace_y = y_resolver._normalize(trace_y)
|
|
75
|
+
|
|
76
|
+
trace_id = f"scatter_{group}"
|
|
77
|
+
trace_data: dict[str, Any] = {
|
|
78
|
+
"group": group,
|
|
79
|
+
"group_idx": idx,
|
|
80
|
+
"x_values": trace_x,
|
|
81
|
+
"y_values": trace_y,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# 3) Pick per-group values and add to trace via mixin methods
|
|
85
|
+
self._add_marker_color_to_trace(
|
|
86
|
+
trace_data,
|
|
87
|
+
self._pick_group_color(indices, resolved_color),
|
|
88
|
+
)
|
|
89
|
+
self._add_marker_symbol_to_trace(
|
|
90
|
+
trace_data,
|
|
91
|
+
self._pick_group_scalar(indices, resolved_symbol),
|
|
92
|
+
)
|
|
93
|
+
self._add_marker_size_to_trace(
|
|
94
|
+
trace_data,
|
|
95
|
+
self._pick_group_values(indices, resolved_size),
|
|
96
|
+
)
|
|
97
|
+
self._add_marker_opacity_to_trace(
|
|
98
|
+
trace_data,
|
|
99
|
+
self._pick_group_values(indices, resolved_opacity),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
traces_data[trace_id] = trace_data
|
|
103
|
+
|
|
104
|
+
return traces_data
|
|
105
|
+
|
|
106
|
+
def _group_entries(
|
|
107
|
+
self,
|
|
108
|
+
entries: dict[str, Entry],
|
|
109
|
+
x_values: list[Any],
|
|
110
|
+
y_values: list[Any],
|
|
111
|
+
) -> tuple[dict[str, list[tuple[float, float]]], dict[str, list[int]]]:
|
|
112
|
+
"""Group (x, y) pairs by the ``group_by`` field.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
groups: mapping from group key to list of (x, y) pairs.
|
|
116
|
+
group_indices: mapping from group key to original entry indices
|
|
117
|
+
(only entries with non-None x *and* y).
|
|
118
|
+
"""
|
|
119
|
+
if self.group_by is not None:
|
|
120
|
+
group_values = get_field_values(entries, self.group_by.field)
|
|
121
|
+
else:
|
|
122
|
+
group_values = ["all"] * len(x_values)
|
|
123
|
+
|
|
124
|
+
groups: dict[str, list[tuple[float, float]]] = {}
|
|
125
|
+
group_indices: dict[str, list[int]] = {}
|
|
126
|
+
|
|
127
|
+
for i, (g_val, x_val, y_val) in enumerate(
|
|
128
|
+
zip(group_values, x_values, y_values, strict=False)
|
|
129
|
+
):
|
|
130
|
+
if x_val is None or y_val is None:
|
|
131
|
+
continue
|
|
132
|
+
key = str(g_val) if g_val is not None else "null"
|
|
133
|
+
groups.setdefault(key, []).append((float(x_val), float(y_val)))
|
|
134
|
+
group_indices.setdefault(key, []).append(i)
|
|
135
|
+
|
|
136
|
+
return groups, group_indices
|
|
137
|
+
|
|
138
|
+
def _get_sorted_group_keys(self, groups: dict[str, list[Any]]) -> list[str]:
|
|
139
|
+
"""Return group keys sorted according to ``group_by`` ordering."""
|
|
140
|
+
group_keys = list(groups.keys())
|
|
141
|
+
if self.group_by is None:
|
|
142
|
+
return group_keys
|
|
143
|
+
order_indices = self.group_by.ordering.argsort(group_keys)
|
|
144
|
+
return [str(group_keys[i]) for i in order_indices]
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _pick_group_color(
|
|
148
|
+
indices: list[int],
|
|
149
|
+
resolved_color: ResolvedColor,
|
|
150
|
+
) -> ResolvedColor:
|
|
151
|
+
"""Build a per-group ResolvedColor from a global ResolvedColor.
|
|
152
|
+
|
|
153
|
+
For continuous ``values``, slices to entries belonging to this group.
|
|
154
|
+
For categorical per-entry colors, picks the first entry's color
|
|
155
|
+
(within a group all entries share the same categorical color).
|
|
156
|
+
For a single color string, passes through as-is.
|
|
157
|
+
"""
|
|
158
|
+
if resolved_color.values is not None:
|
|
159
|
+
filtered = [resolved_color.values[i] for i in indices]
|
|
160
|
+
return ResolvedColor(values=filtered)
|
|
161
|
+
|
|
162
|
+
if resolved_color.color is not None:
|
|
163
|
+
if isinstance(resolved_color.color, list):
|
|
164
|
+
if not resolved_color.color or not indices:
|
|
165
|
+
return ResolvedColor()
|
|
166
|
+
return ResolvedColor(color=resolved_color.color[indices[0]])
|
|
167
|
+
return resolved_color
|
|
168
|
+
|
|
169
|
+
return resolved_color
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _pick_group_scalar(
|
|
173
|
+
indices: list[int],
|
|
174
|
+
resolved: str | list[str] | None,
|
|
175
|
+
) -> str | None:
|
|
176
|
+
"""Pick a single categorical value for a group (first match).
|
|
177
|
+
|
|
178
|
+
If resolved is scalar or None — return as-is.
|
|
179
|
+
If resolved is a per-entry list — return the first entry's value.
|
|
180
|
+
"""
|
|
181
|
+
if resolved is None or not isinstance(resolved, list):
|
|
182
|
+
return resolved
|
|
183
|
+
if not resolved or not indices:
|
|
184
|
+
return None
|
|
185
|
+
return resolved[indices[0]]
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _pick_group_values(
|
|
189
|
+
indices: list[int],
|
|
190
|
+
resolved: float | list[float] | None,
|
|
191
|
+
) -> float | list[float] | None:
|
|
192
|
+
"""Slice a resolved numeric property for a group.
|
|
193
|
+
|
|
194
|
+
If resolved is scalar or None — return as-is.
|
|
195
|
+
If resolved is a per-entry list — filter to entries in this group.
|
|
196
|
+
"""
|
|
197
|
+
if resolved is None or not isinstance(resolved, list):
|
|
198
|
+
return resolved
|
|
199
|
+
return [resolved[i] for i in indices]
|
|
200
|
+
|
|
201
|
+
def _build_figure(self) -> go.Figure:
|
|
202
|
+
"""Build the Plotly figure with scatter traces."""
|
|
203
|
+
fig = go.Figure()
|
|
204
|
+
|
|
205
|
+
if self._traces_data is None:
|
|
206
|
+
return fig
|
|
207
|
+
|
|
208
|
+
for trace_id, trace_data in self._traces_data.items():
|
|
209
|
+
group = trace_data["group"]
|
|
210
|
+
x_vals = trace_data["x_values"]
|
|
211
|
+
y_vals = trace_data["y_values"]
|
|
212
|
+
|
|
213
|
+
if not x_vals:
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
fig.add_trace(
|
|
217
|
+
go.Scatter(
|
|
218
|
+
x=x_vals,
|
|
219
|
+
y=y_vals,
|
|
220
|
+
name=group,
|
|
221
|
+
mode="markers",
|
|
222
|
+
legendgroup=group,
|
|
223
|
+
meta={"trace_id": trace_id},
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Title
|
|
228
|
+
if self.title:
|
|
229
|
+
fig.update_layout(title=self.title)
|
|
230
|
+
else:
|
|
231
|
+
fig.update_layout(title=f"{self.y.field} vs {self.x.field}")
|
|
232
|
+
|
|
233
|
+
return fig
|