3
3
import logging
4
4
5
5
from PyQt5 import QtWidgets
6
- from PyQt5 .QtCore import Qt , QModelIndex , QPoint , pyqtSignal , pyqtSlot , QCoreApplication , QSettings
6
+ from PyQt5 .QtCore import (
7
+ Qt ,
8
+ QModelIndex ,
9
+ pyqtSignal ,
10
+ pyqtSlot ,
11
+ QCoreApplication ,
12
+ QSettings ,
13
+ QSize ,
14
+ QPoint ,
15
+ )
7
16
from PyQt5 .QtGui import QPalette , QShowEvent
8
17
from PyQt5 .QtWidgets import QMenu , QAction
9
18
from iblqt .core import ColoredDataFrameTableModel
@@ -24,14 +33,10 @@ def __init__(self, parent=None, width=5, height=4, dpi=100, wheel=None):
24
33
FigureCanvasQTAgg .__init__ (self , fig )
25
34
self .setParent (parent )
26
35
27
- FigureCanvasQTAgg .setSizePolicy (
28
- self , QtWidgets .QSizePolicy .Expanding , QtWidgets .QSizePolicy .Expanding
29
- )
36
+ FigureCanvasQTAgg .setSizePolicy (self , QtWidgets .QSizePolicy .Expanding , QtWidgets .QSizePolicy .Expanding )
30
37
FigureCanvasQTAgg .updateGeometry (self )
31
38
if wheel :
32
- self .ax , self .ax2 = fig .subplots (
33
- 2 , 1 , gridspec_kw = {"height_ratios" : [2 , 1 ]}, sharex = True
34
- )
39
+ self .ax , self .ax2 = fig .subplots (2 , 1 , gridspec_kw = {'height_ratios' : [2 , 1 ]}, sharex = True )
35
40
else :
36
41
self .ax = fig .add_subplot (111 )
37
42
self .draw ()
@@ -53,12 +58,10 @@ class GraphWindow(QtWidgets.QWidget):
53
58
def __init__ (self , parent = None , wheel = None ):
54
59
QtWidgets .QWidget .__init__ (self , parent = parent )
55
60
56
- # Store layout changes to QSettings
57
- self .settings = QSettings ()
58
-
59
61
self .columnPinned = pyqtSignal (int , bool )
60
62
61
- self .pushButtonLoad = QtWidgets .QPushButton ("Select File" , self )
63
+ # load button
64
+ self .pushButtonLoad = QtWidgets .QPushButton ('Select File' , self )
62
65
self .pushButtonLoad .clicked .connect (self .loadFile )
63
66
64
67
# define table model & view
@@ -70,6 +73,7 @@ def __init__(self, parent=None, wheel=None):
70
73
self .tableView .horizontalHeader ().setSectionsMovable (True )
71
74
self .tableView .horizontalHeader ().setContextMenuPolicy (Qt .CustomContextMenu )
72
75
self .tableView .horizontalHeader ().customContextMenuRequested .connect (self .contextMenu )
76
+ self .tableView .verticalHeader ().hide ()
73
77
self .tableView .doubleClicked .connect (self .tv_double_clicked )
74
78
75
79
# define colors for highlighted cells
@@ -98,6 +102,7 @@ def __init__(self, parent=None, wheel=None):
98
102
99
103
# slider for alpha values
100
104
self .sliderAlpha = QtWidgets .QSlider (Qt .Horizontal , self )
105
+ self .sliderAlpha .setMaximumWidth (100 )
101
106
self .sliderAlpha .setMinimum (0 )
102
107
self .sliderAlpha .setMaximum (255 )
103
108
self .sliderAlpha .setValue (self .tableModel .alpha )
@@ -111,22 +116,36 @@ def __init__(self, parent=None, wheel=None):
111
116
hLayout .addWidget (self .comboboxColormap )
112
117
hLayout .addWidget (QtWidgets .QLabel ('Alpha' , self ))
113
118
hLayout .addWidget (self .sliderAlpha )
114
- hLayout .addStretch ( 1 )
119
+ hLayout .addSpacing ( 50 )
115
120
hLayout .addWidget (self .pushButtonLoad )
116
121
117
122
# Vertical layout
118
123
vLayout = QtWidgets .QVBoxLayout (self )
119
124
vLayout .addLayout (hLayout )
120
125
vLayout .addWidget (self .tableView )
121
126
122
- self .setMinimumSize (500 , 400 )
127
+ # Recover layout from QSettings
128
+ self .settings = QSettings ()
129
+ self .settings .beginGroup ('MainWindow' )
130
+ self .resize (self .settings .value ('size' , QSize (800 , 600 ), QSize ))
131
+ self .comboboxColormap .setCurrentText (self .settings .value ('colormap' , 'plasma' , str ))
132
+ self .sliderAlpha .setValue (self .settings .value ('alpha' , 255 , int ))
133
+ self .settings .endGroup ()
123
134
124
135
self .wplot = PlotWindow (wheel = wheel )
125
136
self .wplot .show ()
126
137
self .tableModel .dataChanged .connect (self .wplot .canvas .draw )
127
138
128
139
self .wheel = wheel
129
140
141
+ def closeEvent (self , _ ) -> bool :
142
+ self .settings .beginGroup ('MainWindow' )
143
+ self .settings .setValue ('size' , self .size ())
144
+ self .settings .setValue ('colormap' , self .tableModel .colormap )
145
+ self .settings .setValue ('alpha' , self .tableModel .alpha )
146
+ self .settings .endGroup ()
147
+ self .wplot .close ()
148
+
130
149
def showEvent (self , a0 : QShowEvent ) -> None :
131
150
super ().showEvent (a0 )
132
151
self .activateWindow ()
@@ -151,36 +170,64 @@ def pinColumn(self, pin: bool, idx: int | None = None):
151
170
self .changeFilter (self .lineEditFilter .text ())
152
171
153
172
def changeFilter (self , string : str ):
154
- headers = [self .tableModel .headerData (x , Qt .Horizontal , Qt .DisplayRole ).value ().lower ()
155
- for x in range (self .tableModel .columnCount ())]
173
+ headers = [
174
+ self .tableModel .headerData (x , Qt .Horizontal , Qt .DisplayRole ).value ().lower ()
175
+ for x in range (self .tableModel .columnCount ())
176
+ ]
156
177
tokens = [y .lower () for y in (x .strip () for x in string .split (',' )) if len (y )]
157
178
showAll = len (tokens ) == 0
158
179
for idx , column in enumerate (headers ):
159
180
show = showAll or any ((t in column for t in tokens )) or idx in self ._pinnedColumns
160
181
self .tableView .setColumnHidden (idx , not show )
161
182
162
183
def loadFile (self ):
163
- fileName , _ = QtWidgets .QFileDialog .getOpenFileName (
164
- self , "Open File" , "" , "CSV Files (*.csv)"
165
- )
184
+ fileName , _ = QtWidgets .QFileDialog .getOpenFileName (self , 'Open File' , '' , 'CSV Files (*.csv)' )
166
185
if len (fileName ) == 0 :
167
186
return
168
187
df = pd .read_csv (fileName )
169
188
self .updateDataframe (df )
170
189
171
- def updateDataframe (self , dataFrame : pd .DataFrame ):
172
- self .tableModel .setDataFrame (dataFrame )
190
+ def updateDataframe (self , df : pd .DataFrame ):
191
+ # clear pinned columns
192
+ self ._pinnedColumns = []
193
+
194
+ # try to identify and sort columns containing timestamps
195
+ col_names = df .columns
196
+ df_interp = df .replace ([- np .inf , np .inf ], np .nan )
197
+ df_interp = df_interp .interpolate (limit_direction = 'both' )
198
+ cols_mono = col_names [[df_interp [c ].is_monotonic_increasing for c in col_names ]]
199
+ cols_mono = [c for c in cols_mono if df [c ].nunique () > 1 ]
200
+ cols_mono = df_interp [cols_mono ].mean ().sort_values ().keys ()
201
+ for idx , col_name in enumerate (cols_mono ):
202
+ df .insert (idx , col_name , df .pop (col_name ))
203
+
204
+ # columns containing boolean values are sorted to the end
205
+ cols_bool = list (df .select_dtypes ('bool' ).columns )
206
+ cols_pass = [cols_bool .pop (i ) for i , c in enumerate (cols_bool ) if 'pass' in c ]
207
+ cols_bool += cols_pass
208
+ for col_name in cols_bool :
209
+ df = df .join (df .pop (col_name ))
210
+
211
+ # trial_no should always be the first column
212
+ if 'trial_no' in col_names :
213
+ df .insert (0 , 'trial_no' , df .pop ('trial_no' ))
214
+
215
+ # define columns that should be pinned by default
216
+ for col in ['trial_no' ]:
217
+ self ._pinnedColumns .append (df .columns .get_loc (col ))
218
+
219
+ self .tableModel .setDataFrame (df )
173
220
174
221
def tv_double_clicked (self , index : QModelIndex ):
175
222
data = self .tableModel .dataFrame .iloc [index .row ()]
176
- t0 = data [" intervals_0" ]
177
- t1 = data [" intervals_1" ]
223
+ t0 = data [' intervals_0' ]
224
+ t1 = data [' intervals_1' ]
178
225
dt = t1 - t0
179
226
if self .wheel :
180
- idx = np .searchsorted (self .wheel [" re_ts" ], np .array ([t0 - dt / 10 , t1 + dt / 10 ]))
181
- period = self .wheel [" re_pos" ][idx [0 ] : idx [1 ]]
227
+ idx = np .searchsorted (self .wheel [' re_ts' ], np .array ([t0 - dt / 10 , t1 + dt / 10 ]))
228
+ period = self .wheel [' re_pos' ][idx [0 ] : idx [1 ]]
182
229
if period .size == 0 :
183
- _logger .warning (" No wheel data during trial #%i" , index .row ())
230
+ _logger .warning (' No wheel data during trial #%i' , index .row ())
184
231
else :
185
232
min_val , max_val = np .min (period ), np .max (period )
186
233
self .wplot .canvas .ax2 .set_ylim (min_val - 1 , max_val + 1 )
@@ -191,10 +238,11 @@ def tv_double_clicked(self, index: QModelIndex):
191
238
192
239
193
240
def viewqc (qc = None , title = None , wheel = None ):
241
+ app = qt .create_app ()
242
+ app .setStyle ('Fusion' )
194
243
QCoreApplication .setOrganizationName ('International Brain Laboratory' )
195
244
QCoreApplication .setOrganizationDomain ('internationalbrainlab.org' )
196
245
QCoreApplication .setApplicationName ('QC Viewer' )
197
- qt .create_app ()
198
246
qcw = GraphWindow (wheel = wheel )
199
247
qcw .setWindowTitle (title )
200
248
if qc is not None :
0 commit comments