pyqttoolkit 0.9.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 (133) hide show
  1. pyqttoolkit/__init__.py +20 -0
  2. pyqttoolkit/_version.py +21 -0
  3. pyqttoolkit/application/__init__.py +18 -0
  4. pyqttoolkit/application/application.py +86 -0
  5. pyqttoolkit/application/garbage_collector.py +65 -0
  6. pyqttoolkit/colors/__init__.py +84 -0
  7. pyqttoolkit/config/__init__.py +20 -0
  8. pyqttoolkit/config/base.py +22 -0
  9. pyqttoolkit/config/composite.py +31 -0
  10. pyqttoolkit/config/default.py +27 -0
  11. pyqttoolkit/config/environment.py +35 -0
  12. pyqttoolkit/config/multi_json_file.py +93 -0
  13. pyqttoolkit/config/with_signal.py +30 -0
  14. pyqttoolkit/config/with_value_changed.py +31 -0
  15. pyqttoolkit/data/__init__.py +21 -0
  16. pyqttoolkit/data/bits.py +23 -0
  17. pyqttoolkit/data/cache.py +64 -0
  18. pyqttoolkit/data/interval.py +26 -0
  19. pyqttoolkit/data/message.py +29 -0
  20. pyqttoolkit/data/names.py +24 -0
  21. pyqttoolkit/datetime/__init__.py +16 -0
  22. pyqttoolkit/datetime/datetime.py +51 -0
  23. pyqttoolkit/datetime/qdatetime.py +53 -0
  24. pyqttoolkit/dependencies/__init__.py +56 -0
  25. pyqttoolkit/errors/__init__.py +19 -0
  26. pyqttoolkit/exceptions/__init__.py +18 -0
  27. pyqttoolkit/filesystem/__init__.py +24 -0
  28. pyqttoolkit/logs/__init__.py +140 -0
  29. pyqttoolkit/metadata/__init__.py +23 -0
  30. pyqttoolkit/metadata/dataframe_metadata_manager.py +105 -0
  31. pyqttoolkit/metadata/dict_metadata_manager.py +63 -0
  32. pyqttoolkit/metadata/id_generator.py +20 -0
  33. pyqttoolkit/metadata/metadata_manager_base.py +54 -0
  34. pyqttoolkit/models/__init__.py +26 -0
  35. pyqttoolkit/models/code_text.py +43 -0
  36. pyqttoolkit/models/dataframetable.py +135 -0
  37. pyqttoolkit/models/dictpropertytree.py +155 -0
  38. pyqttoolkit/models/dicttable.py +140 -0
  39. pyqttoolkit/models/json_text.py +52 -0
  40. pyqttoolkit/models/list_model.py +52 -0
  41. pyqttoolkit/models/module.py +26 -0
  42. pyqttoolkit/models/roles.py +23 -0
  43. pyqttoolkit/models/series_data.py +68 -0
  44. pyqttoolkit/models/span.py +36 -0
  45. pyqttoolkit/models/validation.py +27 -0
  46. pyqttoolkit/modules/__init__.py +18 -0
  47. pyqttoolkit/modules/events/__init__.py +19 -0
  48. pyqttoolkit/modules/events/add_signal.py +31 -0
  49. pyqttoolkit/modules/events/module_opening.py +21 -0
  50. pyqttoolkit/modules/events/rejectable.py +29 -0
  51. pyqttoolkit/modules/launcher_config.py +55 -0
  52. pyqttoolkit/modules/module.py +111 -0
  53. pyqttoolkit/properties/__init__.py +18 -0
  54. pyqttoolkit/properties/auto.py +221 -0
  55. pyqttoolkit/properties/throttled.py +77 -0
  56. pyqttoolkit/scripting/__init__.py +19 -0
  57. pyqttoolkit/scripting/script_context.py +36 -0
  58. pyqttoolkit/scripting/script_runner.py +132 -0
  59. pyqttoolkit/scripting/script_template.py +42 -0
  60. pyqttoolkit/services/__init__.py +32 -0
  61. pyqttoolkit/services/autosave.py +41 -0
  62. pyqttoolkit/services/email.py +25 -0
  63. pyqttoolkit/services/event_registry.py +52 -0
  64. pyqttoolkit/services/file_dialog.py +71 -0
  65. pyqttoolkit/services/hotkey_manager.py +137 -0
  66. pyqttoolkit/services/link_manager.py +135 -0
  67. pyqttoolkit/services/message_board.py +90 -0
  68. pyqttoolkit/services/module_service.py +154 -0
  69. pyqttoolkit/services/object_converter.py +45 -0
  70. pyqttoolkit/services/project_updater.py +149 -0
  71. pyqttoolkit/services/task_runner.py +281 -0
  72. pyqttoolkit/services/theme_manager.py +46 -0
  73. pyqttoolkit/services/tool_window_service.py +46 -0
  74. pyqttoolkit/viewmanagers/__init__.py +17 -0
  75. pyqttoolkit/viewmanagers/tool.py +23 -0
  76. pyqttoolkit/views/__init__.py +50 -0
  77. pyqttoolkit/views/bulk_value_selector.py +112 -0
  78. pyqttoolkit/views/check_box.py +36 -0
  79. pyqttoolkit/views/code/__init__.py +18 -0
  80. pyqttoolkit/views/code/code_edit.py +268 -0
  81. pyqttoolkit/views/code/python_syntax.py +226 -0
  82. pyqttoolkit/views/combo_box.py +101 -0
  83. pyqttoolkit/views/count_selector.py +59 -0
  84. pyqttoolkit/views/datetime.py +111 -0
  85. pyqttoolkit/views/datetime_range_selector.py +231 -0
  86. pyqttoolkit/views/default_text_line_edit.py +50 -0
  87. pyqttoolkit/views/delegates.py +117 -0
  88. pyqttoolkit/views/dtype_edit.py +117 -0
  89. pyqttoolkit/views/editable_combo_box.py +45 -0
  90. pyqttoolkit/views/file_selector.py +51 -0
  91. pyqttoolkit/views/icon.py +67 -0
  92. pyqttoolkit/views/icon_button.py +129 -0
  93. pyqttoolkit/views/json_edit.py +21 -0
  94. pyqttoolkit/views/labelled.py +29 -0
  95. pyqttoolkit/views/layout/__init__.py +21 -0
  96. pyqttoolkit/views/layout/collabsible.py +59 -0
  97. pyqttoolkit/views/layout/group_box.py +48 -0
  98. pyqttoolkit/views/layout/group_panel.py +188 -0
  99. pyqttoolkit/views/layout/lines.py +30 -0
  100. pyqttoolkit/views/layout/link.py +38 -0
  101. pyqttoolkit/views/line_edit.py +61 -0
  102. pyqttoolkit/views/linkable.py +49 -0
  103. pyqttoolkit/views/list_view.py +43 -0
  104. pyqttoolkit/views/main_window.py +91 -0
  105. pyqttoolkit/views/metadata_selector.py +84 -0
  106. pyqttoolkit/views/modulewindow.py +81 -0
  107. pyqttoolkit/views/plot/__init__.py +26 -0
  108. pyqttoolkit/views/plot/legend_control.py +121 -0
  109. pyqttoolkit/views/plot/matplotlib/__init__.py +23 -0
  110. pyqttoolkit/views/plot/matplotlib/base.py +1056 -0
  111. pyqttoolkit/views/plot/matplotlib/colorbar.py +75 -0
  112. pyqttoolkit/views/plot/matplotlib/colormap.py +48 -0
  113. pyqttoolkit/views/plot/matplotlib/font.py +55 -0
  114. pyqttoolkit/views/plot/plot_options.py +211 -0
  115. pyqttoolkit/views/plot/plot_toolbar_options.py +133 -0
  116. pyqttoolkit/views/plot/size.py +41 -0
  117. pyqttoolkit/views/plot/tool_type.py +26 -0
  118. pyqttoolkit/views/plot/toolbar.py +100 -0
  119. pyqttoolkit/views/popout.py +41 -0
  120. pyqttoolkit/views/popup.py +41 -0
  121. pyqttoolkit/views/property_tree.py +26 -0
  122. pyqttoolkit/views/shrunk_push_button.py +34 -0
  123. pyqttoolkit/views/steppable_combo_box.py +67 -0
  124. pyqttoolkit/views/styleable.py +37 -0
  125. pyqttoolkit/views/table.py +146 -0
  126. pyqttoolkit/views/text_edit.py +31 -0
  127. pyqttoolkit/views/tool_window.py +35 -0
  128. pyqttoolkit-0.9.1.dist-info/METADATA +69 -0
  129. pyqttoolkit-0.9.1.dist-info/RECORD +133 -0
  130. pyqttoolkit-0.9.1.dist-info/WHEEL +5 -0
  131. pyqttoolkit-0.9.1.dist-info/licenses/LICENSE.txt +674 -0
  132. pyqttoolkit-0.9.1.dist-info/licenses/LICENSE_SHORT +16 -0
  133. pyqttoolkit-0.9.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1056 @@
1
+ # pyqttoolkit
2
+ # Copyright (C) 2018-2019, Simmovation Ltd.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software Foundation,
16
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+ from io import BytesIO
18
+ import math
19
+ import numpy as np
20
+ from datetime import datetime
21
+
22
+ from PyQt5.QtCore import pyqtSignal, QSize, QTimer
23
+ from PyQt5.QtWidgets import QVBoxLayout, QWidget, QMenu, QAction, QApplication
24
+ from PyQt5.QtGui import QKeySequence, QImage
25
+
26
+ import matplotlib
27
+ import matplotlib.dates as mdates
28
+ from matplotlib.legend import DraggableLegend
29
+ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
30
+ from matplotlib.figure import Figure
31
+ from matplotlib.widgets import RectangleSelector, SpanSelector
32
+ from mpl_toolkits.axes_grid1 import Size, Divider
33
+ from mpl_toolkits.axes_grid1.mpl_axes import Axes
34
+ from matplotlib.projections.polar import PolarAxes
35
+
36
+ import pandas as pd
37
+
38
+ from pyqttoolkit.properties import AutoProperty, connect_all, bind
39
+ from pyqttoolkit.models import SpanModel
40
+ from pyqttoolkit.views import TableView
41
+ from pyqttoolkit.colors import interpolate_rgb
42
+ from contextlib import contextmanager
43
+
44
+ from ..tool_type import ToolType
45
+ from .font import MatPlotLibFont
46
+
47
+ def _to_finite(value):
48
+ if isinstance(value, pd.Timestamp):
49
+ value = value.to_pydatetime()
50
+ if value is None:
51
+ value = np.nan
52
+ return value if isinstance(value, datetime) or np.isfinite(value) else 0
53
+
54
+ def _safe_limits(lower, upper):
55
+ lower = _to_finite(lower)
56
+ upper = _to_finite(upper)
57
+ return min(lower, upper), max(lower, upper)
58
+
59
+ def _cartesian_polar_selector(base_class):
60
+ class _CartesianPolarSelector(base_class):
61
+ def __init__(self, *args, **kwargs):
62
+ super().__init__(*args, **kwargs)
63
+ self._polar_projection = isinstance(self.ax, PolarAxes)
64
+
65
+ def _get_data(self, event):
66
+ if self._polar_projection:
67
+ xdata = event.xdata + 2 * np.pi if event.xdata < 0 else event.xdata
68
+ ydata = np.clip(event.ydata, *self.ax.get_ybound())
69
+ return xdata, ydata
70
+ else:
71
+ return super()._get_data(event)
72
+
73
+ return _CartesianPolarSelector
74
+
75
+ class _SpanSelector(_cartesian_polar_selector(SpanSelector)):
76
+ def __init__(self, *args, **kwargs):
77
+ super().__init__(*args, **kwargs)
78
+ self._select_none_handler = None
79
+ self._min_span = 1.0
80
+
81
+ @property
82
+ def rect(self):
83
+ return self._selection_artist
84
+
85
+ @property
86
+ def handles(self):
87
+ return self._edge_handles
88
+
89
+ def _release(self, event):
90
+ SpanSelector._release(self, event)
91
+ if self.rect.get_width() < self._min_span:
92
+ self.rect.set_visible(False)
93
+ if self._select_none_handler is not None:
94
+ self._select_none_handler()
95
+
96
+ def set_on_select_none(self, handler):
97
+ self._select_none_handler = handler
98
+
99
+ def set_min_span(self, min_span):
100
+ self._min_span = min_span
101
+
102
+ class _RectangleSelector(_cartesian_polar_selector(RectangleSelector)):
103
+ def __init__(self, *args, **kwargs):
104
+ super().__init__(*args, **kwargs)
105
+
106
+ def _press(self, event):
107
+ if not self._interactive:
108
+ x = event.xdata
109
+ y = event.ydata
110
+ self.extents = x, x, y, y
111
+ RectangleSelector._press(self, event)
112
+
113
+ class MatPlotLibBase(QWidget):
114
+ def __init__(self,
115
+ parent, file_dialog_service,
116
+ h_margin=(0.8, 0.1), v_margin=(0.5, 0.15),
117
+ h_axes=None, v_axes=None,
118
+ nx_default=1, ny_default=1
119
+ ):
120
+ QWidget.__init__(self, parent)
121
+ self._file_dialog_service = file_dialog_service
122
+ self._figure = Figure()
123
+ self._canvas = FigureCanvas(self._figure)
124
+ h = [Size.Fixed(h_margin[0]), *(h_axes or [Size.Scaled(1.0)]), Size.Fixed(h_margin[1])]
125
+ v = [Size.Fixed(v_margin[0]), *(v_axes or [Size.Scaled(1.0)]), Size.Fixed(v_margin[1])]
126
+ self._divider = Divider(self._figure, (0.0, 0.0, 1.0, 1.0), h, v, aspect=False)
127
+ self._axes = Axes(self._figure, self._divider.get_position())
128
+ self._axes.set_axes_locator(self._divider.new_locator(nx=nx_default, ny=ny_default))
129
+ self._axes.set_zorder(2)
130
+ self._axes.patch.set_visible(False)
131
+ for spine in ['top', 'right']:
132
+ self._axes.spines[spine].set_visible(False)
133
+ self._figure.add_axes(self._axes)
134
+
135
+ self._canvas.setParent(self)
136
+
137
+ self._layout = QVBoxLayout(self)
138
+ self._layout.setContentsMargins(0, 0, 0, 0)
139
+ self._layout.addWidget(self._canvas)
140
+ self.setLayout(self._layout)
141
+
142
+ self._figure.canvas.mpl_connect('scroll_event', self._on_scroll)
143
+ self._xy_extents = None
144
+ self._background_cache = None
145
+ self._decoration_artists = []
146
+ self._is_panning = False
147
+
148
+ self._zoom_selector = _RectangleSelector(self._axes, self._zoom_selected, interactive=True)
149
+ self._zoom_selector.set_active(False)
150
+ self._x_extent_padding = 0.01
151
+ self._y_extent_padding = 0.01
152
+ self._axes.ticklabel_format(style='sci', axis='x', scilimits=(-4, 4))
153
+ self._axes.ticklabel_format(style='sci', axis='y', scilimits=(-4, 4))
154
+ self._active_tools = {}
155
+ self._span = _SpanSelector(
156
+ self._axes, self._handle_span_select, 'horizontal',
157
+ props=dict(alpha=0.2, facecolor='red', edgecolor='k'),
158
+ interactive=True, drag_from_anywhere=True, handle_props=dict(alpha=0.0),
159
+ ignore_event_outside=True
160
+ )
161
+ self._span.set_on_select_none(self._handle_span_select_none)
162
+ self.span = self._previous_span = None
163
+ self._span_center_mouse_event = None
164
+ self._span_left_mouse_event = None
165
+ self._span_right_mouse_event = None
166
+ self._figure.canvas.mpl_connect('button_press_event', self._handle_press)
167
+ self._figure.canvas.mpl_connect('motion_notify_event', self._handle_move)
168
+ self._figure.canvas.mpl_connect('button_release_event', self._handle_release)
169
+ self._figure.canvas.mpl_connect('resize_event', self._handle_resize)
170
+ self.activateTool(ToolType.span, self.isActiveDefault(ToolType.span))
171
+ self._pan_event = None
172
+ self._pending_draw = None
173
+ self._pending_artists_draw = None
174
+ self._other_draw_events = []
175
+ self._draw_timer = QTimer(self)
176
+ self._draw_timer.timeout.connect(self._do_draw_events)
177
+ self._draw_timer.start(20)
178
+ self._zoom_skew = None
179
+
180
+ self._menu = QMenu(self)
181
+ self._copy_image_action = QAction(self.tr('Copy To Clipboard'), self)
182
+ self._copy_image_action.triggered.connect(self.copyToClipboard)
183
+ self._copy_image_action.setShortcuts(QKeySequence.Copy)
184
+ self._save_image_action = QAction(self.tr('Save As Image'), self)
185
+ self._save_image_action.triggered.connect(self.saveAsImage)
186
+ self._show_table_action = QAction(self.tr('Show Table'), self)
187
+ self._show_table_action.triggered.connect(self.showTable)
188
+ self._menu.addAction(self._copy_image_action)
189
+ self._menu.addAction(self._save_image_action)
190
+ self._menu.addAction(self._show_table_action)
191
+ self.addAction(self._copy_image_action)
192
+
193
+ self._table_view = None
194
+ self._single_axis_zoom_enabled = True
195
+ self._cached_label_width_height = None
196
+
197
+ if hasattr(type(self), 'dataChanged'):
198
+ self.dataChanged.connect(self._on_data_changed)
199
+
200
+ self._options_view = None
201
+ self._secondary_axes = self._secondary_y_extent = self._secondary_x_extent = None
202
+ self._legend = None
203
+ self._draggable_legend = None
204
+ self._setting_axis_limits = False
205
+
206
+ self.hasHiddenSeries = False
207
+ self._axes.grid(False)
208
+
209
+ enabledToolsChanged = pyqtSignal()
210
+ spanChanged = pyqtSignal(SpanModel)
211
+ hasHiddenSeriesChanged = pyqtSignal(bool)
212
+ yTickLabelsWidthAdjusted = pyqtSignal()
213
+
214
+ span = AutoProperty(SpanModel)
215
+ hasHiddenSeries = AutoProperty(bool)
216
+
217
+ @property
218
+ def divider(self):
219
+ return self._divider
220
+
221
+ def setOptionsView(self, options_view):
222
+ self._options_view = options_view
223
+ self._options_view.setSecondaryYLimitsEnabled(self._secondary_y_enabled())
224
+ self._options_view.setSecondaryXLimitsEnabled(self._secondary_x_enabled())
225
+
226
+ self._options_view.showGridLinesChanged.connect(self._update_grid_lines)
227
+ self._options_view.xAxisLowerLimitChanged.connect(self._handle_options_view_limit_changed(x_min_changed=True))
228
+ self._options_view.xAxisUpperLimitChanged.connect(self._handle_options_view_limit_changed(x_max_changed=True))
229
+ self._options_view.yAxisLowerLimitChanged.connect(self._handle_options_view_limit_changed(y_min_changed=True))
230
+ self._options_view.yAxisUpperLimitChanged.connect(self._handle_options_view_limit_changed(y_max_changed=True))
231
+ self._options_view.xAxisLimitsChanged.connect(self._handle_options_view_limit_changed(x_min_changed=True, x_max_changed=True))
232
+ self._options_view.yAxisLimitsChanged.connect(self._handle_options_view_limit_changed(y_min_changed=True, y_max_changed=True))
233
+
234
+ self._options_view.secondaryXAxisLowerLimitChanged.connect(self._handle_options_view_secondary_limit_changed(x_min_changed=True))
235
+ self._options_view.secondaryXAxisUpperLimitChanged.connect(self._handle_options_view_secondary_limit_changed(x_max_changed=True))
236
+ self._options_view.secondaryYAxisLowerLimitChanged.connect(self._handle_options_view_secondary_limit_changed(y_min_changed=True))
237
+ self._options_view.secondaryYAxisUpperLimitChanged.connect(self._handle_options_view_secondary_limit_changed(y_max_changed=True))
238
+ self._options_view.secondaryXAxisLimitsChanged.connect(self._handle_options_view_secondary_limit_changed(x_min_changed=True, x_max_changed=True))
239
+ self._options_view.secondaryYAxisLimitsChanged.connect(self._handle_options_view_secondary_limit_changed(y_min_changed=True, y_max_changed=True))
240
+
241
+ def setLegendControl(self, legend_control):
242
+ self._legend_control = legend_control
243
+ self._legend_control.seriesUpdated.connect(self._legend_series_updated)
244
+ self._legend_control.showLegendChanged.connect(self._show_legend)
245
+ self._legend_control.seriesNameChanged.connect(self._handle_series_name_changed)
246
+ self._legend_control.showSeriesChanged.connect(self._handle_show_series_changed)
247
+ bind(self._legend_control, self, 'hasHiddenSeries', two_way=False)
248
+
249
+ def _legend_series_updated(self):
250
+ if self._legend is not None:
251
+ self._show_legend(self._legend_control.showLegend)
252
+
253
+ def _show_legend(self, show):
254
+ if self._legend and not show:
255
+ self._legend.remove()
256
+ self._legend = None
257
+ self.draw()
258
+ elif show:
259
+ if self._legend:
260
+ self._legend.remove()
261
+ show_series = self._legend_control.showSeries
262
+ handles = [h for h, s in zip(self._legend_control.seriesHandles, show_series) if s]
263
+ names = [n for n, s in zip(self._legend_control.seriesNames, show_series) if s]
264
+ axes = (
265
+ self._secondary_axes
266
+ if self._secondary_axes
267
+ and self._secondary_axes.get_visible()
268
+ and self._secondary_axes.get_zorder() > self._axes.get_zorder()
269
+ else self._axes
270
+ )
271
+ self._legend = self._create_legend(axes, handles, names, markerscale=self._get_legend_markerscale())
272
+ if self._get_legend_text_color() is not None:
273
+ for text in self._legend.texts:
274
+ text.set_color(self._get_legend_text_color())
275
+ self._draggable_legend = DraggableLegend(self._legend)
276
+ self.draw()
277
+
278
+ def _get_legend_markerscale(self):
279
+ return 5
280
+
281
+ def _create_legend(self, axes, handles, names, **kwargs):
282
+ return axes.legend(handles, names, **kwargs)
283
+
284
+ def _get_legend_text_color(self):
285
+ return None
286
+
287
+ def _handle_series_name_changed(self, index, series_name):
288
+ if self._legend is not None and index < len(self._legend_control.seriesHandles):
289
+ visible_handles = [h for h, s in zip(self._legend_control.seriesHandles, self._legend_control.showSeries) if s and h is not None]
290
+ try:
291
+ legend_index = visible_handles.index(self._legend_control.seriesHandles[index])
292
+ except ValueError:
293
+ return
294
+ if legend_index < len(self._legend.texts):
295
+ self._legend.texts[legend_index].set_text(series_name)
296
+ self.draw()
297
+
298
+ def _handle_show_series_changed(self, index, show_series):
299
+ if index < len(self._legend_control.seriesHandles):
300
+ self._set_series_visibility(self._legend_control.seriesHandles[index], show_series)
301
+ if self._legend is not None:
302
+ self._show_legend(self._legend_control.showLegend)
303
+ else:
304
+ self.draw()
305
+
306
+ def _set_series_visibility(self, handle, visible):
307
+ if not handle:
308
+ return
309
+ if hasattr(handle, 'set_visible'):
310
+ handle.set_visible(visible)
311
+ elif hasattr(handle, 'get_children'):
312
+ for child in handle.get_children():
313
+ self._set_series_visibility(child, visible)
314
+
315
+ def _do_update_grid_lines(self, axes):
316
+ show_grid_lines = self._options_view and self._options_view.showGridLines
317
+ gridline_color = axes.spines[list(axes.spines.keys())[0]].get_edgecolor()
318
+ gridline_color = gridline_color[0], gridline_color[1], gridline_color[2], 0.5
319
+ kwargs = dict(color=gridline_color, alpha=0.5) if show_grid_lines else {}
320
+ axes.grid(show_grid_lines, **kwargs)
321
+ self.draw()
322
+
323
+ def _update_grid_lines(self):
324
+ self._do_update_grid_lines(self._axes)
325
+
326
+ def _handle_options_view_limit_changed(self, x_min_changed=False, x_max_changed=False, y_min_changed=False, y_max_changed=False):
327
+ def _():
328
+ if self._options_view is None or self._setting_axis_limits:
329
+ return
330
+ (x_min, x_max), (y_min, y_max) = (new_x_min, new_x_max), (new_y_min, new_y_max) = self._get_xy_extents()
331
+ (x_opt_min, x_opt_max), (y_opt_min, y_opt_max) = self._get_options_view_xy_extents()
332
+ if x_min_changed:
333
+ new_x_min = x_opt_min
334
+ if x_max_changed:
335
+ new_x_max = x_opt_max
336
+ if y_min_changed:
337
+ new_y_min = y_opt_min
338
+ if y_max_changed:
339
+ new_y_max = y_opt_max
340
+ if [new_x_min, new_x_max, new_y_min, new_y_max] != [x_min, x_max, y_min, y_max]:
341
+ self._xy_extents = (new_x_min, new_x_max), (new_y_min, new_y_max)
342
+ self._set_axes_limits()
343
+ self.draw()
344
+ return _
345
+
346
+ def _get_options_view_xy_extents(self):
347
+ (x_data_min, x_data_max), (y_data_min, y_data_max) = self._get_data_xy_extents()
348
+ x_min = x_data_min if np.isnan(self._options_view.xAxisLowerLimit) else self._options_view.xAxisLowerLimit
349
+ x_max = x_data_max if np.isnan(self._options_view.xAxisUpperLimit) else self._options_view.xAxisUpperLimit
350
+ y_min = y_data_min if np.isnan(self._options_view.yAxisLowerLimit) else self._options_view.yAxisLowerLimit
351
+ y_max = y_data_max if np.isnan(self._options_view.yAxisUpperLimit) else self._options_view.yAxisUpperLimit
352
+ return (x_min, x_max), (y_min, y_max)
353
+
354
+ def _handle_options_view_secondary_limit_changed(self, x_min_changed=False, x_max_changed=False, y_min_changed=False, y_max_changed=False):
355
+ def _():
356
+ if self._options_view is None or self._setting_axis_limits:
357
+ return
358
+ updated = False
359
+ (x_opt_min, x_opt_max), (y_opt_min, y_opt_max) = self._get_options_view_secondary_xy_extents()
360
+ if self._has_secondary_y_extent() and (y_min_changed or y_max_changed):
361
+ y_min, y_max = new_y_min, new_y_max = self._get_secondary_y_extent()
362
+ if y_min_changed:
363
+ new_y_min = y_opt_min
364
+ if y_max_changed:
365
+ new_y_max = y_opt_max
366
+ if [new_y_min, new_y_max] != [y_min, y_max]:
367
+ self._secondary_y_extent = (new_y_min, new_y_max)
368
+ updated = True
369
+ if self._has_secondary_x_extent() and (x_min_changed or x_max_changed):
370
+ x_min, x_max = new_x_min, new_x_max = self._get_secondary_x_extent()
371
+ if x_min_changed:
372
+ new_x_min = x_opt_min
373
+ if x_max_changed:
374
+ new_x_max = x_opt_max
375
+ if [new_x_min, new_x_max] != [x_min, x_max]:
376
+ self._secondary_x_extent = (new_x_min, new_x_max)
377
+ updated = True
378
+ if updated:
379
+ self._set_axes_limits()
380
+ self.draw()
381
+ return _
382
+
383
+ def _get_options_view_secondary_xy_extents(self):
384
+ x_data_min, x_data_max = self._get_data_secondary_x_extent()
385
+ y_data_min, y_data_max = self._get_data_secondary_y_extent()
386
+ x_min = x_data_min if np.isnan(self._options_view.secondaryXAxisLowerLimit) else self._options_view.secondaryXAxisLowerLimit
387
+ x_max = x_data_max if np.isnan(self._options_view.secondaryXAxisUpperLimit) else self._options_view.secondaryXAxisUpperLimit
388
+ y_min = y_data_min if np.isnan(self._options_view.secondaryYAxisLowerLimit) else self._options_view.secondaryYAxisLowerLimit
389
+ y_max = y_data_max if np.isnan(self._options_view.secondaryYAxisUpperLimit) else self._options_view.secondaryYAxisUpperLimit
390
+ return (x_min, x_max), (y_min, y_max)
391
+
392
+ def _on_data_changed(self):
393
+ self._cached_label_width_height = None
394
+
395
+ def closeEvent(self, event):
396
+ QWidget.closeEvent(self, event)
397
+ if event.isAccepted():
398
+ self._zoom_selector.onselect = self._span.onselect = self._span._select_none_handler = None
399
+
400
+ def set_divider_h_margin(self, h_margin):
401
+ h = [Size.Fixed(h_margin[0]), Size.Scaled(1.0), Size.Fixed(h_margin[1])]
402
+ self._divider.set_horizontal(h)
403
+
404
+ def set_divider_v_margin(self, v_margin):
405
+ v = [Size.Fixed(v_margin[0]), Size.Scaled(1.0), Size.Fixed(v_margin[1])]
406
+ self._divider.set_vertical(v)
407
+
408
+ @property
409
+ def x_extent_padding(self):
410
+ return self._x_extent_padding
411
+
412
+ @x_extent_padding.setter
413
+ def x_extent_padding(self, value):
414
+ self._x_extent_padding = value
415
+
416
+ @property
417
+ def y_extent_padding(self):
418
+ return self._y_extent_padding
419
+
420
+ @y_extent_padding.setter
421
+ def y_extent_padding(self, value):
422
+ self._y_extent_padding = value
423
+
424
+ def _in_interval(self, value, interval):
425
+ return interval[0] <= value <= interval[1]
426
+
427
+ def _interval_skew(self, value, interval):
428
+ return (value - interval[0]) / (interval[1] - interval[0])
429
+
430
+ def _in_x_scroll_zone(self, event):
431
+ return self._in_interval(event.x, self._axes.bbox.intervalx) and event.y <= self._axes.bbox.intervaly[1]
432
+
433
+ def _in_y_scroll_zone(self, event):
434
+ return self._in_interval(event.y, self._axes.bbox.intervaly) and event.x <= self._axes.bbox.intervalx[1]
435
+
436
+ def _on_scroll(self, event):
437
+ if self._secondary_axes is not None:
438
+ self._handle_scroll_secondary(event)
439
+ in_x = self._in_x_scroll_zone(event)
440
+ in_y = self._in_y_scroll_zone(event)
441
+ if in_x or in_y and event.button in ['up', 'down']:
442
+ (x_min, x_max), (y_min, y_max) = self._get_actual_xy_extents()
443
+ if (in_x and self._single_axis_zoom_enabled) or (in_x and in_y):
444
+ skew = self._zoom_skew and self._zoom_skew[0]
445
+ skew = self._interval_skew(event.x, self._axes.bbox.intervalx) if skew is None else skew
446
+ x_min, x_max = self._zoom(x_min, x_max, skew, event.button)
447
+ if (in_y and self._single_axis_zoom_enabled) or (in_x and in_y):
448
+ skew = self._zoom_skew and self._zoom_skew[1]
449
+ skew = self._interval_skew(event.y, self._axes.bbox.intervaly) if skew is None else skew
450
+ y_min, y_max = self._zoom(y_min, y_max, skew, event.button)
451
+ self._xy_extents = (x_min, x_max), (y_min, y_max)
452
+ self._set_axes_limits()
453
+ self.draw()
454
+
455
+ def _in_secondary_y_scroll_zone(self, event):
456
+ return self._in_interval(event.y, self._axes.bbox.intervaly) and \
457
+ event.x >= self._axes.bbox.intervalx[1]
458
+
459
+ def _in_secondary_x_scroll_zone(self, event):
460
+ return self._in_interval(event.x, self._axes.bbox.intervalx) and \
461
+ event.y >= self._axes.bbox.intervaly[1]
462
+
463
+ def _handle_scroll_secondary(self, event):
464
+ if self._has_secondary_y_extent():
465
+ in_secondary_y = self._in_secondary_y_scroll_zone(event)
466
+ if in_secondary_y and event.button in ['up', 'down']:
467
+ self._secondary_y_extent = self._zoom(
468
+ *self._get_secondary_y_extent(),
469
+ self._interval_skew(event.y, self._axes.bbox.intervaly),
470
+ event.button
471
+ )
472
+ if self._has_secondary_x_extent():
473
+ in_secondary_x = self._in_secondary_x_scroll_zone(event)
474
+ if in_secondary_x and event.button in ['up', 'down']:
475
+ self._secondary_x_extent = self._zoom(
476
+ *self._get_secondary_x_extent(),
477
+ self._interval_skew(event.x, self._axes.bbox.intervalx),
478
+ event.button
479
+ )
480
+
481
+ def _get_zoom_multiplier(self):
482
+ return 20/19
483
+
484
+ def _zoom(self, min_, max_, skew, direction):
485
+ zoom_multiplier = self._get_zoom_multiplier() if direction == 'up' else 1 / self._get_zoom_multiplier()
486
+ range_ = max_ - min_
487
+ diff = (range_ * (1/zoom_multiplier)) - range_
488
+ max_ += diff * (1 - skew)
489
+ min_ -= diff * skew
490
+ return min_, max_
491
+
492
+ def _ensure_x_limit(self, _axes, x_limit):
493
+ return float(x_limit)
494
+
495
+ def _do_set_axes_limits(self, axes):
496
+ try:
497
+ self._setting_axis_limits = True
498
+ if self._secondary_axes is not None:
499
+ self._set_secondary_axes_limits()
500
+ self._update_ticks()
501
+ (x_min, x_max), (y_min, y_max) = self._get_xy_extents()
502
+ if self._options_view is not None:
503
+ if self._options_view.x_limits:
504
+ self._options_view.setXLimits(
505
+ self._ensure_x_limit(axes, x_min),
506
+ self._ensure_x_limit(axes, x_max))
507
+ if self._options_view.y_limits:
508
+ self._options_view.setYLimits(float(y_min), float(y_max))
509
+ axes.set_xlim(*_safe_limits(x_min, x_max))
510
+ axes.set_ylim(*_safe_limits(y_min, y_max))
511
+ finally:
512
+ self._setting_axis_limits = False
513
+
514
+ def _set_axes_limits(self):
515
+ self._do_set_axes_limits(self.axes)
516
+
517
+ def _set_secondary_axes_limits(self):
518
+ if self._options_view is not None:
519
+ if self._options_view.secondary_y_limits:
520
+ enabled = self._secondary_y_enabled()
521
+ secondary_y_min, secondary_y_max = self._get_secondary_y_extent() if enabled else (float('nan'), float('nan'))
522
+ self._options_view.setSecondaryYLimitsEnabled(enabled)
523
+ self._options_view.setSecondaryYLimits(float(secondary_y_min), float(secondary_y_max))
524
+ if self._options_view.secondary_x_limits:
525
+ enabled = self._secondary_x_enabled()
526
+ secondary_x_min, secondary_x_max = self._get_secondary_x_extent() if enabled else (float('nan'), float('nan'))
527
+ self._options_view.setSecondaryXLimitsEnabled(enabled)
528
+ self._options_view.setSecondaryXLimits(float(secondary_x_min), float(secondary_x_max))
529
+ if self._has_secondary_y_extent():
530
+ self._secondary_axes.set_ylim(*_safe_limits(*self._get_secondary_y_extent()))
531
+ if self._has_secondary_x_extent():
532
+ self._secondary_axes.set_xlim(*_safe_limits(*self._get_secondary_x_extent()))
533
+
534
+ def _secondary_y_enabled(self):
535
+ return True if self._secondary_axes and self._secondary_axes.get_visible() and self._has_secondary_y_extent() else False
536
+
537
+ def _secondary_x_enabled(self):
538
+ return True if self._secondary_axes and self._secondary_axes.get_visible() and self._has_secondary_x_extent() else False
539
+
540
+ def _set_axes_labels(self):
541
+ self._axes.set_xlabel(self.data.xAxisTitle)
542
+ self._axes.set_ylabel(self.data.yAxisTitle)
543
+
544
+ def _set_center(self, center):
545
+ if not all(c is not None for c in center):
546
+ center = (0, 0)
547
+ x_extent, y_extent = self._get_xy_extents()
548
+ span = x_extent[1] - x_extent[0], y_extent[1] - y_extent[0]
549
+ x_extent = center[0] - span[0] / 2, center[0] + span[0] / 2
550
+ y_extent = center[1] - span[1] / 2, center[1] + span[1] / 2
551
+ self._xy_extents = x_extent, y_extent
552
+
553
+ def _get_xy_extents(self):
554
+ if self.data is None:
555
+ return (0, 0), (0, 0)
556
+ if self._xy_extents is None:
557
+ return self._get_data_xy_extents()
558
+ return self._xy_extents
559
+
560
+ def _get_data_xy_extents(self):
561
+ if self.data is None:
562
+ return (0, 0), (0, 0)
563
+ (x_min, x_max), (y_min, y_max) = self.data.get_xy_extents()
564
+ return self._pad_extent(x_min, x_max, self.x_extent_padding), self._pad_extent(y_min, y_max, self.y_extent_padding)
565
+
566
+ def _has_secondary_y_extent(self):
567
+ return hasattr(self.data, 'get_secondary_y_extent')
568
+
569
+ def _get_secondary_y_extent(self):
570
+ if self._secondary_y_extent is not None:
571
+ return self._secondary_y_extent
572
+ if self.data is not None:
573
+ return self._get_data_secondary_y_extent()
574
+ return (0, 0)
575
+
576
+ def _get_data_secondary_y_extent(self):
577
+ if self.data is None:
578
+ return (0, 0)
579
+ return self._pad_extent(*self.data.get_secondary_y_extent(), self.y_extent_padding)
580
+
581
+ def _has_secondary_x_extent(self):
582
+ return hasattr(self.data, 'get_secondary_x_extent')
583
+
584
+ def _get_secondary_x_extent(self):
585
+ if self._secondary_x_extent is not None:
586
+ return self._secondary_x_extent
587
+ if self.data is not None:
588
+ return self._get_data_secondary_x_extent()
589
+ return (0, 0)
590
+
591
+ def _get_data_secondary_x_extent(self):
592
+ if self.data is None or not hasattr(self.data, 'get_secondary_x_extent'):
593
+ return (0, 0)
594
+ return self._pad_extent(*self.data.get_secondary_x_extent(), self.x_extent_padding)
595
+
596
+ def _get_actual_xy_extents(self):
597
+ return self._axes.get_xlim(), self._axes.get_ylim()
598
+
599
+ def _pad_extent(self, min_, max_, padding):
600
+ min_, max_ = self._zero_if_nan(min_), self._zero_if_nan(max_)
601
+ range_ = max_ - min_
602
+ return min_ - padding * range_, max_ + padding * range_
603
+
604
+ def _zoom_selected(self, start_pos, end_pos):
605
+ x_min, x_max = min(start_pos.xdata, end_pos.xdata), max(start_pos.xdata, end_pos.xdata)
606
+ y_min, y_max = min(start_pos.ydata, end_pos.ydata), max(start_pos.ydata, end_pos.ydata)
607
+ self._xy_extents = (x_min, x_max), (y_min, y_max)
608
+ self._set_axes_limits()
609
+ self.draw()
610
+
611
+ def _handle_span_select(self, x_min, x_max):
612
+ x_min, x_max = self._round_to_bin_width(x_min, x_max)
613
+ self._update_span_rect(x_min, x_max)
614
+ self.span = SpanModel(self, x_min, x_max)
615
+ self.draw()
616
+
617
+ def _handle_span_select_none(self):
618
+ self.span = None
619
+
620
+ def _handle_press(self, event):
621
+ if event.button == 1:
622
+ if self._is_panning:
623
+ self._pan_event = event
624
+ elif self._span.active:
625
+ self._handle_span_press(event)
626
+
627
+ def _handle_move(self, event):
628
+ if event.xdata and self._pan_event:
629
+ self._handle_pan_move(event)
630
+ elif event.xdata and any(self._span_events()):
631
+ self._handle_span_move(event)
632
+
633
+ def _handle_release(self, event):
634
+ if self._pan_event:
635
+ self._pan_event = None
636
+ elif any(self._span_events()):
637
+ self._handle_span_release(event)
638
+
639
+ def _handle_pan_move(self, event):
640
+ from_x, from_y = self._axes.transData.inverted().transform((self._pan_event.x, self._pan_event.y))
641
+ to_x, to_y = self._axes.transData.inverted().transform((event.x, event.y))
642
+ self._pan(from_x - to_x, from_y - to_y)
643
+ self._pan_event = event
644
+
645
+ def _pan(self, delta_x, delta_y):
646
+ (x_min, x_max), (y_min, y_max) = self._get_xy_extents()
647
+ self._xy_extents = (x_min + delta_x, x_max + delta_x), (y_min + delta_y, y_max + delta_y)
648
+ self._set_axes_limits()
649
+ self.draw()
650
+
651
+ def _span_events(self):
652
+ return self._span_center_mouse_event, self._span_left_mouse_event, self._span_right_mouse_event
653
+
654
+ def _handle_span_press(self, event):
655
+ if not event.xdata:
656
+ return
657
+ span_min, span_max = (self.span.left, self.span.right) if self.span else (0, 0)
658
+ edge_tolerance = self._span_tolerance()
659
+ if abs(span_min - event.xdata) < edge_tolerance:
660
+ self._span.active = False
661
+ self._span_left_mouse_event = event
662
+ elif abs(span_max - event.xdata) < edge_tolerance:
663
+ self._span.active = False
664
+ self._span_right_mouse_event = event
665
+ elif span_min < event.xdata < span_max:
666
+ self._span.active = False
667
+ self._span_center_mouse_event = event
668
+
669
+ def _handle_span_move(self, event):
670
+ if not self.span:
671
+ return
672
+ x_min, x_max = self.span.left, self.span.right
673
+ last_event = next(x for x in self._span_events() if x)
674
+ diff_x = event.xdata - last_event.xdata
675
+ if self._span_center_mouse_event is not None:
676
+ self._update_span_rect(x_min + diff_x)
677
+ elif self._span_left_mouse_event is not None:
678
+ self._update_span_rect(x_min + diff_x, x_max)
679
+ elif self._span_right_mouse_event is not None:
680
+ self._update_span_rect(x_min, x_max + diff_x)
681
+ self.draw([self._span.rect])
682
+
683
+ def _handle_span_release(self, _event):
684
+ x_min = self._span.rect.get_x()
685
+ x_max = x_min + self._span.rect.get_width()
686
+ x_min, x_max = self._round_to_bin_width(x_min, x_max)
687
+ self._update_span_rect(x_min, x_max)
688
+ self.span = SpanModel(self, x_min, x_max)
689
+ self.draw()
690
+ self._span.active = True
691
+ self._span_center_mouse_event = self._span_left_mouse_event = self._span_right_mouse_event = None
692
+
693
+ def _do_update_span_rect(self, span_tool, x_min, x_max):
694
+ if span_tool is None:
695
+ return
696
+ span_tool.rect.set_x(x_min)
697
+ if x_max:
698
+ span_tool.rect.set_width(x_max - x_min)
699
+ span_tool.handles.set_data(span_tool.extents)
700
+
701
+ def _update_span_rect(self, x_min, x_max=None):
702
+ self._do_update_span_rect(self._span, x_min, x_max)
703
+
704
+ def _round_to_bin_width(self, x_min, x_max):
705
+ return x_min, x_max
706
+
707
+ def _span_tolerance(self):
708
+ return 5
709
+
710
+ def toolEnabled(self, _tool_type):
711
+ return False
712
+
713
+ def toolAvailable(self, _tool_type):
714
+ return False
715
+
716
+ def _activate_span_tool(self, span_tool, active, axes=None):
717
+ if span_tool.active and not active:
718
+ self._previous_span = self.span
719
+ self.span = None
720
+ for r in [span_tool.rect]:
721
+ self._remove_artist(r)
722
+ elif not span_tool.active and active:
723
+ self.span = self._previous_span
724
+ for r in [span_tool.rect]:
725
+ self._add_artist(r, axes=axes)
726
+ span_tool.active = active
727
+ self.draw()
728
+
729
+ def activateTool(self, tool_type, active):
730
+ if tool_type == ToolType.zoom:
731
+ self._zoom_selector.set_active(active)
732
+ if not active:
733
+ self._zoom_selector.clear()
734
+ elif tool_type == ToolType.span:
735
+ self._activate_span_tool(self._span, active)
736
+ elif tool_type == ToolType.pan:
737
+ self._is_panning = active
738
+ self._active_tools[tool_type] = active
739
+
740
+ def toolActive(self, tool_type):
741
+ return self._active_tools.get(tool_type, False)
742
+
743
+ def isActiveDefault(self, _tool_type):
744
+ return False
745
+
746
+ def _add_artist(self, artist, axes=None):
747
+ axes = axes or self._axes
748
+ axes.add_artist(artist)
749
+ self._decoration_artists.append(artist)
750
+
751
+ def _remove_artist(self, artist):
752
+ artist.remove()
753
+ if artist in self._decoration_artists:
754
+ self._decoration_artists.remove(artist)
755
+
756
+ def _handle_resize(self, _event):
757
+ self._update_ticks()
758
+ return self.draw()
759
+
760
+
761
+ def _draw(self, artists=None, axes=None):
762
+ if artists is None:
763
+ def _update():
764
+ for a in self._decoration_artists:
765
+ a.remove()
766
+ self._canvas.draw()
767
+ self._background_cache = self._canvas.copy_from_bbox(self._figure.bbox)
768
+ for a in self._decoration_artists:
769
+ axes.add_artist(a)
770
+ axes.draw_artist(a)
771
+ self._canvas.update()
772
+ self._pending_draw = _update
773
+ else:
774
+ def _update():
775
+ if self._background_cache is None:
776
+ raise RuntimeError('Must run draw before drawing artists!')
777
+ self._canvas.restore_region(self._background_cache)
778
+ for a in artists:
779
+ axes.draw_artist(a)
780
+ self._canvas.update()
781
+ self._pending_artists_draw = _update
782
+
783
+ def draw(self, artists=None):
784
+ return self._draw(artists, self._axes)
785
+
786
+ def _do_draw_events(self):
787
+ if self._pending_draw is not None:
788
+ self._pending_draw()
789
+ self._pending_draw = None
790
+ if self._pending_artists_draw is not None:
791
+ self._pending_artists_draw()
792
+ self._pending_artists_draw = None
793
+ if self._other_draw_events:
794
+ for draw_event in self._other_draw_events:
795
+ draw_event()
796
+ self._other_draw_events = []
797
+
798
+ def addDrawEvent(self, draw_event):
799
+ self._other_draw_events.append(draw_event)
800
+
801
+ def resetZoom(self):
802
+ self._secondary_y_extent = self._secondary_x_extent = None
803
+ self._xy_extents = None
804
+ self._set_axes_limits()
805
+ self.draw()
806
+
807
+ def _twinx(self, ylabel):
808
+ axes = self._axes.twinx()
809
+ for spine in ['top', 'left']:
810
+ axes.spines[spine].set_visible(False)
811
+ axes.set_ylabel(ylabel)
812
+ axes.set_zorder(1)
813
+ return axes
814
+
815
+ @property
816
+ def axes(self):
817
+ return self._axes
818
+
819
+ @property
820
+ def secondary_axes(self):
821
+ if self._secondary_axes is None:
822
+ self._set_secondary_axes(self._twinx(''))
823
+ return self._secondary_axes
824
+
825
+ def _set_secondary_axes(self, axes):
826
+ self._secondary_axes = axes
827
+
828
+ @staticmethod
829
+ def sizeHint():
830
+ """function::sizeHint()
831
+ Override the default sizeHint to ensure the plot has an initial size
832
+ """
833
+ return QSize(600, 400)
834
+
835
+ def minimumSizeHint(self):
836
+ """function::sizeHint()
837
+ Override the default sizeHint to ensure the plot does not shrink below minimum size
838
+ """
839
+ return self.sizeHint()
840
+
841
+ @staticmethod
842
+ def _zero_if_nan(value):
843
+ return value if not isinstance(value, float) or not np.isnan(value) else 0
844
+
845
+ def canShowTable(self):
846
+ return hasattr(self, 'data') and self.data is not None and hasattr(self.data, 'table')
847
+
848
+ def contextMenuEvent(self, event):
849
+ self._show_table_action.setEnabled(self.canShowTable())
850
+ self._menu.exec_(event.globalPos())
851
+
852
+ def copyToClipboard(self):
853
+ with BytesIO() as buffer:
854
+ self._figure.savefig(buffer, facecolor=self._figure.get_facecolor())
855
+ QApplication.clipboard().setImage(QImage.fromData(buffer.getvalue()))
856
+
857
+ def saveAsImage(self):
858
+ filename = self._file_dialog_service.get_save_filename(self, self.tr('Portable Network Graphics (*.png)'))
859
+ if filename:
860
+ self._figure.savefig(filename, facecolor=self._figure.get_facecolor())
861
+
862
+ def showTable(self):
863
+ if self.canShowTable():
864
+ self._table_view = TableView(None)
865
+ self._table_view.pasteEnabled = False
866
+ self._table_view.setModel(self.data.table)
867
+ self._table_view.setMinimumSize(800, 600)
868
+ self._table_view.show()
869
+
870
+ def _update_ticks(self):
871
+ if not self.data:
872
+ return
873
+ if hasattr(self.data, 'x_labels'):
874
+ if len(self.data.x_labels) > 0 and all(isinstance(x, (np.datetime64, pd.Timestamp)) for x in self.data.x_labels):
875
+ x_labels = self.data.x_labels
876
+ self._update_date_xticks(x_labels)
877
+ x_ticks_rotation = 0.0
878
+ else:
879
+ step = self.data.x_tick_interval if hasattr(self.data, 'x_tick_interval') else None
880
+ x_ticks, x_labels, x_ticks_rotation = self._get_labels(self.data.x_labels, step, horizontal=True)
881
+ self._axes.set_xticks(x_ticks, x_labels)
882
+ if hasattr(self.data, 'x_ticks_rotation'):
883
+ rotation = x_ticks_rotation if np.isnan(self.data.x_ticks_rotation) else self.data.x_ticks_rotation
884
+ self._axes.xaxis.set_tick_params(labelrotation=rotation)
885
+ matplotlib.artist.setp(self._axes.get_xticklabels(),
886
+ horizontalalignment='right' if rotation else 'center')
887
+ self._adjust_to_xticklabels_height(self._axes.get_xticklabels(), rotation)
888
+
889
+ if hasattr(self.data, 'y_labels'):
890
+ if len(self.data.y_labels) > 0 and all(isinstance(y, (np.datetime64, pd.Timestamp)) for y in self.data.y_labels):
891
+ y_labels = self.data.y_labels
892
+ self._update_date_yticks(y_labels)
893
+ else:
894
+ step = self.data.y_tick_interval if hasattr(self.data, 'y_tick_interval') else None
895
+ y_ticks, y_labels, _ = self._get_labels(self.data.y_labels, step, horizontal=False)
896
+ self._axes.set_yticks(y_ticks)
897
+ self._axes.set_yticklabels(y_labels)
898
+ self._adjust_to_yticklabels_width(y_labels)
899
+
900
+ def _adjust_to_yticklabels_width(self, labels):
901
+ sizes = self._divider.get_horizontal()
902
+ ticklabels_font_family = self._axes.yaxis.get_ticklabels()[0].get_family() if labels else None
903
+ ticklabels_font_size = self._axes.yaxis.get_ticklabels()[0].get_size() if labels else None
904
+ width, _ = self._get_labels_width_height(
905
+ labels, font_family=ticklabels_font_family, font_size=ticklabels_font_size)
906
+
907
+ ylabel_font_family = self._axes.yaxis.get_label().get_family()
908
+ ylabel_font_size = self._axes.yaxis.get_label().get_size()
909
+ y_label_width, _ = self._get_labels_width_height(
910
+ [self._axes.get_ylabel()], font_family=ylabel_font_family, font_size=ylabel_font_size, rotation=90)
911
+
912
+ width += y_label_width + 0.3 * self._figure.dpi # Need this offset to account for the tick marks and ylabel
913
+ sizes[0] = Size.Fixed(width / self._figure.dpi)
914
+ self._divider.set_horizontal(sizes)
915
+ self.yTickLabelsWidthAdjusted.emit()
916
+
917
+ def _adjust_to_xticklabels_height(self, labels, rotation=None):
918
+ sizes = self._divider.get_vertical()
919
+ ticklabels_font_family = self._axes.xaxis.get_ticklabels()[0].get_family() if labels else None
920
+ ticklabels_font_size = self._axes.xaxis.get_ticklabels()[0].get_size() if labels else None
921
+ _, height = self._get_labels_width_height(
922
+ labels, font_family=ticklabels_font_family, font_size=ticklabels_font_size, rotation=rotation)
923
+
924
+ xlabel_font_family = self._axes.xaxis.get_label().get_family()
925
+ xlabel_font_size = self._axes.xaxis.get_label().get_size()
926
+ _, x_label_height = self._get_labels_width_height(
927
+ [self._axes.get_xlabel()], font_family=xlabel_font_family, font_size=xlabel_font_size)
928
+
929
+ height += x_label_height + 0.2 * self._figure.dpi # Need this offset to account for the tick marks and xlabel
930
+ sizes[0] = Size.Fixed(height / self._figure.dpi)
931
+ self._divider.set_vertical(sizes)
932
+
933
+ def _update_date_yticks(self, y_labels):
934
+ x_extent, y_extent = self._get_xy_extents()
935
+ ipositions, tick_formats, axis_label = self._determine_date_ticks(y_labels, self.axes.yaxis, self.data.yAxisTitle, y_extent)
936
+ self._axes.set_yticks(ipositions, tick_formats)
937
+ self._axes.set_ylabel(axis_label)
938
+ self._adjust_to_yticklabels_width(tick_formats)
939
+
940
+ def _update_date_xticks(self, x_labels):
941
+ x_extent, y_extent = self._get_xy_extents()
942
+ ipositions, tick_formats, axis_label = self._determine_date_ticks(x_labels, self.axes.xaxis, self.data.xAxisTitle, x_extent)
943
+ self._axes.set_xticks(ipositions, tick_formats)
944
+ self._axes.set_xlabel(axis_label)
945
+
946
+ def _determine_date_ticks(self, labels, axis_obj, axis_title, extent):
947
+ e0, e1 = extent
948
+ imin, imax = max(0, math.floor(e0)), min(math.ceil(e1), len(labels) - 1)
949
+ # Convert all labels to date numbers for robust min/max handling
950
+ label_dates = [mdates.date2num(lbl) for lbl in labels]
951
+ date_num_min = min(label_dates)
952
+ date_num_max = max(label_dates)
953
+
954
+ locator = mdates.AutoDateLocator(maxticks=max(1, min(imax - imin, 25)))
955
+ offset_formats = ['', '%Y', '%Y-%b', '%Y-%b', '%Y-%m-%d', '%Y-%m-%d']
956
+ formatter = mdates.ConciseDateFormatter(locator,
957
+ offset_formats=offset_formats)
958
+
959
+ if date_num_max == date_num_min:
960
+ tick_vals = np.array([date_num_min])
961
+ ipositions = np.array([(imin + imax) / 2.0])
962
+ tick_formats = formatter.format_ticks(tick_vals)
963
+ axis_label = f'{axis_title} ({formatter.get_offset()})' if formatter.get_offset() else axis_title
964
+ return ipositions, tick_formats, axis_label
965
+
966
+ # Ensure correct order for tick_values
967
+ if label_dates[imin] <= label_dates[imax]:
968
+ tick_vals = locator.tick_values(labels[imin], labels[imax])
969
+ ipositions = (len(labels) - 1) * (tick_vals - date_num_min) / (date_num_max - date_num_min) - 0.5
970
+ else:
971
+ tick_vals = locator.tick_values(labels[imax], labels[imin])
972
+ ipositions = (len(labels) - 1) * (tick_vals[::-1] - date_num_min) / (date_num_max - date_num_min) - 0.5
973
+
974
+ tick_formats = formatter.format_ticks(tick_vals)
975
+ axis_label = f'{axis_title} ({formatter.get_offset()})' if formatter.get_offset() else axis_title
976
+ return ipositions, tick_formats, axis_label
977
+
978
+
979
+ def _get_labels(self, labels, step, horizontal=True):
980
+ (x0, x1), (y0, y1) = self._get_xy_extents()
981
+ start, end = (int(x0), int(x1)) if horizontal else (int(y0), int(y1))
982
+ visible_points = end - start
983
+ labels_rotation = None
984
+ axes_bbox = self._axes.get_window_extent(self._figure.canvas.get_renderer()).transformed(self._figure.dpi_scale_trans.inverted())
985
+ plot_size = (axes_bbox.width if horizontal else axes_bbox.height) * self._figure.dpi
986
+ width, height = self._get_labels_width_height(labels)
987
+ size = (width if horizontal else height)
988
+ if not (step and step > 0):
989
+ if plot_size == 0 or size == 0:
990
+ n_labels = 16
991
+ else:
992
+ n_labels = int(plot_size / size)
993
+ if n_labels == 0:
994
+ n_labels = 16
995
+ step = int(visible_points / n_labels) + 1
996
+ else:
997
+ step = int(step)
998
+ if horizontal and (visible_points / step) * size > plot_size:
999
+ labels_rotation = 30
1000
+ indexes = list(range(len(labels)))
1001
+ display_labels = list(labels)
1002
+ for i in indexes:
1003
+ if i % step:
1004
+ display_labels[i] = ''
1005
+ return indexes, display_labels, labels_rotation
1006
+
1007
+ def _get_labels_width_height(self, labels, font_family=None, font_size=None, rotation=None):
1008
+ self._cached_label_width_height = self._cached_label_width_height or {}
1009
+ cache_key = (tuple(labels), ''.join([str(i) for i in (font_family, font_size)]))
1010
+ if cache_key not in self._cached_label_width_height:
1011
+ font = MatPlotLibFont(font=font_family) if font_family else MatPlotLibFont.default()
1012
+ width = 0
1013
+ height = 0
1014
+ font_size = font_size or matplotlib.rcParams['font.size']
1015
+ for label in labels:
1016
+ next_width, next_height = font.get_size(str(label), font_size, self._figure.dpi)
1017
+ width = max(width, next_width)
1018
+ height = max(height, next_height)
1019
+ self._cached_label_width_height[cache_key] = width, height
1020
+ width, height = self._cached_label_width_height[cache_key]
1021
+ return (abs(width * np.cos(np.radians(rotation))), abs(width * np.sin(np.radians(rotation)) + height)) if rotation is not None else (width, height)
1022
+
1023
+ def _create_new_axes(self, nx=1, ny=1) -> Axes:
1024
+ axes = Axes(self._figure, self._divider.get_position())
1025
+ axes.set_axes_locator(self._divider.new_locator(nx=nx, ny=ny))
1026
+ self._figure.add_axes(axes)
1027
+ return axes
1028
+
1029
+ @staticmethod
1030
+ def _create_secondary_xy_axes(figure, divider, nx=1, ny=1, visible=False, z_order=1):
1031
+ axes = Axes(figure, divider.get_position())
1032
+ axes.set_axes_locator(divider.new_locator(nx=nx, ny=ny))
1033
+ axes.xaxis.tick_top()
1034
+ axes.xaxis.set_label_position('top')
1035
+ axes.yaxis.tick_right()
1036
+ axes.yaxis.set_label_position('right')
1037
+ axes.patch.set_visible(visible)
1038
+ axes.set_zorder(z_order)
1039
+ figure.add_axes(axes)
1040
+ axes.ticklabel_format(style='sci', axis='x', scilimits=(-4, 4))
1041
+ axes.ticklabel_format(style='sci', axis='y', scilimits=(-4, 4))
1042
+ return axes
1043
+
1044
+ @staticmethod
1045
+ def _create_shared_axes(figure, divider, shared_axes, nx=1, ny=1, visible=False, z_order=1):
1046
+ axes = Axes(figure, divider.get_position(), sharex=shared_axes, sharey=shared_axes, frameon=False)
1047
+ axes.set_axes_locator(divider.new_locator(nx=nx, ny=ny))
1048
+ for spine in axes.spines.values():
1049
+ spine.set_visible(False)
1050
+ for axis in axes.axis.values():
1051
+ axis.set_visible(False)
1052
+ axes.patch.set_visible(False)
1053
+ axes.set_visible(False)
1054
+ axes.set_zorder(z_order)
1055
+ figure.add_axes(axes)
1056
+ return axes