PySide6Plot 0.0.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.
- PySide6Plot/__init__.py +187 -0
- PySide6Plot/compoents/__init__.py +3 -0
- PySide6Plot/compoents/average_line.py +232 -0
- PySide6Plot/compoents/draw_line.py +624 -0
- PySide6Plot/compoents/frame_recorder.py +213 -0
- PySide6Plot/compoents/zoom_move.py +160 -0
- PySide6Plot/libs/__init__.py +3 -0
- PySide6Plot/libs/constant.py +19 -0
- PySide6Plot/libs/data_handler.py +230 -0
- PySide6Plot/libs/helpers.py +401 -0
- PySide6Plot/libs/plot_item.py +233 -0
- PySide6Plot/libs/style.py +119 -0
- PySide6Plot/resources/icons/ChevronLeft_black.svg +44 -0
- PySide6Plot/resources/icons/ChevronLeft_white.svg +44 -0
- PySide6Plot/resources/qss/dark/navigation_view_interface.qss +16 -0
- PySide6Plot/resources/qss/light/navigation_view_interface.qss +16 -0
- PySide6Plot/widgets/__init__.py +3 -0
- PySide6Plot/widgets/colorful_toggle_button.py +131 -0
- PySide6Plot/widgets/fluent_scroller.py +450 -0
- PySide6Plot/widgets/line_card.py +138 -0
- PySide6Plot/widgets/navigation_widget.py +49 -0
- PySide6Plot/widgets/q_plot_widget.py +750 -0
- PySide6Plot/widgets/removable_table.py +259 -0
- PySide6Plot/widgets/transparent_Line_edit.py +69 -0
- PySide6Plot/widgets/transparent_selector.py +208 -0
- PySide6Plot/widgets/value_select_box.py +369 -0
- PySide6Plot/widgets/zoom_bar.py +136 -0
- pyside6plot-0.0.1.dist-info/METADATA +37 -0
- pyside6plot-0.0.1.dist-info/RECORD +31 -0
- pyside6plot-0.0.1.dist-info/WHEEL +5 -0
- pyside6plot-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
from pyqtgraph import PlotWidget, SignalProxy, AxisItem
|
|
2
|
+
from PySide6.QtCore import Qt, Signal, QRectF
|
|
3
|
+
from math import ceil, log10
|
|
4
|
+
from qfluentwidgets import (
|
|
5
|
+
qconfig,
|
|
6
|
+
Theme,
|
|
7
|
+
isDarkTheme,
|
|
8
|
+
MenuAnimationType,
|
|
9
|
+
FluentIcon,
|
|
10
|
+
Action,
|
|
11
|
+
RoundMenu,
|
|
12
|
+
MenuIndicatorType,
|
|
13
|
+
CheckableMenu,
|
|
14
|
+
PillPushButton,
|
|
15
|
+
)
|
|
16
|
+
from pyqtgraph import PlotCurveItem
|
|
17
|
+
from ..libs.style import LIGHT_BACKGROUND_COLOR, DARK_BACKGROUND_COLOR
|
|
18
|
+
from ..libs.constant import ZOOM_MODEL, YLOC_MODEL, SCALE_LOC_MODEL
|
|
19
|
+
from ..libs.helpers import limit_in_range, GeneralDataClass
|
|
20
|
+
from .value_select_box import select_value
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CustomizedAxis(AxisItem):
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
orientation,
|
|
27
|
+
plot_strs=None,
|
|
28
|
+
pen=None,
|
|
29
|
+
textPen=None,
|
|
30
|
+
tickPen=None,
|
|
31
|
+
linkView=None,
|
|
32
|
+
parent=None,
|
|
33
|
+
maxTickLength=-5,
|
|
34
|
+
showValues=True,
|
|
35
|
+
text="",
|
|
36
|
+
units="",
|
|
37
|
+
unitPrefix="",
|
|
38
|
+
**args,
|
|
39
|
+
):
|
|
40
|
+
"""
|
|
41
|
+
CustomizedAxis class represents a customized axis for a plot.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
orientation (str): The orientation of the axis. Can be 'left', 'right', 'top', or 'bottom'.
|
|
45
|
+
plot_strs (dict): A dictionary mapping index values to plot strings.
|
|
46
|
+
pen (QPen): The pen used to draw the axis line.
|
|
47
|
+
textPen (QPen): The pen used to draw the axis labels.
|
|
48
|
+
tickPen (QPen): The pen used to draw the axis ticks.
|
|
49
|
+
linkView (ViewBox): The view box to link the axis to.
|
|
50
|
+
parent (QObject): The parent object of the axis.
|
|
51
|
+
maxTickLength (int): The maximum length of the tick lines.
|
|
52
|
+
showValues (bool): Whether to show the tick values.
|
|
53
|
+
text (str): The text to display next to the axis.
|
|
54
|
+
units (str): The units of the axis values.
|
|
55
|
+
unitPrefix (str): The prefix to use for the units.
|
|
56
|
+
**args: Additional keyword arguments.
|
|
57
|
+
"""
|
|
58
|
+
super().__init__(
|
|
59
|
+
orientation,
|
|
60
|
+
pen,
|
|
61
|
+
textPen,
|
|
62
|
+
tickPen,
|
|
63
|
+
linkView,
|
|
64
|
+
parent,
|
|
65
|
+
maxTickLength,
|
|
66
|
+
showValues,
|
|
67
|
+
text,
|
|
68
|
+
units,
|
|
69
|
+
unitPrefix,
|
|
70
|
+
**args,
|
|
71
|
+
)
|
|
72
|
+
self.plot_strs = plot_strs
|
|
73
|
+
self.min_index = 0
|
|
74
|
+
self.max_index = 0
|
|
75
|
+
|
|
76
|
+
def set_tick_strings(self, plot_strs):
|
|
77
|
+
"""
|
|
78
|
+
Set the plot strings for the axis.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
plot_strs (dict): A dictionary mapping index values to plot strings.
|
|
82
|
+
"""
|
|
83
|
+
self.plot_strs = plot_strs
|
|
84
|
+
indexs = list(plot_strs.keys())
|
|
85
|
+
self.min_index = indexs[0]
|
|
86
|
+
self.max_index = indexs[-1]
|
|
87
|
+
|
|
88
|
+
def tick_str(self, value):
|
|
89
|
+
"""
|
|
90
|
+
Return the tick string for the given value.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
value (float): The value of the tick.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
str: The tick string.
|
|
97
|
+
"""
|
|
98
|
+
if self.plot_strs is None:
|
|
99
|
+
return f"{value:.1f}"
|
|
100
|
+
else:
|
|
101
|
+
if value >= self.min_index and value <= self.max_index:
|
|
102
|
+
return self.plot_strs[round(value)]
|
|
103
|
+
else:
|
|
104
|
+
return " "
|
|
105
|
+
|
|
106
|
+
def tickStrings(self, values, zoom, spacing):
|
|
107
|
+
"""
|
|
108
|
+
Return the strings that should be placed next to ticks.
|
|
109
|
+
|
|
110
|
+
This method is called when redrawing the axis and is a good method to override in subclasses.
|
|
111
|
+
The method is called with a list of tick values, a scaling factor, and the spacing between ticks.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
values (list): The list of tick values.
|
|
115
|
+
zoom (float): The scaling factor for the axis label.
|
|
116
|
+
spacing (float): The spacing between ticks.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
list: The list of tick strings.
|
|
120
|
+
"""
|
|
121
|
+
if self.logMode:
|
|
122
|
+
return self.logTickStrings(values, zoom, spacing)
|
|
123
|
+
|
|
124
|
+
places = max(0, ceil(-log10(spacing * zoom)))
|
|
125
|
+
if self.plot_strs is None:
|
|
126
|
+
strings = []
|
|
127
|
+
for v in values:
|
|
128
|
+
vs = v * zoom
|
|
129
|
+
vstr = f"{vs:.{places}f}" if 1e-3 <= abs(vs) < 1e4 else f"{vs:g}"
|
|
130
|
+
strings.append(vstr)
|
|
131
|
+
else:
|
|
132
|
+
strings = [self.plot_strs[int(v)] if (self.min_index <= v <= self.max_index) else " " for v in values]
|
|
133
|
+
return strings
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# NOTE: the y of viewRect is reversed, i.e. the top is the max value and the bottom is the min value
|
|
137
|
+
class QPlotWidget(PlotWidget):
|
|
138
|
+
sigBoundingUpdated = Signal()
|
|
139
|
+
sigSizeChanged = Signal()
|
|
140
|
+
sigZoomModelChanged = Signal()
|
|
141
|
+
sigYLocModelChanged = Signal()
|
|
142
|
+
sigMouseLeaved = Signal()
|
|
143
|
+
sigEnterPressed = Signal()
|
|
144
|
+
sigEscapePressed = Signal()
|
|
145
|
+
sigViewChanged = Signal()
|
|
146
|
+
sigViewChangedByDrag = Signal()
|
|
147
|
+
sigViewChangedNotByDrag = Signal()
|
|
148
|
+
sigItemAdded = Signal()
|
|
149
|
+
sigItemRemoved = Signal()
|
|
150
|
+
|
|
151
|
+
def __init__(self, parent=None, background="default", plotItem=None, **kargs):
|
|
152
|
+
super().__init__(parent, background, plotItem, **kargs)
|
|
153
|
+
self.__init_configuration()
|
|
154
|
+
self.__init_variables()
|
|
155
|
+
self.__init__config_variables()
|
|
156
|
+
self.__init_connections()
|
|
157
|
+
self.__init_context_menu()
|
|
158
|
+
|
|
159
|
+
self.__on_theme_changed()
|
|
160
|
+
self.set_zoom_model(ZOOM_MODEL.AUTO_RANGE)
|
|
161
|
+
self.set_y_loc_model(YLOC_MODEL.DATA_CENTERED)
|
|
162
|
+
|
|
163
|
+
def __init_configuration(self):
|
|
164
|
+
self.setMouseEnabled(x=True, y=False)
|
|
165
|
+
self.setMenuEnabled(False)
|
|
166
|
+
self.setAxisItems({"bottom": CustomizedAxis(orientation="bottom")})
|
|
167
|
+
self.setAxisItems({"left": CustomizedAxis(orientation="left")})
|
|
168
|
+
self.getAxis("bottom").setHeight(32)
|
|
169
|
+
self.getAxis("bottom").setStyle(tickTextOffset=10)
|
|
170
|
+
self.getAxis("left").setWidth(70)
|
|
171
|
+
self.getAxis("left").setStyle(tickTextOffset=10)
|
|
172
|
+
self.setCursor(Qt.CursorShape.CrossCursor)
|
|
173
|
+
self.showGrid(x=True, y=True)
|
|
174
|
+
|
|
175
|
+
def __init_variables(self):
|
|
176
|
+
self.plotted_items = []
|
|
177
|
+
self.x_start = None
|
|
178
|
+
self.x_end = None
|
|
179
|
+
self.y_end = None
|
|
180
|
+
self.y_start = None
|
|
181
|
+
self.__reset_bounding()
|
|
182
|
+
self.fixed_yx_ratio = (self.x_end - self.x_start) / (self.y_start - self.y_end)
|
|
183
|
+
self.fixed_y_loc = self.y_start
|
|
184
|
+
self.fixed_y_range = self.y_end - self.y_start
|
|
185
|
+
self.x_start_button_held = False
|
|
186
|
+
self.zoom_model = ZOOM_MODEL.AUTO_RANGE
|
|
187
|
+
self.y_loc_model = YLOC_MODEL.DATA_CENTERED
|
|
188
|
+
self.move_from_code = False
|
|
189
|
+
|
|
190
|
+
def __init__config_variables(self):
|
|
191
|
+
self.y_autorange_bounding_factor = 0.05
|
|
192
|
+
self.zoom_loc_model = SCALE_LOC_MODEL.RIGHT
|
|
193
|
+
|
|
194
|
+
def __init_connections(self):
|
|
195
|
+
self.view_changed_slot = SignalProxy(self.sigRangeChanged, rateLimit=50, slot=self.__on_range_changed)
|
|
196
|
+
self.mouse_move_slot = SignalProxy(self.scene().sigMouseMoved, rateLimit=50, slot=self.__show_loc)
|
|
197
|
+
self.show_cursor_slot = SignalProxy(self.scene().sigMouseMoved, rateLimit=50, slot=self.__update_cursor)
|
|
198
|
+
qconfig.themeChanged.connect(self.__on_theme_changed)
|
|
199
|
+
self.sigBoundingUpdated.connect(
|
|
200
|
+
lambda: self.update_plot(x_loc=self.viewRect().left(), x_range=self.viewRect().width())
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def __init_context_menu(self):
|
|
204
|
+
self.context_menu_actions = GeneralDataClass(
|
|
205
|
+
zoom_models=GeneralDataClass(
|
|
206
|
+
auto_range=Action("Adaptive"),
|
|
207
|
+
fixed_aspect_ratio=Action("Fixed aspect ratio"),
|
|
208
|
+
fixed_yrange=Action("Fixed y range"),
|
|
209
|
+
),
|
|
210
|
+
y_loc_models=GeneralDataClass(
|
|
211
|
+
data_centered=Action("Data centered"),
|
|
212
|
+
fixed=Action("Fixed y location"),
|
|
213
|
+
free=Action("Free to move"),
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
for _, action in self.context_menu_actions.zoom_models:
|
|
217
|
+
action.setCheckable(True)
|
|
218
|
+
self.context_menu_actions.zoom_models.auto_range.setChecked(True)
|
|
219
|
+
for _, action in self.context_menu_actions.y_loc_models:
|
|
220
|
+
action.setCheckable(True)
|
|
221
|
+
self.context_menu_actions.zoom_models.auto_range.triggered.connect(
|
|
222
|
+
lambda: self.set_zoom_model(ZOOM_MODEL.AUTO_RANGE)
|
|
223
|
+
)
|
|
224
|
+
self.context_menu_actions.zoom_models.fixed_aspect_ratio.triggered.connect(
|
|
225
|
+
lambda: self.set_zoom_model(ZOOM_MODEL.FIXED_RATIO)
|
|
226
|
+
)
|
|
227
|
+
self.context_menu_actions.zoom_models.fixed_yrange.triggered.connect(
|
|
228
|
+
lambda: self.set_zoom_model(ZOOM_MODEL.FIXED_YRANGE)
|
|
229
|
+
)
|
|
230
|
+
self.context_menu_actions.y_loc_models.free.triggered.connect(lambda: self.set_y_loc_model(YLOC_MODEL.FREE))
|
|
231
|
+
self.context_menu_actions.y_loc_models.data_centered.triggered.connect(
|
|
232
|
+
lambda: self.set_y_loc_model(YLOC_MODEL.DATA_CENTERED)
|
|
233
|
+
)
|
|
234
|
+
self.context_menu_actions.y_loc_models.fixed.triggered.connect(lambda: self.set_y_loc_model(YLOC_MODEL.FIXED))
|
|
235
|
+
self.zoom_model_menu = CheckableMenu("Zoom model", self, indicatorType=MenuIndicatorType.RADIO)
|
|
236
|
+
self.zoom_model_menu.setIcon(FluentIcon.ZOOM)
|
|
237
|
+
self.zoom_model_menu.addActions([action for _, action in self.context_menu_actions.zoom_models])
|
|
238
|
+
self.zoom_model_menu.addSeparator()
|
|
239
|
+
self.zoom_model_menu.addActions([action for _, action in self.context_menu_actions.y_loc_models])
|
|
240
|
+
self.context_menu = RoundMenu(parent=self)
|
|
241
|
+
self.context_menu.addMenu(self.zoom_model_menu)
|
|
242
|
+
|
|
243
|
+
def __reset_bounding(self):
|
|
244
|
+
view_rect = self.viewRect()
|
|
245
|
+
self.x_start = view_rect.left()
|
|
246
|
+
self.x_end = view_rect.right()
|
|
247
|
+
self.y_end = view_rect.bottom()
|
|
248
|
+
self.y_start = view_rect.top()
|
|
249
|
+
self.__on_plot_bounding_updated()
|
|
250
|
+
self.sigBoundingUpdated.emit()
|
|
251
|
+
|
|
252
|
+
def __on_plot_bounding_updated(self):
|
|
253
|
+
# the reason for not conncecting this function to sigBoundingUpdated is that
|
|
254
|
+
# we need to update the bounding rect before emitting the signal
|
|
255
|
+
# so that outside world can get the updated range_max/min
|
|
256
|
+
self.x_range_max = self.x_end - self.x_start
|
|
257
|
+
self.x_range_min = self.x_range_max / 1000
|
|
258
|
+
# self.x_range_min=5
|
|
259
|
+
self.y_range_max = self.y_end - self.y_start
|
|
260
|
+
self.y_range_min = self.y_range_max / 1000
|
|
261
|
+
self.setLimits(
|
|
262
|
+
xMin=self.x_start,
|
|
263
|
+
xMax=self.x_end,
|
|
264
|
+
yMin=self.y_start,
|
|
265
|
+
yMax=self.y_end,
|
|
266
|
+
minXRange=self.x_range_min,
|
|
267
|
+
maxXRange=self.x_range_max,
|
|
268
|
+
minYRange=self.y_range_min,
|
|
269
|
+
maxYRange=self.y_range_max,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def __on_range_changed(self):
|
|
273
|
+
self.sigViewChanged.emit()
|
|
274
|
+
if not self.move_from_code:
|
|
275
|
+
self.update_plot()
|
|
276
|
+
self.sigViewChangedByDrag.emit()
|
|
277
|
+
else:
|
|
278
|
+
self.move_from_code = False
|
|
279
|
+
self.sigViewChangedNotByDrag.emit()
|
|
280
|
+
|
|
281
|
+
def __on_theme_changed(self, theme=None):
|
|
282
|
+
"""
|
|
283
|
+
Callback method triggered when the theme is changed.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
theme (Theme): The new theme.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
None
|
|
290
|
+
"""
|
|
291
|
+
if theme == Theme.DARK:
|
|
292
|
+
self.setBackground(DARK_BACKGROUND_COLOR)
|
|
293
|
+
elif theme == Theme.LIGHT:
|
|
294
|
+
self.setBackground(LIGHT_BACKGROUND_COLOR)
|
|
295
|
+
else:
|
|
296
|
+
if isDarkTheme():
|
|
297
|
+
self.setBackground(DARK_BACKGROUND_COLOR)
|
|
298
|
+
else:
|
|
299
|
+
self.setBackground(LIGHT_BACKGROUND_COLOR)
|
|
300
|
+
|
|
301
|
+
def __plot_bounding(self):
|
|
302
|
+
x_starts = []
|
|
303
|
+
x_ends = []
|
|
304
|
+
y_ends = []
|
|
305
|
+
y_starts = []
|
|
306
|
+
for item in self.plotted_items:
|
|
307
|
+
x_starts.append(item.boundingRect().left())
|
|
308
|
+
x_ends.append(item.boundingRect().right())
|
|
309
|
+
y_ends.append(item.boundingRect().bottom())
|
|
310
|
+
y_starts.append(item.boundingRect().top())
|
|
311
|
+
return min(x_starts), max(x_ends), min(y_starts), max(y_ends)
|
|
312
|
+
|
|
313
|
+
def __show_loc(self, event):
|
|
314
|
+
if not hasattr(self, "loc_xlabel"):
|
|
315
|
+
# pen = pg.mkPen(self.main_item.style.cross_line_color, width=1)
|
|
316
|
+
# self.vline = pg.InfiniteLine(angle=90, movable=False, pen=pen)
|
|
317
|
+
# self.hline = pg.InfiniteLine(angle=0, movable=False, pen=pen)
|
|
318
|
+
# self.addItem(self.vline, ignoreBounds=True)
|
|
319
|
+
# self.addItem(self.hline, ignoreBounds=True)
|
|
320
|
+
self.loc_xlabel = PillPushButton(parent=self)
|
|
321
|
+
self.loc_ylabel = PillPushButton(parent=self)
|
|
322
|
+
self.loc_ylabel.setFixedHeight(int(self.getAxis("bottom").height()))
|
|
323
|
+
self.loc_xlabel.setFixedHeight(int(self.getAxis("bottom").height()))
|
|
324
|
+
self.loc_ylabel.setFixedWidth(int(self.getAxis("left").width()))
|
|
325
|
+
# Use event.position() if available (PySide6), otherwise use event[0]
|
|
326
|
+
if hasattr(event, "position"):
|
|
327
|
+
pos = event.position()
|
|
328
|
+
else:
|
|
329
|
+
pos = event[0]
|
|
330
|
+
mouse_point = self.plotItem.vb.mapSceneToView(pos)
|
|
331
|
+
if self.viewRect().contains(mouse_point):
|
|
332
|
+
self.loc_ylabel.setText(str(round(mouse_point.y(), 1)))
|
|
333
|
+
self.loc_ylabel.move(0, int(pos.y() - self.loc_ylabel.geometry().height() / 2))
|
|
334
|
+
if self.loc_ylabel.isHidden():
|
|
335
|
+
self.loc_ylabel.show()
|
|
336
|
+
self.loc_xlabel.setText(self.getAxis("bottom").tick_str(mouse_point.x()))
|
|
337
|
+
self.loc_xlabel.move(
|
|
338
|
+
int(pos.x() - self.loc_xlabel.geometry().width() / 2),
|
|
339
|
+
int(self.geometry().height() - self.loc_xlabel.geometry().height()),
|
|
340
|
+
)
|
|
341
|
+
self.loc_xlabel.setFixedWidth(len(self.loc_xlabel.text()) * 10)
|
|
342
|
+
if self.loc_xlabel.isHidden():
|
|
343
|
+
self.loc_xlabel.show()
|
|
344
|
+
else:
|
|
345
|
+
self.loc_xlabel.hide()
|
|
346
|
+
self.loc_ylabel.hide()
|
|
347
|
+
|
|
348
|
+
def __update_cursor(self, event):
|
|
349
|
+
if self.x_start_button_held:
|
|
350
|
+
self.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
351
|
+
else:
|
|
352
|
+
mouse_point = self.plotItem.vb.mapSceneToView(event[0])
|
|
353
|
+
find = False
|
|
354
|
+
for item in self.scene().items():
|
|
355
|
+
if isinstance(item, PlotCurveItem):
|
|
356
|
+
if item.clickable and item.mouseShape().contains(mouse_point):
|
|
357
|
+
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
358
|
+
find = True
|
|
359
|
+
break
|
|
360
|
+
if not find:
|
|
361
|
+
self.setCursor(Qt.CursorShape.CrossCursor)
|
|
362
|
+
|
|
363
|
+
def __update_zoom_loc_menu(self):
|
|
364
|
+
"""
|
|
365
|
+
Update the y range and location menu based on the current zoom model.
|
|
366
|
+
|
|
367
|
+
This method enables or disables the y location menu and sets the appropriate
|
|
368
|
+
checked state for the y location models based on the current zoom model.
|
|
369
|
+
|
|
370
|
+
Raises:
|
|
371
|
+
Exception: If the zoom model is invalid.
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
def enable_y_loc_menu(enable: bool):
|
|
375
|
+
if enable:
|
|
376
|
+
for _, actions in self.context_menu_actions.y_loc_models:
|
|
377
|
+
actions.setChecked(False)
|
|
378
|
+
actions.setVisible(True)
|
|
379
|
+
if self.y_loc_model == YLOC_MODEL.FREE:
|
|
380
|
+
self.context_menu_actions.y_loc_models.free.setChecked(True)
|
|
381
|
+
self.setMouseEnabled(x=True, y=True)
|
|
382
|
+
elif self.y_loc_model == YLOC_MODEL.DATA_CENTERED:
|
|
383
|
+
self.context_menu_actions.y_loc_models.data_centered.setChecked(True)
|
|
384
|
+
self.setMouseEnabled(x=True, y=False)
|
|
385
|
+
elif self.y_loc_model == YLOC_MODEL.FIXED:
|
|
386
|
+
self.context_menu_actions.y_loc_models.fixed.setChecked(True)
|
|
387
|
+
self.setMouseEnabled(x=True, y=False)
|
|
388
|
+
else:
|
|
389
|
+
raise Exception("Invalid y_loc model")
|
|
390
|
+
else:
|
|
391
|
+
for _, actions in self.context_menu_actions.y_loc_models:
|
|
392
|
+
actions.setChecked(False)
|
|
393
|
+
actions.setVisible(False)
|
|
394
|
+
|
|
395
|
+
if self.zoom_model == ZOOM_MODEL.AUTO_RANGE:
|
|
396
|
+
for _, actions in self.context_menu_actions.zoom_models:
|
|
397
|
+
actions.setChecked(False)
|
|
398
|
+
self.context_menu_actions.zoom_models.auto_range.setChecked(True)
|
|
399
|
+
self.setMouseEnabled(x=True, y=False)
|
|
400
|
+
enable_y_loc_menu(False)
|
|
401
|
+
elif self.zoom_model == ZOOM_MODEL.FIXED_RATIO:
|
|
402
|
+
for _, actions in self.context_menu_actions.zoom_models:
|
|
403
|
+
actions.setChecked(False)
|
|
404
|
+
self.context_menu_actions.zoom_models.fixed_aspect_ratio.setChecked(True)
|
|
405
|
+
enable_y_loc_menu(True)
|
|
406
|
+
elif self.zoom_model == ZOOM_MODEL.FIXED_YRANGE:
|
|
407
|
+
for _, actions in self.context_menu_actions.zoom_models:
|
|
408
|
+
actions.setChecked(False)
|
|
409
|
+
self.context_menu_actions.zoom_models.fixed_yrange.setChecked(True)
|
|
410
|
+
enable_y_loc_menu(True)
|
|
411
|
+
else:
|
|
412
|
+
raise Exception("Invalid zoom model")
|
|
413
|
+
|
|
414
|
+
def mouseReleaseEvent(self, event):
|
|
415
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
416
|
+
self.setCursor(Qt.CursorShape.CrossCursor)
|
|
417
|
+
self.x_start_button_held = False
|
|
418
|
+
return super().mouseReleaseEvent(event)
|
|
419
|
+
|
|
420
|
+
def mousePressEvent(self, event):
|
|
421
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
422
|
+
self.x_start_button_held = True
|
|
423
|
+
return super().mousePressEvent(event)
|
|
424
|
+
|
|
425
|
+
def contextMenuEvent(self, e):
|
|
426
|
+
# show context menu
|
|
427
|
+
self.context_menu.exec(e.globalPos(), aniType=MenuAnimationType.DROP_DOWN)
|
|
428
|
+
|
|
429
|
+
def leaveEvent(self, event):
|
|
430
|
+
if hasattr(self, "loc_xlabel"):
|
|
431
|
+
self.loc_xlabel.hide()
|
|
432
|
+
self.loc_ylabel.hide()
|
|
433
|
+
self.sigMouseLeaved.emit()
|
|
434
|
+
return super().leaveEvent(event)
|
|
435
|
+
|
|
436
|
+
def resizeEvent(self, event):
|
|
437
|
+
self.sigSizeChanged.emit()
|
|
438
|
+
return super().resizeEvent(event)
|
|
439
|
+
|
|
440
|
+
def add_item(self, plot_item, x_ticks=None, y_ticks=None):
|
|
441
|
+
"""
|
|
442
|
+
Add a plot item to the plot widget.
|
|
443
|
+
|
|
444
|
+
Parameters:
|
|
445
|
+
plot_item (PlotItem): The plot item to be added.
|
|
446
|
+
x_ticks (list, optional): The tick labels for the x-axis. Defaults to None.
|
|
447
|
+
y_ticks (list, optional): The tick labels for the y-axis. Defaults to None.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
None
|
|
451
|
+
"""
|
|
452
|
+
self.plotted_items.append(plot_item)
|
|
453
|
+
self.addItem(plot_item)
|
|
454
|
+
self.refresh_bounding(x_ticks, y_ticks)
|
|
455
|
+
self.sigItemAdded.emit()
|
|
456
|
+
|
|
457
|
+
def refresh_bounding(self, x_ticks=None, y_ticks=None):
|
|
458
|
+
if len(self.plotted_items) == 1:
|
|
459
|
+
self.x_start, self.x_end, self.y_start, self.y_end = self.__plot_bounding()
|
|
460
|
+
else:
|
|
461
|
+
x_start, x_end, y_start, y_end = self.__plot_bounding()
|
|
462
|
+
self.x_start = min(self.x_start, x_start)
|
|
463
|
+
self.x_end = max(self.x_end, x_end)
|
|
464
|
+
self.y_start = min(self.y_start, y_start)
|
|
465
|
+
self.y_end = max(self.y_end, y_end)
|
|
466
|
+
if x_ticks is not None:
|
|
467
|
+
self.getAxis("bottom").set_tick_strings(x_ticks)
|
|
468
|
+
if y_ticks is not None:
|
|
469
|
+
self.getAxis("left").set_tick_strings(y_ticks)
|
|
470
|
+
|
|
471
|
+
self.__on_plot_bounding_updated()
|
|
472
|
+
self.update_plot()
|
|
473
|
+
self.sigBoundingUpdated.emit()
|
|
474
|
+
|
|
475
|
+
def remove_item(self, plot_item):
|
|
476
|
+
"""
|
|
477
|
+
Removes the specified plot item from the plot widget.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
plot_item: The plot item to be removed.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
The return value of the removeItem() method.
|
|
484
|
+
|
|
485
|
+
"""
|
|
486
|
+
self.plotted_items.remove(plot_item)
|
|
487
|
+
return_value = self.removeItem(plot_item)
|
|
488
|
+
if len(self.plotted_items) > 0:
|
|
489
|
+
self.x_start, self.x_end, self.y_start, self.y_end = self.__plot_bounding()
|
|
490
|
+
self.__on_plot_bounding_updated()
|
|
491
|
+
self.sigBoundingUpdated.emit()
|
|
492
|
+
else:
|
|
493
|
+
self.__reset_bounding()
|
|
494
|
+
self.update_plot()
|
|
495
|
+
self.sigItemRemoved.emit()
|
|
496
|
+
return return_value
|
|
497
|
+
|
|
498
|
+
def get_local_range(self, start, end):
|
|
499
|
+
"""
|
|
500
|
+
Get the local plot range within the specified start and end values.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
start (float): The start value of the range.
|
|
504
|
+
end (float): The end value of the range.
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
tuple: A tuple containing the minimum and maximum values of the local plot range.
|
|
508
|
+
If no valid local range is found, the range of the view rectangle is returned.
|
|
509
|
+
"""
|
|
510
|
+
mins, maxs = [], []
|
|
511
|
+
for item in self.plotted_items:
|
|
512
|
+
if hasattr(item, "get_local_plot_range"):
|
|
513
|
+
local_range = item.get_local_plot_range(start, end)
|
|
514
|
+
if local_range is not None:
|
|
515
|
+
mins.append(local_range[0])
|
|
516
|
+
maxs.append(local_range[1])
|
|
517
|
+
if len(mins) > 0:
|
|
518
|
+
return min(mins), max(maxs)
|
|
519
|
+
else:
|
|
520
|
+
return self.viewRect().top(), self.viewRect().bottom()
|
|
521
|
+
|
|
522
|
+
def update_plot(self, x_loc: float | None = None, x_range: float | None = None):
|
|
523
|
+
"""
|
|
524
|
+
Update the plot with new x-location and x-range values.
|
|
525
|
+
|
|
526
|
+
Parameters:
|
|
527
|
+
- x_loc Optional[float]: The x-location of the plot. If None, the leftmost x-coordinate of the view rectangle is used.
|
|
528
|
+
- x_range Optional[float]: The x-range of the plot. If None, the width of the view rectangle is used.
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
None
|
|
532
|
+
"""
|
|
533
|
+
if x_loc is None and x_range is not None:
|
|
534
|
+
if self.zoom_loc_model == SCALE_LOC_MODEL.CENTRAL:
|
|
535
|
+
x_loc = (self.viewRect().left() + self.viewRect().right()) / 2 - x_range / 2
|
|
536
|
+
elif self.zoom_loc_model == SCALE_LOC_MODEL.LEFT:
|
|
537
|
+
x_loc = self.viewRect().left()
|
|
538
|
+
elif self.zoom_loc_model == SCALE_LOC_MODEL.RIGHT:
|
|
539
|
+
x_loc = self.viewRect().right() - x_range
|
|
540
|
+
else:
|
|
541
|
+
if x_loc is None:
|
|
542
|
+
x_loc = self.viewRect().left()
|
|
543
|
+
if x_range is None:
|
|
544
|
+
x_range = self.viewRect().width()
|
|
545
|
+
x_range = limit_in_range(x_range, self.x_range_min, self.x_range_max)
|
|
546
|
+
x_loc = limit_in_range(x_loc, self.x_start, self.x_end - x_range)
|
|
547
|
+
x_right = x_loc + x_range
|
|
548
|
+
view_rect = self.viewRect()
|
|
549
|
+
y_loc = view_rect.top()
|
|
550
|
+
y_range = view_rect.height()
|
|
551
|
+
y_center = y_loc + y_range / 2
|
|
552
|
+
# calculate the yzoom
|
|
553
|
+
if self.zoom_model == ZOOM_MODEL.AUTO_RANGE:
|
|
554
|
+
y_loc, y_top = self.get_local_range(x_loc, x_right)
|
|
555
|
+
y_range_bounding = self.y_autorange_bounding_factor * (y_top - y_loc)
|
|
556
|
+
y_loc -= y_range_bounding / 2
|
|
557
|
+
y_top += y_range_bounding / 2
|
|
558
|
+
y_range = y_top - y_loc
|
|
559
|
+
elif self.zoom_model in {ZOOM_MODEL.FIXED_RATIO, ZOOM_MODEL.FIXED_YRANGE}:
|
|
560
|
+
if self.zoom_model == ZOOM_MODEL.FIXED_RATIO:
|
|
561
|
+
y_range = x_range * self.fixed_yx_ratio
|
|
562
|
+
else:
|
|
563
|
+
y_range = self.fixed_y_range
|
|
564
|
+
# make sure the zoom is in y center in fixed ratio model
|
|
565
|
+
if self.y_loc_model == YLOC_MODEL.DATA_CENTERED:
|
|
566
|
+
y_start, y_end = self.get_local_range(x_loc, x_right)
|
|
567
|
+
y_loc = (y_start + y_end) / 2 - y_range / 2
|
|
568
|
+
elif self.y_loc_model == YLOC_MODEL.FREE:
|
|
569
|
+
y_loc = y_center - y_range / 2
|
|
570
|
+
elif self.y_loc_model == YLOC_MODEL.FIXED:
|
|
571
|
+
y_loc = self.fixed_y_loc
|
|
572
|
+
else:
|
|
573
|
+
raise Exception("Invalid y_loc model")
|
|
574
|
+
else:
|
|
575
|
+
raise Exception("Invalid zoom model")
|
|
576
|
+
# make sure the yzoom is in the range
|
|
577
|
+
y_loc = limit_in_range(y_loc, self.y_start, self.y_end - y_range)
|
|
578
|
+
y_range = limit_in_range(y_range, self.y_range_min, self.y_range_max)
|
|
579
|
+
self.move_from_code = True
|
|
580
|
+
self.setRange(QRectF(x_loc, y_loc, x_range, y_range), padding=0)
|
|
581
|
+
|
|
582
|
+
def move_y_loc(self, y_loc):
|
|
583
|
+
"""
|
|
584
|
+
Move the y location of the plot.
|
|
585
|
+
|
|
586
|
+
Parameters:
|
|
587
|
+
y_loc (float): The new y location.
|
|
588
|
+
|
|
589
|
+
Raises:
|
|
590
|
+
Exception: If the y_loc_model is not YLOC_MODEL.FREE.
|
|
591
|
+
Exception: If the zoom_model is ZOOM_MODEL.AUTO_RANGE.
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
None
|
|
595
|
+
"""
|
|
596
|
+
if self.y_loc_model != YLOC_MODEL.FREE:
|
|
597
|
+
raise Exception("you can only move y in y_loc free model")
|
|
598
|
+
if self.zoom_model == ZOOM_MODEL.AUTO_RANGE:
|
|
599
|
+
raise Exception("you can not move y in auto_range model")
|
|
600
|
+
y_loc = limit_in_range(y_loc, self.y_start, self.y_end - self.viewRect().height())
|
|
601
|
+
self.move_from_code = True
|
|
602
|
+
self.setRange(
|
|
603
|
+
QRectF(self.viewRect().left(), y_loc, self.viewRect().width(), self.viewRect().height()), padding=0
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
def set_zoom_model(self, zoom_model):
|
|
607
|
+
"""
|
|
608
|
+
Set the y range model for the plotter.
|
|
609
|
+
|
|
610
|
+
Parameters:
|
|
611
|
+
zoom_model (int): The zoom model to set. Should be one of the values from the ZOOM_MODEL enum.
|
|
612
|
+
|
|
613
|
+
Raises:
|
|
614
|
+
Exception: If an invalid zoom model is provided.
|
|
615
|
+
"""
|
|
616
|
+
if zoom_model == ZOOM_MODEL.AUTO_RANGE:
|
|
617
|
+
self.zoom_model = ZOOM_MODEL.AUTO_RANGE
|
|
618
|
+
self.__update_zoom_loc_menu()
|
|
619
|
+
self.update_plot()
|
|
620
|
+
elif zoom_model == ZOOM_MODEL.FIXED_RATIO:
|
|
621
|
+
new_yx_ratio = select_value(
|
|
622
|
+
parent=self.window(),
|
|
623
|
+
title="Set aspect ratio",
|
|
624
|
+
allowed_min=0.1,
|
|
625
|
+
current=self.viewRect().height() / self.viewRect().width(),
|
|
626
|
+
)
|
|
627
|
+
if new_yx_ratio is not None:
|
|
628
|
+
self.fixed_yx_ratio = new_yx_ratio
|
|
629
|
+
self.zoom_model = ZOOM_MODEL.FIXED_RATIO
|
|
630
|
+
self.__update_zoom_loc_menu()
|
|
631
|
+
self.update_plot()
|
|
632
|
+
elif zoom_model == ZOOM_MODEL.FIXED_YRANGE:
|
|
633
|
+
new_y_range = select_value(
|
|
634
|
+
parent=self.window(),
|
|
635
|
+
title="Set y range",
|
|
636
|
+
allowed_min=self.y_range_min,
|
|
637
|
+
allowed_max=self.y_range_max,
|
|
638
|
+
current=self.viewRect().height(),
|
|
639
|
+
)
|
|
640
|
+
if new_y_range is not None:
|
|
641
|
+
self.fixed_y_range = new_y_range
|
|
642
|
+
self.zoom_model = ZOOM_MODEL.FIXED_YRANGE
|
|
643
|
+
self.__update_zoom_loc_menu()
|
|
644
|
+
self.update_plot()
|
|
645
|
+
else:
|
|
646
|
+
raise Exception("Invalid zoom model")
|
|
647
|
+
self.sigZoomModelChanged.emit()
|
|
648
|
+
|
|
649
|
+
def set_y_loc_model(self, y_loc_model):
|
|
650
|
+
"""
|
|
651
|
+
Set the y location model for the plotter.
|
|
652
|
+
|
|
653
|
+
Parameters:
|
|
654
|
+
y_loc_model (YLOC_MODEL): The y location model to set.
|
|
655
|
+
|
|
656
|
+
Returns:
|
|
657
|
+
None
|
|
658
|
+
"""
|
|
659
|
+
if y_loc_model == YLOC_MODEL.FREE:
|
|
660
|
+
self.y_loc_model = YLOC_MODEL.FREE
|
|
661
|
+
self.__update_zoom_loc_menu()
|
|
662
|
+
self.update_plot()
|
|
663
|
+
elif y_loc_model == YLOC_MODEL.DATA_CENTERED:
|
|
664
|
+
self.y_loc_model = YLOC_MODEL.DATA_CENTERED
|
|
665
|
+
self.__update_zoom_loc_menu()
|
|
666
|
+
self.update_plot()
|
|
667
|
+
elif y_loc_model == YLOC_MODEL.FIXED:
|
|
668
|
+
new_y_loc = select_value(
|
|
669
|
+
parent=self.window(),
|
|
670
|
+
title="Set y start location",
|
|
671
|
+
allowed_min=self.y_start,
|
|
672
|
+
allowed_max=self.y_end,
|
|
673
|
+
current=self.viewRect().top(),
|
|
674
|
+
)
|
|
675
|
+
if new_y_loc is not None:
|
|
676
|
+
self.y_loc = new_y_loc
|
|
677
|
+
self.fixed_y_loc = new_y_loc
|
|
678
|
+
self.y_loc_model = YLOC_MODEL.FIXED
|
|
679
|
+
self.__update_zoom_loc_menu()
|
|
680
|
+
self.update_plot()
|
|
681
|
+
else:
|
|
682
|
+
raise Exception("Invalid y_loc model")
|
|
683
|
+
self.sigYLocModelChanged.emit()
|
|
684
|
+
|
|
685
|
+
def add_context_menu(self, item: Action | RoundMenu):
|
|
686
|
+
if isinstance(item, Action):
|
|
687
|
+
self.context_menu.addAction(item)
|
|
688
|
+
elif isinstance(item, RoundMenu):
|
|
689
|
+
self.context_menu.addMenu(item)
|
|
690
|
+
|
|
691
|
+
def insert_context_menu(self, item: Action | RoundMenu):
|
|
692
|
+
"""
|
|
693
|
+
Inserts an item into the context menu.
|
|
694
|
+
|
|
695
|
+
Parameters:
|
|
696
|
+
item (Union[Action, RoundMenu]): The item to be inserted into the context menu.
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
None
|
|
700
|
+
"""
|
|
701
|
+
if isinstance(item, Action):
|
|
702
|
+
item = self.context_menu._createActionItem(item, before=None)
|
|
703
|
+
self.context_menu.view.insertItem(0, item)
|
|
704
|
+
self.context_menu.adjustSize()
|
|
705
|
+
elif isinstance(item, RoundMenu):
|
|
706
|
+
item, w = self.context_menu._createSubMenuItem(item)
|
|
707
|
+
self.context_menu.view.insertItem(0, item)
|
|
708
|
+
self.context_menu.view.setItemWidget(item, w)
|
|
709
|
+
|
|
710
|
+
def set_full_range_enabled(self, enabled: bool):
|
|
711
|
+
"""
|
|
712
|
+
Set whether the full range is enabled.
|
|
713
|
+
|
|
714
|
+
Parameters:
|
|
715
|
+
enabled (bool): Whether the full range is enabled.
|
|
716
|
+
|
|
717
|
+
Returns:
|
|
718
|
+
None
|
|
719
|
+
"""
|
|
720
|
+
if enabled:
|
|
721
|
+
self.plotItem.showButtons()
|
|
722
|
+
else:
|
|
723
|
+
self.plotItem.hideButtons()
|
|
724
|
+
|
|
725
|
+
def set_x_range(self, x_start: float | None = None, x_end: float | None = None):
|
|
726
|
+
if x_start is not None:
|
|
727
|
+
self.x_start = x_start
|
|
728
|
+
if x_end is not None:
|
|
729
|
+
self.x_end = x_end
|
|
730
|
+
if x_start is not None or x_end is not None:
|
|
731
|
+
self.__on_plot_bounding_updated()
|
|
732
|
+
self.sigBoundingUpdated.emit()
|
|
733
|
+
|
|
734
|
+
def move_to_end(self):
|
|
735
|
+
"""
|
|
736
|
+
Move the plot to the end (right side).
|
|
737
|
+
"""
|
|
738
|
+
self.update_plot(x_loc=self.x_end - self.viewRect().width(), x_range=self.viewRect().width())
|
|
739
|
+
|
|
740
|
+
def move_to_start(self):
|
|
741
|
+
"""
|
|
742
|
+
Move the plot to the start (left side).
|
|
743
|
+
"""
|
|
744
|
+
self.update_plot(x_loc=self.x_start, x_range=self.viewRect().width())
|
|
745
|
+
|
|
746
|
+
def full_range(self):
|
|
747
|
+
"""
|
|
748
|
+
Set the plot to the full range.
|
|
749
|
+
"""
|
|
750
|
+
self.update_plot(x_loc=self.x_start, x_range=self.x_end - self.x_start)
|