"""Definition of the main widgets, with recordings.
"""
from datetime import time, datetime, timedelta
from functools import partial
from logging import getLogger
from re import compile
from numpy import (abs, amax, arange, argmin, around, asarray, ceil, empty, floor,
in1d, max, min, linspace, log2, logical_or, nan_to_num,
nanmean, pad, power)
from PyQt5.QtCore import QPointF, Qt, QRectF
from PyQt5.QtGui import (QBrush,
QColor,
QIcon,
QKeyEvent,
QKeySequence,
QPen,
)
from PyQt5.QtWidgets import (QAction,
QErrorMessage,
QFormLayout,
QGraphicsItem,
QGraphicsRectItem,
QGraphicsScene,
QGraphicsSimpleTextItem,
QGraphicsView,
QGroupBox,
QInputDialog,
QMessageBox,
QVBoxLayout,
)
from .. import ChanTime
from ..trans import montage, filter_, _select_channels
from .settings import Config
from .utils import (convert_name_to_color,
ICON,
LINE_COLOR,
LINE_WIDTH,
Path,
RectMarker,
TextItem_with_BG,
FormFloat,
FormInt,
FormBool,
export_graphics,
)
lg = getLogger(__name__)
# undo the chan + (group) naming
take_raw_name = lambda x: ' ('.join(x.split(' (')[:-1])
NoPen = QPen()
NoPen.setStyle(Qt.NoPen)
MINIMUM_N_SAMPLES = 32 # at least this number of samples to compute fft
CHECK_TIME_STR = compile('[0-9:-]+$')
[docs]class ConfigTraces(Config):
"""Widget with preferences in Settings window for Overview."""
def __init__(self, update_widget):
super().__init__('traces', update_widget)
[docs] def create_config(self):
box0 = QGroupBox('Signals')
self.index['y_distance'] = FormFloat()
self.index['y_scale'] = FormFloat()
self.index['label_ratio'] = FormFloat()
self.index['n_time_labels'] = FormInt()
self.index['max_s_freq'] = FormInt()
form_layout = QFormLayout()
box0.setLayout(form_layout)
form_layout.addRow('Signal scaling',
self.index['y_scale'])
form_layout.addRow('Distance between signals',
self.index['y_distance'])
form_layout.addRow('Label width ratio',
self.index['label_ratio'])
form_layout.addRow('Number of time labels',
self.index['n_time_labels'])
form_layout.addRow('Maximum Sampling Frequency',
self.index['max_s_freq'])
box1 = QGroupBox('Grid')
self.index['grid_x'] = FormBool('Grid on time axis')
self.index['grid_xtick'] = FormFloat()
self.index['grid_y'] = FormBool('Grid on voltage axis')
self.index['grid_ytick'] = FormFloat()
form_layout = QFormLayout()
box1.setLayout(form_layout)
form_layout.addRow(self.index['grid_x'])
form_layout.addRow('Tick every (s)', self.index['grid_xtick'])
form_layout.addRow(self.index['grid_y'])
form_layout.addRow('Lines at + and - (uV):', self.index['grid_ytick'])
box2 = QGroupBox('Current Window')
self.index['window_start'] = FormFloat()
self.index['window_length'] = FormFloat()
self.index['window_step'] = FormInt()
form_layout = QFormLayout()
box2.setLayout(form_layout)
form_layout.addRow('Window start time',
self.index['window_start'])
form_layout.addRow('Window length',
self.index['window_length'])
form_layout.addRow('Step size',
self.index['window_step'])
main_layout = QVBoxLayout()
main_layout.addWidget(box0)
main_layout.addWidget(box1)
main_layout.addWidget(box2)
main_layout.addStretch(1)
self.setLayout(main_layout)
[docs]class Traces(QGraphicsView):
"""Main widget that contains the recordings to be plotted.
Attributes
----------
parent : instance of QMainWindow
the main window.
config : instance of ConfigTraces
settings for this widget
y_scrollbar_value : int
position of the vertical scrollbar
data : instance of ChanTime
filtered and reref'ed data
chan : list of str
list of channels (labels and channel group)
chan_pos : list of int
y-position of each channel (based on value at 0)
chan_scale : list of float
scaling factor for each channel
time_pos : list of QPointF
we need to keep track of the position of time label during creation
sel_chan : int
index of self.chan of the first selected channel
sel_xy : tuple of 2 floats
x and y position of the first selected point
scene : instance of QGraphicsScene
the main scene.
idx_label : list of instance of QGraphicsSimpleTextItem
the channel labels on the y-axis
idx_time : list of instance of QGraphicsSimpleTextItem
the time labels on the x-axis
idx_sel : instance of QGraphicsRectItem
the rectangle showing the selection (both for selection and event)
idx_info : instance of QGraphicsSimpleTextItem
the rectangle showing the selection
idx_markers : list of QGraphicsRectItem
list of markers in the dataset
idx_annot : list of QGraphicsRectItem
list of user-made annotations
"""
def __init__(self, parent):
super().__init__()
self.parent = parent
self.config = ConfigTraces(self.parent.overview.update_position)
self.y_scrollbar_value = 0
self.data = None
self.chan = []
self.chan_pos = [] # used later to find out which channel we're using
self.chan_scale = []
self.time_pos = []
self.sel_chan = None
self.sel_xy = (None, None)
self.scene = None
self.idx_label = []
self.idx_time = []
self.idx_sel = None
self.idx_info = None
self.idx_markers = []
self.idx_annot = []
self.idx_annot_labels = []
self.cross_chan_mrk = True
self.highlight = None
self.event_sel = None
self.current_event = None
self.current_event_row = None
self.current_etype = None
self.deselect = None
self.ready = True
self.create_action()
[docs] def create_action(self):
"""Create actions associated with this widget."""
actions = {}
act = QAction(QIcon(ICON['step_prev']), 'Previous Step', self)
act.setShortcut('[')
act.triggered.connect(self.step_prev)
actions['step_prev'] = act
act = QAction(QIcon(ICON['step_next']), 'Next Step', self)
act.setShortcut(']')
act.triggered.connect(self.step_next)
actions['step_next'] = act
act = QAction(QIcon(ICON['page_prev']), 'Previous Page', self)
act.setShortcut(QKeySequence.MoveToPreviousChar)
act.triggered.connect(self.page_prev)
actions['page_prev'] = act
act = QAction(QIcon(ICON['page_next']), 'Next Page', self)
act.setShortcut(QKeySequence.MoveToNextChar)
act.triggered.connect(self.page_next)
actions['page_next'] = act
act = QAction('Go to Epoch', self)
act.setShortcut(QKeySequence.FindNext)
act.triggered.connect(self.go_to_epoch)
actions['go_to_epoch'] = act
act = QAction('Line Up with Epoch', self)
act.setShortcut('F4')
act.triggered.connect(self.line_up_with_epoch)
actions['line_up_with_epoch'] = act
act = QAction(QIcon(ICON['zoomprev']), 'Wider Time Window', self)
act.setShortcut(QKeySequence.ZoomIn)
act.triggered.connect(self.X_more)
actions['X_more'] = act
act = QAction(QIcon(ICON['zoomnext']), 'Narrower Time Window', self)
act.setShortcut(QKeySequence.ZoomOut)
act.triggered.connect(self.X_less)
actions['X_less'] = act
act = QAction(QIcon(ICON['zoomin']), 'Larger Scaling', self)
act.setShortcut(QKeySequence.MoveToPreviousLine)
act.triggered.connect(self.Y_more)
actions['Y_less'] = act
act = QAction(QIcon(ICON['zoomout']), 'Smaller Scaling', self)
act.setShortcut(QKeySequence.MoveToNextLine)
act.triggered.connect(self.Y_less)
actions['Y_more'] = act
act = QAction(QIcon(ICON['ydist_more']), 'Larger Y Distance', self)
act.triggered.connect(self.Y_wider)
actions['Y_wider'] = act
act = QAction(QIcon(ICON['ydist_less']), 'Smaller Y Distance', self)
act.triggered.connect(self.Y_tighter)
actions['Y_tighter'] = act
act = QAction(QIcon(ICON['chronometer']), '6 Hours Earlier', self)
act.triggered.connect(partial(self.add_time, -6 * 60 * 60))
actions['addtime_-6h'] = act
act = QAction(QIcon(ICON['chronometer']), '1 Hour Earlier', self)
act.triggered.connect(partial(self.add_time, -60 * 60))
actions['addtime_-1h'] = act
act = QAction(QIcon(ICON['chronometer']), '10 Minutes Earlier', self)
act.triggered.connect(partial(self.add_time, -10 * 60))
actions['addtime_-10min'] = act
act = QAction(QIcon(ICON['chronometer']), '10 Minutes Later', self)
act.triggered.connect(partial(self.add_time, 10 * 60))
actions['addtime_10min'] = act
act = QAction(QIcon(ICON['chronometer']), '1 Hour Later', self)
act.triggered.connect(partial(self.add_time, 60 * 60))
actions['addtime_1h'] = act
act = QAction(QIcon(ICON['chronometer']), '6 Hours Later', self)
act.triggered.connect(partial(self.add_time, 6 * 60 * 60))
actions['addtime_6h'] = act
act = QAction('Go to Next Event', self)
act.setShortcut('s')
act.triggered.connect(self.next_event)
actions['next_event'] = act
act = QAction('Delete Event and Go to Next', self)
act.setShortcut('d')
act.triggered.connect(partial(self.next_event, True))
actions['del_and_next_event'] = act
act = QAction('Next Event of Same Type', self)
act.setCheckable(True)
act.setChecked(True)
actions['next_of_same_type'] = act
act = QAction('Change Event Type', self)
act.setShortcut('e')
act.triggered.connect(self.change_event_type)
actions['change_event_type'] = act
act = QAction('Centre Window Around Event', self)
act.setCheckable(True)
act.setChecked(True)
actions['centre_event'] = act
act = QAction('Full-length Markers', self)
act.setCheckable(True)
act.setChecked(True)
act.triggered.connect(self.display_annotations)
actions['cross_chan_mrk'] = act
# Misc
act = QAction('Export to svg...', self)
act.triggered.connect(partial(export_graphics, MAIN=self.parent))
actions['export_svg'] = act
self.action = actions
[docs] def read_data(self):
"""Read the data to plot."""
window_start = self.parent.value('window_start')
window_end = window_start + self.parent.value('window_length')
dataset = self.parent.info.dataset
groups = self.parent.channels.groups
chan_to_read = []
for one_grp in groups:
chan_to_read.extend(one_grp['chan_to_plot'] + one_grp['ref_chan'])
if not chan_to_read:
return
lg.debug(f'Reading data from dataset: begtime={window_start:10.3f}, endtime={window_end:10.3f}, {len(chan_to_read)} channels')
data = dataset.read_data(chan=chan_to_read,
begtime=window_start,
endtime=window_end)
max_s_freq = self.parent.value('max_s_freq')
if data.s_freq > max_s_freq:
q = int(data.s_freq / max_s_freq)
lg.debug('Decimate (no low-pass filter) at ' + str(q))
data.data[0] = data.data[0][:, slice(None, None, q)]
data.axis['time'][0] = data.axis['time'][0][slice(None, None, q)]
data.s_freq = int(data.s_freq / q)
self.data = _create_data_to_plot(data, self.parent.channels.groups)
[docs] def display(self):
"""Display the recordings."""
if self.data is None:
return
if self.scene is not None:
self.y_scrollbar_value = self.verticalScrollBar().value()
self.scene.clear()
self.create_chan_labels()
self.create_time_labels()
window_start = self.parent.value('window_start')
window_length = self.parent.value('window_length')
time_height = max([x.boundingRect().height() for x in self.idx_time])
label_width = window_length * self.parent.value('label_ratio')
scene_height = (len(self.idx_label) * self.parent.value('y_distance') +
time_height)
self.scene = QGraphicsScene(window_start - label_width,
0,
window_length + label_width,
scene_height)
self.setScene(self.scene)
self.idx_markers = []
self.idx_annot = []
self.idx_annot_labels = []
self.add_chan_labels()
self.add_time_labels()
self.add_traces()
self.display_grid()
self.display_markers()
self.display_annotations()
self.resizeEvent(None)
self.verticalScrollBar().setValue(self.y_scrollbar_value)
self.parent.info.display_view()
self.parent.overview.display_current()
[docs] def create_chan_labels(self):
"""Create the channel labels, but don't plot them yet.
Notes
-----
It's necessary to have the width of the labels, so that we can adjust
the main scene.
"""
self.idx_label = []
for one_grp in self.parent.channels.groups:
for one_label in one_grp['chan_to_plot']:
item = QGraphicsSimpleTextItem(one_label)
item.setBrush(QBrush(QColor(one_grp['color'])))
item.setFlag(QGraphicsItem.ItemIgnoresTransformations)
self.idx_label.append(item)
[docs] def create_time_labels(self):
"""Create the time labels, but don't plot them yet.
Notes
-----
It's necessary to have the height of the time labels, so that we can
adjust the main scene.
Not very robust, because it uses seconds as integers.
"""
min_time = int(floor(min(self.data.axis['time'][0])))
max_time = int(ceil(max(self.data.axis['time'][0])))
n_time_labels = self.parent.value('n_time_labels')
self.idx_time = []
self.time_pos = []
for one_time in linspace(min_time, max_time, n_time_labels):
x_label = (self.data.start_time +
timedelta(seconds=one_time)).strftime('%H:%M:%S')
item = QGraphicsSimpleTextItem(x_label)
item.setFlag(QGraphicsItem.ItemIgnoresTransformations)
self.idx_time.append(item)
self.time_pos.append(QPointF(one_time,
len(self.idx_label) *
self.parent.value('y_distance')))
[docs] def add_chan_labels(self):
"""Add channel labels on the left."""
window_start = self.parent.value('window_start')
window_length = self.parent.value('window_length')
label_width = window_length * self.parent.value('label_ratio')
for row, one_label_item in enumerate(self.idx_label):
self.scene.addItem(one_label_item)
one_label_item.setPos(window_start - label_width,
self.parent.value('y_distance') * row +
self.parent.value('y_distance') / 2)
[docs] def add_time_labels(self):
"""Add time labels at the bottom."""
for text, pos in zip(self.idx_time, self.time_pos):
self.scene.addItem(text)
text.setPos(pos)
[docs] def add_traces(self):
"""Add traces based on self.data."""
y_distance = self.parent.value('y_distance')
self.chan = []
self.chan_pos = []
self.chan_scale = []
row = 0
for one_grp in self.parent.channels.groups:
for one_chan in one_grp['chan_to_plot']:
# channel name
chan_name = one_chan + ' (' + one_grp['name'] + ')'
# trace
dat = (self.data(trial=0, chan=chan_name) *
self.parent.value('y_scale'))
dat *= -1 # flip data, upside down (because y grows downward)
path = self.scene.addPath(Path(self.data.axis['time'][0],
dat))
path.setPen(QPen(QColor(one_grp['color']), LINE_WIDTH))
# adjust position
chan_pos = y_distance * row + y_distance / 2
path.setPos(0, chan_pos)
row += 1
self.chan.append(chan_name)
self.chan_scale.append(one_grp['scale'])
self.chan_pos.append(chan_pos)
[docs] def display_grid(self):
"""Display grid on x-axis and y-axis."""
window_start = self.parent.value('window_start')
window_length = self.parent.value('window_length')
window_end = window_start + window_length
if self.parent.value('grid_x'):
x_tick = self.parent.value('grid_xtick')
x_ticks = arange(window_start, window_end + x_tick, x_tick)
for x in x_ticks:
x_pos = [x, x]
y_pos = [0,
self.parent.value('y_distance') * len(self.idx_label)]
path = self.scene.addPath(Path(x_pos, y_pos))
path.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH,
Qt.DotLine))
if self.parent.value('grid_y'):
y_tick = (self.parent.value('grid_ytick') *
self.parent.value('y_scale'))
for one_label_item in self.idx_label:
x_pos = [window_start, window_end]
y = one_label_item.y()
y_pos_0 = [y, y]
path_0 = self.scene.addPath(Path(x_pos, y_pos_0))
path_0.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH,
Qt.DotLine))
y_up = one_label_item.y() + y_tick
y_pos_up = [y_up, y_up]
path_up = self.scene.addPath(Path(x_pos, y_pos_up))
path_up.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH,
Qt.DotLine))
y_down = one_label_item.y() - y_tick
y_pos_down = [y_down, y_down]
path_down = self.scene.addPath(Path(x_pos, y_pos_down))
path_down.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH,
Qt.DotLine))
[docs] def display_markers(self):
"""Add markers on top of first plot."""
for item in self.idx_markers:
self.scene.removeItem(item)
self.idx_markers = []
window_start = self.parent.value('window_start')
window_length = self.parent.value('window_length')
window_end = window_start + window_length
y_distance = self.parent.value('y_distance')
markers = []
if self.parent.info.markers is not None:
if self.parent.value('marker_show'):
markers = self.parent.info.markers
for mrk in markers:
if window_start <= mrk['end'] and window_end >= mrk['start']:
mrk_start = max((mrk['start'], window_start))
mrk_end = min((mrk['end'], window_end))
color = QColor(self.parent.value('marker_color'))
h_annot = len(self.idx_label) * y_distance
mrk_dur = amax((mrk_end - mrk_start,
self.parent.value('min_marker_display_dur')))
item = RectMarker(mrk_start, 0, mrk_dur,
h_annot, zvalue=-9,
color=color)
self.scene.addItem(item)
item = TextItem_with_BG(color.darker(200))
item.setText(str(mrk['name']))
item.setPos(mrk['start'],
len(self.idx_label) *
self.parent.value('y_distance'))
item.setFlag(QGraphicsItem.ItemIgnoresTransformations)
item.setRotation(-90)
self.scene.addItem(item)
self.idx_markers.append(item)
[docs] def display_annotations(self):
"""Mark all the bookmarks/events, on top of first plot."""
for item in self.idx_annot:
self.scene.removeItem(item)
self.idx_annot = []
for item in self.idx_annot_labels:
self.scene.removeItem(item)
self.idx_annot_labels = []
self.highlight = None
window_start = self.parent.value('window_start')
window_length = self.parent.value('window_length')
window_end = window_start + window_length
y_distance = self.parent.value('y_distance')
bookmarks = []
events = []
if self.parent.notes.annot is not None:
if self.parent.value('annot_show'):
bookmarks = self.parent.notes.annot.get_bookmarks()
events = self.parent.notes.get_selected_events((window_start,
window_end))
annotations = bookmarks + events
for annot in annotations:
if window_start <= annot['end'] and window_end >= annot['start']:
mrk_start = max((annot['start'], window_start))
mrk_end = min((annot['end'], window_end))
if annot in bookmarks:
color = QColor(self.parent.value('annot_bookmark_color'))
if annot in events:
color = convert_name_to_color(annot['name'])
if logical_or(annot['chan'] == [''],
self.action['cross_chan_mrk'].isChecked()):
h_annot = len(self.idx_label) * y_distance
item = TextItem_with_BG(color.darker(200))
item.setText(annot['name'])
item.setPos(annot['start'],
len(self.idx_label) * y_distance)
item.setFlag(QGraphicsItem.ItemIgnoresTransformations)
item.setRotation(-90)
self.scene.addItem(item)
self.idx_annot_labels.append(item)
mrk_dur = amax((mrk_end - mrk_start,
self.parent.value('min_marker_display_dur')))
item = RectMarker(mrk_start, 0, mrk_dur,
h_annot, zvalue=-8,
color=color.lighter(120))
self.scene.addItem(item)
self.idx_annot.append(item)
if annot['chan'] != ['']:
# find indices of channels with annotations
chan_idx_in_mrk = in1d(self.chan, annot['chan'])
y_annot = asarray(self.chan_pos)[chan_idx_in_mrk]
y_annot -= y_distance / 2
mrk_dur = amax((mrk_end - mrk_start,
self.parent.value('min_marker_display_dur')))
for y in y_annot:
item = RectMarker(mrk_start, y, mrk_dur,
y_distance, zvalue=-7, color=color)
self.scene.addItem(item)
self.idx_annot.append(item)
[docs] def step_prev(self):
"""Go to the previous step."""
window_start = around(self.parent.value('window_start') -
self.parent.value('window_length') /
self.parent.value('window_step'), 2)
if window_start < 0:
return
self.parent.overview.update_position(window_start)
[docs] def step_next(self):
"""Go to the next step."""
window_start = around(self.parent.value('window_start') +
self.parent.value('window_length') /
self.parent.value('window_step'), 2)
self.parent.overview.update_position(window_start)
[docs] def page_prev(self):
"""Go to the previous page."""
window_start = (self.parent.value('window_start') -
self.parent.value('window_length'))
if window_start < 0:
return
self.parent.overview.update_position(window_start)
[docs] def page_next(self):
"""Go to the next page."""
window_start = (self.parent.value('window_start') +
self.parent.value('window_length'))
self.parent.overview.update_position(window_start)
[docs] def go_to_epoch(self, checked=False, test_text_str=None):
"""Go to any window"""
if test_text_str is not None:
time_str = test_text_str
ok = True
else:
time_str, ok = QInputDialog.getText(self,
'Go To Epoch',
'Enter start time of the '
'epoch,\nin seconds ("1560") '
'or\nas absolute time '
'("22:30")')
if not ok:
return
try:
rec_start_time = self.parent.info.dataset.header['start_time']
window_start = _convert_timestr_to_seconds(time_str, rec_start_time)
except ValueError as err:
error_dialog = QErrorMessage()
error_dialog.setWindowTitle('Error moving to epoch')
error_dialog.showMessage(str(err))
if test_text_str is None:
error_dialog.exec()
self.parent.statusBar().showMessage(str(err))
return
self.parent.overview.update_position(window_start)
[docs] def line_up_with_epoch(self):
"""Go to the start of the present epoch."""
if self.parent.notes.annot is None: # TODO: remove if buttons are disabled
error_dialog = QErrorMessage()
error_dialog.setWindowTitle('Error moving to epoch')
error_dialog.showMessage('No score file loaded')
error_dialog.exec()
return
new_window_start = self.parent.notes.annot.get_epoch_start(
self.parent.value('window_start'))
self.parent.overview.update_position(new_window_start)
[docs] def add_time(self, extra_time):
"""Go to the predefined time forward."""
window_start = self.parent.value('window_start') + extra_time
self.parent.overview.update_position(window_start)
[docs] def X_more(self):
"""Zoom out on the x-axis."""
new_length = self.parent.value('window_length') * 2
self.parent.value('window_length', new_length)
new_start = self.parent.value('window_start') - new_length / 4
self.parent.value('window_start', new_start)
self.parent.overview.update_position()
[docs] def X_less(self):
"""Zoom in on the x-axis."""
new_length = self.parent.value('window_length') / 2
self.parent.value('window_length', new_length)
new_start = self.parent.value('window_start') + new_length / 2
self.parent.value('window_start', new_start)
self.parent.overview.update_position()
[docs] def X_length(self, new_window_length):
"""Use presets for length of the window."""
self.parent.value('window_length', new_window_length)
self.parent.overview.update_position()
[docs] def Y_more(self):
"""Increase the scaling."""
self.parent.value('y_scale', self.parent.value('y_scale') * 2)
self.parent.traces.display()
[docs] def Y_less(self):
"""Decrease the scaling."""
self.parent.value('y_scale', self.parent.value('y_scale') / 2)
self.parent.traces.display()
[docs] def Y_ampl(self, new_y_scale):
"""Make scaling on Y axis using predefined values"""
self.parent.value('y_scale', new_y_scale)
self.parent.traces.display()
[docs] def Y_wider(self):
"""Increase the distance of the lines."""
self.parent.value('y_distance', self.parent.value('y_distance') * 1.4)
self.parent.traces.display()
[docs] def Y_tighter(self):
"""Decrease the distance of the lines."""
self.parent.value('y_distance', self.parent.value('y_distance') / 1.4)
self.parent.traces.display()
[docs] def Y_dist(self, new_y_distance):
"""Use preset values for the distance between lines."""
self.parent.value('y_distance', new_y_distance)
self.parent.traces.display()
[docs] def mousePressEvent(self, event):
"""Create a marker or start selection
Parameters
----------
event : instance of QtCore.QEvent
it contains the position that was clicked.
"""
if not self.scene:
return
if self.event_sel or self.current_event:
self.parent.notes.idx_eventtype.setCurrentText(self.current_etype)
self.current_etype = None
self.current_event = None
self.deselect = True
self.event_sel = None
self.current_event_row = None
self.scene.removeItem(self.highlight)
self.highlight = None
self.parent.statusBar().showMessage('')
return
self.ready = False
self.event_sel = None
xy_scene = self.mapToScene(event.pos())
chan_idx = argmin(abs(asarray(self.chan_pos) - xy_scene.y()))
self.sel_chan = chan_idx
self.sel_xy = (xy_scene.x(), xy_scene.y())
chk_marker = self.parent.notes.action['new_bookmark'].isChecked()
chk_event = self.parent.notes.action['new_event'].isChecked()
if not (chk_marker or chk_event):
channame = self.chan[self.sel_chan] + ' in selected window'
self.parent.spectrum.show_channame(channame)
# Make annotations clickable
else:
for annot in self.idx_annot:
if annot.contains(xy_scene):
self.highlight_event(annot)
if chk_event:
row = self.parent.notes.find_row(annot.marker.x(),
annot.marker.x() + annot.marker.width())
self.parent.notes.idx_annot_list.setCurrentCell(row, 0)
break
self.ready = True
[docs] def mouseMoveEvent(self, event):
"""When normal selection, update power spectrum with current selection.
Otherwise, show the range of the new marker.
"""
if not self.scene:
return
if self.event_sel or self.deselect:
return
if self.sel_xy[0] is None or self.sel_xy[1] is None:
return
if self.idx_sel in self.scene.items():
self.scene.removeItem(self.idx_sel)
self.idx_sel = None
chk_marker = self.parent.notes.action['new_bookmark'].isChecked()
chk_event = self.parent.notes.action['new_event'].isChecked()
if chk_marker or chk_event:
xy_scene = self.mapToScene(event.pos())
y_distance = self.parent.value('y_distance')
pos = QRectF(self.sel_xy[0],
0,
xy_scene.x() - self.sel_xy[0],
len(self.idx_label) * y_distance)
item = QGraphicsRectItem(pos.normalized())
item.setPen(NoPen)
if chk_marker:
color = QColor(self.parent.value('annot_bookmark_color'))
elif chk_event:
eventtype = self.parent.notes.idx_eventtype.currentText()
color = convert_name_to_color(eventtype)
item.setBrush(QBrush(color.lighter(115)))
item.setZValue(-10)
self.scene.addItem(item)
self.idx_sel = item
return
xy_scene = self.mapToScene(event.pos())
pos = QRectF(self.sel_xy[0], self.sel_xy[1],
xy_scene.x() - self.sel_xy[0],
xy_scene.y() - self.sel_xy[1])
self.idx_sel = QGraphicsRectItem(pos.normalized())
self.idx_sel.setPen(QPen(QColor(LINE_COLOR), LINE_WIDTH))
self.scene.addItem(self.idx_sel)
if self.idx_info in self.scene.items():
self.scene.removeItem(self.idx_info)
duration = '{0:0.3f}s'.format(abs(xy_scene.x() - self.sel_xy[0]))
# get y-size, based on scaling too
y = abs(xy_scene.y() - self.sel_xy[1])
scale = self.parent.value('y_scale') * self.chan_scale[self.sel_chan]
height = '{0:0.3f}uV'.format(y / scale)
item = TextItem_with_BG()
item.setText(duration + ' ' + height)
item.setPos(self.sel_xy[0], self.sel_xy[1])
self.scene.addItem(item)
self.idx_info = item
trial = 0
time = self.parent.traces.data.axis['time'][trial]
beg_win = min((self.sel_xy[0], xy_scene.x()))
end_win = max((self.sel_xy[0], xy_scene.x()))
time_of_interest = time[(time >= beg_win) & (time < end_win)]
if len(time_of_interest) > MINIMUM_N_SAMPLES:
data = self.parent.traces.data(trial=trial,
chan=self.chan[self.sel_chan],
time=time_of_interest)
n_data = len(data)
n_pad = (power(2, ceil(log2(n_data))) - n_data) / 2
data = pad(data, (int(ceil(n_pad)), int(floor(n_pad))), 'constant')
self.parent.spectrum.display(data)
[docs] def mouseReleaseEvent(self, event):
"""Create a new event or marker, or show the previous power spectrum
"""
if not self.scene:
return
if self.event_sel:
return
if self.deselect:
self.deselect = False
return
if not self.ready:
return
chk_marker = self.parent.notes.action['new_bookmark'].isChecked()
chk_event = self.parent.notes.action['new_event'].isChecked()
y_distance = self.parent.value('y_distance')
if chk_marker or chk_event:
x_in_scene = self.mapToScene(event.pos()).x()
y_in_scene = self.mapToScene(event.pos()).y()
# it can happen that selection is empty (f.e. double-click)
if self.sel_xy[0] is not None:
# max resolution = sampling frequency
# in case there is no data
s_freq = self.parent.info.dataset.header['s_freq']
at_s_freq = lambda x: round(x * s_freq) / s_freq
start = at_s_freq(self.sel_xy[0])
end = at_s_freq(x_in_scene)
if abs(end - start) < self.parent.value('min_marker_dur'):
end = start
if start <= end:
time = (start, end)
else:
time = (end, start)
if chk_marker:
self.parent.notes.add_bookmark(time)
elif chk_event and start != end:
eventtype = self.parent.notes.idx_eventtype.currentText()
# if dragged across > 1.5 chan, event is marked on all chan
if abs(y_in_scene - self.sel_xy[1]) > 1.5 * y_distance:
chan = ''
else:
chan_idx = int(floor(self.sel_xy[1] / y_distance))
chan = self.chan[chan_idx]
self.parent.notes.add_event(eventtype, time, chan)
else: # normal selection
if self.idx_info in self.scene.items():
self.scene.removeItem(self.idx_info)
self.idx_info = None
# restore spectrum
self.parent.spectrum.update()
self.parent.spectrum.display_window()
# general garbage collection
self.sel_chan = None
self.sel_xy = (None, None)
if self.idx_sel in self.scene.items():
self.scene.removeItem(self.idx_sel)
self.idx_sel = None
[docs] def keyPressEvent(self, event):
chk_event = self.parent.notes.action['new_event'].isChecked()
chk_book = self.parent.notes.action['new_bookmark'].isChecked()
if not ((chk_event or chk_book) and self.event_sel):
return
annot = self.event_sel
highlight = self.highlight
annot_start = annot.marker.x()
annot_end = annot_start + annot.marker.width()
if type(event) == QKeyEvent and (
event.key() == Qt.Key_Delete or event.key() == Qt.Key_Backspace):
if chk_event:
self.parent.notes.remove_event(time=(annot_start, annot_end))
elif chk_book:
self.parent.notes.remove_bookmark(
time=(annot_start, annot_end))
self.scene.removeItem(highlight)
msg = 'Deleted event from {} to {}'.format(annot_start, annot_end)
self.parent.statusBar().showMessage(msg)
self.event_sel = None
self.highlight = None
self.parent.notes.idx_eventtype.setCurrentText(self.current_etype)
self.current_etype = None
self.current_event = None
self.display_annotations
[docs] def highlight_event(self, annot):
"""Highlight an annotation on the trace.
Parameters
----------
annot : intance of wonambi.widgets.utils.RectMarker
existing annotation
"""
beg = annot.marker.x()
end = beg + annot.marker.width()
window_start = self.parent.value('window_start')
window_length = self.parent.value('window_length')
events = self.parent.notes.get_selected_events((window_start,
window_start + window_length))
ev = [x for x in events if (x['start'] == annot.marker.x() or \
x['end'] == annot.marker.y())]
if ev:
annot_name = ev[0]['name']
msg = "Event of type '{}' from {} to {}".format(
annot_name, beg, end)
self.current_etype = self.parent.notes.idx_eventtype.currentText()
self.parent.notes.idx_eventtype.setCurrentText(annot_name)
self.current_event = ev[0]
else:
msg = "Marker from {} to {}".format(beg, end)
self.parent.statusBar().showMessage(msg)
highlight = self.highlight = RectMarker(annot.marker.x(),
annot.marker.y(),
annot.marker.width(),
annot.marker.height(),
zvalue=-5,
color=QColor(255, 255, 51))
self.scene.addItem(highlight)
self.event_sel = annot
[docs] def next_event(self, delete=False):
"""Go to next event."""
if delete:
msg = "Delete this event? This cannot be undone."
msgbox = QMessageBox(QMessageBox.Question, 'Delete event', msg)
msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
msgbox.setDefaultButton(QMessageBox.Yes)
response = msgbox.exec_()
if response == QMessageBox.No:
return
event_sel = self.event_sel
if event_sel is None:
return
notes = self.parent.notes
if not self.current_event_row:
row = notes.find_row(event_sel.marker.x(),
event_sel.marker.x() + event_sel.marker.width())
else:
row = self.current_event_row
same_type = self.action['next_of_same_type'].isChecked()
if same_type:
target = notes.idx_annot_list.item(row, 2).text()
if delete:
notes.delete_row()
msg = 'Deleted event from {} to {}.'.format(event_sel.marker.x(),
event_sel.marker.x() + event_sel.marker.width())
self.parent.statusBar().showMessage(msg)
row -= 1
if row + 1 == notes.idx_annot_list.rowCount():
return
if not same_type:
next_row = row + 1
else:
next_row = None
types = notes.idx_annot_list.property('name')[row + 1:]
for i, ty in enumerate(types):
if ty == target:
next_row = row + 1 + i
break
if next_row is None:
return
self.current_event_row = next_row
notes.go_to_marker(next_row, 0, 'annot')
notes.idx_annot_list.setCurrentCell(next_row, 0)
[docs] def change_event_type(self):
"""Action: change highlighted event's type by cycling through event
type list."""
if self.current_event is None:
return
hl_params = self.highlight.params
self.scene.removeItem(self.highlight)
ev = self.current_event
new_name = self.parent.notes.change_event_type(name=ev['name'],
time=(ev['start'],
ev['end']),
chan=ev['chan'])
msg = "Event from {} to {} changed type from '{}' to '{}'".format(
ev['start'], ev['end'], ev['name'], new_name)
ev['name'] = new_name
self.current_event = ev
self.current_etype = new_name
#self.event_sel = True
self.parent.notes.idx_eventtype.setCurrentText(new_name)
self.parent.statusBar().showMessage(msg)
self.display_annotations()
self.highlight = RectMarker(*hl_params)
self.scene.addItem(self.highlight)
[docs] def resizeEvent(self, event):
"""Resize scene so that it fits the whole widget.
Parameters
----------
event : instance of QtCore.QEvent
not important
Notes
-----
This function overwrites Qt function, therefore the non-standard
name. Argument also depends on Qt.
The function is used to change the scale of view, so that the scene
fits the whole scene. There are two problems that I could not fix: 1)
how to give the width of the label in absolute width, 2) how to strech
scene just enough that it doesn't trigger a scrollbar. However, it's
pretty good as it is now.
"""
if self.scene is not None:
ratio = self.width() / (self.scene.width() * 1.1)
self.resetTransform()
self.scale(ratio, 1)
[docs] def reset(self):
self.y_scrollbar_value = 0
self.data = None
self.chan = []
self.chan_pos = []
self.chan_scale = []
self.sel_chan = None
self.sel_xy = (None, None)
if self.scene is not None:
self.scene.clear()
self.scene = None
self.idx_sel = None
self.idx_info = None
self.idx_label = []
self.idx_time = []
self.time_pos = []
def _create_data_to_plot(data, chan_groups):
"""Create data after montage and filtering.
Parameters
----------
data : instance of ChanTime
the raw data
chan_groups : list of dict
information about channels to plot, to use as reference and about
filtering etc.
Returns
-------
instance of ChanTime
data ready to be plotted.
"""
# chan_to_plot only gives the number of channels to plot, for prealloc
chan_to_plot = [one_chan for one_grp in chan_groups
for one_chan in one_grp['chan_to_plot']]
output = ChanTime()
output.s_freq = data.s_freq
output.start_time = data.start_time
output.axis['time'] = data.axis['time']
output.axis['chan'] = empty(1, dtype='O')
output.data = empty(1, dtype='O')
output.data[0] = empty((len(chan_to_plot), data.number_of('time')[0]),
dtype='f')
all_chan_grp_name = []
i_ch = 0
for one_grp in chan_groups:
sel_data = _select_channels(data,
one_grp['chan_to_plot'] +
one_grp['ref_chan'])
data1 = montage(sel_data, ref_chan=one_grp['ref_chan'])
data1.data[0] = nan_to_num(data1.data[0])
if one_grp['hp'] is not None:
data1 = filter_(data1, low_cut=one_grp['hp'])
if one_grp['lp'] is not None:
data1 = filter_(data1, high_cut=one_grp['lp'])
if one_grp['notch'] is not None:
data1 = filter_(data1, ftype='notch', notchfreq=one_grp['notch'])
for chan in one_grp['chan_to_plot']:
chan_grp_name = chan + ' (' + one_grp['name'] + ')'
all_chan_grp_name.append(chan_grp_name)
dat = data1(chan=chan, trial=0)
if one_grp['demean']:
dat = dat - nanmean(dat)
output.data[0][i_ch, :] = dat * one_grp['scale']
i_ch += 1
output.axis['chan'][0] = asarray(all_chan_grp_name, dtype='U')
return output
def _convert_timestr_to_seconds(time_str, rec_start):
"""Convert input from user about time string to an absolute time for
the recordings.
Parameters
----------
time_str : str
time information as '123' or '22:30' or '22:30:22'
rec_start: instance of datetime
absolute start time of the recordings.
Returns
-------
int
start time of the window, in s, from the start of the recordings
Raises
------
ValueError
if it cannot convert the string
"""
if not CHECK_TIME_STR.match(time_str):
raise ValueError('Input can only contain digits and colons')
if ':' in time_str:
time_split = [int(x) for x in time_str.split(':')]
# if it's in 'HH:MM' format, add ':SS'
if len(time_split) == 2:
time_split.append(0)
clock_time = time(*time_split)
chosen_start = datetime.combine(rec_start.date(), clock_time)
# if the clock time is after start of the recordings, assume it's the next day
if clock_time < rec_start.time():
chosen_start += timedelta(days=1)
window_start = int((chosen_start - rec_start).total_seconds())
else:
window_start = int(time_str)
return window_start