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,339 @@
|
|
|
1
|
+
"""Coloraxis remapping and aggregation for subplot composition."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import fields, is_dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from diffract.viz.plots.base.plot import Plot
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
import plotly.graph_objects as go
|
|
13
|
+
|
|
14
|
+
_COLORAXIS_RE = re.compile(r"^coloraxis(?P<id>[1-9]\d*)?$")
|
|
15
|
+
_MIN_COLORBAR_BOTTOM_MARGIN = 110
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ColoraxisRegistry:
|
|
19
|
+
"""Registry for remapping child coloraxis references to parent coloraxis."""
|
|
20
|
+
|
|
21
|
+
__slots__ = ("_configs", "_counter", "_share_key_to_axis")
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._counter = 0
|
|
25
|
+
self._share_key_to_axis: dict[str, str] = {}
|
|
26
|
+
self._configs: dict[str, dict[str, Any]] = {}
|
|
27
|
+
|
|
28
|
+
def get_or_create(self, *, share_key: str | None = None) -> str:
|
|
29
|
+
"""Get a target coloraxis name, optionally shared across subplots."""
|
|
30
|
+
if share_key is not None and share_key in self._share_key_to_axis:
|
|
31
|
+
return self._share_key_to_axis[share_key]
|
|
32
|
+
|
|
33
|
+
self._counter += 1
|
|
34
|
+
axis_name = _coloraxis_name(self._counter)
|
|
35
|
+
if share_key is not None:
|
|
36
|
+
self._share_key_to_axis[share_key] = axis_name
|
|
37
|
+
return axis_name
|
|
38
|
+
|
|
39
|
+
def store_config(self, axis_name: str, config: dict[str, Any]) -> None:
|
|
40
|
+
"""Store/merge coloraxis layout config for final parent layout."""
|
|
41
|
+
normalized = _normalize_coloraxis_ref(axis_name)
|
|
42
|
+
if normalized is None:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
existing = self._configs.get(normalized)
|
|
46
|
+
if existing is None:
|
|
47
|
+
self._configs[normalized] = dict(config)
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
self._configs[normalized] = _merge_shared_coloraxis_config(
|
|
51
|
+
existing,
|
|
52
|
+
config,
|
|
53
|
+
axis_name=normalized,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def get_all_configs(self) -> dict[str, dict[str, Any]]:
|
|
57
|
+
"""Get all merged configs sorted in Plotly coloraxis order."""
|
|
58
|
+
result: dict[str, dict[str, Any]] = {}
|
|
59
|
+
for axis_name in sorted(self._configs, key=_coloraxis_sort_key):
|
|
60
|
+
result[axis_name] = dict(self._configs[axis_name])
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def extract_coloraxis_share_keys(plot: Plot) -> dict[str, str]:
|
|
65
|
+
"""Collect explicit coloraxis share keys from `*_coloraxis_id` fields."""
|
|
66
|
+
share_keys: dict[str, str] = {}
|
|
67
|
+
|
|
68
|
+
for attr_name in _iter_coloraxis_id_attrs(plot):
|
|
69
|
+
coloraxis_id = getattr(plot, attr_name, None)
|
|
70
|
+
if coloraxis_id is None:
|
|
71
|
+
continue
|
|
72
|
+
if not isinstance(coloraxis_id, int) or coloraxis_id < 1:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"Invalid '{attr_name}={coloraxis_id}'. coloraxis_id must be >= 1."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
axis_name = _coloraxis_name(coloraxis_id)
|
|
78
|
+
share_keys[axis_name] = f"id:{coloraxis_id}"
|
|
79
|
+
|
|
80
|
+
return share_keys
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def extract_legacy_coloraxis_name(plot: Plot) -> str | None:
|
|
84
|
+
"""Read legacy `coloraxis_name` field for backward compatibility."""
|
|
85
|
+
value = getattr(plot, "coloraxis_name", None)
|
|
86
|
+
if value is None:
|
|
87
|
+
return None
|
|
88
|
+
normalized = str(value).strip()
|
|
89
|
+
return normalized or None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def remap_coloraxis(
|
|
93
|
+
fig: go.Figure,
|
|
94
|
+
registry: ColoraxisRegistry,
|
|
95
|
+
*,
|
|
96
|
+
explicit_share_keys: dict[str, str],
|
|
97
|
+
legacy_share_name: str | None,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Remap child coloraxis refs and store merged configs in registry."""
|
|
100
|
+
layout_configs = _extract_layout_coloraxis_configs(fig)
|
|
101
|
+
remap: dict[str, str] = {}
|
|
102
|
+
|
|
103
|
+
for trace in fig.data:
|
|
104
|
+
for location, old_axis_raw in _iter_trace_coloraxis_refs(trace):
|
|
105
|
+
old_axis = _normalize_coloraxis_ref(old_axis_raw)
|
|
106
|
+
if old_axis is None:
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
target_axis = remap.get(old_axis)
|
|
110
|
+
if target_axis is None:
|
|
111
|
+
share_key = explicit_share_keys.get(old_axis)
|
|
112
|
+
if share_key is None and legacy_share_name is not None:
|
|
113
|
+
share_key = f"name:{legacy_share_name}"
|
|
114
|
+
target_axis = registry.get_or_create(share_key=share_key)
|
|
115
|
+
remap[old_axis] = target_axis
|
|
116
|
+
|
|
117
|
+
if location == "marker":
|
|
118
|
+
trace.marker.coloraxis = target_axis
|
|
119
|
+
elif location == "line":
|
|
120
|
+
trace.line.coloraxis = target_axis
|
|
121
|
+
else:
|
|
122
|
+
trace.coloraxis = target_axis
|
|
123
|
+
|
|
124
|
+
if not remap:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
for old_axis, new_axis in remap.items():
|
|
128
|
+
registry.store_config(new_axis, layout_configs.get(old_axis, {}))
|
|
129
|
+
if hasattr(fig.layout, old_axis):
|
|
130
|
+
fig.update_layout(**{old_axis: None})
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def apply_coloraxis_configs(fig: go.Figure, registry: ColoraxisRegistry) -> None:
|
|
134
|
+
"""Apply merged coloraxis configs to the parent figure."""
|
|
135
|
+
for axis_name, config in registry.get_all_configs().items():
|
|
136
|
+
fig.update_layout(**{axis_name: config})
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def distribute_colorbars(fig: go.Figure) -> None:
|
|
140
|
+
"""Spread multiple colorbars horizontally and ensure bottom margin."""
|
|
141
|
+
coloraxis_names = _collect_visible_coloraxis_names(fig)
|
|
142
|
+
count = len(coloraxis_names)
|
|
143
|
+
if count <= 1:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
# Leave visible gaps between the bars: several identically scaled
|
|
147
|
+
# colorbars packed edge to edge read as one bar with garbled ticks.
|
|
148
|
+
spacing = 0.9 / count
|
|
149
|
+
colorbar_len = max(0.12, min(0.45, 0.72 / count))
|
|
150
|
+
|
|
151
|
+
for idx, axis_name in enumerate(coloraxis_names):
|
|
152
|
+
coloraxis = getattr(fig.layout, axis_name, None)
|
|
153
|
+
if coloraxis is None:
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
colorbar_obj = getattr(coloraxis, "colorbar", None)
|
|
157
|
+
colorbar = colorbar_obj.to_plotly_json() if colorbar_obj is not None else {}
|
|
158
|
+
|
|
159
|
+
colorbar.update(
|
|
160
|
+
orientation="h",
|
|
161
|
+
len=colorbar_len,
|
|
162
|
+
x=0.05 + spacing * idx + (spacing / 2),
|
|
163
|
+
xanchor="center",
|
|
164
|
+
y=-0.15,
|
|
165
|
+
yanchor="top",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
fig.update_layout(**{axis_name: {"colorbar": colorbar}})
|
|
169
|
+
|
|
170
|
+
_ensure_bottom_margin_for_colorbars(fig)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _iter_coloraxis_id_attrs(plot: Plot) -> list[str]:
|
|
174
|
+
if is_dataclass(plot):
|
|
175
|
+
return [
|
|
176
|
+
field.name for field in fields(plot) if field.name.endswith("_coloraxis_id")
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
return [
|
|
180
|
+
name
|
|
181
|
+
for name in dir(plot)
|
|
182
|
+
if name.endswith("_coloraxis_id") and not name.startswith("_")
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _iter_trace_coloraxis_refs(trace: go.BaseTraceType) -> list[tuple[str, str]]:
|
|
187
|
+
refs: list[tuple[str, str]] = []
|
|
188
|
+
|
|
189
|
+
marker = getattr(trace, "marker", None)
|
|
190
|
+
marker_axis = getattr(marker, "coloraxis", None)
|
|
191
|
+
if marker_axis:
|
|
192
|
+
refs.append(("marker", marker_axis))
|
|
193
|
+
|
|
194
|
+
line = getattr(trace, "line", None)
|
|
195
|
+
line_axis = getattr(line, "coloraxis", None)
|
|
196
|
+
if line_axis:
|
|
197
|
+
refs.append(("line", line_axis))
|
|
198
|
+
|
|
199
|
+
trace_axis = getattr(trace, "coloraxis", None)
|
|
200
|
+
if trace_axis:
|
|
201
|
+
refs.append(("trace", trace_axis))
|
|
202
|
+
|
|
203
|
+
return refs
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _extract_layout_coloraxis_configs(fig: go.Figure) -> dict[str, dict[str, Any]]:
|
|
207
|
+
configs: dict[str, dict[str, Any]] = {}
|
|
208
|
+
|
|
209
|
+
for key, value in fig.layout.to_plotly_json().items():
|
|
210
|
+
axis_name = _normalize_coloraxis_ref(key)
|
|
211
|
+
if axis_name is None or not isinstance(value, dict):
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
existing = configs.get(axis_name)
|
|
215
|
+
if existing is None:
|
|
216
|
+
configs[axis_name] = dict(value)
|
|
217
|
+
else:
|
|
218
|
+
configs[axis_name] = _merge_dict(existing, value)
|
|
219
|
+
|
|
220
|
+
return configs
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _normalize_coloraxis_ref(value: Any) -> str | None:
|
|
224
|
+
if not isinstance(value, str):
|
|
225
|
+
return None
|
|
226
|
+
match = _COLORAXIS_RE.fullmatch(value.strip())
|
|
227
|
+
if match is None:
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
suffix = match.group("id")
|
|
231
|
+
if suffix is None or suffix == "1":
|
|
232
|
+
return "coloraxis"
|
|
233
|
+
return f"coloraxis{int(suffix)}"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _coloraxis_name(axis_id: int) -> str:
|
|
237
|
+
if axis_id < 1:
|
|
238
|
+
raise ValueError(f"Coloraxis id must be >= 1, got {axis_id}.")
|
|
239
|
+
if axis_id == 1:
|
|
240
|
+
return "coloraxis"
|
|
241
|
+
return f"coloraxis{axis_id}"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _coloraxis_sort_key(axis_name: str) -> int:
|
|
245
|
+
if axis_name == "coloraxis":
|
|
246
|
+
return 1
|
|
247
|
+
return int(axis_name.removeprefix("coloraxis"))
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _merge_dict(base: dict[str, Any], extra: dict[str, Any]) -> dict[str, Any]:
|
|
251
|
+
merged = dict(base)
|
|
252
|
+
for key, extra_value in extra.items():
|
|
253
|
+
if key not in merged:
|
|
254
|
+
merged[key] = extra_value
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
base_value = merged[key]
|
|
258
|
+
if base_value is None:
|
|
259
|
+
merged[key] = extra_value
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
if isinstance(base_value, dict) and isinstance(extra_value, dict):
|
|
263
|
+
merged[key] = _merge_dict(base_value, extra_value)
|
|
264
|
+
|
|
265
|
+
return merged
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _merge_shared_coloraxis_config(
|
|
269
|
+
base: dict[str, Any],
|
|
270
|
+
extra: dict[str, Any],
|
|
271
|
+
*,
|
|
272
|
+
axis_name: str,
|
|
273
|
+
path: tuple[str, ...] = (),
|
|
274
|
+
) -> dict[str, Any]:
|
|
275
|
+
"""Merge shared coloraxis configs and fail on conflicting non-null values."""
|
|
276
|
+
merged = dict(base)
|
|
277
|
+
|
|
278
|
+
for key, extra_value in extra.items():
|
|
279
|
+
if key not in merged:
|
|
280
|
+
merged[key] = extra_value
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
base_value = merged[key]
|
|
284
|
+
if base_value is None:
|
|
285
|
+
merged[key] = extra_value
|
|
286
|
+
continue
|
|
287
|
+
if extra_value is None:
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
next_path = (*path, key)
|
|
291
|
+
|
|
292
|
+
if isinstance(base_value, dict) and isinstance(extra_value, dict):
|
|
293
|
+
merged[key] = _merge_shared_coloraxis_config(
|
|
294
|
+
base_value,
|
|
295
|
+
extra_value,
|
|
296
|
+
axis_name=axis_name,
|
|
297
|
+
path=next_path,
|
|
298
|
+
)
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
if base_value != extra_value:
|
|
302
|
+
path_label = ".".join(next_path)
|
|
303
|
+
raise ValueError(
|
|
304
|
+
"Conflicting shared coloraxis config for "
|
|
305
|
+
f"'{axis_name}' at '{path_label}': "
|
|
306
|
+
f"{base_value!r} vs {extra_value!r}."
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return merged
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _collect_visible_coloraxis_names(fig: go.Figure) -> list[str]:
|
|
313
|
+
axis_names: list[str] = []
|
|
314
|
+
|
|
315
|
+
for key in fig.layout.to_plotly_json():
|
|
316
|
+
axis_name = _normalize_coloraxis_ref(key)
|
|
317
|
+
if axis_name is None or axis_name in axis_names:
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
coloraxis = getattr(fig.layout, axis_name, None)
|
|
321
|
+
if coloraxis is None:
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
if getattr(coloraxis, "showscale", None) is False:
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
axis_names.append(axis_name)
|
|
328
|
+
|
|
329
|
+
return sorted(axis_names, key=_coloraxis_sort_key)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _ensure_bottom_margin_for_colorbars(fig: go.Figure) -> None:
|
|
333
|
+
margin_obj = fig.layout.margin
|
|
334
|
+
current_margin = margin_obj.to_plotly_json() if margin_obj is not None else {}
|
|
335
|
+
if current_margin.get("b", 0) >= _MIN_COLORBAR_BOTTOM_MARGIN:
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
current_margin["b"] = _MIN_COLORBAR_BOTTOM_MARGIN
|
|
339
|
+
fig.update_layout(margin=current_margin)
|