qplotter 1.6.0b3__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 (119) hide show
  1. qplot/__init__.py +46 -0
  2. qplot/__main__.py +232 -0
  3. qplot/_auxiliary_paths.py +63 -0
  4. qplot/_metadata.py +12 -0
  5. qplot/_repair.py +14 -0
  6. qplot/_shutdown_supervisor.py +4049 -0
  7. qplot/_version.py +12 -0
  8. qplot/_windows_shutdown_job.py +1166 -0
  9. qplot/configuration/__init__.py +3 -0
  10. qplot/configuration/config.py +573 -0
  11. qplot/configuration/config_schema.json +319 -0
  12. qplot/configuration/scripts.py +255 -0
  13. qplot/configuration/themes/__init__.py +13 -0
  14. qplot/configuration/themes/_base.py +579 -0
  15. qplot/configuration/themes/blank.py +17 -0
  16. qplot/configuration/themes/dark.py +54 -0
  17. qplot/configuration/themes/light.py +54 -0
  18. qplot/datahandling/LoadFromDB.py +224 -0
  19. qplot/datahandling/__init__.py +47 -0
  20. qplot/datahandling/_trusted_live_helper.py +812 -0
  21. qplot/datahandling/_trusted_live_protocol.py +1745 -0
  22. qplot/datahandling/database.py +2330 -0
  23. qplot/datahandling/dimensions.py +43 -0
  24. qplot/datahandling/file_identity.py +461 -0
  25. qplot/datahandling/parameter_data.py +149 -0
  26. qplot/datahandling/plot_progress.py +17 -0
  27. qplot/datahandling/qcodes_cache.py +277 -0
  28. qplot/datahandling/qcodes_compat.py +14 -0
  29. qplot/datahandling/readSQL.py +2442 -0
  30. qplot/datahandling/readonly.py +2561 -0
  31. qplot/datahandling/source_activity.py +21 -0
  32. qplot/datahandling/trusted_array_preview.py +127 -0
  33. qplot/datahandling/trusted_derived_cache.py +1605 -0
  34. qplot/datahandling/trusted_derived_rendering.py +868 -0
  35. qplot/datahandling/trusted_heatmap.py +241 -0
  36. qplot/datahandling/trusted_live.py +2808 -0
  37. qplot/datahandling/trusted_live_queries.py +5907 -0
  38. qplot/datahandling/trusted_live_service.py +1894 -0
  39. qplot/datahandling/trusted_live_supervisor.py +2161 -0
  40. qplot/datahandling/trusted_plot.py +283 -0
  41. qplot/datahandling/trusted_presentation.py +1065 -0
  42. qplot/datahandling/trusted_snapshot.py +1132 -0
  43. qplot/datahandling/trusted_work_coordinator.py +2157 -0
  44. qplot/datahandling/trusted_work_scheduler.py +1204 -0
  45. qplot/diagnostics.py +241 -0
  46. qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
  47. qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
  48. qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
  49. qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
  50. qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
  51. qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
  52. qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
  53. qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
  54. qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
  55. qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
  56. qplot/testdata.py +1733 -0
  57. qplot/tools/__init__.py +13 -0
  58. qplot/tools/finite_means.py +114 -0
  59. qplot/tools/general.py +134 -0
  60. qplot/tools/heatmap_geometry.py +381 -0
  61. qplot/tools/operation_registry.py +179 -0
  62. qplot/tools/plot_tools.py +625 -0
  63. qplot/tools/sample_statistics.py +56 -0
  64. qplot/tools/worker.py +2787 -0
  65. qplot/windows/__init__.py +9 -0
  66. qplot/windows/_colorbar.py +422 -0
  67. qplot/windows/_commands.py +523 -0
  68. qplot/windows/_config_persistence.py +123 -0
  69. qplot/windows/_database_actions.py +4904 -0
  70. qplot/windows/_dataset_handle.py +221 -0
  71. qplot/windows/_dragdrop.py +71 -0
  72. qplot/windows/_export_paths.py +1167 -0
  73. qplot/windows/_help.py +169 -0
  74. qplot/windows/_native_averaging.py +173 -0
  75. qplot/windows/_native_transforms.py +389 -0
  76. qplot/windows/_plot1d_snap.py +608 -0
  77. qplot/windows/_plot1d_traces.py +2183 -0
  78. qplot/windows/_plot2d_colorbar.py +1126 -0
  79. qplot/windows/_plot2d_colorbar_dialog.py +786 -0
  80. qplot/windows/_plot2d_layers.py +2197 -0
  81. qplot/windows/_plot2d_sweeps.py +712 -0
  82. qplot/windows/_plotWin.py +1993 -0
  83. qplot/windows/_plot_actions.py +2695 -0
  84. qplot/windows/_plot_appearance.py +276 -0
  85. qplot/windows/_plot_axis_scaling.py +1485 -0
  86. qplot/windows/_plot_export.py +1445 -0
  87. qplot/windows/_plot_feedback.py +110 -0
  88. qplot/windows/_plot_marquee.py +656 -0
  89. qplot/windows/_plot_refresh.py +942 -0
  90. qplot/windows/_plot_state.py +239 -0
  91. qplot/windows/_plot_transform_labels.py +60 -0
  92. qplot/windows/_preferences.py +635 -0
  93. qplot/windows/_refresh_interval.py +70 -0
  94. qplot/windows/_run_controls.py +828 -0
  95. qplot/windows/_shortcuts.py +47 -0
  96. qplot/windows/_subplots/__init__.py +8 -0
  97. qplot/windows/_subplots/subplot1d.py +531 -0
  98. qplot/windows/_subplots/subplot2d.py +918 -0
  99. qplot/windows/_svg_export.py +209 -0
  100. qplot/windows/_trusted_derived_qt.py +1759 -0
  101. qplot/windows/_widgets/__init__.py +23 -0
  102. qplot/windows/_widgets/_run_formatting.py +387 -0
  103. qplot/windows/_widgets/details_tables.py +1119 -0
  104. qplot/windows/_widgets/dropbox.py +290 -0
  105. qplot/windows/_widgets/operations.py +460 -0
  106. qplot/windows/_widgets/preview.py +2997 -0
  107. qplot/windows/_widgets/run_list_items.py +488 -0
  108. qplot/windows/_widgets/toolbar.py +111 -0
  109. qplot/windows/_widgets/treeWidgets.py +2572 -0
  110. qplot/windows/_window_controls.py +413 -0
  111. qplot/windows/main.py +1635 -0
  112. qplot/windows/plot1d.py +311 -0
  113. qplot/windows/plot2d.py +1901 -0
  114. qplotter-1.6.0b3.dist-info/METADATA +318 -0
  115. qplotter-1.6.0b3.dist-info/RECORD +119 -0
  116. qplotter-1.6.0b3.dist-info/WHEEL +5 -0
  117. qplotter-1.6.0b3.dist-info/entry_points.txt +4 -0
  118. qplotter-1.6.0b3.dist-info/licenses/LICENSE +21 -0
  119. qplotter-1.6.0b3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1126 @@
1
+ import math
2
+ from typing import TYPE_CHECKING, Any
3
+
4
+ import numpy as np
5
+ import pyqtgraph as pg
6
+ from PyQt6 import QtCore
7
+
8
+ from ._colorbar import (
9
+ _CET_COLORBAR_SUBTYPES,
10
+ _COLORBAR_COLORMAPS,
11
+ _DEFAULT_HIDDEN_COLORBAR_NAMES,
12
+ _DEFAULT_HIDDEN_COLORBAR_PREFIXES,
13
+ _DEFAULT_HIDDEN_COLORBAR_SUFFIXES,
14
+ _MATPLOTLIB_COLORBAR_SUBTYPES,
15
+ _cet_colorbar_colormap_subtype,
16
+ _colorbar_colormap_for_name,
17
+ _colorbar_colormap_group,
18
+ _colorbar_subtype_config_key,
19
+ _config_value,
20
+ _matplotlib_colorbar_colormap_subtype,
21
+ )
22
+ from ._plot2d_colorbar_dialog import ColorbarScaleDialogMixin
23
+ from ._plot_axis_scaling import (
24
+ _axis_scale_power_text,
25
+ _install_flush_axis_draw_specs,
26
+ )
27
+
28
+ COLORBAR_WIDTH_KEY = "user_preference.colorbar_width"
29
+ DEFAULT_COLORBAR_WIDTH = 15
30
+
31
+
32
+ class Plot2DColorbarMixin(ColorbarScaleDialogMixin):
33
+ """
34
+ Heatmap colorbar controls for plot2d windows.
35
+
36
+ This mixin owns color autoscaling, colorbar interaction handlers, color-map
37
+ filtering, and the color scale dialog.
38
+ """
39
+
40
+ def _configured_colorbar_width(self):
41
+ """Return the configured heatmap colour-scale width in pixels."""
42
+
43
+ return int(_config_value(
44
+ self.__dict__.get("config"),
45
+ COLORBAR_WIDTH_KEY,
46
+ DEFAULT_COLORBAR_WIDTH,
47
+ ))
48
+
49
+ def apply_colorbar_width_preference(self):
50
+ """Apply the configured width to an existing colour bar, if present."""
51
+
52
+ bar = self.__dict__.get("bar")
53
+ layout = getattr(bar, "layout", None)
54
+ if layout is None:
55
+ return
56
+
57
+ width = self._configured_colorbar_width()
58
+ if getattr(bar, "horizontal", False):
59
+ layout.setRowFixedHeight(1, width)
60
+ else:
61
+ layout.setColumnFixedWidth(1, width)
62
+
63
+ if TYPE_CHECKING:
64
+ plot: Any
65
+
66
+ def _heatmap_colorbar_items(self) -> list[Any]: ...
67
+ def _reset_heatmap_hover(self) -> None: ...
68
+ def _set_sweep_lines_visible(self, visible: bool) -> None: ...
69
+ def show_plot_state(self, title: object, detail: object | None = None, kind: str = "info") -> None: ...
70
+
71
+ @QtCore.pyqtSlot(bool)
72
+ def scaleColorbar(self, event = None):
73
+ """
74
+ Sets colorbar range to match heatmap value range, giving effect of
75
+ rescaling color bar
76
+
77
+ Parameters
78
+ ----------
79
+ Unused but required by slot
80
+
81
+ """
82
+ levels = self._data_colorbar_levels()
83
+ if levels is None:
84
+ return
85
+
86
+ self._colorbar_manual_levels = None
87
+ self._set_colorbar_levels(*levels)
88
+
89
+ def _data_colorbar_levels(self):
90
+ """
91
+ Return finite min/max levels from the current heatmap data.
92
+
93
+ """
94
+ data_range = self._finite_data_colorbar_range()
95
+ if data_range is None:
96
+ return None
97
+
98
+ vmin, vmax = data_range
99
+ if vmin == vmax:
100
+ return self._constant_colorbar_levels(vmin)
101
+ if vmin > vmax:
102
+ return None
103
+
104
+ return vmin, vmax
105
+
106
+ @staticmethod
107
+ def _constant_colorbar_levels(value):
108
+ """Return a small finite range containing one constant data value."""
109
+
110
+ value = float(value)
111
+ if not np.isfinite(value):
112
+ return None
113
+
114
+ if value == 0.0:
115
+ padding = 1e-6
116
+ else:
117
+ padding = abs(value) * 1e-6
118
+
119
+ with np.errstate(over="ignore", under="ignore", invalid="ignore"):
120
+ low = value - padding
121
+ high = value + padding
122
+ if np.isfinite(low) and np.isfinite(high) and low < high:
123
+ return float(low), float(high)
124
+
125
+ low = float(np.nextafter(value, -np.inf))
126
+ high = float(np.nextafter(value, np.inf))
127
+ if not np.isfinite(low):
128
+ low = value
129
+ if not np.isfinite(high):
130
+ high = value
131
+ if low < high:
132
+ return low, high
133
+ return None
134
+
135
+ def _finite_data_colorbar_range(self):
136
+ """Return the finite range across every heatmap sharing the colorbar."""
137
+
138
+ data_provider = getattr(self, "_heatmap_colorbar_data_arrays", None)
139
+ if callable(data_provider):
140
+ arrays = data_provider()
141
+ else:
142
+ data = getattr(self, "dataGrid", None)
143
+ arrays = () if data is None else (data,)
144
+
145
+ lower = None
146
+ upper = None
147
+ for data in arrays:
148
+ values = np.asarray(data, dtype=float)
149
+ finite_values = values[np.isfinite(values)]
150
+ if finite_values.size == 0:
151
+ continue
152
+ data_lower = float(np.min(finite_values))
153
+ data_upper = float(np.max(finite_values))
154
+ lower = data_lower if lower is None else min(lower, data_lower)
155
+ upper = data_upper if upper is None else max(upper, data_upper)
156
+
157
+ if lower is None or upper is None:
158
+ return None
159
+ return lower, upper
160
+
161
+ def _data_colorbar_rounding(self):
162
+ """Return a finite, positive colorbar interaction step."""
163
+
164
+ levels = self._data_colorbar_levels()
165
+ if levels is None:
166
+ return 1e-5
167
+
168
+ return self._colorbar_rounding_for_levels(*levels)
169
+
170
+ @staticmethod
171
+ def _colorbar_rounding_for_levels(vmin, vmax):
172
+ """Return an interaction step appropriate for specific levels."""
173
+
174
+ with np.errstate(over="ignore", invalid="ignore"):
175
+ span = float(vmax) - float(vmin)
176
+ if not np.isfinite(span) or span <= 0:
177
+ span = max(abs(vmin), abs(vmax))
178
+ if not np.isfinite(span) or span <= 0:
179
+ span = 1.0
180
+
181
+ rounding = span / 1e5
182
+ minimum_rounding = np.nextafter(0.0, 1.0)
183
+ if not np.isfinite(rounding) or rounding < minimum_rounding:
184
+ return float(minimum_rounding)
185
+ return float(rounding)
186
+
187
+ def _set_colorbar_levels(self, vmin, vmax):
188
+ """
189
+ Apply levels to the colorbar and mirror them in the menu fields.
190
+
191
+ """
192
+ if not np.isfinite(float(vmax) - float(vmin)):
193
+ self._reject_heatmap_color_range()
194
+ return False
195
+
196
+ bar = self.__dict__.get("bar")
197
+ if bar is not None:
198
+ bar.rounding = self._colorbar_rounding_for_levels(vmin, vmax)
199
+ finite_limit = np.finfo(float).max
200
+ bar.lo_lim = -finite_limit
201
+ bar.hi_lim = finite_limit
202
+ with np.errstate(over="ignore", invalid="ignore"):
203
+ span = float(vmax) - float(vmin)
204
+ interaction_enabled = bool(np.isfinite(span) and span > 0)
205
+ region = getattr(bar, "region", None)
206
+ if region is not None:
207
+ region.setEnabled(interaction_enabled)
208
+ bar.region_changed_enable = interaction_enabled
209
+ bar.setLevels((vmin, vmax))
210
+ self._sync_colorbar_axis_scaling()
211
+
212
+ self._sync_colorbar_level_fields(vmin, vmax)
213
+ return True
214
+
215
+ def _reject_heatmap_color_range(self):
216
+ """Hide an unusable color display while retaining measured samples."""
217
+ self._heatmap_color_range_rejected = True
218
+ for item in self._heatmap_colorbar_items():
219
+ item.hide()
220
+ bar = self.__dict__.get("bar")
221
+ if bar is not None:
222
+ bar.hide()
223
+ self._reset_heatmap_hover()
224
+ self._set_sweep_lines_visible(False)
225
+ message = (
226
+ "The heatmap color range is too wide to display safely. "
227
+ "Recorded samples remain available for CSV export and operations."
228
+ )
229
+ self.show_status(message, 10_000)
230
+ self.show_plot_state("Unsupported heatmap color range", message, kind="error")
231
+
232
+ def _set_colorbar_tick_formatter(self):
233
+ """
234
+ Use plain scaled tick labels and show the scale in the colorbar title.
235
+
236
+ """
237
+ bar = self.__dict__.get("bar")
238
+ axis = getattr(bar, "axis", None)
239
+ if axis is None:
240
+ return
241
+
242
+ layout = getattr(bar, "layout", None)
243
+ get_margins = getattr(layout, "getContentsMargins", None)
244
+ set_margins = getattr(layout, "setContentsMargins", None)
245
+ if callable(get_margins) and callable(set_margins):
246
+ left, _top, right, _bottom = get_margins()
247
+ set_margins(left, 0, right, 0)
248
+
249
+ get_axis = getattr(bar, "getAxis", None)
250
+ if callable(get_axis):
251
+ for side in ("left", "right", "top", "bottom"):
252
+ _install_flush_axis_draw_specs(get_axis(side))
253
+
254
+ self._restore_colorbar_default_tick_formatter(axis)
255
+ axis.autoSIPrefix = False
256
+ axis.setWidth(70)
257
+ axis.setStyle(tickTextWidth=60)
258
+ self._set_colorbar_label_direction(bar)
259
+ self._install_colorbar_axis_scale_sync(bar)
260
+ self._install_colorbar_mirrored_ticks(bar)
261
+ self._sync_colorbar_axis_scaling()
262
+ self._install_colorbar_scale_bar_handlers(bar)
263
+ self._install_colorbar_scale_axis_handlers(axis)
264
+
265
+ def _restore_colorbar_default_tick_formatter(self, axis):
266
+ try:
267
+ del axis.tickStrings
268
+ except AttributeError:
269
+ pass
270
+
271
+ def _install_colorbar_axis_scale_sync(self, bar):
272
+ axis = getattr(bar, "axis", None)
273
+ if axis is None or getattr(axis, "_qplot_colorbar_scale_sync_installed", False):
274
+ return
275
+
276
+ previous_set_range = getattr(axis, "setRange", None)
277
+ if previous_set_range is None:
278
+ return
279
+
280
+ def set_range(low, high, previous_set_range=previous_set_range):
281
+ previous_set_range(low, high)
282
+ self._sync_colorbar_axis_scaling()
283
+
284
+ axis.setRange = set_range
285
+ axis._qplot_colorbar_scale_sync_installed = True
286
+
287
+ def _install_colorbar_mirrored_ticks(self, bar):
288
+ """Mirror vertical colour-bar ticks on the left without their labels."""
289
+
290
+ if getattr(bar, "horizontal", False):
291
+ return
292
+
293
+ get_axis = getattr(bar, "getAxis", None)
294
+ axis = getattr(bar, "axis", None)
295
+ if not callable(get_axis) or axis is None:
296
+ return
297
+
298
+ mirror = get_axis("left")
299
+ if mirror is None:
300
+ return
301
+
302
+ mirror.unlinkFromView()
303
+ mirror.setStyle(showValues=False)
304
+
305
+ def sync_ticks():
306
+ low, high = axis.range
307
+ mirror.setRange(low, high)
308
+ length = max(1, int(axis.height()))
309
+ tick_values = axis.tickValues(low, high, length)
310
+ mirror.setTicks([
311
+ [(value, "") for value in values]
312
+ for _spacing, values in tick_values
313
+ ])
314
+
315
+ sync_ticks()
316
+ if getattr(axis, "_qplot_colorbar_mirrored_ticks_installed", False):
317
+ return
318
+
319
+ previous_set_range = axis.setRange
320
+
321
+ def set_range(low, high, previous_set_range=previous_set_range):
322
+ previous_set_range(low, high)
323
+ sync_ticks()
324
+
325
+ previous_resize_event = getattr(axis, "resizeEvent", None)
326
+
327
+ def resize_event(event=None, previous_handler=previous_resize_event):
328
+ if previous_handler is not None:
329
+ previous_handler(event)
330
+ sync_ticks()
331
+
332
+ axis.setRange = set_range
333
+ axis.resizeEvent = resize_event
334
+ axis._qplot_colorbar_mirrored_ticks_installed = True
335
+
336
+ def _sync_colorbar_axis_scaling(self):
337
+ bar = self.__dict__.get("bar")
338
+ axis = getattr(bar, "axis", None)
339
+ if axis is None:
340
+ return
341
+
342
+ scale = self._colorbar_axis_display_scale(bar, axis)
343
+ axis.autoSIPrefixScale = scale
344
+ axis.labelUnitPrefix = ""
345
+ axis.picture = None
346
+ axis.update()
347
+ self._set_colorbar_scaled_label(bar, scale)
348
+
349
+ def _colorbar_axis_display_scale(self, bar, axis):
350
+ levels = self._colorbar_levels_from_bar(bar)
351
+ if levels is None:
352
+ levels = getattr(axis, "range", None)
353
+ if levels is None:
354
+ return 1.0
355
+
356
+ low, high = levels
357
+ scale_value = max(abs(low), abs(high))
358
+ if not np.isfinite(scale_value):
359
+ return 1.0
360
+
361
+ scale, _prefix = pg.functions.siScale(scale_value, allowUnicode=False)
362
+ return scale
363
+
364
+ def _set_colorbar_scaled_label(self, bar, scale):
365
+ param = self.__dict__.get("display_param", self.__dict__.get("param"))
366
+ if param is None or not hasattr(bar, "setLabel"):
367
+ return
368
+
369
+ label = getattr(param, "label", "")
370
+ unit = getattr(param, "unit", "")
371
+ unit_scale = 1.0 / scale if scale else 1.0
372
+ scale_text = _axis_scale_power_text(unit_scale)
373
+
374
+ if unit and scale_text:
375
+ unit_text = f"{scale_text} {unit}"
376
+ elif unit:
377
+ unit_text = unit
378
+ else:
379
+ unit_text = scale_text
380
+
381
+ text = f"{label} ({unit_text})" if unit_text else label
382
+ axis = "bottom" if getattr(bar, "horizontal", False) else "right"
383
+ bar.setLabel(axis, text)
384
+ self._set_colorbar_label_direction(bar)
385
+
386
+ def _set_colorbar_label_direction(self, bar):
387
+ if getattr(bar, "horizontal", False):
388
+ return
389
+
390
+ axis = getattr(bar, "axis", None)
391
+ label = getattr(axis, "label", None)
392
+ if axis is None or label is None or not hasattr(label, "setRotation"):
393
+ return
394
+
395
+ label.setRotation(90)
396
+ if not getattr(axis, "_qplot_downward_colorbar_label_installed", False):
397
+ previous_resize_event = getattr(axis, "resizeEvent", None)
398
+
399
+ def resize_event(event=None, previous_handler=previous_resize_event):
400
+ if previous_handler is not None:
401
+ previous_handler(event)
402
+ self._position_downward_colorbar_label(axis)
403
+
404
+ axis.resizeEvent = resize_event
405
+ axis._qplot_downward_colorbar_label_installed = True
406
+
407
+ self._position_downward_colorbar_label(axis)
408
+
409
+ def _position_downward_colorbar_label(self, axis):
410
+ label = getattr(axis, "label", None)
411
+ if label is None or not all(
412
+ hasattr(label, name) for name in ("boundingRect", "setPos")
413
+ ):
414
+ return
415
+
416
+ nudge = 5
417
+ bounds = label.boundingRect()
418
+ size = axis.size()
419
+ position = QtCore.QPointF(
420
+ int(size.width() + nudge),
421
+ int(size.height() / 2 - bounds.width() / 2),
422
+ )
423
+ label.setPos(position)
424
+ axis.picture = None
425
+
426
+ def _install_colorbar_scale_bar_handlers(self, bar):
427
+ """
428
+ Open the color-scale dialog from colorbar interactions.
429
+
430
+ """
431
+ self._install_colorbar_scale_double_click_handler(bar)
432
+ self._suppress_colorbar_right_click_menu(bar)
433
+ self._install_colorbar_level_sync_handlers(bar)
434
+ self._install_colorbar_alt_range_drag_handler(bar)
435
+
436
+ def _install_colorbar_scale_axis_handlers(self, axis):
437
+ """
438
+ Open the color-scale dialog from direct colorbar axis interactions.
439
+
440
+ """
441
+ self._install_colorbar_scale_double_click_handler(axis)
442
+
443
+ def _install_colorbar_scale_double_click_handler(self, item):
444
+ """
445
+ Open the color-scale dialog when a colorbar item is double-clicked.
446
+
447
+ """
448
+ if getattr(item, "_qplot_colorbar_scale_handlers_installed", False):
449
+ return
450
+
451
+ previous_double_click_handler = getattr(item, "mouseDoubleClickEvent", None)
452
+
453
+ def mouse_double_click(event, previous_handler=previous_double_click_handler):
454
+ if event.button() == QtCore.Qt.MouseButton.LeftButton:
455
+ self.open_colorbar_scale_dialog()
456
+ event.accept()
457
+ return
458
+
459
+ if previous_handler is not None:
460
+ previous_handler(event)
461
+
462
+ item.mouseDoubleClickEvent = mouse_double_click
463
+ item._qplot_colorbar_scale_handlers_installed = True
464
+
465
+ def _suppress_colorbar_right_click_menu(self, bar):
466
+ """
467
+ Prevent pyqtgraph's colorbar right-click menu from opening.
468
+
469
+ """
470
+ if getattr(bar, "_qplot_colorbar_right_click_suppressed", False):
471
+ return
472
+
473
+ previous_mouse_click_handler = getattr(bar, "mouseClickEvent", None)
474
+
475
+ def mouse_click(event, previous_handler=previous_mouse_click_handler):
476
+ if event.button() == QtCore.Qt.MouseButton.RightButton:
477
+ event.accept()
478
+ return
479
+
480
+ if previous_handler is not None:
481
+ previous_handler(event)
482
+
483
+ bar.mouseClickEvent = mouse_click
484
+ bar._qplot_colorbar_right_click_suppressed = True
485
+
486
+ def _install_colorbar_level_sync_handlers(self, bar):
487
+ """
488
+ Treat direct colorbar handle adjustment as a manual color range.
489
+
490
+ """
491
+ if getattr(bar, "_qplot_colorbar_level_sync_installed", False):
492
+ return
493
+
494
+ changed = getattr(bar, "sigLevelsChanged", None)
495
+ if changed is not None:
496
+ changed.connect(self._colorbar_interactive_levels_changed)
497
+
498
+ finished = getattr(bar, "sigLevelsChangeFinished", None)
499
+ if finished is not None:
500
+ finished.connect(self._colorbar_interactive_levels_finished)
501
+
502
+ bar._qplot_colorbar_level_sync_installed = True
503
+
504
+ def _install_colorbar_alt_range_drag_handler(self, bar):
505
+ """
506
+ Add outside-bar drag on colorbar regions to widen or narrow levels.
507
+
508
+ """
509
+ region = getattr(bar, "region", None)
510
+ if region is None:
511
+ return
512
+
513
+ if getattr(region, "_qplot_colorbar_alt_range_drag_installed", False):
514
+ return
515
+
516
+ previous_bounding_rect = getattr(region, "boundingRect", None)
517
+ previous_paint = getattr(region, "paint", None)
518
+ previous_mouse_drag_handler = getattr(region, "mouseDragEvent", None)
519
+ previous_hover_handler = getattr(region, "hoverEvent", None)
520
+
521
+ def bounding_rect(previous_handler=previous_bounding_rect):
522
+ rect = self._colorbar_full_region_rect(region)
523
+ if rect is not None:
524
+ return rect
525
+ if previous_handler is not None:
526
+ return previous_handler()
527
+ return QtCore.QRectF()
528
+
529
+ def paint(painter, *args, previous_handler=previous_paint):
530
+ mode = getattr(region, "_qplot_colorbar_drag_visual_area", "inside")
531
+ if mode in ("inside", "outside"):
532
+ rects = self._colorbar_drag_visual_rects(region, mode)
533
+ if rects:
534
+ painter.setBrush(region.currentBrush)
535
+ painter.setPen(pg.mkPen(None))
536
+ for rect in rects:
537
+ painter.drawRect(rect)
538
+ return
539
+
540
+ if previous_handler is not None:
541
+ previous_handler(painter, *args)
542
+
543
+ def mouse_drag(event, previous_handler=previous_mouse_drag_handler):
544
+ active = getattr(region, "_qplot_colorbar_alt_range_drag_source", None)
545
+ source, direction = self._colorbar_alt_range_drag_source_at(
546
+ bar,
547
+ region,
548
+ event.buttonDownPos(),
549
+ )
550
+ if (
551
+ event.button() == QtCore.Qt.MouseButton.LeftButton
552
+ and (
553
+ active is not None
554
+ or (
555
+ source is not None
556
+ and getattr(region, "movable", True)
557
+ )
558
+ )
559
+ ):
560
+ if active is not None:
561
+ source = active
562
+ direction = active[1]
563
+ self._colorbar_alt_range_drag_event(
564
+ bar,
565
+ region,
566
+ event,
567
+ source,
568
+ direction,
569
+ region,
570
+ )
571
+ return
572
+
573
+ if previous_handler is not None:
574
+ previous_handler(event)
575
+
576
+ def hover(event, previous_handler=previous_hover_handler):
577
+ if previous_handler is not None:
578
+ previous_handler(event)
579
+
580
+ if event.isExit():
581
+ region._qplot_colorbar_drag_visual_area = None
582
+ else:
583
+ source, _direction = self._colorbar_alt_range_drag_source_at(
584
+ bar,
585
+ region,
586
+ event.pos(),
587
+ )
588
+ region._qplot_colorbar_drag_visual_area = (
589
+ "outside" if source is not None else "inside"
590
+ )
591
+ region.update()
592
+
593
+ region.boundingRect = bounding_rect
594
+ region.paint = paint
595
+ region.mouseDragEvent = mouse_drag
596
+ region.hoverEvent = hover
597
+ region._qplot_colorbar_drag_visual_area = None
598
+
599
+ region._qplot_colorbar_alt_range_drag_installed = True
600
+
601
+ def _install_colorbar_alt_handle_drag_handler(self, bar, region, line, index):
602
+ """
603
+ Patch one pyqtgraph colorbar handle for symmetric Alt/Option-drag.
604
+
605
+ """
606
+ if index not in (0, 1):
607
+ return
608
+
609
+ if getattr(line, "_qplot_colorbar_alt_range_drag_installed", False):
610
+ return
611
+
612
+ previous_mouse_drag_handler = getattr(line, "mouseDragEvent", None)
613
+ source = ("line", index)
614
+ direction = -1.0 if index == 0 else 1.0
615
+
616
+ def mouse_drag(event, previous_handler=previous_mouse_drag_handler):
617
+ modifiers = event.modifiers()
618
+ active = getattr(region, "_qplot_colorbar_alt_range_drag_source", None)
619
+ if (
620
+ event.button() == QtCore.Qt.MouseButton.LeftButton
621
+ and (
622
+ active == source
623
+ or (
624
+ modifiers & QtCore.Qt.KeyboardModifier.AltModifier
625
+ and getattr(line, "movable", True)
626
+ and getattr(region, "movable", True)
627
+ )
628
+ )
629
+ ):
630
+ self._colorbar_alt_range_drag_event(
631
+ bar,
632
+ region,
633
+ event,
634
+ source,
635
+ direction,
636
+ line,
637
+ )
638
+ return
639
+
640
+ if previous_handler is not None:
641
+ previous_handler(event)
642
+
643
+ line.mouseDragEvent = mouse_drag
644
+ line._qplot_colorbar_alt_range_drag_installed = True
645
+
646
+ def _colorbar_full_region_rect(self, region):
647
+ """
648
+ Return the full colorbar interaction rectangle in region coordinates.
649
+
650
+ """
651
+ try:
652
+ rect = QtCore.QRectF(region.viewRect())
653
+ except (AttributeError, RuntimeError, TypeError):
654
+ return None
655
+
656
+ span = getattr(region, "span", (0, 1))
657
+ orientation = getattr(region, "orientation", None)
658
+ if orientation in ("vertical", 0):
659
+ length = rect.height()
660
+ rect.setBottom(rect.top() + length * span[1])
661
+ rect.setTop(rect.top() + length * span[0])
662
+ else:
663
+ length = rect.width()
664
+ rect.setRight(rect.left() + length * span[1])
665
+ rect.setLeft(rect.left() + length * span[0])
666
+
667
+ return rect.normalized()
668
+
669
+ def _colorbar_drag_visual_rects(self, region, mode):
670
+ """
671
+ Return shaded rectangles for colorbar range-slide or outside-scale zones.
672
+
673
+ """
674
+ rect = self._colorbar_full_region_rect(region)
675
+ if rect is None:
676
+ return ()
677
+
678
+ levels = self._colorbar_region_line_positions(region)
679
+ if levels is None:
680
+ return ()
681
+ low, high = levels
682
+
683
+ orientation = getattr(region, "orientation", None)
684
+ if orientation in ("vertical", 0):
685
+ inside = QtCore.QRectF(rect)
686
+ inside.setLeft(low)
687
+ inside.setRight(high)
688
+ outside_low = QtCore.QRectF(
689
+ rect.left(),
690
+ rect.top(),
691
+ low - rect.left(),
692
+ rect.height(),
693
+ )
694
+ outside_high = QtCore.QRectF(
695
+ high,
696
+ rect.top(),
697
+ rect.right() - high,
698
+ rect.height(),
699
+ )
700
+ else:
701
+ inside = QtCore.QRectF(rect)
702
+ inside.setTop(low)
703
+ inside.setBottom(high)
704
+ outside_low = QtCore.QRectF(
705
+ rect.left(),
706
+ rect.top(),
707
+ rect.width(),
708
+ low - rect.top(),
709
+ )
710
+ outside_high = QtCore.QRectF(
711
+ rect.left(),
712
+ high,
713
+ rect.width(),
714
+ rect.bottom() - high,
715
+ )
716
+
717
+ if mode == "inside":
718
+ return (inside.normalized(),)
719
+
720
+ return tuple(
721
+ outside.normalized()
722
+ for outside in (outside_low, outside_high)
723
+ if outside.width() > 0 and outside.height() > 0
724
+ )
725
+
726
+ def _colorbar_region_line_positions(self, region):
727
+ """
728
+ Return sorted colorbar handle positions in region coordinates.
729
+
730
+ """
731
+ lines = getattr(region, "lines", None)
732
+ if lines is None or len(lines) != 2:
733
+ return None
734
+
735
+ positions = []
736
+ for line in lines:
737
+ value = getattr(line, "value", None)
738
+ if callable(value):
739
+ positions.append(float(value()))
740
+ else:
741
+ position = getattr(line, "position", None)
742
+ if position is None:
743
+ return None
744
+ positions.append(float(position))
745
+
746
+ if not all(np.isfinite(position) for position in positions):
747
+ return None
748
+
749
+ return tuple(sorted(positions))
750
+
751
+ def _colorbar_alt_range_drag_source_at(self, bar, region, position):
752
+ """
753
+ Return the outside-bar range-scale drag source and direction at a point.
754
+
755
+ """
756
+ levels = self._colorbar_region_line_positions(region)
757
+ if levels is None:
758
+ return None, 0.0
759
+
760
+ axis_position = self._colorbar_alt_range_drag_axis_position(
761
+ bar,
762
+ position,
763
+ region,
764
+ )
765
+ low, high = levels
766
+ if axis_position < low:
767
+ return ("bar", -1.0), -1.0
768
+ if axis_position > high:
769
+ return ("bar", 1.0), 1.0
770
+ return None, 0.0
771
+
772
+ def _colorbar_alt_range_drag_event(
773
+ self,
774
+ bar,
775
+ region,
776
+ event,
777
+ source,
778
+ direction,
779
+ event_item,
780
+ ):
781
+ """
782
+ Expand or contract the color range around its midpoint during Alt-drag.
783
+
784
+ """
785
+ event.accept()
786
+
787
+ if event.isStart():
788
+ levels = self._colorbar_levels_from_bar(bar)
789
+ if levels is None:
790
+ region._qplot_colorbar_alt_range_drag_active = False
791
+ region._qplot_colorbar_alt_range_drag_source = None
792
+ return
793
+
794
+ region._qplot_colorbar_alt_range_drag_active = True
795
+ region._qplot_colorbar_alt_range_drag_source = source
796
+ region._qplot_colorbar_alt_range_drag_levels = levels
797
+ region._qplot_colorbar_alt_range_drag_start_pos = (
798
+ self._colorbar_alt_range_drag_axis_position(
799
+ bar,
800
+ event.buttonDownPos(),
801
+ event_item,
802
+ )
803
+ )
804
+ region._qplot_colorbar_drag_visual_area = "outside"
805
+
806
+ if (
807
+ not getattr(region, "_qplot_colorbar_alt_range_drag_active", False)
808
+ or getattr(region, "_qplot_colorbar_alt_range_drag_source", None) != source
809
+ ):
810
+ return
811
+
812
+ start_levels = region._qplot_colorbar_alt_range_drag_levels
813
+ start_pos = region._qplot_colorbar_alt_range_drag_start_pos
814
+ current_pos = self._colorbar_alt_range_drag_axis_position(
815
+ bar,
816
+ event.pos(),
817
+ event_item,
818
+ )
819
+ axis_delta = (current_pos - start_pos) * direction
820
+ self._set_colorbar_alt_range_drag_visual(region, axis_delta)
821
+ levels = self._colorbar_alt_range_drag_levels(
822
+ bar,
823
+ start_levels,
824
+ axis_delta,
825
+ )
826
+ if levels is not None:
827
+ bar.setLevels(levels)
828
+ self._colorbar_interactive_levels_changed(bar)
829
+
830
+ if event.isFinish():
831
+ region._qplot_colorbar_alt_range_drag_active = False
832
+ region._qplot_colorbar_alt_range_drag_source = None
833
+ region._qplot_colorbar_drag_visual_area = None
834
+ self._set_colorbar_alt_range_drag_visual(region, 0.0)
835
+ self._colorbar_interactive_levels_finished(bar)
836
+
837
+ def _colorbar_alt_range_drag_axis_position(self, bar, position, item=None):
838
+ """
839
+ Return the colorbar-axis coordinate from a region mouse position.
840
+
841
+ """
842
+ if item is not None:
843
+ try:
844
+ position = item.mapToParent(position)
845
+ except (AttributeError, RuntimeError):
846
+ pass
847
+
848
+ if getattr(bar, "horizontal", False):
849
+ return position.x()
850
+
851
+ return position.y()
852
+
853
+ def _set_colorbar_alt_range_drag_visual(self, region, axis_delta):
854
+ """
855
+ Move pyqtgraph's range markers apart or together during Alt-drag.
856
+
857
+ """
858
+ lines = getattr(region, "lines", None)
859
+ if lines is None or len(lines) != 2:
860
+ return
861
+
862
+ axis_delta = min(max(axis_delta, -63.0), 63.0)
863
+ previous_block = getattr(region, "blockLineSignal", False)
864
+ region.blockLineSignal = True
865
+ try:
866
+ lines[0].setPos(63.0 - axis_delta)
867
+ lines[1].setPos(191.0 + axis_delta)
868
+ region.prepareGeometryChange()
869
+ finally:
870
+ region.blockLineSignal = previous_block
871
+
872
+ def _colorbar_alt_range_drag_levels(self, bar, start_levels, axis_delta):
873
+ """
874
+ Return levels widened or narrowed symmetrically by an Alt-drag delta.
875
+
876
+ """
877
+ low, high = start_levels
878
+ span = high - low
879
+ if not np.isfinite(span) or span <= 0:
880
+ return None
881
+
882
+ rounding = getattr(bar, "rounding", 0.0)
883
+ try:
884
+ rounding = float(rounding)
885
+ except (TypeError, ValueError):
886
+ rounding = 0.0
887
+ if not np.isfinite(rounding) or rounding <= 0:
888
+ rounding = 0.0
889
+
890
+ drag_units = axis_delta / 64.0
891
+ signed_drag = math.copysign(drag_units * drag_units, drag_units)
892
+ scale = span + 2 * rounding
893
+ center = (low + high) / 2.0
894
+ half_span = span / 2.0 + scale * signed_drag
895
+ min_span = rounding if rounding > 0 else max(abs(span) * 1e-9, 1e-300)
896
+ half_span = max(half_span, min_span / 2.0)
897
+
898
+ lo_lim = getattr(bar, "lo_lim", None)
899
+ hi_lim = getattr(bar, "hi_lim", None)
900
+ if lo_lim is not None and np.isfinite(lo_lim):
901
+ half_span = min(half_span, center - lo_lim)
902
+ if hi_lim is not None and np.isfinite(hi_lim):
903
+ half_span = min(half_span, hi_lim - center)
904
+ if half_span <= 0:
905
+ return None
906
+
907
+ low = center - half_span
908
+ high = center + half_span
909
+
910
+ if rounding > 0:
911
+ low = rounding * round(low / rounding)
912
+ high = rounding * round(high / rounding)
913
+
914
+ if lo_lim is not None and np.isfinite(lo_lim):
915
+ low = max(low, lo_lim)
916
+ if hi_lim is not None and np.isfinite(hi_lim):
917
+ high = min(high, hi_lim)
918
+ if not np.isfinite(low) or not np.isfinite(high) or low >= high:
919
+ return None
920
+
921
+ return float(low), float(high)
922
+
923
+ def _colorbar_levels_from_bar(self, bar):
924
+ """
925
+ Return validated numeric levels from a pyqtgraph colorbar.
926
+
927
+ """
928
+ try:
929
+ low, high = bar.levels()
930
+ except (AttributeError, TypeError, ValueError):
931
+ return None
932
+
933
+ try:
934
+ low = float(low)
935
+ high = float(high)
936
+ except (TypeError, ValueError):
937
+ return None
938
+
939
+ if not np.isfinite(low) or not np.isfinite(high) or low >= high:
940
+ return None
941
+
942
+ return low, high
943
+
944
+ def _colorbar_interactive_levels_changed(self, bar):
945
+ """
946
+ Mirror interactively adjusted levels into the scale dialog.
947
+
948
+ """
949
+ levels = self._colorbar_levels_from_bar(bar)
950
+ if levels is None:
951
+ return
952
+
953
+ self._sync_colorbar_axis_scaling()
954
+ self._sync_colorbar_level_fields(*levels)
955
+
956
+ def _colorbar_interactive_levels_finished(self, bar):
957
+ """
958
+ Persist interactively adjusted colorbar levels as manual scaling.
959
+
960
+ """
961
+ levels = self._colorbar_levels_from_bar(bar)
962
+ if levels is None:
963
+ return
964
+
965
+ self._colorbar_manual_levels = levels
966
+ if "relevel_refresh" in self.__dict__:
967
+ self.relevel_refresh.setChecked(False)
968
+ if "colorbar_manual_radio" in self.__dict__:
969
+ self.colorbar_manual_radio.setChecked(True)
970
+ if "colorbar_auto_radio" in self.__dict__:
971
+ self.colorbar_auto_radio.setChecked(False)
972
+ self._sync_colorbar_level_fields(*levels)
973
+
974
+ def _current_colorbar_levels(self):
975
+ """
976
+ Return the currently displayed colorbar levels for menu synchronisation.
977
+
978
+ """
979
+ bar = self.__dict__.get("bar")
980
+ if bar is not None:
981
+ return bar.levels()
982
+
983
+ manual_levels = getattr(self, "_colorbar_manual_levels", None)
984
+ if manual_levels is not None:
985
+ return manual_levels
986
+
987
+ return self._data_colorbar_levels()
988
+
989
+ def _current_colorbar_colormap_name(self):
990
+ """
991
+ Return the selected colorbar color map preference.
992
+
993
+ """
994
+ available_colormaps = self._available_colorbar_colormaps()
995
+ name = getattr(self, "_colorbar_colormap_name", None)
996
+ if name in available_colormaps:
997
+ return name
998
+
999
+ name = _config_value(
1000
+ self.__dict__.get("config"),
1001
+ "user_preference.bar_colour",
1002
+ "viridis",
1003
+ )
1004
+
1005
+ if name not in available_colormaps:
1006
+ name = self._fallback_colorbar_colormap_name()
1007
+
1008
+ self._colorbar_colormap_name = name
1009
+ return name
1010
+
1011
+ def _available_colorbar_colormaps(self):
1012
+ """
1013
+ Return color maps after applying persistent user filters.
1014
+
1015
+ """
1016
+ config_obj = self.__dict__.get("config")
1017
+ include_cet = _config_value(
1018
+ config_obj,
1019
+ "user_preference.bar_colour_include_cet",
1020
+ True,
1021
+ )
1022
+ include_matplotlib = _config_value(
1023
+ config_obj,
1024
+ "user_preference.bar_colour_include_matplotlib",
1025
+ True,
1026
+ )
1027
+ include_local = _config_value(
1028
+ config_obj,
1029
+ "user_preference.bar_colour_include_local",
1030
+ True,
1031
+ )
1032
+ include_custom = _config_value(
1033
+ config_obj,
1034
+ "user_preference.bar_colour_include_custom",
1035
+ True,
1036
+ )
1037
+ include_cet_subtypes = {
1038
+ subtype: _config_value(
1039
+ config_obj,
1040
+ _colorbar_subtype_config_key("cet", subtype),
1041
+ True,
1042
+ )
1043
+ for subtype, _label in _CET_COLORBAR_SUBTYPES
1044
+ }
1045
+ include_matplotlib_subtypes = {
1046
+ subtype: _config_value(
1047
+ config_obj,
1048
+ _colorbar_subtype_config_key("matplotlib", subtype),
1049
+ True,
1050
+ )
1051
+ for subtype, _label in _MATPLOTLIB_COLORBAR_SUBTYPES
1052
+ }
1053
+ excluded_names = set(_config_value(
1054
+ config_obj,
1055
+ "user_preference.bar_colour_excluded",
1056
+ [],
1057
+ ))
1058
+ excluded_prefixes = tuple(_config_value(
1059
+ config_obj,
1060
+ "user_preference.bar_colour_excluded_prefixes",
1061
+ [],
1062
+ ))
1063
+
1064
+ available = []
1065
+ for name in _COLORBAR_COLORMAPS:
1066
+ if name in _DEFAULT_HIDDEN_COLORBAR_NAMES:
1067
+ continue
1068
+ if name.startswith(_DEFAULT_HIDDEN_COLORBAR_PREFIXES):
1069
+ continue
1070
+ if name.endswith(_DEFAULT_HIDDEN_COLORBAR_SUFFIXES):
1071
+ continue
1072
+ if name in excluded_names:
1073
+ continue
1074
+ if excluded_prefixes and name.startswith(excluded_prefixes):
1075
+ continue
1076
+
1077
+ group = _colorbar_colormap_group(name)
1078
+ if group == "cet" and not include_cet:
1079
+ continue
1080
+ if (
1081
+ group == "cet"
1082
+ and not include_cet_subtypes[_cet_colorbar_colormap_subtype(name)]
1083
+ ):
1084
+ continue
1085
+ if group == "matplotlib" and not include_matplotlib:
1086
+ continue
1087
+ if (
1088
+ group == "matplotlib"
1089
+ and not include_matplotlib_subtypes[
1090
+ _matplotlib_colorbar_colormap_subtype(name)
1091
+ ]
1092
+ ):
1093
+ continue
1094
+ if group == "pyqtgraph" and not include_local:
1095
+ continue
1096
+ if group == "custom" and not include_custom:
1097
+ continue
1098
+
1099
+ available.append(name)
1100
+
1101
+ return tuple(available)
1102
+
1103
+ def _fallback_colorbar_colormap_name(self):
1104
+ """
1105
+ Choose a usable map when the saved preference is filtered out.
1106
+
1107
+ """
1108
+ available_colormaps = self._available_colorbar_colormaps()
1109
+ if "viridis" in available_colormaps:
1110
+ return "viridis"
1111
+ if not available_colormaps:
1112
+ return "viridis"
1113
+ return available_colormaps[0]
1114
+
1115
+ def _colorbar_colormap(self, name=None):
1116
+ """
1117
+ Return a pyqtgraph color map or built-in map name for the colorbar.
1118
+
1119
+ """
1120
+ if name is None:
1121
+ name = self._current_colorbar_colormap_name()
1122
+
1123
+ return _colorbar_colormap_for_name(name)
1124
+
1125
+ ###############################################################################
1126
+ # Subplot control