QuLab 2.11.7__py3-none-any.whl → 2.11.9__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.
@@ -1,7 +1,15 @@
1
+ """
2
+ QuLab Monitor Main Window Module
3
+
4
+ This module implements the main window interface for the QuLab monitor application.
5
+ It provides a scrollable grid layout of plots with configurable columns and
6
+ interactive features like axis linking and data transformation.
7
+ """
8
+
1
9
  from multiprocessing import Queue
2
10
  from typing import Literal
3
11
 
4
- from .config import forms, ridx, style
12
+ from .config import TRANSFORMS, ROLL_INDICES, STYLE
5
13
  from .dataset import Dataset
6
14
  from .event_queue import EventQueue
7
15
  from .ploter import PlotWidget
@@ -12,223 +20,249 @@ from .toolbar import ToolBar
12
20
 
13
21
 
14
22
  class MainWindow(QtWidgets.QMainWindow):
23
+ """
24
+ Main window for the QuLab monitor application.
25
+
26
+ This window manages a grid of plot widgets and provides controls for data
27
+ visualization and interaction. It includes features like:
28
+ - Configurable number of columns in the plot grid
29
+ - Scrollable plot area
30
+ - Toolbar with plot controls
31
+ - Real-time data updates
32
+ - Axis linking between plots
33
+ - Data transformation options
34
+
35
+ Args:
36
+ queue: Multiprocessing queue for receiving data and commands
37
+ num_columns: Number of columns in the plot grid
38
+ plot_minimum_height: Minimum height for each plot widget
39
+ plot_colors: List of RGB color tuples for plot lines
40
+ """
15
41
 
16
42
  def __init__(self,
17
43
  queue: Queue,
18
- ncols=3,
19
- plot_minimum_height=350,
44
+ num_columns: int = 3,
45
+ plot_minimum_height: int = 350,
20
46
  plot_colors: list[tuple[int, int, int]] | None = None):
21
47
  super().__init__()
22
- self.ncols = ncols
23
- self.need_reshuffled = False
48
+ self.num_columns = num_columns
49
+ self.needs_reshuffle = False
24
50
  self.plot_minimum_height = plot_minimum_height
25
51
  self.plot_widgets: list[PlotWidget] = []
26
52
  self.plot_colors = plot_colors
53
+
54
+ # Initialize components
27
55
  self.toolbar = ToolBar()
28
56
  self.trace_data_box = Dataset()
29
57
  self.point_data_box = Dataset()
30
58
  self.queue = EventQueue(queue, self.toolbar, self.point_data_box,
31
- self.trace_data_box)
59
+ self.trace_data_box)
32
60
 
33
61
  self.init_ui()
34
62
 
63
+ # Set up update timer
35
64
  self.timer = QtCore.QTimer()
36
65
  self.timer.timeout.connect(self.update)
37
- self.timer.start(250)
66
+ self.timer.start(250) # Update every 250ms
38
67
 
39
68
  def init_ui(self):
40
- self.setStyleSheet(style)
69
+ """Initialize the user interface components."""
70
+ self.setStyleSheet(STYLE)
41
71
  self.setMinimumHeight(500)
42
72
  self.setMinimumWidth(700)
43
- self.scroll = QtWidgets.QScrollArea(
44
- ) # Scroll Area which contains the widgets, set as the centralWidget
45
- self.widget = QtWidgets.QWidget(
46
- ) # Widget that contains the collection of Vertical Box
73
+
74
+ # Create scroll area
75
+ self.scroll = QtWidgets.QScrollArea()
76
+ self.widget = QtWidgets.QWidget()
47
77
  self.layout = QtWidgets.QGridLayout()
48
78
  self.widget.setLayout(self.layout)
49
79
 
50
- #Scroll Area Properties
51
- #self.setCorner(Qt.TopSection, Qt.TopDockWidgetArea);
80
+ # Configure scroll area
52
81
  self.scroll.setVerticalScrollBarPolicy(ScrollBarAlwaysOn)
53
82
  self.scroll.setHorizontalScrollBarPolicy(ScrollBarAlwaysOff)
54
83
  self.scroll.setWidgetResizable(True)
55
84
  self.scroll.setWidget(self.widget)
56
85
  self.setCentralWidget(self.scroll)
57
86
 
87
+ # Set up toolbar dock widget
58
88
  self.dock = QtWidgets.QDockWidget(self)
59
89
  self.dock.setAllowedAreas(TopDockWidgetArea | BottomDockWidgetArea)
60
90
  self.addDockWidget(TopDockWidgetArea, self.dock)
61
91
  self.dock.setFloating(False)
62
92
  self.dock.setWidget(self.toolbar)
63
93
  self.dock.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable)
64
- #self.tabifyDockWidget(self.dock,None);
65
- #self.addDockWidget(self.dock);
66
- #self.setStatusBar(self.toolbar);
67
- #self.layout.addWidget(self.toolbar , 0 , 0 , 1, self.ncol);
68
94
 
69
- self.setWindowTitle('Scroll multi view')
95
+ self.setWindowTitle('QuLab Monitor')
70
96
  self.show()
71
97
  self.toolbar.set_mainwindow(self)
72
98
  self.toolbar.pb.setChecked(True)
73
99
 
74
100
  @property
75
101
  def mode(self) -> Literal["P", "T"]:
102
+ """Current plotting mode (Points or Traces)."""
76
103
  return self.toolbar.mode
77
104
 
78
105
  @property
79
106
  def dataset(self) -> Dataset:
107
+ """Current active dataset based on mode."""
80
108
  return {"P": self.point_data_box, "T": self.trace_data_box}[self.mode]
81
109
 
82
- def set_ncols(self, x: int):
83
- x = max(1, min(10, int(x)))
84
- if (x != self.ncols):
85
- self.need_reshuffled = True
86
- self.ncols = x
87
-
88
- def add_subplot(self):
89
- n = len(self.plot_widgets)
90
- pw = PlotWidget(self.plot_minimum_height, self.plot_colors)
91
- self.plot_widgets.append(pw)
92
- grid_row = n // self.ncols
93
- grid_col = n % self.ncols
94
- self.layout.addWidget(pw, grid_row + 1, grid_col)
95
- return pw
96
-
97
- def create_subplots(self, xy_pairs):
98
- for xn, yn in xy_pairs:
99
- pw = self.add_subplot()
100
- pw.set_X_label(xn)
101
- pw.set_Y_label(yn)
110
+ def set_num_columns(self, columns: int):
111
+ """Set the number of columns in the plot grid."""
112
+ columns = max(1, min(10, int(columns)))
113
+ if columns != self.num_columns:
114
+ self.needs_reshuffle = True
115
+ self.num_columns = columns
116
+
117
+ def add_subplot(self) -> PlotWidget:
118
+ """Add a new plot widget to the grid."""
119
+ plot_count = len(self.plot_widgets)
120
+ plot_widget = PlotWidget(self.plot_minimum_height, self.plot_colors)
121
+ self.plot_widgets.append(plot_widget)
122
+
123
+ grid_row = plot_count // self.num_columns
124
+ grid_col = plot_count % self.num_columns
125
+ self.layout.addWidget(plot_widget, grid_row + 1, grid_col)
126
+ return plot_widget
127
+
128
+ def create_subplots(self, xy_pairs: list[tuple[str, str]]):
129
+ """Create multiple subplots with given X-Y axis pairs."""
130
+ for x_name, y_name in xy_pairs:
131
+ plot_widget = self.add_subplot()
132
+ plot_widget.set_x_label(x_name)
133
+ plot_widget.set_y_label(y_name)
102
134
  self.do_link()
103
135
  self.all_enable_auto_range()
104
136
 
105
137
  def clear_subplots(self):
106
- for i in range(len(self.plot_widgets)):
107
- self.layout.removeWidget(self.plot_widgets[i])
108
- self.plot_widgets[i].setParent(None)
138
+ """Remove all plot widgets from the grid."""
139
+ for plot_widget in self.plot_widgets:
140
+ self.layout.removeWidget(plot_widget)
141
+ plot_widget.setParent(None)
109
142
  self.plot_widgets.clear()
110
143
 
111
- def remove_plot(self, w: PlotWidget):
112
- w.setParent(None)
113
- self.plot_widgets.remove(w)
144
+ def remove_plot(self, widget: PlotWidget):
145
+ """Remove a specific plot widget from the grid."""
146
+ widget.setParent(None)
147
+ self.plot_widgets.remove(widget)
114
148
  self.reshuffle()
115
149
 
116
- def drop_last_plot(self, i_=-1):
117
- # delete the one
118
- i = int(i_)
119
- if (i < len(self.plot_widgets)):
120
- w = self.plot_widgets[i]
121
- w.setParent(None)
122
- del w
123
- del self.plot_widgets[i]
150
+ def drop_last_plot(self, index: int = -1):
151
+ """Remove the plot at the specified index."""
152
+ index = int(index)
153
+ if index < len(self.plot_widgets):
154
+ widget = self.plot_widgets[index]
155
+ widget.setParent(None)
156
+ del widget
157
+ del self.plot_widgets[index]
124
158
  self.reshuffle()
125
159
 
126
160
  def reshuffle(self):
161
+ """Rearrange plot widgets in the grid."""
127
162
  for idx, widget in enumerate(self.plot_widgets):
128
163
  widget.setParent(None)
129
- grid_row = idx // self.ncols
130
- grid_col = idx % self.ncols
164
+ grid_row = idx // self.num_columns
165
+ grid_col = idx % self.num_columns
131
166
  self.layout.addWidget(widget, grid_row + 1, grid_col)
132
167
 
133
- def keyPressEvent(self, ev):
134
- #print(ev.text());
135
- tx = ev.text()
136
- if (tx in ['_', '-']):
137
- self.set_ncols(self.ncols - 1)
138
- elif (tx in ['=', '+']):
139
- self.set_ncols(self.ncols + 1)
140
-
141
- def mouse_click(self):
142
- pass
168
+ def keyPressEvent(self, event):
169
+ """Handle keyboard events for column adjustment."""
170
+ key = event.text()
171
+ if key in ['_', '-']:
172
+ self.set_num_columns(self.num_columns - 1)
173
+ elif key in ['=', '+']:
174
+ self.set_num_columns(self.num_columns + 1)
143
175
 
144
176
  def do_link(self):
145
- """
146
- link the plot
147
-
148
- share the same x or y axis
149
- """
150
- same_X = {}
177
+ """Link plots that share the same X or Y axis."""
178
+ same_x_axis = {}
151
179
  xy_pairs = self.toolbar.xypairs
152
- for idx, xyn in enumerate(xy_pairs):
153
- xn, yn = xyn
154
- if xn not in same_X:
155
- same_X[xn] = []
156
- same_X[xn].append(idx)
157
-
158
- sharex, sharey = self.toolbar.sharexy()
159
-
160
- s_A = not (sharex and sharey)
161
- for x, yidxs in same_X.items():
162
- pre_yidx = -1
163
- for yidx in yidxs:
164
- if (-1 != pre_yidx):
165
- if (s_A):
166
- self.plot_widgets[pre_yidx].plotItem.vb.setXLink(None)
167
- self.plot_widgets[pre_yidx].plotItem.vb.setYLink(None)
168
-
169
- if sharex:
170
- self.plot_widgets[pre_yidx].plotItem.vb.setXLink(
171
- self.plot_widgets[yidx].plotItem.vb)
172
-
173
- if sharey:
174
- self.plot_widgets[pre_yidx].plotItem.vb.setYLink(
175
- self.plot_widgets[yidx].plotItem.vb)
176
- pre_yidx = yidx
180
+
181
+ # Group plots by X axis
182
+ for idx, (x_name, y_name) in enumerate(xy_pairs):
183
+ if x_name not in same_x_axis:
184
+ same_x_axis[x_name] = []
185
+ same_x_axis[x_name].append(idx)
186
+
187
+ share_x, share_y = self.toolbar.sharexy()
188
+ should_unlink = not (share_x and share_y)
189
+
190
+ # Link or unlink axes
191
+ for x_name, plot_indices in same_x_axis.items():
192
+ prev_idx = -1
193
+ for curr_idx in plot_indices:
194
+ if prev_idx != -1:
195
+ if should_unlink:
196
+ self.plot_widgets[prev_idx].plotItem.vb.setXLink(None)
197
+ self.plot_widgets[prev_idx].plotItem.vb.setYLink(None)
198
+
199
+ if share_x:
200
+ self.plot_widgets[prev_idx].plotItem.vb.setXLink(
201
+ self.plot_widgets[curr_idx].plotItem.vb)
202
+
203
+ if share_y:
204
+ self.plot_widgets[prev_idx].plotItem.vb.setYLink(
205
+ self.plot_widgets[curr_idx].plotItem.vb)
206
+ prev_idx = curr_idx
177
207
 
178
208
  def all_auto_range(self):
179
- for pw in self.plot_widgets:
180
- pw.auto_range()
209
+ """Auto-range all plot widgets."""
210
+ for plot_widget in self.plot_widgets:
211
+ plot_widget.auto_range()
181
212
 
182
213
  def all_enable_auto_range(self):
183
- for pw in self.plot_widgets:
184
- pw.enable_auto_range()
214
+ """Enable auto-range for all plot widgets."""
215
+ for plot_widget in self.plot_widgets:
216
+ plot_widget.enable_auto_range()
185
217
 
186
218
  def update(self):
187
- # update the queue
219
+ """Update plots with new data and handle UI changes."""
188
220
  self.queue.flush()
221
+ needs_rescale = False
189
222
 
190
- rescale = False
191
-
192
- # setup the xyfm
193
- if (self.toolbar.xypairs_dirty):
223
+ # Handle plot layout changes
224
+ if self.toolbar.xypairs_dirty:
194
225
  self.clear_subplots()
195
226
  self.create_subplots(self.toolbar.xypairs)
196
227
  self.toolbar.xypairs_dirty = False
197
- rescale = True
228
+ needs_rescale = True
198
229
 
199
- if (self.toolbar.link_dirty):
230
+ if self.toolbar.link_dirty:
200
231
  self.do_link()
201
232
  self.toolbar.link_dirty = False
202
233
 
203
- if (self.need_reshuffled):
204
- self.need_reshuffled = False
234
+ if self.needs_reshuffle:
235
+ self.needs_reshuffle = False
205
236
  self.reshuffle()
206
237
 
207
- # checking the log space
208
- if (self.toolbar.xyfm_dirty):
209
- for pw in self.plot_widgets:
210
- pw.plotItem.ctrl.logXCheck.setChecked(self.toolbar.lx)
211
- pw.plotItem.ctrl.logYCheck.setChecked(self.toolbar.ly)
238
+ # Update plot settings
239
+ if self.toolbar.xyfm_dirty:
240
+ for plot_widget in self.plot_widgets:
241
+ plot_widget.plotItem.ctrl.logXCheck.setChecked(self.toolbar.lx)
242
+ plot_widget.plotItem.ctrl.logYCheck.setChecked(self.toolbar.ly)
212
243
 
213
- #update the plot
214
- # if clear is set then do the clear :
215
- if (self.toolbar.CR_flag):
244
+ # Handle data updates
245
+ if self.toolbar.CR_flag:
216
246
  self.toolbar.CR_flag = False
217
247
  self.dataset.clear_history()
218
248
  self.dataset.dirty = True
219
249
 
220
- if (self.dataset.dirty or self.toolbar.xyfm_dirty or rescale):
250
+ if self.dataset.dirty or self.toolbar.xyfm_dirty or needs_rescale:
221
251
  self.dataset.dirty = False
222
- self.xyfm_dirty = False
223
- for pw in self.plot_widgets:
224
-
225
- fx = forms[self.toolbar.fx]
226
- fy = forms[self.toolbar.fy]
227
- for i in ridx:
228
- x, y = self.dataset.get_data(i, pw.xname, pw.yname)
229
- l = min(len(x), len(y))
230
- x, y = fx(x[:l], 0), fy(y[:l], 0)
231
- pw.set_data(i, x, y)
232
- pw.update()
233
- if rescale:
234
- pw.auto_range()
252
+ self.toolbar.xyfm_dirty = False
253
+
254
+ # Update plot data
255
+ for plot_widget in self.plot_widgets:
256
+ x_transform = TRANSFORMS[self.toolbar.fx]
257
+ y_transform = TRANSFORMS[self.toolbar.fy]
258
+
259
+ for idx in ROLL_INDICES:
260
+ x_data, y_data = self.dataset.get_data(idx, plot_widget.x_name, plot_widget.y_name)
261
+ data_length = min(len(x_data), len(y_data))
262
+ x_data = x_transform(x_data[:data_length], 0)
263
+ y_data = y_transform(y_data[:data_length], 0)
264
+ plot_widget.set_data(idx, x_data, y_data)
265
+
266
+ plot_widget.update()
267
+ if needs_rescale:
268
+ plot_widget.auto_range()