diffract-core 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. diffract/__init__.py +52 -0
  2. diffract/configs/fast_speed_without_disk.ini +48 -0
  3. diffract/configs/hybrid.ini +74 -0
  4. diffract/configs/sqlite.ini +62 -0
  5. diffract/containers.py +405 -0
  6. diffract/core/__init__.py +23 -0
  7. diffract/core/cache/__init__.py +24 -0
  8. diffract/core/cache/containers.py +93 -0
  9. diffract/core/cache/interface.py +117 -0
  10. diffract/core/cache/redis_manager.py +464 -0
  11. diffract/core/cache/simple_manager.py +478 -0
  12. diffract/core/compute/__init__.py +46 -0
  13. diffract/core/compute/config.py +48 -0
  14. diffract/core/compute/containers.py +81 -0
  15. diffract/core/compute/decorator.py +125 -0
  16. diffract/core/compute/exceptions.py +31 -0
  17. diffract/core/compute/execution/__init__.py +14 -0
  18. diffract/core/compute/execution/_types.py +70 -0
  19. diffract/core/compute/execution/aggregation.py +74 -0
  20. diffract/core/compute/execution/aggregation_runner.py +362 -0
  21. diffract/core/compute/execution/enums.py +38 -0
  22. diffract/core/compute/execution/executor.py +125 -0
  23. diffract/core/compute/execution/parameter_runner.py +125 -0
  24. diffract/core/compute/execution/restrictions.py +61 -0
  25. diffract/core/compute/execution/strategy.py +118 -0
  26. diffract/core/compute/execution/utils.py +112 -0
  27. diffract/core/compute/extensions/__init__.py +0 -0
  28. diffract/core/compute/extensions/power_law.py +1051 -0
  29. diffract/core/compute/extensions/rmt.py +150 -0
  30. diffract/core/compute/extensions/utils.py +36 -0
  31. diffract/core/compute/field_signature.py +183 -0
  32. diffract/core/compute/kernels/__init__.py +48 -0
  33. diffract/core/compute/kernels/alignment.py +106 -0
  34. diffract/core/compute/kernels/heavy_tailed.py +394 -0
  35. diffract/core/compute/kernels/marchenko_pastur.py +99 -0
  36. diffract/core/compute/kernels/mat_decomposition.py +101 -0
  37. diffract/core/compute/kernels/mat_properties.py +53 -0
  38. diffract/core/compute/kernels/model_quality.py +27 -0
  39. diffract/core/compute/kernels/norms.py +88 -0
  40. diffract/core/compute/kernels/ranks.py +35 -0
  41. diffract/core/compute/kernels/tracy_widom.py +36 -0
  42. diffract/core/compute/metadata.py +67 -0
  43. diffract/core/compute/registry.py +485 -0
  44. diffract/core/constants.py +147 -0
  45. diffract/core/data/__init__.py +32 -0
  46. diffract/core/data/interface.py +304 -0
  47. diffract/core/data/metadata/__init__.py +15 -0
  48. diffract/core/data/metadata/containers.py +60 -0
  49. diffract/core/data/metadata/interface.py +208 -0
  50. diffract/core/data/metadata/sqlite_index.py +604 -0
  51. diffract/core/data/nn/__init__.py +43 -0
  52. diffract/core/data/nn/aggregates/__init__.py +35 -0
  53. diffract/core/data/nn/aggregates/metadata.py +96 -0
  54. diffract/core/data/nn/aggregates/proxy.py +57 -0
  55. diffract/core/data/nn/aggregates/repository.py +87 -0
  56. diffract/core/data/nn/aggregates/view.py +307 -0
  57. diffract/core/data/nn/containers.py +86 -0
  58. diffract/core/data/nn/extractors/__init__.py +49 -0
  59. diffract/core/data/nn/extractors/base.py +300 -0
  60. diffract/core/data/nn/extractors/factory.py +234 -0
  61. diffract/core/data/nn/extractors/flax.py +112 -0
  62. diffract/core/data/nn/extractors/handlers/__init__.py +39 -0
  63. diffract/core/data/nn/extractors/handlers/base.py +110 -0
  64. diffract/core/data/nn/extractors/handlers/flax_handlers.py +71 -0
  65. diffract/core/data/nn/extractors/handlers/numpy_handlers.py +63 -0
  66. diffract/core/data/nn/extractors/handlers/onnx_handlers.py +67 -0
  67. diffract/core/data/nn/extractors/handlers/tensorflow_handlers.py +82 -0
  68. diffract/core/data/nn/extractors/handlers/torch_handlers.py +124 -0
  69. diffract/core/data/nn/extractors/interface.py +56 -0
  70. diffract/core/data/nn/extractors/numpy.py +94 -0
  71. diffract/core/data/nn/extractors/onnx.py +108 -0
  72. diffract/core/data/nn/extractors/tensorflow.py +99 -0
  73. diffract/core/data/nn/extractors/torch.py +204 -0
  74. diffract/core/data/nn/params/__init__.py +27 -0
  75. diffract/core/data/nn/params/interface.py +402 -0
  76. diffract/core/data/nn/params/metadata.py +110 -0
  77. diffract/core/data/nn/params/proxy.py +93 -0
  78. diffract/core/data/nn/params/repository.py +45 -0
  79. diffract/core/data/nn/params/schema.py +82 -0
  80. diffract/core/data/nn/params/view.py +393 -0
  81. diffract/core/data/proxy.py +246 -0
  82. diffract/core/data/repository.py +227 -0
  83. diffract/core/data/utils.py +33 -0
  84. diffract/core/data/view.py +389 -0
  85. diffract/core/export/__init__.py +27 -0
  86. diffract/core/export/containers.py +47 -0
  87. diffract/core/export/exporters.py +191 -0
  88. diffract/core/export/formatters/__init__.py +23 -0
  89. diffract/core/export/formatters/base.py +68 -0
  90. diffract/core/export/formatters/dict_formatter.py +38 -0
  91. diffract/core/export/formatters/json_formatter.py +58 -0
  92. diffract/core/export/formatters/list_formatter.py +41 -0
  93. diffract/core/export/formatters/pandas_formatter.py +86 -0
  94. diffract/core/export/formatters/polars_formatter.py +86 -0
  95. diffract/core/export/formatters/registry.py +84 -0
  96. diffract/core/export/interface.py +105 -0
  97. diffract/core/parallel/__init__.py +21 -0
  98. diffract/core/parallel/containers.py +57 -0
  99. diffract/core/parallel/execution.py +91 -0
  100. diffract/core/parallel/runtime.py +91 -0
  101. diffract/core/storage/__init__.py +35 -0
  102. diffract/core/storage/base_manager.py +330 -0
  103. diffract/core/storage/containers.py +122 -0
  104. diffract/core/storage/hdf5_manager.py +856 -0
  105. diffract/core/storage/hybrid_manager.py +218 -0
  106. diffract/core/storage/interface.py +222 -0
  107. diffract/core/storage/metadata.py +89 -0
  108. diffract/core/storage/ram_manager.py +159 -0
  109. diffract/core/storage/sqlite_manager.py +1062 -0
  110. diffract/core/storage/zarr_manager.py +992 -0
  111. diffract/core/utils/__init__.py +45 -0
  112. diffract/core/utils/build.py +46 -0
  113. diffract/core/utils/exceptions.py +11 -0
  114. diffract/core/utils/hashing.py +90 -0
  115. diffract/core/utils/imports.py +292 -0
  116. diffract/core/utils/math.py +15 -0
  117. diffract/py.typed +0 -0
  118. diffract/session/__init__.py +24 -0
  119. diffract/session/errors.py +17 -0
  120. diffract/session/field_cache.py +216 -0
  121. diffract/session/namespaces/__init__.py +15 -0
  122. diffract/session/namespaces/compute/__init__.py +219 -0
  123. diffract/session/namespaces/models/__init__.py +282 -0
  124. diffract/session/namespaces/models/parameters/__init__.py +133 -0
  125. diffract/session/namespaces/models/parameters/meta_patcher.py +167 -0
  126. diffract/session/namespaces/results/__init__.py +378 -0
  127. diffract/session/namespaces/results/eraser.py +129 -0
  128. diffract/session/namespaces/results/injester.py +249 -0
  129. diffract/session/namespaces/utils/__init__.py +141 -0
  130. diffract/session/namespaces/utils/merger.py +295 -0
  131. diffract/session/namespaces/viz/__init__.py +91 -0
  132. diffract/session/namespaces/viz/_utils.py +14 -0
  133. diffract/session/namespaces/viz/box.py +207 -0
  134. diffract/session/namespaces/viz/grid.py +160 -0
  135. diffract/session/namespaces/viz/heatmap.py +164 -0
  136. diffract/session/namespaces/viz/scatter.py +202 -0
  137. diffract/session/namespaces/viz/sparkline.py +270 -0
  138. diffract/session/namespaces/viz/violin.py +212 -0
  139. diffract/session/session.py +260 -0
  140. diffract/session/utils.py +81 -0
  141. diffract/viz/__init__.py +42 -0
  142. diffract/viz/data/__init__.py +34 -0
  143. diffract/viz/data/detection.py +57 -0
  144. diffract/viz/data/extraction.py +117 -0
  145. diffract/viz/data/filtering.py +57 -0
  146. diffract/viz/data/ordering.py +129 -0
  147. diffract/viz/data/provider.py +99 -0
  148. diffract/viz/data/types.py +98 -0
  149. diffract/viz/plots/__init__.py +37 -0
  150. diffract/viz/plots/base/__init__.py +20 -0
  151. diffract/viz/plots/base/axis.py +219 -0
  152. diffract/viz/plots/base/coloraxis.py +133 -0
  153. diffract/viz/plots/base/configurator.py +18 -0
  154. diffract/viz/plots/base/jitter.py +368 -0
  155. diffract/viz/plots/base/line.py +197 -0
  156. diffract/viz/plots/base/marker.py +278 -0
  157. diffract/viz/plots/base/overlay.py +14 -0
  158. diffract/viz/plots/base/plot.py +110 -0
  159. diffract/viz/plots/base/update.py +50 -0
  160. diffract/viz/plots/boxplot.py +283 -0
  161. diffract/viz/plots/cluster.py +374 -0
  162. diffract/viz/plots/heatmap.py +168 -0
  163. diffract/viz/plots/scatter.py +233 -0
  164. diffract/viz/plots/sparkline.py +405 -0
  165. diffract/viz/plots/subplots/__init__.py +32 -0
  166. diffract/viz/plots/subplots/coloraxis.py +339 -0
  167. diffract/viz/plots/subplots/factory.py +713 -0
  168. diffract/viz/plots/subplots/grid.py +214 -0
  169. diffract/viz/plots/subplots/layout.py +370 -0
  170. diffract/viz/plots/subplots/spec.py +39 -0
  171. diffract/viz/plots/violin.py +298 -0
  172. diffract/viz/renderer.py +513 -0
  173. diffract/viz/styling/__init__.py +78 -0
  174. diffract/viz/styling/palettes/__init__.py +20 -0
  175. diffract/viz/styling/palettes/bundle.py +16 -0
  176. diffract/viz/styling/palettes/color.py +83 -0
  177. diffract/viz/styling/palettes/dashes.py +27 -0
  178. diffract/viz/styling/palettes/symbols.py +42 -0
  179. diffract/viz/styling/resolvers/__init__.py +11 -0
  180. diffract/viz/styling/resolvers/categorical.py +68 -0
  181. diffract/viz/styling/resolvers/color.py +77 -0
  182. diffract/viz/styling/resolvers/numeric.py +108 -0
  183. diffract/viz/styling/sources.py +27 -0
  184. diffract/viz/styling/theme/__init__.py +29 -0
  185. diffract/viz/styling/theme/applier.py +114 -0
  186. diffract/viz/styling/theme/components.py +66 -0
  187. diffract/viz/styling/theme/presets.py +58 -0
  188. diffract/viz/styling/theme/theme.py +36 -0
  189. diffract_core-0.2.1.dist-info/METADATA +530 -0
  190. diffract_core-0.2.1.dist-info/RECORD +193 -0
  191. diffract_core-0.2.1.dist-info/WHEEL +4 -0
  192. diffract_core-0.2.1.dist-info/licenses/LICENSE +201 -0
  193. diffract_core-0.2.1.dist-info/licenses/NOTICE +2 -0
@@ -0,0 +1,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)