11# -*- coding: utf-8 -*-
2- """
3- Functions and class for performing interactive editing of physiological data
4- """
2+ """Functions and class for performing interactive editing of physiological data."""
53
64import functools
75import numpy as np
1210
1311class _PhysioEditor ():
1412 """
15- Class for editing physiological data
13+ Class for editing physiological data.
1614
1715 Parameters
1816 ----------
@@ -25,30 +23,49 @@ def __init__(self, data):
2523 self .data = utils .check_physio (data , copy = True )
2624 fs = 1 if data .fs is None else data .fs
2725 self .time = np .arange (0 , len (data .data ) / fs , 1 / fs )
26+ # Read if there is support data
27+ self .suppdata = data .suppdata
2828
2929 # we need to create these variables in case someone doesn't "quit"
3030 # the plot appropriately (i.e., clicks X instead of pressing ctrl+q)
31- self .deleted , self .rejected = set (), set ()
31+ self .deleted , self .rejected , self .included = set (), set (), set ()
32+
33+ # make main plot objects depending on supplementary data
34+ if self .suppdata is None :
35+ self .fig , self ._ax = plt .subplots (nrows = 1 , ncols = 1 ,
36+ tight_layout = True , sharex = True )
37+ else :
38+ self .fig , self ._ax = plt .subplots (nrows = 2 , ncols = 1 ,
39+ tight_layout = True , sharex = True ,
40+ gridspec_kw = {'height_ratios' : [3 , 2 ]})
3241
33- # make main plot objects
34- self .fig , self .ax = plt .subplots (nrows = 1 , ncols = 1 , tight_layout = True )
3542 self .fig .canvas .mpl_connect ('scroll_event' , self .on_wheel )
3643 self .fig .canvas .mpl_connect ('key_press_event' , self .on_key )
3744
38- # two selectors for rejection (left mouse) and deletion (right mouse)
39- reject = functools .partial (self .on_remove , reject = True )
40- delete = functools .partial (self .on_remove , reject = False )
41- self .span1 = SpanSelector (self .ax , reject , 'horizontal' ,
45+ # Set axis handler
46+ self .ax = self ._ax if self .suppdata is None else self ._ax [0 ]
47+
48+ # three selectors for:
49+ # 1. rejection (central mouse),
50+ # 2. addition (right mouse), and
51+ # 3. deletion (left mouse)
52+ delete = functools .partial (self .on_edit , method = 'delete' )
53+ reject = functools .partial (self .on_edit , method = 'reject' )
54+ insert = functools .partial (self .on_edit , method = 'insert' )
55+ self .span2 = SpanSelector (self .ax , delete , 'horizontal' ,
4256 button = 1 , useblit = True ,
4357 rectprops = dict (facecolor = 'red' , alpha = 0.3 ))
44- self .span2 = SpanSelector (self .ax , delete , 'horizontal' ,
45- button = 3 , useblit = True ,
58+ self .span1 = SpanSelector (self .ax , reject , 'horizontal' ,
59+ button = 2 , useblit = True ,
4660 rectprops = dict (facecolor = 'blue' , alpha = 0.3 ))
61+ self .span3 = SpanSelector (self .ax , insert , 'horizontal' ,
62+ button = 3 , useblit = True ,
63+ rectprops = dict (facecolor = 'green' , alpha = 0.3 ))
4764
4865 self .plot_signals (False )
4966
5067 def plot_signals (self , plot = True ):
51- """ Clears axes and plots data / peaks / troughs """
68+ """Clear axes and plots data / peaks / troughs. """
5269 # don't reset x-/y-axis zooms on replot
5370 if plot :
5471 xlim , ylim = self .ax .get_xlim (), self .ax .get_ylim ()
@@ -62,50 +79,79 @@ def plot_signals(self, plot=True):
6279 self .data [self .data .peaks ], '.r' ,
6380 self .time [self .data .troughs ],
6481 self .data [self .data .troughs ], '.g' )
82+
83+ if self .suppdata is not None :
84+ self ._ax [1 ].plot (self .time , self .suppdata , 'k' , linewidth = 0.7 )
85+ self ._ax [1 ].set_ylim (- .5 , .5 )
86+
6587 self .ax .set (xlim = xlim , ylim = ylim , yticklabels = '' )
6688 self .fig .canvas .draw ()
6789
6890 def on_wheel (self , event ):
69- """ Moves axis on wheel scroll """
91+ """Move axis on wheel scroll. """
7092 (xlo , xhi ), move = self .ax .get_xlim (), event .step * - 10
7193 self .ax .set_xlim (xlo + move , xhi + move )
7294 self .fig .canvas .draw ()
7395
7496 def quit (self ):
75- """ Quits editor """
97+ """Quit editor. """
7698 plt .close (self .fig )
7799
78100 def on_key (self , event ):
79- """ Undoes last span select or quits peak editor """
101+ """Undo last span select or quits peak editor. """
80102 # accept both control or Mac command key as selector
81103 if event .key in ['ctrl+z' , 'super+d' ]:
82104 self .undo ()
83105 elif event .key in ['ctrl+q' , 'super+d' ]:
84106 self .quit ()
85107
86- def on_remove (self , xmin , xmax , * , reject ):
87- """ Removes specified peaks by either rejection / deletion """
108+ def on_edit (self , xmin , xmax , * , method ):
109+ """
110+ Edit peaks by rejection, deletion, or insert.
111+
112+ Removes specified peaks by either rejection / deletion, OR
113+ Include one peak by finding the max in the selection.
114+
115+ method accepts 'insert', 'reject', 'delete'
116+ """
117+ if method not in ['insert' , 'reject' , 'delete' ]:
118+ raise ValueError (f'Action "{ method } " not supported.' )
119+
88120 tmin , tmax = np .searchsorted (self .time , (xmin , xmax ))
89121 pmin , pmax = np .searchsorted (self .data .peaks , (tmin , tmax ))
90- bad = np .arange (pmin , pmax , dtype = int )
91122
92- if len (bad ) == 0 :
93- return
123+ if method == 'insert' :
124+ tmp = np .argmax (self .data .data [tmin :tmax ]) if tmin != tmax else 0
125+ newpeak = tmin + tmp
126+ if newpeak == tmin :
127+ self .plot_signals ()
128+ return
129+ else :
130+ bad = np .arange (pmin , pmax , dtype = int )
131+ if len (bad ) == 0 :
132+ self .plot_signals ()
133+ return
94134
95- if reject :
135+ if method == ' reject' :
96136 rej , fcn = self .rejected , operations .reject_peaks
97- else :
137+ elif method == 'delete' :
98138 rej , fcn = self .deleted , operations .delete_peaks
99139
100- # store edits in local history
101- rej .update (self .data .peaks [bad ].tolist ())
102- self .data = fcn (self .data , self .data .peaks [bad ])
140+ # store edits in local history & call function
141+ if method == 'insert' :
142+ self .included .add (newpeak )
143+ self .data = operations .add_peaks (self .data , newpeak )
144+ else :
145+ rej .update (self .data .peaks [bad ].tolist ())
146+ self .data = fcn (self .data , self .data .peaks [bad ])
147+
103148 self .plot_signals ()
104149
105150 def undo (self ):
106- """ Resets last span select peak removal """
151+ """Reset last span select peak removal. """
107152 # check if last history entry was a manual reject / delete
108- if self .data ._history [- 1 ][0 ] not in ['reject_peaks' , 'delete_peaks' ]:
153+ relevant = ['reject_peaks' , 'delete_peaks' , 'add_peaks' ]
154+ if self .data ._history [- 1 ][0 ] not in relevant :
109155 return
110156
111157 # pop off last edit and delete
@@ -123,7 +169,12 @@ def undo(self):
123169 peaks ['remove' ]
124170 )
125171 self .deleted .difference_update (peaks ['remove' ])
126-
172+ elif func == 'add_peaks' :
173+ self .data ._metadata ['peaks' ] = np .delete (
174+ self .data ._metadata ['peaks' ],
175+ np .searchsorted (self .data ._metadata ['peaks' ], peaks ['add' ]),
176+ )
177+ self .included .remove (peaks ['add' ])
127178 self .data ._metadata ['troughs' ] = utils .check_troughs (self .data ,
128179 self .data .peaks ,
129180 self .data .troughs )
0 commit comments