qplotter 1.6.0__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 (130) hide show
  1. qplot/__init__.py +46 -0
  2. qplot/__main__.py +232 -0
  3. qplot/_auxiliary_paths.py +67 -0
  4. qplot/_build_info.json +1 -0
  5. qplot/_metadata.py +75 -0
  6. qplot/_repair.py +14 -0
  7. qplot/_shutdown_supervisor.py +4049 -0
  8. qplot/_version.py +12 -0
  9. qplot/_windows_shutdown_job.py +1166 -0
  10. qplot/configuration/__init__.py +3 -0
  11. qplot/configuration/config.py +578 -0
  12. qplot/configuration/config_schema.json +357 -0
  13. qplot/configuration/fonts.py +3 -0
  14. qplot/configuration/scripts.py +255 -0
  15. qplot/configuration/themes/__init__.py +13 -0
  16. qplot/configuration/themes/_base.py +592 -0
  17. qplot/configuration/themes/blank.py +17 -0
  18. qplot/configuration/themes/dark.py +54 -0
  19. qplot/configuration/themes/light.py +54 -0
  20. qplot/datahandling/LoadFromDB.py +224 -0
  21. qplot/datahandling/__init__.py +47 -0
  22. qplot/datahandling/_trusted_live_helper.py +812 -0
  23. qplot/datahandling/_trusted_live_protocol.py +1745 -0
  24. qplot/datahandling/database.py +2340 -0
  25. qplot/datahandling/dimensions.py +43 -0
  26. qplot/datahandling/file_identity.py +461 -0
  27. qplot/datahandling/parameter_data.py +149 -0
  28. qplot/datahandling/plot_progress.py +17 -0
  29. qplot/datahandling/qcodes_cache.py +277 -0
  30. qplot/datahandling/qcodes_compat.py +14 -0
  31. qplot/datahandling/readSQL.py +2442 -0
  32. qplot/datahandling/readonly.py +2566 -0
  33. qplot/datahandling/source_activity.py +21 -0
  34. qplot/datahandling/trusted_array_preview.py +148 -0
  35. qplot/datahandling/trusted_derived_cache.py +1605 -0
  36. qplot/datahandling/trusted_derived_rendering.py +876 -0
  37. qplot/datahandling/trusted_heatmap.py +246 -0
  38. qplot/datahandling/trusted_live.py +2808 -0
  39. qplot/datahandling/trusted_live_queries.py +5907 -0
  40. qplot/datahandling/trusted_live_service.py +1919 -0
  41. qplot/datahandling/trusted_live_supervisor.py +2161 -0
  42. qplot/datahandling/trusted_plot.py +283 -0
  43. qplot/datahandling/trusted_presentation.py +1065 -0
  44. qplot/datahandling/trusted_snapshot.py +1132 -0
  45. qplot/datahandling/trusted_work_coordinator.py +2157 -0
  46. qplot/datahandling/trusted_work_scheduler.py +1204 -0
  47. qplot/diagnostics.py +241 -0
  48. qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
  49. qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
  50. qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
  51. qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
  52. qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
  53. qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
  54. qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
  55. qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
  56. qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
  57. qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
  58. qplot/testdata.py +1804 -0
  59. qplot/tools/__init__.py +13 -0
  60. qplot/tools/finite_means.py +123 -0
  61. qplot/tools/general.py +139 -0
  62. qplot/tools/heatmap_geometry.py +391 -0
  63. qplot/tools/operation_registry.py +179 -0
  64. qplot/tools/plot_tools.py +650 -0
  65. qplot/tools/sample_statistics.py +111 -0
  66. qplot/tools/worker.py +2838 -0
  67. qplot/windows/__init__.py +9 -0
  68. qplot/windows/_colorbar.py +422 -0
  69. qplot/windows/_commands.py +523 -0
  70. qplot/windows/_config_persistence.py +123 -0
  71. qplot/windows/_database_actions.py +4933 -0
  72. qplot/windows/_dataset_handle.py +221 -0
  73. qplot/windows/_display_precision.py +48 -0
  74. qplot/windows/_dragdrop.py +70 -0
  75. qplot/windows/_export_paths.py +1167 -0
  76. qplot/windows/_help.py +210 -0
  77. qplot/windows/_mixed_appearance.py +303 -0
  78. qplot/windows/_mixed_plots.py +461 -0
  79. qplot/windows/_native_averaging.py +323 -0
  80. qplot/windows/_native_transforms.py +551 -0
  81. qplot/windows/_plot1d_snap.py +612 -0
  82. qplot/windows/_plot1d_traces.py +2236 -0
  83. qplot/windows/_plot2d_colorbar.py +1127 -0
  84. qplot/windows/_plot2d_colorbar_dialog.py +786 -0
  85. qplot/windows/_plot2d_layers.py +2291 -0
  86. qplot/windows/_plot2d_sweeps.py +712 -0
  87. qplot/windows/_plotWin.py +2082 -0
  88. qplot/windows/_plot_actions.py +2703 -0
  89. qplot/windows/_plot_annotation.py +186 -0
  90. qplot/windows/_plot_appearance.py +283 -0
  91. qplot/windows/_plot_axis_scaling.py +1521 -0
  92. qplot/windows/_plot_data_viewbox.py +94 -0
  93. qplot/windows/_plot_drop_axes.py +224 -0
  94. qplot/windows/_plot_export.py +1445 -0
  95. qplot/windows/_plot_feedback.py +112 -0
  96. qplot/windows/_plot_marquee.py +659 -0
  97. qplot/windows/_plot_refresh.py +950 -0
  98. qplot/windows/_plot_source_promotion.py +178 -0
  99. qplot/windows/_plot_state.py +237 -0
  100. qplot/windows/_plot_status.py +115 -0
  101. qplot/windows/_plot_tiling.py +35 -0
  102. qplot/windows/_plot_transform_labels.py +60 -0
  103. qplot/windows/_preferences.py +724 -0
  104. qplot/windows/_refresh_interval.py +172 -0
  105. qplot/windows/_run_controls.py +789 -0
  106. qplot/windows/_shortcuts.py +47 -0
  107. qplot/windows/_subplots/__init__.py +8 -0
  108. qplot/windows/_subplots/subplot1d.py +532 -0
  109. qplot/windows/_subplots/subplot2d.py +919 -0
  110. qplot/windows/_svg_export.py +209 -0
  111. qplot/windows/_trusted_derived_qt.py +1759 -0
  112. qplot/windows/_widgets/__init__.py +23 -0
  113. qplot/windows/_widgets/_run_formatting.py +387 -0
  114. qplot/windows/_widgets/details_tables.py +1119 -0
  115. qplot/windows/_widgets/dropbox.py +290 -0
  116. qplot/windows/_widgets/operations.py +460 -0
  117. qplot/windows/_widgets/preview.py +2997 -0
  118. qplot/windows/_widgets/run_list_items.py +488 -0
  119. qplot/windows/_widgets/toolbar.py +111 -0
  120. qplot/windows/_widgets/treeWidgets.py +2572 -0
  121. qplot/windows/_window_controls.py +414 -0
  122. qplot/windows/main.py +1632 -0
  123. qplot/windows/plot1d.py +313 -0
  124. qplot/windows/plot2d.py +1935 -0
  125. qplotter-1.6.0.dist-info/METADATA +322 -0
  126. qplotter-1.6.0.dist-info/RECORD +130 -0
  127. qplotter-1.6.0.dist-info/WHEEL +5 -0
  128. qplotter-1.6.0.dist-info/entry_points.txt +4 -0
  129. qplotter-1.6.0.dist-info/licenses/LICENSE +21 -0
  130. qplotter-1.6.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1127 @@
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, *, message=None, title="Unsupported heatmap color range"):
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 = message or (
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(title, 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
+ provider = getattr(self, "_colorbar_display_parameter", None)
366
+ param = provider() if callable(provider) else self.__dict__.get("display_param", self.__dict__.get("param"))
367
+ if param is None or not hasattr(bar, "setLabel"):
368
+ return
369
+
370
+ label = getattr(param, "label", "")
371
+ unit = getattr(param, "unit", "")
372
+ unit_scale = 1.0 / scale if scale else 1.0
373
+ scale_text = _axis_scale_power_text(unit_scale)
374
+
375
+ if unit and scale_text:
376
+ unit_text = f"{scale_text} {unit}"
377
+ elif unit:
378
+ unit_text = unit
379
+ else:
380
+ unit_text = scale_text
381
+
382
+ text = f"{label} ({unit_text})" if unit_text else label
383
+ axis = "bottom" if getattr(bar, "horizontal", False) else "right"
384
+ bar.setLabel(axis, text)
385
+ self._set_colorbar_label_direction(bar)
386
+
387
+ def _set_colorbar_label_direction(self, bar):
388
+ if getattr(bar, "horizontal", False):
389
+ return
390
+
391
+ axis = getattr(bar, "axis", None)
392
+ label = getattr(axis, "label", None)
393
+ if axis is None or label is None or not hasattr(label, "setRotation"):
394
+ return
395
+
396
+ label.setRotation(90)
397
+ if not getattr(axis, "_qplot_downward_colorbar_label_installed", False):
398
+ previous_resize_event = getattr(axis, "resizeEvent", None)
399
+
400
+ def resize_event(event=None, previous_handler=previous_resize_event):
401
+ if previous_handler is not None:
402
+ previous_handler(event)
403
+ self._position_downward_colorbar_label(axis)
404
+
405
+ axis.resizeEvent = resize_event
406
+ axis._qplot_downward_colorbar_label_installed = True
407
+
408
+ self._position_downward_colorbar_label(axis)
409
+
410
+ def _position_downward_colorbar_label(self, axis):
411
+ label = getattr(axis, "label", None)
412
+ if label is None or not all(
413
+ hasattr(label, name) for name in ("boundingRect", "setPos")
414
+ ):
415
+ return
416
+
417
+ nudge = 5
418
+ bounds = label.boundingRect()
419
+ size = axis.size()
420
+ position = QtCore.QPointF(
421
+ int(size.width() + nudge),
422
+ int(size.height() / 2 - bounds.width() / 2),
423
+ )
424
+ label.setPos(position)
425
+ axis.picture = None
426
+
427
+ def _install_colorbar_scale_bar_handlers(self, bar):
428
+ """
429
+ Open the color-scale dialog from colorbar interactions.
430
+
431
+ """
432
+ self._install_colorbar_scale_double_click_handler(bar)
433
+ self._suppress_colorbar_right_click_menu(bar)
434
+ self._install_colorbar_level_sync_handlers(bar)
435
+ self._install_colorbar_alt_range_drag_handler(bar)
436
+
437
+ def _install_colorbar_scale_axis_handlers(self, axis):
438
+ """
439
+ Open the color-scale dialog from direct colorbar axis interactions.
440
+
441
+ """
442
+ self._install_colorbar_scale_double_click_handler(axis)
443
+
444
+ def _install_colorbar_scale_double_click_handler(self, item):
445
+ """
446
+ Open the color-scale dialog when a colorbar item is double-clicked.
447
+
448
+ """
449
+ if getattr(item, "_qplot_colorbar_scale_handlers_installed", False):
450
+ return
451
+
452
+ previous_double_click_handler = getattr(item, "mouseDoubleClickEvent", None)
453
+
454
+ def mouse_double_click(event, previous_handler=previous_double_click_handler):
455
+ if event.button() == QtCore.Qt.MouseButton.LeftButton:
456
+ self.open_colorbar_scale_dialog()
457
+ event.accept()
458
+ return
459
+
460
+ if previous_handler is not None:
461
+ previous_handler(event)
462
+
463
+ item.mouseDoubleClickEvent = mouse_double_click
464
+ item._qplot_colorbar_scale_handlers_installed = True
465
+
466
+ def _suppress_colorbar_right_click_menu(self, bar):
467
+ """
468
+ Prevent pyqtgraph's colorbar right-click menu from opening.
469
+
470
+ """
471
+ if getattr(bar, "_qplot_colorbar_right_click_suppressed", False):
472
+ return
473
+
474
+ previous_mouse_click_handler = getattr(bar, "mouseClickEvent", None)
475
+
476
+ def mouse_click(event, previous_handler=previous_mouse_click_handler):
477
+ if event.button() == QtCore.Qt.MouseButton.RightButton:
478
+ event.accept()
479
+ return
480
+
481
+ if previous_handler is not None:
482
+ previous_handler(event)
483
+
484
+ bar.mouseClickEvent = mouse_click
485
+ bar._qplot_colorbar_right_click_suppressed = True
486
+
487
+ def _install_colorbar_level_sync_handlers(self, bar):
488
+ """
489
+ Treat direct colorbar handle adjustment as a manual color range.
490
+
491
+ """
492
+ if getattr(bar, "_qplot_colorbar_level_sync_installed", False):
493
+ return
494
+
495
+ changed = getattr(bar, "sigLevelsChanged", None)
496
+ if changed is not None:
497
+ changed.connect(self._colorbar_interactive_levels_changed)
498
+
499
+ finished = getattr(bar, "sigLevelsChangeFinished", None)
500
+ if finished is not None:
501
+ finished.connect(self._colorbar_interactive_levels_finished)
502
+
503
+ bar._qplot_colorbar_level_sync_installed = True
504
+
505
+ def _install_colorbar_alt_range_drag_handler(self, bar):
506
+ """
507
+ Add outside-bar drag on colorbar regions to widen or narrow levels.
508
+
509
+ """
510
+ region = getattr(bar, "region", None)
511
+ if region is None:
512
+ return
513
+
514
+ if getattr(region, "_qplot_colorbar_alt_range_drag_installed", False):
515
+ return
516
+
517
+ previous_bounding_rect = getattr(region, "boundingRect", None)
518
+ previous_paint = getattr(region, "paint", None)
519
+ previous_mouse_drag_handler = getattr(region, "mouseDragEvent", None)
520
+ previous_hover_handler = getattr(region, "hoverEvent", None)
521
+
522
+ def bounding_rect(previous_handler=previous_bounding_rect):
523
+ rect = self._colorbar_full_region_rect(region)
524
+ if rect is not None:
525
+ return rect
526
+ if previous_handler is not None:
527
+ return previous_handler()
528
+ return QtCore.QRectF()
529
+
530
+ def paint(painter, *args, previous_handler=previous_paint):
531
+ mode = getattr(region, "_qplot_colorbar_drag_visual_area", "inside")
532
+ if mode in ("inside", "outside"):
533
+ rects = self._colorbar_drag_visual_rects(region, mode)
534
+ if rects:
535
+ painter.setBrush(region.currentBrush)
536
+ painter.setPen(pg.mkPen(None))
537
+ for rect in rects:
538
+ painter.drawRect(rect)
539
+ return
540
+
541
+ if previous_handler is not None:
542
+ previous_handler(painter, *args)
543
+
544
+ def mouse_drag(event, previous_handler=previous_mouse_drag_handler):
545
+ active = getattr(region, "_qplot_colorbar_alt_range_drag_source", None)
546
+ source, direction = self._colorbar_alt_range_drag_source_at(
547
+ bar,
548
+ region,
549
+ event.buttonDownPos(),
550
+ )
551
+ if (
552
+ event.button() == QtCore.Qt.MouseButton.LeftButton
553
+ and (
554
+ active is not None
555
+ or (
556
+ source is not None
557
+ and getattr(region, "movable", True)
558
+ )
559
+ )
560
+ ):
561
+ if active is not None:
562
+ source = active
563
+ direction = active[1]
564
+ self._colorbar_alt_range_drag_event(
565
+ bar,
566
+ region,
567
+ event,
568
+ source,
569
+ direction,
570
+ region,
571
+ )
572
+ return
573
+
574
+ if previous_handler is not None:
575
+ previous_handler(event)
576
+
577
+ def hover(event, previous_handler=previous_hover_handler):
578
+ if previous_handler is not None:
579
+ previous_handler(event)
580
+
581
+ if event.isExit():
582
+ region._qplot_colorbar_drag_visual_area = None
583
+ else:
584
+ source, _direction = self._colorbar_alt_range_drag_source_at(
585
+ bar,
586
+ region,
587
+ event.pos(),
588
+ )
589
+ region._qplot_colorbar_drag_visual_area = (
590
+ "outside" if source is not None else "inside"
591
+ )
592
+ region.update()
593
+
594
+ region.boundingRect = bounding_rect
595
+ region.paint = paint
596
+ region.mouseDragEvent = mouse_drag
597
+ region.hoverEvent = hover
598
+ region._qplot_colorbar_drag_visual_area = None
599
+
600
+ region._qplot_colorbar_alt_range_drag_installed = True
601
+
602
+ def _install_colorbar_alt_handle_drag_handler(self, bar, region, line, index):
603
+ """
604
+ Patch one pyqtgraph colorbar handle for symmetric Alt/Option-drag.
605
+
606
+ """
607
+ if index not in (0, 1):
608
+ return
609
+
610
+ if getattr(line, "_qplot_colorbar_alt_range_drag_installed", False):
611
+ return
612
+
613
+ previous_mouse_drag_handler = getattr(line, "mouseDragEvent", None)
614
+ source = ("line", index)
615
+ direction = -1.0 if index == 0 else 1.0
616
+
617
+ def mouse_drag(event, previous_handler=previous_mouse_drag_handler):
618
+ modifiers = event.modifiers()
619
+ active = getattr(region, "_qplot_colorbar_alt_range_drag_source", None)
620
+ if (
621
+ event.button() == QtCore.Qt.MouseButton.LeftButton
622
+ and (
623
+ active == source
624
+ or (
625
+ modifiers & QtCore.Qt.KeyboardModifier.AltModifier
626
+ and getattr(line, "movable", True)
627
+ and getattr(region, "movable", True)
628
+ )
629
+ )
630
+ ):
631
+ self._colorbar_alt_range_drag_event(
632
+ bar,
633
+ region,
634
+ event,
635
+ source,
636
+ direction,
637
+ line,
638
+ )
639
+ return
640
+
641
+ if previous_handler is not None:
642
+ previous_handler(event)
643
+
644
+ line.mouseDragEvent = mouse_drag
645
+ line._qplot_colorbar_alt_range_drag_installed = True
646
+
647
+ def _colorbar_full_region_rect(self, region):
648
+ """
649
+ Return the full colorbar interaction rectangle in region coordinates.
650
+
651
+ """
652
+ try:
653
+ rect = QtCore.QRectF(region.viewRect())
654
+ except (AttributeError, RuntimeError, TypeError):
655
+ return None
656
+
657
+ span = getattr(region, "span", (0, 1))
658
+ orientation = getattr(region, "orientation", None)
659
+ if orientation in ("vertical", 0):
660
+ length = rect.height()
661
+ rect.setBottom(rect.top() + length * span[1])
662
+ rect.setTop(rect.top() + length * span[0])
663
+ else:
664
+ length = rect.width()
665
+ rect.setRight(rect.left() + length * span[1])
666
+ rect.setLeft(rect.left() + length * span[0])
667
+
668
+ return rect.normalized()
669
+
670
+ def _colorbar_drag_visual_rects(self, region, mode):
671
+ """
672
+ Return shaded rectangles for colorbar range-slide or outside-scale zones.
673
+
674
+ """
675
+ rect = self._colorbar_full_region_rect(region)
676
+ if rect is None:
677
+ return ()
678
+
679
+ levels = self._colorbar_region_line_positions(region)
680
+ if levels is None:
681
+ return ()
682
+ low, high = levels
683
+
684
+ orientation = getattr(region, "orientation", None)
685
+ if orientation in ("vertical", 0):
686
+ inside = QtCore.QRectF(rect)
687
+ inside.setLeft(low)
688
+ inside.setRight(high)
689
+ outside_low = QtCore.QRectF(
690
+ rect.left(),
691
+ rect.top(),
692
+ low - rect.left(),
693
+ rect.height(),
694
+ )
695
+ outside_high = QtCore.QRectF(
696
+ high,
697
+ rect.top(),
698
+ rect.right() - high,
699
+ rect.height(),
700
+ )
701
+ else:
702
+ inside = QtCore.QRectF(rect)
703
+ inside.setTop(low)
704
+ inside.setBottom(high)
705
+ outside_low = QtCore.QRectF(
706
+ rect.left(),
707
+ rect.top(),
708
+ rect.width(),
709
+ low - rect.top(),
710
+ )
711
+ outside_high = QtCore.QRectF(
712
+ rect.left(),
713
+ high,
714
+ rect.width(),
715
+ rect.bottom() - high,
716
+ )
717
+
718
+ if mode == "inside":
719
+ return (inside.normalized(),)
720
+
721
+ return tuple(
722
+ outside.normalized()
723
+ for outside in (outside_low, outside_high)
724
+ if outside.width() > 0 and outside.height() > 0
725
+ )
726
+
727
+ def _colorbar_region_line_positions(self, region):
728
+ """
729
+ Return sorted colorbar handle positions in region coordinates.
730
+
731
+ """
732
+ lines = getattr(region, "lines", None)
733
+ if lines is None or len(lines) != 2:
734
+ return None
735
+
736
+ positions = []
737
+ for line in lines:
738
+ value = getattr(line, "value", None)
739
+ if callable(value):
740
+ positions.append(float(value()))
741
+ else:
742
+ position = getattr(line, "position", None)
743
+ if position is None:
744
+ return None
745
+ positions.append(float(position))
746
+
747
+ if not all(np.isfinite(position) for position in positions):
748
+ return None
749
+
750
+ return tuple(sorted(positions))
751
+
752
+ def _colorbar_alt_range_drag_source_at(self, bar, region, position):
753
+ """
754
+ Return the outside-bar range-scale drag source and direction at a point.
755
+
756
+ """
757
+ levels = self._colorbar_region_line_positions(region)
758
+ if levels is None:
759
+ return None, 0.0
760
+
761
+ axis_position = self._colorbar_alt_range_drag_axis_position(
762
+ bar,
763
+ position,
764
+ region,
765
+ )
766
+ low, high = levels
767
+ if axis_position < low:
768
+ return ("bar", -1.0), -1.0
769
+ if axis_position > high:
770
+ return ("bar", 1.0), 1.0
771
+ return None, 0.0
772
+
773
+ def _colorbar_alt_range_drag_event(
774
+ self,
775
+ bar,
776
+ region,
777
+ event,
778
+ source,
779
+ direction,
780
+ event_item,
781
+ ):
782
+ """
783
+ Expand or contract the color range around its midpoint during Alt-drag.
784
+
785
+ """
786
+ event.accept()
787
+
788
+ if event.isStart():
789
+ levels = self._colorbar_levels_from_bar(bar)
790
+ if levels is None:
791
+ region._qplot_colorbar_alt_range_drag_active = False
792
+ region._qplot_colorbar_alt_range_drag_source = None
793
+ return
794
+
795
+ region._qplot_colorbar_alt_range_drag_active = True
796
+ region._qplot_colorbar_alt_range_drag_source = source
797
+ region._qplot_colorbar_alt_range_drag_levels = levels
798
+ region._qplot_colorbar_alt_range_drag_start_pos = (
799
+ self._colorbar_alt_range_drag_axis_position(
800
+ bar,
801
+ event.buttonDownPos(),
802
+ event_item,
803
+ )
804
+ )
805
+ region._qplot_colorbar_drag_visual_area = "outside"
806
+
807
+ if (
808
+ not getattr(region, "_qplot_colorbar_alt_range_drag_active", False)
809
+ or getattr(region, "_qplot_colorbar_alt_range_drag_source", None) != source
810
+ ):
811
+ return
812
+
813
+ start_levels = region._qplot_colorbar_alt_range_drag_levels
814
+ start_pos = region._qplot_colorbar_alt_range_drag_start_pos
815
+ current_pos = self._colorbar_alt_range_drag_axis_position(
816
+ bar,
817
+ event.pos(),
818
+ event_item,
819
+ )
820
+ axis_delta = (current_pos - start_pos) * direction
821
+ self._set_colorbar_alt_range_drag_visual(region, axis_delta)
822
+ levels = self._colorbar_alt_range_drag_levels(
823
+ bar,
824
+ start_levels,
825
+ axis_delta,
826
+ )
827
+ if levels is not None:
828
+ bar.setLevels(levels)
829
+ self._colorbar_interactive_levels_changed(bar)
830
+
831
+ if event.isFinish():
832
+ region._qplot_colorbar_alt_range_drag_active = False
833
+ region._qplot_colorbar_alt_range_drag_source = None
834
+ region._qplot_colorbar_drag_visual_area = None
835
+ self._set_colorbar_alt_range_drag_visual(region, 0.0)
836
+ self._colorbar_interactive_levels_finished(bar)
837
+
838
+ def _colorbar_alt_range_drag_axis_position(self, bar, position, item=None):
839
+ """
840
+ Return the colorbar-axis coordinate from a region mouse position.
841
+
842
+ """
843
+ if item is not None:
844
+ try:
845
+ position = item.mapToParent(position)
846
+ except (AttributeError, RuntimeError):
847
+ pass
848
+
849
+ if getattr(bar, "horizontal", False):
850
+ return position.x()
851
+
852
+ return position.y()
853
+
854
+ def _set_colorbar_alt_range_drag_visual(self, region, axis_delta):
855
+ """
856
+ Move pyqtgraph's range markers apart or together during Alt-drag.
857
+
858
+ """
859
+ lines = getattr(region, "lines", None)
860
+ if lines is None or len(lines) != 2:
861
+ return
862
+
863
+ axis_delta = min(max(axis_delta, -63.0), 63.0)
864
+ previous_block = getattr(region, "blockLineSignal", False)
865
+ region.blockLineSignal = True
866
+ try:
867
+ lines[0].setPos(63.0 - axis_delta)
868
+ lines[1].setPos(191.0 + axis_delta)
869
+ region.prepareGeometryChange()
870
+ finally:
871
+ region.blockLineSignal = previous_block
872
+
873
+ def _colorbar_alt_range_drag_levels(self, bar, start_levels, axis_delta):
874
+ """
875
+ Return levels widened or narrowed symmetrically by an Alt-drag delta.
876
+
877
+ """
878
+ low, high = start_levels
879
+ span = high - low
880
+ if not np.isfinite(span) or span <= 0:
881
+ return None
882
+
883
+ rounding = getattr(bar, "rounding", 0.0)
884
+ try:
885
+ rounding = float(rounding)
886
+ except (TypeError, ValueError):
887
+ rounding = 0.0
888
+ if not np.isfinite(rounding) or rounding <= 0:
889
+ rounding = 0.0
890
+
891
+ drag_units = axis_delta / 64.0
892
+ signed_drag = math.copysign(drag_units * drag_units, drag_units)
893
+ scale = span + 2 * rounding
894
+ center = (low + high) / 2.0
895
+ half_span = span / 2.0 + scale * signed_drag
896
+ min_span = rounding if rounding > 0 else max(abs(span) * 1e-9, 1e-300)
897
+ half_span = max(half_span, min_span / 2.0)
898
+
899
+ lo_lim = getattr(bar, "lo_lim", None)
900
+ hi_lim = getattr(bar, "hi_lim", None)
901
+ if lo_lim is not None and np.isfinite(lo_lim):
902
+ half_span = min(half_span, center - lo_lim)
903
+ if hi_lim is not None and np.isfinite(hi_lim):
904
+ half_span = min(half_span, hi_lim - center)
905
+ if half_span <= 0:
906
+ return None
907
+
908
+ low = center - half_span
909
+ high = center + half_span
910
+
911
+ if rounding > 0:
912
+ low = rounding * round(low / rounding)
913
+ high = rounding * round(high / rounding)
914
+
915
+ if lo_lim is not None and np.isfinite(lo_lim):
916
+ low = max(low, lo_lim)
917
+ if hi_lim is not None and np.isfinite(hi_lim):
918
+ high = min(high, hi_lim)
919
+ if not np.isfinite(low) or not np.isfinite(high) or low >= high:
920
+ return None
921
+
922
+ return float(low), float(high)
923
+
924
+ def _colorbar_levels_from_bar(self, bar):
925
+ """
926
+ Return validated numeric levels from a pyqtgraph colorbar.
927
+
928
+ """
929
+ try:
930
+ low, high = bar.levels()
931
+ except (AttributeError, TypeError, ValueError):
932
+ return None
933
+
934
+ try:
935
+ low = float(low)
936
+ high = float(high)
937
+ except (TypeError, ValueError):
938
+ return None
939
+
940
+ if not np.isfinite(low) or not np.isfinite(high) or low >= high:
941
+ return None
942
+
943
+ return low, high
944
+
945
+ def _colorbar_interactive_levels_changed(self, bar):
946
+ """
947
+ Mirror interactively adjusted levels into the scale dialog.
948
+
949
+ """
950
+ levels = self._colorbar_levels_from_bar(bar)
951
+ if levels is None:
952
+ return
953
+
954
+ self._sync_colorbar_axis_scaling()
955
+ self._sync_colorbar_level_fields(*levels)
956
+
957
+ def _colorbar_interactive_levels_finished(self, bar):
958
+ """
959
+ Persist interactively adjusted colorbar levels as manual scaling.
960
+
961
+ """
962
+ levels = self._colorbar_levels_from_bar(bar)
963
+ if levels is None:
964
+ return
965
+
966
+ self._colorbar_manual_levels = levels
967
+ if "relevel_refresh" in self.__dict__:
968
+ self.relevel_refresh.setChecked(False)
969
+ if "colorbar_manual_radio" in self.__dict__:
970
+ self.colorbar_manual_radio.setChecked(True)
971
+ if "colorbar_auto_radio" in self.__dict__:
972
+ self.colorbar_auto_radio.setChecked(False)
973
+ self._sync_colorbar_level_fields(*levels)
974
+
975
+ def _current_colorbar_levels(self):
976
+ """
977
+ Return the currently displayed colorbar levels for menu synchronisation.
978
+
979
+ """
980
+ bar = self.__dict__.get("bar")
981
+ if bar is not None:
982
+ return bar.levels()
983
+
984
+ manual_levels = getattr(self, "_colorbar_manual_levels", None)
985
+ if manual_levels is not None:
986
+ return manual_levels
987
+
988
+ return self._data_colorbar_levels()
989
+
990
+ def _current_colorbar_colormap_name(self):
991
+ """
992
+ Return the selected colorbar color map preference.
993
+
994
+ """
995
+ available_colormaps = self._available_colorbar_colormaps()
996
+ name = getattr(self, "_colorbar_colormap_name", None)
997
+ if name in available_colormaps:
998
+ return name
999
+
1000
+ name = _config_value(
1001
+ self.__dict__.get("config"),
1002
+ "user_preference.bar_colour",
1003
+ "viridis",
1004
+ )
1005
+
1006
+ if name not in available_colormaps:
1007
+ name = self._fallback_colorbar_colormap_name()
1008
+
1009
+ self._colorbar_colormap_name = name
1010
+ return name
1011
+
1012
+ def _available_colorbar_colormaps(self):
1013
+ """
1014
+ Return color maps after applying persistent user filters.
1015
+
1016
+ """
1017
+ config_obj = self.__dict__.get("config")
1018
+ include_cet = _config_value(
1019
+ config_obj,
1020
+ "user_preference.bar_colour_include_cet",
1021
+ True,
1022
+ )
1023
+ include_matplotlib = _config_value(
1024
+ config_obj,
1025
+ "user_preference.bar_colour_include_matplotlib",
1026
+ True,
1027
+ )
1028
+ include_local = _config_value(
1029
+ config_obj,
1030
+ "user_preference.bar_colour_include_local",
1031
+ True,
1032
+ )
1033
+ include_custom = _config_value(
1034
+ config_obj,
1035
+ "user_preference.bar_colour_include_custom",
1036
+ True,
1037
+ )
1038
+ include_cet_subtypes = {
1039
+ subtype: _config_value(
1040
+ config_obj,
1041
+ _colorbar_subtype_config_key("cet", subtype),
1042
+ True,
1043
+ )
1044
+ for subtype, _label in _CET_COLORBAR_SUBTYPES
1045
+ }
1046
+ include_matplotlib_subtypes = {
1047
+ subtype: _config_value(
1048
+ config_obj,
1049
+ _colorbar_subtype_config_key("matplotlib", subtype),
1050
+ True,
1051
+ )
1052
+ for subtype, _label in _MATPLOTLIB_COLORBAR_SUBTYPES
1053
+ }
1054
+ excluded_names = set(_config_value(
1055
+ config_obj,
1056
+ "user_preference.bar_colour_excluded",
1057
+ [],
1058
+ ))
1059
+ excluded_prefixes = tuple(_config_value(
1060
+ config_obj,
1061
+ "user_preference.bar_colour_excluded_prefixes",
1062
+ [],
1063
+ ))
1064
+
1065
+ available = []
1066
+ for name in _COLORBAR_COLORMAPS:
1067
+ if name in _DEFAULT_HIDDEN_COLORBAR_NAMES:
1068
+ continue
1069
+ if name.startswith(_DEFAULT_HIDDEN_COLORBAR_PREFIXES):
1070
+ continue
1071
+ if name.endswith(_DEFAULT_HIDDEN_COLORBAR_SUFFIXES):
1072
+ continue
1073
+ if name in excluded_names:
1074
+ continue
1075
+ if excluded_prefixes and name.startswith(excluded_prefixes):
1076
+ continue
1077
+
1078
+ group = _colorbar_colormap_group(name)
1079
+ if group == "cet" and not include_cet:
1080
+ continue
1081
+ if (
1082
+ group == "cet"
1083
+ and not include_cet_subtypes[_cet_colorbar_colormap_subtype(name)]
1084
+ ):
1085
+ continue
1086
+ if group == "matplotlib" and not include_matplotlib:
1087
+ continue
1088
+ if (
1089
+ group == "matplotlib"
1090
+ and not include_matplotlib_subtypes[
1091
+ _matplotlib_colorbar_colormap_subtype(name)
1092
+ ]
1093
+ ):
1094
+ continue
1095
+ if group == "pyqtgraph" and not include_local:
1096
+ continue
1097
+ if group == "custom" and not include_custom:
1098
+ continue
1099
+
1100
+ available.append(name)
1101
+
1102
+ return tuple(available)
1103
+
1104
+ def _fallback_colorbar_colormap_name(self):
1105
+ """
1106
+ Choose a usable map when the saved preference is filtered out.
1107
+
1108
+ """
1109
+ available_colormaps = self._available_colorbar_colormaps()
1110
+ if "viridis" in available_colormaps:
1111
+ return "viridis"
1112
+ if not available_colormaps:
1113
+ return "viridis"
1114
+ return available_colormaps[0]
1115
+
1116
+ def _colorbar_colormap(self, name=None):
1117
+ """
1118
+ Return a pyqtgraph color map or built-in map name for the colorbar.
1119
+
1120
+ """
1121
+ if name is None:
1122
+ name = self._current_colorbar_colormap_name()
1123
+
1124
+ return _colorbar_colormap_for_name(name)
1125
+
1126
+ ###############################################################################
1127
+ # Subplot control