diff --git a/nixview/ui/plotscreen.py b/nixview/ui/plotscreen.py index 22ae796..8d724e8 100644 --- a/nixview/ui/plotscreen.py +++ b/nixview/ui/plotscreen.py @@ -1,3 +1,4 @@ +from nixview.util import dataview from nixview.util.enums import PlotterTypes from PyQt5.QtWidgets import QGroupBox, QHBoxLayout, QLabel, QPushButton, QSizePolicy, QSlider, QVBoxLayout, QWidget from PyQt5.QtCore import pyqtSignal, Qt @@ -31,6 +32,11 @@ def create_label(item): class MplCanvas(FigureCanvas): + """ MplCanvas extends FigureCanvasQtAgg Matplotlib backend. + + Args: + FigureCanvas ([type]): [description] + """ view_changed = pyqtSignal() def __init__(self, parent=None, width=5, height=4, dpi=100): @@ -46,24 +52,28 @@ class MplCanvas(FigureCanvas): def on_enter_figure(self, event): - print('enter_figure', event.canvas.figure) + # print('enter_figure', event.canvas.figure) # event.canvas.figure.patch.set_facecolor('red') # event.canvas.draw() + pass def on_leave_figure(self, event): - print('leave_figure', event.canvas.figure) + # print('leave_figure', event.canvas.figure) # event.canvas.figure.patch.set_facecolor('grey') # event.canvas.draw() + pass def on_enter_axes(self, event): - print('enter_axes', event.inaxes) + # print('enter_axes', event.inaxes) # event.inaxes.patch.set_facecolor('yellow') # event.canvas.draw() + pass def on_leave_axes(self, event): - print('leave_axes', event.inaxes) + # print('leave_axes', event.inaxes) # event.inaxes.patch.set_facecolor('white') # event.canvas.draw() + pass def on_pick(self, event): line = event.artist @@ -81,6 +91,11 @@ class MplCanvas(FigureCanvas): class Plotter(MplCanvas): + """ Abstract class for visual display of data (plotting) + Inheriting classes need to implement the current_view and is_full_view methods + + Plotter extends MplCanvas. + """ def __init__(self, file_handler, item, data_view, parent=None) -> None: super().__init__(parent=parent) self._file_handler = file_handler @@ -90,9 +105,13 @@ class Plotter(MplCanvas): def current_view(self): raise NotImplementedError("current_view is not implemented on the current plotter") + @property def is_full_view(self): raise NotImplementedError("is_full_view is not implemented on the current plotter") + @property + def can_pan_horizontally(self): + raise NotImplementedError("can_pan_left is not implemented on the current plotter") class EventPlotter(Plotter): @@ -264,17 +283,17 @@ class ImagePlotter(Plotter): class LinePlotter(Plotter): + """ LinePlotter extends and implements the Plotter class. It shows line plot data. Either single or multiple line + + Args: + Plotter ([type]): [description] + """ def __init__(self, file_handler, item, data_view, xdim=-1, parent=None): super().__init__(file_handler, item, data_view, parent) #self.canvas = FigureCanvas(self.figure) #self.toolbar = NavigationToolbar(self.canvas, self) - self.dimensions = self._file_handler.request_dimensions(self._item.block_id, self._item.id) - self._min_x = None - self._max_x = None - self._min_x = None - self.min_x = None self.lines = [] self.dim_count = len(self._dataview.full_shape) if xdim == -1: @@ -284,71 +303,114 @@ class LinePlotter(Plotter): "Cannot plot that kind of data") else: self.xdim = xdim + self._data_xmin = 0 + self._data_xmax = self._dataview.current_shape[self.xdim] + + self._abs_xmin = 0 + self._abs_xmax = self._dataview.full_shape[self.xdim] + + self._view_xmin = 0 + self._view_xmax = 0 self.axis.callbacks.connect('xlim_changed', self.on_xlims_change) self.axis.callbacks.connect('ylim_changed', self.on_ylims_change) + self._zoom_level = 0 + self._segment_length = 0 def on_xlims_change(self, event_ax): - print("updated xlims: ", event_ax.get_xlim()) - + #print("updated xlims: ", event_ax.get_xlim()) + pass + def on_ylims_change(self, event_ax): - - print("updated ylims: ", event_ax.get_ylim()) - + # print("updated ylims: ", event_ax.get_ylim()) + pass + def current_view(self): cv = [] return cv @property def is_full_view(self): - xlims = self.axis.get_xlim() - full = self._min_x == xlims[0] and self._max_x == xlims[-1] - full = full and self._dataview.fully_loaded + full = self._data_xmin == self._view_xmin and self._data_xmax == self._view_xmax return full + @property + def can_pan_horizontally(self): + return self.can_pan_left or self.can_pan_right + + @property + def can_pan_left(self): + return self._view_xmin > self._abs_xmin + + @property + def can_pan_right(self): + return self._view_xmax < self._abs_xmax + + @property + def horizontal_pan_position(self): + return self._view_xmax/self._abs_xmax + + def horizontal_pan_to_position(self, new_position, zoomlevel): + new_xmax = int(np.min([np.ceil(new_position * self._abs_xmax), self._abs_xmax])) + segment_length = zoomlevel * self._abs_xmax + start = np.max([0, new_xmax - segment_length]) + while not self._dataview.fully_loaded and new_xmax < self._dataview.current_shape[self.xdim]: + self._dataview.request_more() + + self.plot(start, zoomlevel) + def on_zoom_in(self, new_position): print("plotter ZOOM In!", new_position) def on_zoom_out(self, new_position): print("plotter ZOOM out!", new_position) - def plot(self, maxpoints=100000): - self.maxpoints = maxpoints + def plot(self, start=0, zoomlevel=1.0): + if zoomlevel > 1: + zoomlevel = 1.0 + self._segment_length = zoomlevel * self._abs_xmax + self._zoom_level = zoomlevel if self.dim_count > 2: return if self.dim_count == 1: - self.plot_array_1d() - else: - self.plot_array_2d() - - def __draw(self, start, end): - if self.dim_count == 1: - self.__draw_1d(start, end) + self.plot_array_1d(start) else: - self.__draw_2d(start, end) + self.plot_array_2d(start) - def _set_xlims(self, data_xmin, data_xmax): - if self._min_x is None or data_xmin < self._min_x: - self._min_x = data_xmin - if self._max_x is None or data_xmax > self._max_x: - self._max_x = data_xmax + def _update_abs_extremes(self, display_x_min, display_xmax): + if self._data_xmin is None or display_x_min < self._data_xmin: + self._data_xmin = display_x_min + if self._data_xmax is None or display_xmax > self._data_xmax: + self._data_xmax = display_xmax + def _update_current_view(self, current_xmin, current_xmax): + self._view_xmax = current_xmax + self._view_xmin = current_xmin + def __draw_1d(self, start, end): + """ draw the data from start to end index. + + Args: + start (int): start index in the data + end (int): end index in the data + """ if start < 0: start = 0 if end > self._dataview.current_shape[self.xdim]: end = self._dataview.current_shape[self.xdim] - - y = self._dataview._buffer[int(start):int(end)] - x = self._file_handler.request_axis(self._item.block_id, self._item.id, 0, len(y), start) - self._set_xlims(x[0], x[-1]) + y_values = self._dataview._buffer[int(start):int(end)] + x_values = self._file_handler.request_axis(self._item.block_id, self._item.id, 0, len(y_values), int(start)) + self._update_abs_extremes(start, end) + self._update_current_view(start, end) if len(self.lines) == 0: - l, = self.axis.plot(x, y, label=self._item.name, picker=5) + label = self._item.name + l, = self.axis.plot(x_values, y_values, label=label) + l.set_pickradius(5) self.lines.append(l) else: - self.lines[0].set_ydata(y) - self.lines[0].set_xdata(x) - - self.axis.set_xlim([x[0], x[-1]]) + self.lines[-1].set_data(x_values[:len(y_values)], y_values) + self.figure.canvas.draw_idle() + self.axis.set_ylim([np.min(y_values), np.max(y_values)]) + self.axis.set_xlim([x_values[0], x_values[-1]]) def __draw_2d(self, start, end): if start < 0: @@ -359,7 +421,7 @@ class LinePlotter(Plotter): x = self._file_handler.request_axis(self._item.block_id, self._item.id, self.xdim, int(end-start), start) line_count = self._dataview.current_shape[1 - self.xdim] line_labels = self._file_handler.request_axis(self._item.block_id, self._item.id, 1-self.xdim, line_count, 0) - self._set_xlims(x[0], x[-1]) + self._update_abs_extremes(x[0], x[-1]) for i, l in enumerate(line_labels): if (self.xdim == 0): @@ -376,8 +438,8 @@ class LinePlotter(Plotter): self.axis.set_xlim([x[0], x[-1]]) - def plot_array_1d(self): - self.__draw_1d(0, self.maxpoints) + def plot_array_1d(self, start=0): + self.__draw_1d(start, start + self._segment_length) xlabel = create_label(self.dimensions[self.xdim]) ylabel = create_label(self._item) self.axis.set_xlabel(xlabel) @@ -425,7 +487,8 @@ class PlotScreen(QWidget): self.layout().addWidget(close_btn) self._data_view = None - self.zoom_position = 0 + self._software_slide = False + self.plotter = None def _create_plot_controls(self): plot_controls = QGroupBox() @@ -437,10 +500,10 @@ class PlotScreen(QWidget): self._zoom_slider.setFixedWidth(120) self._zoom_slider.setFixedHeight(20) self._zoom_slider.setTickPosition(QSlider.TicksBelow) - self._zoom_slider.setSliderPosition(50) - self._zoom_slider.setMinimum(0) - self._zoom_slider.setMaximum(100) - self._zoom_slider.setTickInterval(25) + self._zoom_slider.setSliderPosition(500) + self._zoom_slider.setMinimum(1) + self._zoom_slider.setMaximum(1000) + self._zoom_slider.setTickInterval(250) self._zoom_slider.valueChanged.connect(self.on_zoom) self._pan_slider = QSlider(Qt.Horizontal) @@ -448,9 +511,9 @@ class PlotScreen(QWidget): self._pan_slider.setFixedHeight(20) self._pan_slider.setTickPosition(QSlider.TicksBelow) self._pan_slider.setSliderPosition(0) - self._pan_slider.setMinimum(0) - self._pan_slider.setMaximum(100) - self._pan_slider.setTickInterval(25) + self._pan_slider.setMinimum(1) + self._pan_slider.setMaximum(1000) + self._pan_slider.setTickInterval(250) self._pan_slider.valueChanged.connect(self.on_pan) pl = QLabel("horiz. pos.:") @@ -471,23 +534,22 @@ class PlotScreen(QWidget): self.close_signal.emit() def on_zoom(self, new_position): - if self.zoom_position < new_position: - self.plotter.on_zoom_out(new_position) - else: - self.plotter.on_zoom_out(new_position) - self.zoom_position = new_position + if self.plotter is None: + return + if self._software_slide: + self._software_slide = False + return + self.plotter.horizontal_pan_to_position(self._pan_slider.sliderPosition()/1000, self._zoom_slider.sliderPosition()/1000) def on_pan(self, new_position): - - print("pan", new_position) + # self._pan_slider.setEnabled(False) + if self._software_slide: + self._software_slide = False + return + self.plotter.horizontal_pan_to_position(new_position/1000., self._zoom_slider.sliderPosition()/1000.) def on_view_changed(self): - print("view changed!") - print(self.plotter.current_view()) - if self.plotter.is_full_view: - self._zoom_slider.setSliderPosition(100) - self._pan_slider.setEnabled(not self.plotter.is_full_view) - + self._pan_slider.setEnabled(self.plotter.can_pan_horizontally) def plot(self, item): try: @@ -497,13 +559,13 @@ class PlotScreen(QWidget): return if self._data_view is None: return - #while not self._data_view.fully_loaded: - # self._data_view.request_more() # TODO this is just a test, needs to be removed + if item.suggested_plotter == PlotterTypes.LinePlotter: + zoom_slider_position = np.round(1000 / (self._data_view.full_shape[item.best_xdim] / 50000)) + self._software_slide = True + self._zoom_slider.setSliderPosition(zoom_slider_position) self.plotter = LinePlotter(self._file_handler, item, self._data_view) self.plotter.view_changed.connect(self.on_view_changed) self._container.set_plotter(self.plotter) - self.plotter.plot(maxpoints=10000) - self._zoom_slider.setSliderPosition(100) - self.zoom_position = 100 - + self.plotter.plot(zoomlevel=zoom_slider_position/1000.) + self._software_slide = False