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.
- pyqttoolkit/__init__.py +20 -0
- pyqttoolkit/_version.py +21 -0
- pyqttoolkit/application/__init__.py +18 -0
- pyqttoolkit/application/application.py +86 -0
- pyqttoolkit/application/garbage_collector.py +65 -0
- pyqttoolkit/colors/__init__.py +84 -0
- pyqttoolkit/config/__init__.py +20 -0
- pyqttoolkit/config/base.py +22 -0
- pyqttoolkit/config/composite.py +31 -0
- pyqttoolkit/config/default.py +27 -0
- pyqttoolkit/config/environment.py +35 -0
- pyqttoolkit/config/multi_json_file.py +93 -0
- pyqttoolkit/config/with_signal.py +30 -0
- pyqttoolkit/config/with_value_changed.py +31 -0
- pyqttoolkit/data/__init__.py +21 -0
- pyqttoolkit/data/bits.py +23 -0
- pyqttoolkit/data/cache.py +64 -0
- pyqttoolkit/data/interval.py +26 -0
- pyqttoolkit/data/message.py +29 -0
- pyqttoolkit/data/names.py +24 -0
- pyqttoolkit/datetime/__init__.py +16 -0
- pyqttoolkit/datetime/datetime.py +51 -0
- pyqttoolkit/datetime/qdatetime.py +53 -0
- pyqttoolkit/dependencies/__init__.py +56 -0
- pyqttoolkit/errors/__init__.py +19 -0
- pyqttoolkit/exceptions/__init__.py +18 -0
- pyqttoolkit/filesystem/__init__.py +24 -0
- pyqttoolkit/logs/__init__.py +140 -0
- pyqttoolkit/metadata/__init__.py +23 -0
- pyqttoolkit/metadata/dataframe_metadata_manager.py +105 -0
- pyqttoolkit/metadata/dict_metadata_manager.py +63 -0
- pyqttoolkit/metadata/id_generator.py +20 -0
- pyqttoolkit/metadata/metadata_manager_base.py +54 -0
- pyqttoolkit/models/__init__.py +26 -0
- pyqttoolkit/models/code_text.py +43 -0
- pyqttoolkit/models/dataframetable.py +135 -0
- pyqttoolkit/models/dictpropertytree.py +155 -0
- pyqttoolkit/models/dicttable.py +140 -0
- pyqttoolkit/models/json_text.py +52 -0
- pyqttoolkit/models/list_model.py +52 -0
- pyqttoolkit/models/module.py +26 -0
- pyqttoolkit/models/roles.py +23 -0
- pyqttoolkit/models/series_data.py +68 -0
- pyqttoolkit/models/span.py +36 -0
- pyqttoolkit/models/validation.py +27 -0
- pyqttoolkit/modules/__init__.py +18 -0
- pyqttoolkit/modules/events/__init__.py +19 -0
- pyqttoolkit/modules/events/add_signal.py +31 -0
- pyqttoolkit/modules/events/module_opening.py +21 -0
- pyqttoolkit/modules/events/rejectable.py +29 -0
- pyqttoolkit/modules/launcher_config.py +55 -0
- pyqttoolkit/modules/module.py +111 -0
- pyqttoolkit/properties/__init__.py +18 -0
- pyqttoolkit/properties/auto.py +221 -0
- pyqttoolkit/properties/throttled.py +77 -0
- pyqttoolkit/scripting/__init__.py +19 -0
- pyqttoolkit/scripting/script_context.py +36 -0
- pyqttoolkit/scripting/script_runner.py +132 -0
- pyqttoolkit/scripting/script_template.py +42 -0
- pyqttoolkit/services/__init__.py +32 -0
- pyqttoolkit/services/autosave.py +41 -0
- pyqttoolkit/services/email.py +25 -0
- pyqttoolkit/services/event_registry.py +52 -0
- pyqttoolkit/services/file_dialog.py +71 -0
- pyqttoolkit/services/hotkey_manager.py +137 -0
- pyqttoolkit/services/link_manager.py +135 -0
- pyqttoolkit/services/message_board.py +90 -0
- pyqttoolkit/services/module_service.py +154 -0
- pyqttoolkit/services/object_converter.py +45 -0
- pyqttoolkit/services/project_updater.py +149 -0
- pyqttoolkit/services/task_runner.py +281 -0
- pyqttoolkit/services/theme_manager.py +46 -0
- pyqttoolkit/services/tool_window_service.py +46 -0
- pyqttoolkit/viewmanagers/__init__.py +17 -0
- pyqttoolkit/viewmanagers/tool.py +23 -0
- pyqttoolkit/views/__init__.py +50 -0
- pyqttoolkit/views/bulk_value_selector.py +112 -0
- pyqttoolkit/views/check_box.py +36 -0
- pyqttoolkit/views/code/__init__.py +18 -0
- pyqttoolkit/views/code/code_edit.py +268 -0
- pyqttoolkit/views/code/python_syntax.py +226 -0
- pyqttoolkit/views/combo_box.py +101 -0
- pyqttoolkit/views/count_selector.py +59 -0
- pyqttoolkit/views/datetime.py +111 -0
- pyqttoolkit/views/datetime_range_selector.py +231 -0
- pyqttoolkit/views/default_text_line_edit.py +50 -0
- pyqttoolkit/views/delegates.py +117 -0
- pyqttoolkit/views/dtype_edit.py +117 -0
- pyqttoolkit/views/editable_combo_box.py +45 -0
- pyqttoolkit/views/file_selector.py +51 -0
- pyqttoolkit/views/icon.py +67 -0
- pyqttoolkit/views/icon_button.py +129 -0
- pyqttoolkit/views/json_edit.py +21 -0
- pyqttoolkit/views/labelled.py +29 -0
- pyqttoolkit/views/layout/__init__.py +21 -0
- pyqttoolkit/views/layout/collabsible.py +59 -0
- pyqttoolkit/views/layout/group_box.py +48 -0
- pyqttoolkit/views/layout/group_panel.py +188 -0
- pyqttoolkit/views/layout/lines.py +30 -0
- pyqttoolkit/views/layout/link.py +38 -0
- pyqttoolkit/views/line_edit.py +61 -0
- pyqttoolkit/views/linkable.py +49 -0
- pyqttoolkit/views/list_view.py +43 -0
- pyqttoolkit/views/main_window.py +91 -0
- pyqttoolkit/views/metadata_selector.py +84 -0
- pyqttoolkit/views/modulewindow.py +81 -0
- pyqttoolkit/views/plot/__init__.py +26 -0
- pyqttoolkit/views/plot/legend_control.py +121 -0
- pyqttoolkit/views/plot/matplotlib/__init__.py +23 -0
- pyqttoolkit/views/plot/matplotlib/base.py +1056 -0
- pyqttoolkit/views/plot/matplotlib/colorbar.py +75 -0
- pyqttoolkit/views/plot/matplotlib/colormap.py +48 -0
- pyqttoolkit/views/plot/matplotlib/font.py +55 -0
- pyqttoolkit/views/plot/plot_options.py +211 -0
- pyqttoolkit/views/plot/plot_toolbar_options.py +133 -0
- pyqttoolkit/views/plot/size.py +41 -0
- pyqttoolkit/views/plot/tool_type.py +26 -0
- pyqttoolkit/views/plot/toolbar.py +100 -0
- pyqttoolkit/views/popout.py +41 -0
- pyqttoolkit/views/popup.py +41 -0
- pyqttoolkit/views/property_tree.py +26 -0
- pyqttoolkit/views/shrunk_push_button.py +34 -0
- pyqttoolkit/views/steppable_combo_box.py +67 -0
- pyqttoolkit/views/styleable.py +37 -0
- pyqttoolkit/views/table.py +146 -0
- pyqttoolkit/views/text_edit.py +31 -0
- pyqttoolkit/views/tool_window.py +35 -0
- pyqttoolkit-0.9.1.dist-info/METADATA +69 -0
- pyqttoolkit-0.9.1.dist-info/RECORD +133 -0
- pyqttoolkit-0.9.1.dist-info/WHEEL +5 -0
- pyqttoolkit-0.9.1.dist-info/licenses/LICENSE.txt +674 -0
- pyqttoolkit-0.9.1.dist-info/licenses/LICENSE_SHORT +16 -0
- 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
|