Source code for uos_activpal.gui.raw_viewer

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""A GUI application to view the raw activPAL accelerometer data."""

# Created on 21 Jun 2017
#
# @author: R-Broadley

import sys
import os
from datetime import datetime
from PyQt5 import QtCore
from PyQt5.QtWidgets import (QFileDialog, QWidget, QVBoxLayout, QLabel,
                             QAction, qApp)
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.gridspec import GridSpec
import matplotlib.dates as mdates
from .base import BaseMainWindow, SpacerWidget
from ..io.raw import ActivpalData


[docs]class MainWindow(BaseMainWindow): """A BaseMainWindow subclass which displays a plot of raw activPAL data."""
[docs] def __init__(self, parent=None): """ Create an instance of a MainWindow. Parameters ---------- parent : object The parent object. """ super(MainWindow, self).__init__(parent) self.file_path = None self.show() self._setup_window() self._setup_toolbar() self.statusBar().showMessage('Requesting File ...') qApp.processEvents() self.select_file() self.statusBar().showMessage('Plotting File ...') self.run_main() qApp.processEvents() self.statusBar().showMessage('Ready')
def _add_plot_controls(self): """Add pan and zoom controls to main toolbar.""" zoomlabel = QLabel('Zoom') zoominAct = QAction('+', self) # import ipdb; ipdb.set_trace() zoominAct.triggered.connect( lambda: self.FilePlot.basic_zoom(scale_factor=0.5)) zoomoutAct = QAction('–', self) zoomoutAct.triggered.connect( lambda: self.FilePlot.basic_zoom(scale_factor=2)) self.toolbar.addAction(zoominAct) self.toolbar.addWidget(zoomlabel) self.toolbar.addAction(zoomoutAct) self.toolbar.addSeparator() panlabel = QLabel('Pan') panlAct = QAction('←', self) panlAct.triggered.connect( lambda: self.FilePlot.basic_pan(move_factor=-0.1)) panrAct = QAction('→', self) panrAct.triggered.connect( lambda: self.FilePlot.basic_pan(move_factor=0.1)) self.toolbar.addAction(panlAct) self.toolbar.addWidget(panlabel) self.toolbar.addAction(panrAct) def _setup_toolbar(self): """Set up toolbar with plot controls (zoom and pan).""" self.toolbar.addWidget(SpacerWidget()) self._add_plot_controls() self.toolbar.addWidget(SpacerWidget()) # @QtCore.pyqtSlot() def select_file(self): """Bring up a file dialog to pick a file.""" file_dir = os.path.expanduser('~') self.file_path, _ = QFileDialog.getOpenFileName( self, "Select Files", file_dir, "activpal data (*.dat, *.datx)") def _setup_window(self): """Set up window and plot canvas.""" self.FilePlot = UIFilePlot(parent=self) self.setWindowTitle("Raw activPAL Data Viewer") self.setCentralWidget(self.FilePlot) def run_main(self): """Fetch a data file and plot it.""" self.FilePlot.load_data(self.file_path) self.FilePlot.new_plot()
[docs]class UIFilePlot(QWidget): """A QWidget which displays activPAL raw data (x, y, z and rss)."""
[docs] def __init__(self, parent=None, file_path=None, center=None, width=None): """Create a UIFilePlot widget.""" super(UIFilePlot, self).__init__(parent) self.file_path = file_path main_layout = QVBoxLayout() self._create_canvas() main_layout.addWidget(self.canvas) self.canvas.mpl_connect('key_press_event', self._key_press) self.setLayout(main_layout) self.setMinimumHeight(500) self.setMinimumWidth(500) if self.file_path is not None: self.load_data()
def _create_canvas(self): """Create the plot canvas.""" self.fig = Figure(dpi=100, facecolor='none', frameon=False) self.canvas = FigureCanvas(self.fig) gs = GridSpec(3, 1, height_ratios=[3.5, 6, 0.5], hspace=0, left=0.1, top=0.95, right=0.95) self.axes = [self.fig.add_subplot(gs[0])] for i in range(1, 3): self.axes.append(self.fig.add_subplot(gs[i], sharex=self.axes[0])) self.axes[1].set_prop_cycle('color', ['r', 'g', 'b']) self.axes[0].yaxis.grid(True) self.axes[1].yaxis.grid(True) self.axes[0].axes.get_xaxis().set_visible(False) self.axes[1].axes.get_xaxis().set_visible(False) self.axes[0].set_ylim([0, 3.5]) self.axes[1].set_ylim([-2, 2]) self.axes[2].set_ylim([0, 1]) self.axes[0].set_yticks([0.5, 1, 1.5, 2, 2.5, 3, 3.5]) self.axes[2].axes.get_yaxis().set_visible(False) self.axes[0].set_ylabel('RSS Acceleration (g)') self.axes[1].set_ylabel('3D Acceleration (g)') self.canvas.setFocusPolicy(QtCore.Qt.WheelFocus) self.canvas.setFocus() self.canvas.draw() def load_data(self, file_path=None): """ Load the given data file. Parameters ---------- file_path : str The path to a raw activPAL data file. """ if file_path is not None: self.file_path = file_path self.data = ActivpalData(self.file_path) def new_plot(self, center=None, width=None): """ Create a fresh plot from the loaded file. Parameters ---------- center : float The point the plot should be horizontally centered around. width : float The width of the plot (range of the x axis). """ self.clear_plot() if center is None and width is not None: # Set center start_datetime = mdates.date2num(self.data.signals.index[0]) center = start_datetime + (width / 2) if width is not None: # Set X lim (zoom) if isinstance(center, datetime): center = mdates.date2num(center) half_win = width / 2 new_xlim = (center - half_win, center + half_win) else: new_xlim = (mdates.date2num(self.data.signals.index[0]), mdates.date2num(self.data.signals.index[-1])) self.axes[0].set_xlim(new_xlim) self.axes[0].set_title(os.path.basename(self.file_path)) self.axes[0].plot(self.data.signals.index, self.data.rss, color='tab:blue') self.axes[1].plot(self.data.signals.index, self.data.signals[['x', 'y', 'z']], alpha=0.6) self._set_xticks() self.canvas.draw() def _set_xticks(self): """Set the format and location of the plots xticks.""" loc = mdates.AutoDateLocator(minticks=3, maxticks=7) datetimefmt = mdates.AutoDateFormatter(loc) self.axes[0].xaxis.set_major_formatter(datetimefmt) self.axes[0].xaxis.set_major_locator(loc) def clear_plot(self): """Clear lines from the plot.""" for i in range(len(self.axes)): lines = self.axes[i].get_lines() for j in range(len(lines)): lines.pop(0).remove() self.canvas.draw() def _key_press(self, event): """Respond to key presses.""" zoom_keys = {'up', 'down', 'shift+up', 'shift+down'} pan_keys = {'left', 'right', 'shift+left', 'shift+right'} if event.key in zoom_keys: self._keyboard_zoom(event) elif event.key in pan_keys: self._keyboard_pan(event) # else: # # Debug code # print(event.key) def basic_zoom(self, scale_factor=2, xhold=None): """ Zoom in on the plot (horizontally). Parameters ---------- scale_factor : int The path to a raw activPAL data file. xhold : The x axis value to hold static. """ ax = self.axes[0] cur_xlim = ax.get_xlim() cur_xrange = (cur_xlim[1] - cur_xlim[0]) # set new limits new_xrange = cur_xrange * scale_factor if xhold is None: # Make xhold the (x)center of the plot xhold = cur_xlim[0] + (cur_xrange / 2) # Set minimum xrange as the time between samples / data points try: min_xrange = 1 / (86400 * self.data.metadata.hz) except AttributeError: min_xrange = 1 / 86400 if new_xrange > min_xrange: cur_left_proportion = (xhold - cur_xlim[0]) / cur_xrange new_left_distance = cur_left_proportion * new_xrange new_right_distance = new_xrange - new_left_distance new_xlim = (xhold - new_left_distance, xhold + new_right_distance) ax.set_xlim(new_xlim) self._set_xticks() self.canvas.draw() def _keyboard_zoom(self, event): """Control zoom using keyboard up and down arrow keys.""" if event.inaxes is not None: xdata = event.xdata else: # basic_zoom will hold center of the xaxis if xhold/xdata is None xdata = None base_scale = 2 if event.key == 'up': # deal with zoom in scale_factor = 1/base_scale elif event.key == 'down': # deal with zoom out scale_factor = base_scale elif event.key == 'shift+up': # deal with zoom in scale_factor = 1 - ((1/base_scale) * 0.1) elif event.key == 'shift+down': # deal with zoom out scale_factor = 1 + ((base_scale - 1) * 0.1) else: # deal with something that should never happen scale_factor = 1 self.basic_zoom(scale_factor=scale_factor, xhold=xdata) def basic_pan(self, move_factor=0): """ Pan the plot (horizontally). Parameters ---------- move_factor : float Decimal fraction which controls how far to jump left (-) or right (+). """ ax = self.axes[0] cur_xlim = ax.get_xlim() cur_xrange = (cur_xlim[1] - cur_xlim[0]) mv_amount = cur_xrange * move_factor new_xlim = (cur_xlim[0] + mv_amount, cur_xlim[1] + mv_amount) # Not trying to scroll too far left cond1 = new_xlim[1] > mdates.date2num(self.data.signals.index[0]) # Not trying to scroll too far right cond2 = new_xlim[0] < mdates.date2num(self.data.signals.index[-1]) if cond1 and cond2: ax.set_xlim(new_xlim) self._set_xticks() self.canvas.draw() def _keyboard_pan(self, event): """Control pan using keyboard left and right arrow keys.""" base_scale = 0.5 if event.key == 'left': # deal with zoom in move_factor = 0 - base_scale elif event.key == 'right': # deal with zoom out move_factor = base_scale elif event.key == 'shift+left': # deal with zoom in move_factor = 0 - (base_scale * 0.1) elif event.key == 'shift+right': # deal with zoom out move_factor = base_scale * 0.1 self.basic_pan(move_factor=move_factor)
if __name__ == '__main__': from PyQt5.QtWidgets import QApplication app = QApplication(sys.argv) w = MainWindow() w.show() sys.exit(app.exec_())