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,298 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, replace
|
|
4
|
+
from typing import Any, Literal
|
|
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.jitter import SupportsJitter
|
|
16
|
+
from diffract.viz.plots.base.marker import SupportsMarker
|
|
17
|
+
from diffract.viz.plots.base.plot import Plot
|
|
18
|
+
from diffract.viz.styling import (
|
|
19
|
+
CategoricalPropertyResolver,
|
|
20
|
+
NumericPropertyResolver,
|
|
21
|
+
ResolvedColor,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(kw_only=True)
|
|
26
|
+
class ViolinPlot(
|
|
27
|
+
Plot,
|
|
28
|
+
SupportsJitter,
|
|
29
|
+
SupportsAxis("x", AxisType.CATEGORICAL),
|
|
30
|
+
SupportsAxis("y", AxisType.NUMERIC),
|
|
31
|
+
SupportsMarker,
|
|
32
|
+
):
|
|
33
|
+
"""Violin plot for a scalar or vector numeric field.
|
|
34
|
+
|
|
35
|
+
Supports grouping by categorical x-axis and color customization via marker
|
|
36
|
+
properties. Violin-specific options include box/meanline visibility, side
|
|
37
|
+
rendering, and bandwidth.
|
|
38
|
+
|
|
39
|
+
When ``y`` is a vector field (e.g., an eigenvalue spectrum per entry), the array
|
|
40
|
+
is automatically expanded into individual observations for the violin.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
y: FieldRef
|
|
44
|
+
y_rescale_range: tuple[float, float] | None = None
|
|
45
|
+
y_rescale_traces_separately: bool = False
|
|
46
|
+
|
|
47
|
+
x: FieldRef
|
|
48
|
+
|
|
49
|
+
points: Literal["all", "outliers", False] = "outliers"
|
|
50
|
+
box_visible: bool = True
|
|
51
|
+
meanline_visible: bool = True
|
|
52
|
+
side: Literal["positive", "negative", "both"] = "positive"
|
|
53
|
+
bandwidth: float | None = None
|
|
54
|
+
|
|
55
|
+
def _build_traces_data(
|
|
56
|
+
self, entries: dict[str, Entry]
|
|
57
|
+
) -> dict[str, dict[str, Any]]:
|
|
58
|
+
"""Build trace data dict for each group (category on x-axis)."""
|
|
59
|
+
x_resolver = CategoricalPropertyResolver()
|
|
60
|
+
y_resolver = NumericPropertyResolver(self.y_rescale_range)
|
|
61
|
+
|
|
62
|
+
# The x axis is categorical by design; without an explicit
|
|
63
|
+
# data_type digit-like string fields (layer_id, head_id) would be
|
|
64
|
+
# detected as numeric and rejected.
|
|
65
|
+
x_ref = replace(self.x, data_type=self.x.data_type or DataType.CATEGORICAL)
|
|
66
|
+
x_values = x_resolver.resolve(x_ref, entries)
|
|
67
|
+
y_values = y_resolver.resolve(self.y, entries)
|
|
68
|
+
|
|
69
|
+
sorted_categories = self._get_sorted_categories(x_values)
|
|
70
|
+
category_order = self._get_effective_category_order(sorted_categories)
|
|
71
|
+
groups = self._group_by_category(x_values, y_values, category_order)
|
|
72
|
+
|
|
73
|
+
# 1) Resolve globally for all entries
|
|
74
|
+
resolved_size = self._resolve_marker_size(entries, self._theme)
|
|
75
|
+
resolved_opacity = self._resolve_marker_opacity(entries, self._theme)
|
|
76
|
+
resolved_symbol = self._resolve_marker_symbol(entries, self._theme)
|
|
77
|
+
resolved_color = self._resolve_marker_color(entries, self._theme)
|
|
78
|
+
resolved_jitter_color = self._resolve_jitter_color(entries, self._theme)
|
|
79
|
+
|
|
80
|
+
# 2) Build per-category traces
|
|
81
|
+
traces_data: dict[str, dict[str, Any]] = {}
|
|
82
|
+
|
|
83
|
+
for idx, category in enumerate(category_order):
|
|
84
|
+
trace_id = f"violin_{category}"
|
|
85
|
+
|
|
86
|
+
trace_y = groups[category]
|
|
87
|
+
if self.y_rescale_traces_separately:
|
|
88
|
+
trace_y = y_resolver._normalize(trace_y)
|
|
89
|
+
|
|
90
|
+
trace_data: dict[str, Any] = {
|
|
91
|
+
"category": category,
|
|
92
|
+
"category_idx": idx,
|
|
93
|
+
"jitter_x_center": float(idx),
|
|
94
|
+
"jitter_xaxis": "x2",
|
|
95
|
+
"y_values": trace_y,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# 3) Slice resolved values for this category and add to trace
|
|
99
|
+
self._add_marker_size_to_trace(
|
|
100
|
+
trace_data,
|
|
101
|
+
self._pick_category_values(x_values, y_values, category, resolved_size),
|
|
102
|
+
)
|
|
103
|
+
self._add_marker_opacity_to_trace(
|
|
104
|
+
trace_data,
|
|
105
|
+
self._pick_category_values(
|
|
106
|
+
x_values, y_values, category, resolved_opacity
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
self._add_marker_symbol_to_trace(
|
|
110
|
+
trace_data,
|
|
111
|
+
self._pick_category_scalar(x_values, category, resolved_symbol),
|
|
112
|
+
)
|
|
113
|
+
self._add_marker_color_to_trace(
|
|
114
|
+
trace_data,
|
|
115
|
+
self._pick_category_color(x_values, y_values, category, resolved_color),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if resolved_jitter_color is not None:
|
|
119
|
+
self._add_jitter_color_to_trace(
|
|
120
|
+
trace_data,
|
|
121
|
+
self._pick_category_color(
|
|
122
|
+
x_values, y_values, category, resolved_jitter_color
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
traces_data[trace_id] = trace_data
|
|
127
|
+
|
|
128
|
+
return traces_data
|
|
129
|
+
|
|
130
|
+
def _get_sorted_categories(self, x_values: list[Any]) -> list[str]:
|
|
131
|
+
"""Get unique categories sorted according to x ordering."""
|
|
132
|
+
unique_categories = list(dict.fromkeys(x_values))
|
|
133
|
+
order_indices = self.x.ordering.argsort(unique_categories)
|
|
134
|
+
return [str(unique_categories[i]) for i in order_indices]
|
|
135
|
+
|
|
136
|
+
def _get_effective_category_order(self, sorted_categories: list[str]) -> list[str]:
|
|
137
|
+
"""Resolve category order used for both traces and jitter centers."""
|
|
138
|
+
if self.x_categoryorder != "array" or self.x_categoryarray is None:
|
|
139
|
+
return sorted_categories
|
|
140
|
+
|
|
141
|
+
preferred = [str(category) for category in self.x_categoryarray]
|
|
142
|
+
preferred_existing = [
|
|
143
|
+
category for category in preferred if category in sorted_categories
|
|
144
|
+
]
|
|
145
|
+
remaining = [
|
|
146
|
+
category
|
|
147
|
+
for category in sorted_categories
|
|
148
|
+
if category not in preferred_existing
|
|
149
|
+
]
|
|
150
|
+
return preferred_existing + remaining
|
|
151
|
+
|
|
152
|
+
def _group_by_category(
|
|
153
|
+
self, x_values: list[Any], y_values: list[Any], categories: list[str]
|
|
154
|
+
) -> dict[str, list[float]]:
|
|
155
|
+
"""Group y-values by their x category.
|
|
156
|
+
|
|
157
|
+
Scalar y-values are appended as single floats.
|
|
158
|
+
Array y-values (``np.ndarray``) are exploded into individual floats,
|
|
159
|
+
so that each element of the vector becomes a separate observation
|
|
160
|
+
in the violin (useful for per-parameter spectra like ESD).
|
|
161
|
+
"""
|
|
162
|
+
groups: dict[str, list[float]] = {cat: [] for cat in categories}
|
|
163
|
+
|
|
164
|
+
for x_val, y_val in zip(x_values, y_values, strict=False):
|
|
165
|
+
if y_val is None:
|
|
166
|
+
continue
|
|
167
|
+
cat = str(x_val)
|
|
168
|
+
if isinstance(y_val, np.ndarray):
|
|
169
|
+
groups[cat].extend(y_val.ravel().tolist())
|
|
170
|
+
else:
|
|
171
|
+
groups[cat].append(float(y_val))
|
|
172
|
+
|
|
173
|
+
return groups
|
|
174
|
+
|
|
175
|
+
def _build_figure(self) -> go.Figure:
|
|
176
|
+
"""Build the plotly figure with violin traces."""
|
|
177
|
+
fig = go.Figure()
|
|
178
|
+
|
|
179
|
+
if self._traces_data is None:
|
|
180
|
+
return fig
|
|
181
|
+
|
|
182
|
+
for trace_id, trace_data in self._traces_data.items():
|
|
183
|
+
category = trace_data["category"]
|
|
184
|
+
y_values = trace_data["y_values"]
|
|
185
|
+
|
|
186
|
+
if len(y_values) == 0:
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
fig.add_trace(
|
|
190
|
+
go.Violin(
|
|
191
|
+
name=category,
|
|
192
|
+
x=[category] * len(y_values),
|
|
193
|
+
y=y_values,
|
|
194
|
+
points=self.points,
|
|
195
|
+
box_visible=self.box_visible,
|
|
196
|
+
meanline_visible=self.meanline_visible,
|
|
197
|
+
width=0.5,
|
|
198
|
+
side=self.side,
|
|
199
|
+
bandwidth=self.bandwidth,
|
|
200
|
+
meta={"trace_id": trace_id},
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Set title
|
|
205
|
+
if self.title:
|
|
206
|
+
fig.update_layout(title=self.title)
|
|
207
|
+
else:
|
|
208
|
+
fig.update_layout(title=f"{self.y.field} by {self.x.field}")
|
|
209
|
+
|
|
210
|
+
categoryorder = self.x_categoryorder or "array"
|
|
211
|
+
|
|
212
|
+
if categoryorder == "array":
|
|
213
|
+
categoryarray = self.x_categoryarray or [
|
|
214
|
+
trace_data["category"]
|
|
215
|
+
for trace_data in sorted(
|
|
216
|
+
self._traces_data.values(),
|
|
217
|
+
key=lambda data: data["category_idx"],
|
|
218
|
+
)
|
|
219
|
+
]
|
|
220
|
+
else:
|
|
221
|
+
categoryarray = None
|
|
222
|
+
|
|
223
|
+
half_span = max(0.5, abs(self.jitter_offset) + self.jitter_width + 0.05)
|
|
224
|
+
fig.update_layout(
|
|
225
|
+
xaxis=dict(
|
|
226
|
+
categoryorder=categoryorder,
|
|
227
|
+
categoryarray=categoryarray,
|
|
228
|
+
range=[-half_span, len(self._traces_data) - 1 + 0.5 * half_span],
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return fig
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def _pick_category_values(
|
|
236
|
+
x_values: list[Any],
|
|
237
|
+
y_values: list[Any],
|
|
238
|
+
category: str,
|
|
239
|
+
resolved: float | list[float] | None,
|
|
240
|
+
) -> float | list[float] | None:
|
|
241
|
+
"""Slice a resolved numeric value for a specific category.
|
|
242
|
+
|
|
243
|
+
If resolved is scalar or None — return as-is.
|
|
244
|
+
If resolved is a per-entry list — filter to entries matching this category.
|
|
245
|
+
"""
|
|
246
|
+
if resolved is None or not isinstance(resolved, list):
|
|
247
|
+
return resolved
|
|
248
|
+
return [
|
|
249
|
+
val
|
|
250
|
+
for x_val, y_val, val in zip(x_values, y_values, resolved, strict=False)
|
|
251
|
+
if str(x_val) == category and y_val is not None
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def _pick_category_scalar(
|
|
256
|
+
x_values: list[Any],
|
|
257
|
+
category: str,
|
|
258
|
+
resolved: str | list[str] | None,
|
|
259
|
+
) -> str | None:
|
|
260
|
+
"""Pick a single categorical value for a category (first match).
|
|
261
|
+
|
|
262
|
+
If resolved is scalar or None — return as-is.
|
|
263
|
+
If resolved is a per-entry list — return the first matching value.
|
|
264
|
+
"""
|
|
265
|
+
if resolved is None or not isinstance(resolved, list):
|
|
266
|
+
return resolved
|
|
267
|
+
for x_val, val in zip(x_values, resolved, strict=False):
|
|
268
|
+
if str(x_val) == category:
|
|
269
|
+
return val
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def _pick_category_color(
|
|
274
|
+
x_values: list[Any],
|
|
275
|
+
y_values: list[Any],
|
|
276
|
+
category: str,
|
|
277
|
+
resolved_color: ResolvedColor,
|
|
278
|
+
) -> ResolvedColor:
|
|
279
|
+
"""Build a per-category ResolvedColor from a global ResolvedColor."""
|
|
280
|
+
if resolved_color.values is not None:
|
|
281
|
+
filtered = [
|
|
282
|
+
c_val
|
|
283
|
+
for x_val, y_val, c_val in zip(
|
|
284
|
+
x_values, y_values, resolved_color.values, strict=False
|
|
285
|
+
)
|
|
286
|
+
if str(x_val) == category and y_val is not None
|
|
287
|
+
]
|
|
288
|
+
return ResolvedColor(values=filtered)
|
|
289
|
+
|
|
290
|
+
if resolved_color.color is not None:
|
|
291
|
+
if isinstance(resolved_color.color, list):
|
|
292
|
+
for x_val, color in zip(x_values, resolved_color.color, strict=False):
|
|
293
|
+
if str(x_val) == category:
|
|
294
|
+
return ResolvedColor(color=color)
|
|
295
|
+
return ResolvedColor()
|
|
296
|
+
return resolved_color
|
|
297
|
+
|
|
298
|
+
return resolved_color
|