class TkBase:
    id_generator = itertools.count(1)

    def __init__(self, master, path, toolitems):
        logging.basicConfig(filename='event_log.log', level=logging.INFO)
        FIGSIZE = (8, 3)
        self.window_id = next(self.id_generator)
        self.master = master
        self.toolitems = toolitems

        self.master.protocol("WM_DELETE_WINDOW", self.master.quit)
        master.title("BrainWave Visualization")
        master.state('zoomed')
        master.protocol("WM_DELETE_WINDOW", self.root_close)

        self.initialize_annotation_display()

        self.initialize_graph_display(FIGSIZE)

        self.project_path = path
        self.json_path = self.project_path + "annotations.json"
        try:
            self.data, self.timestamps, self.annotations = data.open_project(
                path)
            if self.annotations != []:
                if self.annotations[0] == -1:
                    messagebox.showerror("Error: ", self.annotations[1])
                    self.annotations = []
            self.draw_graph(self.data, self.timestamps, self.annotations)
            for id in self.annotations:
                id = id.id
                self.index_to_ids.append(id)
            for a in self.annotations:
                self.listb.insert(tkinter.END, a.title)
        except Exception as e:
            logging.error('Error during opening initial project')
            logging.error(e)
            messagebox.showerror("Error:", e)

        # put the plot with navbar on the tkinter window
        self.main_canvas.mpl_connect('button_release_event', self.butrelease)

        # add span selector to the axes but set it defaultly to not visible,
        # only activate it when the button annotate is pressed
        self.span = SpanSelector(self.main_graph_ax,
                                 self.onselect,
                                 'horizontal',
                                 useblit=True,
                                 rectprops=dict(alpha=0.5, facecolor='red'),
                                 span_stays=True)
        self.span.set_visible(False)

        # create buttons for interaction
        self.annotate_button = Button(master, command=self.annotate)

        self.export_button = Button(master, command=self.export)

        self.close_button = Button(master, command=master.quit)

        # variables for storing min and max of the current span selection
        self.span_min = None
        self.span_max = None

    def initialize_annotation_display(self):
        """
        initializes the functionalities of the annotation display like the list and the buttons to browse annotations
        """
        logging.info('Initializing annotation display')

        self.id_to_shape = dict()

        self.annotation_frame = tkinter.Frame(self.master, bg="#949494")
        self.annotation_frame.pack(side=tkinter.RIGHT, padx=(10, 10))

        self.listbox_frame = tkinter.Frame(self.annotation_frame, bg="#949494")
        self.listbox_frame.pack()

        # list to convert from indices in listbox to annotation ids
        self.index_to_ids = list()

        self.scrollbar = Scrollbar(self.listbox_frame, orient=tkinter.VERTICAL)
        self.listb = tkinter.Listbox(self.listbox_frame,
                                     width=30,
                                     height=int(0.1 *
                                                self.master.winfo_reqheight()),
                                     yscrollcommand=self.scrollbar.set)
        self.scrollbar.config(command=self.listb.yview)
        self.scrollbar.pack(side="right", fill="y")

        self.listb.bind('<<ListboxSelect>>', self.listbox_selection)
        self.listb.pack(side="bottom", fill="y")

        self.labelTitle = tkinter.Label(self.annotation_frame,
                                        text="Title:",
                                        bg="#949494",
                                        anchor='w')
        self.labelTitle.pack(side="top")

        self.labelDescription = tkinter.Label(self.annotation_frame,
                                              text="description:",
                                              wraplength=150,
                                              bg="#949494",
                                              anchor='w')
        self.labelDescription.pack(side="top")

        self.go_to_annotation = ttk.Button(self.annotation_frame,
                                           text='Go-To',
                                           width=30,
                                           command=self.goto_callback)
        self.go_to_annotation.pack(side="top")

        self.edit_annotation = ttk.Button(self.annotation_frame,
                                          text='Edit',
                                          width=30,
                                          command=self.edit_callback)
        self.edit_annotation.pack(side="top")

        self.delete_annotation = ttk.Button(self.annotation_frame,
                                            text='Delete',
                                            width=30,
                                            command=self.delete_callback)
        self.delete_annotation.pack(side="top")

    def initialize_graph_display(self, FIGSIZE):
        """
        initializes the functionalities of the graph display including the main and reference graph
        """

        logging.info('Initializing graph display')

        # create matplotlib figures with single axes on which the data will be
        # displayed
        self.main_graph, self.main_graph_ax = plt.subplots(figsize=FIGSIZE)
        self.main_graph.set_facecolor('xkcd:grey')
        self.main_graph_ax.set_facecolor('xkcd:dark grey')

        # second, reference graph
        self.reference_graph, self.reference_graph_ax = plt.subplots(
            figsize=FIGSIZE)
        self.reference_graph.set_facecolor('xkcd:grey')
        self.reference_graph_ax.set_facecolor('xkcd:dark grey')
        self.main_canvas = FigureCanvasTkAgg(self.main_graph,
                                             master=self.master)
        self.main_canvas.get_tk_widget().pack(side=tkinter.BOTTOM,
                                              fill=tkinter.BOTH,
                                              expand=1)
        self.toolbar = NavigationToolbar(self.main_canvas,
                                         self.master,
                                         tkbase_=self,
                                         toolitems=self.toolitems)

        self.reference_canvas = FigureCanvasTkAgg(self.reference_graph,
                                                  master=self.master)
        self.reference_canvas.get_tk_widget().pack(side=tkinter.BOTTOM,
                                                   fill=tkinter.BOTH,
                                                   expand=1)

    def open(self):
        """
         callback method for the open button, opens an existing project
        """
        logging.info('Opening file...')
        path = filedialog.askdirectory()
        logging.info('Path given: {}'.format(path))
        self.project_path = path + "/"
        self.json_path = self.project_path + "annotations.json"
        try:
            logging.info('Checking validity of path')
            if data.check_valid_path(path):
                self.data, self.timestamps, self.annotations = data.open_project(
                    self.project_path)
                if self.annotations != []:
                    if self.annotations[0] == -1:
                        messagebox.showerror("Error: ", self.annotations[1])
                        self.annotations = []
                self.draw_graph(self.data, self.timestamps, self.annotations)
                self.index_to_ids = list()
                self.id_to_shape = dict()
                for id in self.annotations:
                    id = id.id
                    self.index_to_ids.append(id)
                self.listb.delete(0, tkinter.END)
                for a in self.annotations:
                    self.listb.insert(tkinter.END, a.title)
                self.span = SpanSelector(self.main_graph_ax,
                                         self.onselect,
                                         'horizontal',
                                         useblit=True,
                                         rectprops=dict(alpha=0.5,
                                                        facecolor='red'),
                                         span_stays=True)
                self.span.set_visible(False)
                self.span_min = None
                self.span_max = None
            else:
                logging.warning('Invalid path given.')
            logging.info('File open successfully')
        except Exception as e:
            logging.error(e)
            messagebox.showerror("Error:", e)

    def open_concurrent(self):
        """
        callback method for the open concurrent button, opens a new window with a new project identical in functionality to the original application
        """

        logging.info('Opening concurrent window')
        second_toolitems = (
            ('Home', 'Reset original view', 'home', 'home'),
            ('Back', 'Back to previous view', 'back', 'back'),
            ('Forward', 'Forward to next view', 'forward', 'forward'),
            (None, None, None, None),
            ('Pan', 'Pan axes with left mouse, zoom with right', 'move',
             'pan'),
            ('Zoom', 'Zoom to rectangle', 'zoom_to_rect', 'zoom'),
            ('Subplots', 'Configure subplots', 'subplots',
             'configure_subplots'),
            (None, None, None, None),
            ('Annotate', 'Create an annotation', 'annotate', 'call_annotate'),
            ('Confirm', 'Confirm annotation', 'confirm', 'call_confirm'),
            (None, None, None, None),
            ('Open', 'Opens a new project', 'open', 'call_open'),
            ('Export', 'Export to PDF', 'export', 'call_export'),
            ('Save', 'Save the graph as PNG', 'filesave', 'save_figure'),
            (None, None, None, None),
            ('Quit', 'Quit application', 'quit', 'call_quit'),
        )
        path = filedialog.askdirectory()
        path = path + "/"
        logging.info('Path given: {}'.format(path))
        try:
            logging.info('Checking validity of path')
            if data.check_valid_path(path):
                new_root = Toplevel(self.master)
                new_root.configure(bg='#949494')
                child_gui = TkBase(new_root, path, second_toolitems)
                child_gui.master.protocol("WM_DELETE_WINDOW",
                                          child_gui.child_close)
                child_gui.master.iconbitmap(r'res/general_images/favicon.ico')
            else:
                logging.warning('Invalid path given.')
        except Exception as e:
            logging.error(e)
            raise Exception(e)

    def butrelease(self, event):
        """
        callback method for the annotate button activates the span selector
        """

        # deactivate toolbar functionalities if any are active
        if (self.toolbar._active == 'PAN'):
            self.toolbar.pan()

        if (self.toolbar._active == 'ZOOM'):
            self.toolbar.zoom()

    def export(self):
        """
        callback method for the export button opens a prompt asking for filename in order to save the figure
        """
        def cancel():
            logging.info('Canceling export')
            self.span_min = False
            popup.destroy()
            popup.update()

        def save():
            logging.info('Asking for filename')
            if not export_popup_entry.get().strip():
                logging.info('No filename given')
                error_label = Label(popup,
                                    text="Please add a filename!",
                                    fg="red")
                error_label.grid(row=1, column=0)
            else:
                filename = self.project_path + export_popup_entry.get(
                ) + '.pdf'
                logging.info('Saving figure at {}'.format(filename))
                with PdfPages(filename) as export_pdf:
                    plt.figure(self.window_id * 2 - 1)
                    export_pdf.savefig()
                    plt.figure(self.window_id * 2)
                    export_pdf.savefig()
                logging.info('Export finished')
                cancel()

        logging.info('Exporting graph to pdf')
        popup = Toplevel(self.master)
        popup.title('')
        popup.iconbitmap(r'res/general_images/favicon.ico')
        popup.grab_set()

        export_popup_label = Label(popup, text="Enter desired file name: ")
        export_popup_label.grid(row=0, column=0)

        export_popup_entry = Entry(popup)
        export_popup_entry.grid(row=0, column=1)

        close_export_popup_button = Button(popup, text="Confirm", command=save)
        close_export_popup_button.grid(row=1, column=1)

    def listbox_selection(self, event):
        """
        callback function for the listbox widget
        """
        if (self.listb.curselection()):
            id = self.index_to_ids[self.listb.curselection()[0]]

            for a in self.annotations:
                if a.id == id:

                    self.labelTitle['text'] = "Title: " + a.title
                    self.labelDescription[
                        'text'] = "Description: \n" + a.content

    def goto_callback(self):
        """
        callback for go to annotation button
        """
        if (self.listb.curselection()):
            id = self.index_to_ids[self.listb.curselection()[0]]

            for a in self.annotations:
                if a.id == id:

                    if (a.end != a.start):
                        range = self.get_vertical_range(a)
                        diff = (range[0] - range[1]) / 2
                        delta = (a.end - a.start) / 15
                        self.main_graph_ax.axis([
                            a.start - delta, a.end + delta, range[1] - diff,
                            range[0] + diff
                        ])

                    else:

                        delta = datetime.timedelta(seconds=5)

                        range_indices = np.where(
                            np.logical_and(
                                self.timestamps >
                                a.start - datetime.timedelta(milliseconds=19),
                                self.timestamps <
                                a.end + datetime.timedelta(milliseconds=19)))
                        range_data = self.data[range_indices]
                        ypoint = range_data[np.argmax(range_data)]

                        self.main_graph_ax.axis([
                            a.start - delta, a.end + delta, ypoint - 30,
                            ypoint + 30
                        ])

                    self.main_graph.canvas.toolbar.push_current()
                    self.main_graph.canvas.draw()

    def edit_callback(self):
        """
        callback for edit annotation button
        """
        if (self.listb.curselection()):
            # method called when cancel button on popup is pressed
            def cancel():
                top.destroy()
                top.update()

            # method called when save button on popup is pressed
            def save():

                if not title_entry.get().strip():
                    error_label = Label(top,
                                        text="Please add a title!",
                                        fg="red")
                    error_label.grid(row=3)
                else:
                    annotation.title = title_entry.get()
                    annotation.content = description_entry.get(
                        1.0, tkinter.END)
                    save_json(self.annotations, self.json_path)
                    self.listb.delete(index)
                    self.listb.insert(index, title_entry.get())
                    cancel()

            index = self.listb.curselection()[0]
            id = self.index_to_ids[self.listb.curselection()[0]]

            annotation = None

            for a in self.annotations:
                if a.id == id:
                    # popup in which you edit the annotation
                    annotation = a
                    top = Toplevel(self.master)
                    top.title('edit annotation')
                    top.grab_set()

                    # labels in top level window showing annotation start time
                    # and end time
                    annotation_start_label = Label(
                        top, text='Annotation start time: ' + str(a.start))
                    annotation_end_label = Label(top,
                                                 text='Annotation end time: ' +
                                                 str(a.end))
                    annotation_start_label.grid(row=0)
                    annotation_end_label.grid(row=1)

                    annotation_title_label = Label(top, text='Title')
                    annotation_title_label.grid(row=2)
                    title_entry = Entry(top, font=("Courier", 12))
                    title_entry.insert(tkinter.END, a.title)
                    title_entry.grid(row=4)

                    description_label = Label(top, text='Description')
                    description_label.grid(row=5)
                    description_entry = tkinter.Text(top, height=6, width=30)
                    description_entry.insert(tkinter.END, a.content)
                    description_entry.grid(row=6)

                    cancel_button = Button(master=top,
                                           text="Cancel",
                                           command=cancel,
                                           bg='white')
                    cancel_button.grid(row=8)

                    save_button = Button(master=top,
                                         text="Save",
                                         command=save,
                                         bg='white')
                    save_button.grid(row=7)

                    top.resizable(False, False)
                    top.iconbitmap(r"./res/general_images/favicon.ico")
                    top.protocol("WM_DELETE_WINDOW", cancel)

    def delete_callback(self):
        """
        callback for the delete annotation button
        """
        if (self.listb.curselection()):
            index = self.listb.curselection()[0]
            id = self.index_to_ids[self.listb.curselection()[0]]

            for a in self.annotations:
                if a.id == id:
                    self.index_to_ids.remove(id)
                    self.annotations.remove(a)
                    self.id_to_shape[id].remove()
                    del self.id_to_shape[id]
                    self.main_graph.canvas.draw()
                    save_json(self.annotations, self.json_path)
                    self.listb.delete(index)

    def pick_color(self):
        color = colorchooser.askcolor()
        return color[0]

    def annotate(self):
        """
        callback for the annotate button on the toolbar
        """

        # activate the span selector
        self.span.set_visible(True)

        # deactivate toolbar functionalities if any are active
        if (self.toolbar._active == 'PAN'):
            self.toolbar.pan()

        if (self.toolbar._active == 'ZOOM'):
            self.toolbar.zoom()

        self.annotate_button.config(text='Confirm', command=self.confirm)

    def confirm(self):
        """
        callback method for the annotate button after span is sellected this button
        is pressed to add descriptions to the annotation and confirm selection
        """
        annotation_color = None
        # if something is selected
        if (self.span_min):

            def pick_color():
                nonlocal annotation_color
                annotation_color = self.pick_color()

            # method called when cancel button on popup is pressed
            def cancel():
                self.span_min = False
                top.destroy()
                top.update()

            # method called when save button on popup is pressed
            def save():
                if not title_entry.get().strip():
                    error_label = Label(top,
                                        text="Please add a title!",
                                        fg="red")
                    error_label.grid(row=3)
                else:
                    nonlocal annotation_color
                    if annotation_color is None:
                        annotation_color = (256, 0, 0)
                    new_annotation = Annotation(
                        title_entry.get(),
                        description_entry.get(1.0, tkinter.END), self.span_min,
                        self.span_max, annotation_color)
                    self.annotations.append(new_annotation)
                    save_json(self.annotations, self.json_path)
                    self.draw_annotation(new_annotation)
                    self.index_to_ids.append(new_annotation.id)
                    self.listb.insert(tkinter.END, new_annotation.title)

                    # set spans back to none after the annotation is saved to
                    # prevent buggy behavior
                    self.span_min = None
                    self.span_max = None

                    # destroy popup after annotation is saved
                    cancel()

            # create popup where you add text to the annotation
            top = Toplevel(self.master)
            top.title('Confirm Annotation')
            top.grab_set()

            # labels in top level window showing annotation start time and end
            # time
            annotation_start_label = Label(top,
                                           text='Annotation start time: ' +
                                           str(self.span_min))
            annotation_end_label = Label(top,
                                         text='Annotation end time: ' +
                                         str(self.span_max))
            annotation_start_label.grid(row=0)
            annotation_end_label.grid(row=1)

            annotation_title_label = Label(top, text='Title')
            annotation_title_label.grid(row=2)
            title_entry = Entry(top, font=("Courier", 12))
            title_entry.grid(row=4)

            description_label = Label(top, text='Description')
            description_label.grid(row=5)
            description_entry = tkinter.Text(top, height=6, width=30)
            description_entry.grid(row=6)

            save_button = Button(master=top,
                                 text="Save",
                                 command=save,
                                 bg='white')
            save_button.grid(row=7)

            cancel_button = Button(master=top,
                                   text="Cancel",
                                   command=cancel,
                                   bg='white')
            cancel_button.grid(row=8)

            color_button = Button(master=top,
                                  text="Choose color",
                                  command=pick_color,
                                  bg='white')
            color_button.grid(row=9)

            # change button back to annotate button and hide span selector
            # again
            self.annotate_button.config(text='Annotate', command=self.annotate)
            self.span.set_visible(False)

            # hide the rectangle after confirm button is pressed
            self.span.stay_rect.set_visible(False)
            self.main_canvas.draw()

            top.resizable(False, False)
            top.iconbitmap(r"./res/general_images/favicon.ico")
            top.protocol("WM_DELETE_WINDOW", cancel)

    def onselect(self, min, max):
        """
        callback method of the span selector, after every selection it writes
        the selected range to class variables
        """
        self.span_min = datetime.datetime.fromordinal(
            int(min)) + datetime.timedelta(seconds=divmod(min, 1)[1] * 86400)
        self.span_max = datetime.datetime.fromordinal(
            int(max)) + datetime.timedelta(seconds=divmod(max, 1)[1] * 86400)

    def get_vertical_range(self, annotation):
        """
        get vertical range for a given annotation
        """

        range_indices = np.where(
            np.logical_and(self.timestamps > annotation.start,
                           self.timestamps < annotation.end))

        range_data = self.data[range_indices]
        return range_data[np.argmax(range_data)], range_data[np.argmin(
            range_data)]

    def draw_annotation(self, annotation):
        """
        draws annotation to the main graph as a box if it's a span or a line if it's a point annotation
        """

        # if date range annotation draw rectangle
        annotation_color = annotation.color
        annotation_color = tuple(map(lambda x: x / 256, annotation_color))
        annotation_color = annotation_color + (0.5, )
        if (annotation.start != annotation.end):
            vmax, vmin = self.get_vertical_range(annotation)
            self.id_to_shape[annotation.id] = self.main_graph_ax.add_patch(
                plt.Rectangle(
                    (date2num(annotation.start), vmin - 10),
                    date2num(annotation.end) - date2num(annotation.start),
                    vmax - vmin + 20,
                    color=annotation_color))
        # if point annotation draw a vertical line
        if (annotation.start == annotation.end):
            plt.figure(self.window_id * 2 - 1)
            self.id_to_shape[annotation.id] = plt.axvline(
                x=date2num(annotation.start), color=annotation_color)
        self.main_graph.canvas.draw()

    def draw_graph(self, data, timestamps, annotations):
        """
        draws the main graph and the referece graph given data, timestamps and annotations
        """
        logging.info('Drawing main graph')
        self.main_graph_ax.clear()
        # plot values on the axe and set plot hue to NHS blue
        self.main_graph_ax.plot(timestamps, data, color='#5436ff')
        # draw all saved annotations
        logging.info('Drawing annotations')
        for annotation in annotations:
            self.draw_annotation(annotation)

        self.main_graph_ax.xaxis_date()
        plt.gcf().autofmt_xdate()
        # adding grid
        self.main_graph_ax.grid(color='grey',
                                linestyle='-',
                                linewidth=0.25,
                                alpha=0.5)
        # removing top and right borders
        self.main_graph_ax.spines['top'].set_visible(False)
        self.main_graph_ax.spines['right'].set_visible(False)
        # put the plot with navbar on the tkinter window
        self.main_canvas.draw()
        self.toolbar.update()
        self.main_graph.canvas.toolbar.push_current()

        # second, reference graph displayed
        logging.info('Drawing reference graph')
        self.reference_graph_ax.clear()
        self.reference_graph_ax.plot(self.timestamps,
                                     self.data,
                                     color="cyan",
                                     linewidth=1)
        self.reference_graph_ax.xaxis_date()
        # put the second plot on the tkinter window
        self.reference_canvas.draw()

    def root_close(self):
        if messagebox.askokcancel(
                "Close app",
                "Closing this window will close all windows, are you sure?"):
            self.master.quit()

    def child_close(self):
        self.master.destroy()
Esempio n. 2
0
class SelectFromCollection(object):
    """Select indices from a matplotlib collection using `LassoSelector`.

    Selected indices are saved in the `ind` attribute. This tool highlights
    selected points by fading them out (i.e., reducing their alpha values).
    If your collection has alpha < 1, this tool will permanently alter them.

    Note that this tool selects collection objects based on their *origins*
    (i.e., `offsets`).

    Parameters
    ----------
    ax : :class:`~matplotlib.axes.Axes`
        Axes to interact with.

    collection : :class:`matplotlib.collections.Collection` subclass
        Collection you want to select from.

    alpha_other : 0 <= float <= 1
        To highlight a selection, this tool sets all selected points to an
        alpha value of 1 and non-selected points to `alpha_other`.
    """
    def __init__(self, ax, xcollection, ycollection, gid, parent, method):
        self.ax = ax
        self.canvas = ax.figure.canvas
        self.x = xcollection
        self.y = ycollection
        self.parent = parent
        self.gid = gid

        if method == 'manual':
            self.lasso = RectangleSelector(ax, onselect=self.onselect)
        elif method == 'time':
            w = CalendarDialog(self.ax.get_xlim())
            values = w.getResults()
            if values:
                xmin = datetime(values[0][0], values[0][1], values[0][2])
                xmax = datetime(values[1][0], values[1][1], values[1][2])
                self.selector(
                    lim=[date2num(xmin),
                         date2num(xmax), -np.inf, np.inf])
                self.canvas.draw_idle()
        elif method == 'peaks':
            w = PeaksDialog()
            values = w.getResults()
            if values:
                self.selector(peaks=values)
                self.canvas.draw_idle()

        elif method == 'spanselector':

            self.span = SpanSelector(
                ax,
                self.printspanselector,
                "horizontal",
                useblit=True,
                rectprops=dict(alpha=0.5, facecolor="red"),
            )
            self.canvas.draw_idle()

        self.ind = []

    def printspanselector(self, xmin, xmax):
        for child in self.parent.plotCanvas.fig1.get_children():
            if child.get_gid() == 'ax':
                objs = child.get_children()

        statf = ['mean', 'min', 'max', [25, 50, 75]]
        mat = []
        columnName = []
        for stat in statf:
            if isinstance(stat, str):
                columnName.append(stat)
            elif isinstance(stat, list):
                for p in stat:
                    columnName.append('P' + str(p))

        rowName = []
        color = []
        for i in range(0, len(self.x)):
            gid = self.gid[i]
            for obj in objs:
                if hasattr(obj, 'get_xydata') and obj.get_gid() == gid:
                    color.append(obj.get_color())
                    break

            Y = self.y[i]

            rowName.append(gid)
            row = []
            X = self.x[i]
            if isinstance(X[0], np.datetime64):
                X = date2num(X)

            ind = np.nonzero(((X >= xmin) & (X <= xmax)))[0]
            for stat in statf:
                if isinstance(stat, str):
                    fct = getattr(np, 'nan' + stat)
                    row.append('%.2f' % fct(Y[ind]))
                elif isinstance(stat, list):
                    perc = list(np.nanpercentile(Y[ind], stat))
                    row += ['%.2f' % x for x in perc]

            mat.append(row)

        tb = self.ax.table(cellText=mat,
                           rowLabels=rowName,
                           colLabels=columnName,
                           loc='top',
                           cellLoc='center')
        for k, cell in six.iteritems(tb._cells):
            cell.set_edgecolor('black')
            if k[0] == 0:
                cell.set_text_props(weight='bold', color='w')
                cell.set_facecolor(self.ax.get_facecolor())
            else:
                cell.set_text_props(color=color[k[0] - 1])
                cell.set_facecolor(self.ax.get_facecolor())

        #tb._cells[(0, 0)].set_facecolor(self.ax.get_facecolor())
        self.span.set_visible(False)

    def selector(self, lim=None, peaks=None):
        if lim:
            xmin = lim[0]
            xmax = lim[1]
            ymin = lim[2]
            ymax = lim[3]

        for i in range(0, len(self.x)):
            Y = self.y[i]
            gid = self.gid[i]
            X = self.x[i]
            if isinstance(X[0], np.datetime64):
                X = date2num(X)

            if lim:
                self.ind =np.nonzero(((X>=xmin) & (X<=xmax))\
                    & ((Y>=ymin) & (Y<=ymax)))[0]

            if peaks:
                self.ind = find_peaks(Y, **peaks)[0]

            x_data = X[self.ind]
            y_data = Y[self.ind]

            self.ax.plot(x_data, y_data, 'r+', gid='selected_' + gid)
        self.ax.set_xlim(X[0], X[-1])

    def onselect(self, eclick, erelease):
        # if self.parent._active == "ZOOM" or self.parent._active == "PAN":
        #         return
        self.selector(
            lim=[eclick.xdata, erelease.xdata, eclick.ydata, erelease.ydata])

    def disconnect(self):
        if hasattr(self, 'lasso'):
            self.lasso.disconnect_events()
        self.canvas.draw_idle()
Esempio n. 3
0
class AlignTime:
    def __init__(self, filter=True, filt_ord=4, filt_cut=5, datetime=False):
        """
        Method for aligning time-stamps of different time series data

        Parameters
        ----------
        filter : {bool, array_like}, optional
            Filter the input data. Either bool for both data series, or an array_like of bools if only one series of
            data should be filtered.
        filt_ord : {int, array_like}, optional
            Filter order for the filtering process. Either a single int for both data series, or an array_like of 2
            ints, with orders for the first and second time series. Default is 4. Ignored if filter is False.
        filt_cut : {float, array_like}, optional
            Low-pass filter cutoffs for the filtering process. Either a signal float for both data series, or an
            array_like of 2 floats, with orders for the first and second time series. Default is 5Hz. Ignored if filter
            is False.
        datetime : bool, optional
            Whether or not datetime units are provided for the the time. Default is False, in which case seconds
            are expected
        """
        # assign values as appropriate
        if isinstance(filter, (tuple, list, ndarray)):
            self._filt1, self._filt2 = filter
        else:
            self._filt1, self._filt2 = filter, filter

        if isinstance(filt_ord, (tuple, list, ndarray)):
            self._ord1, self._ord2 = filt_ord
        else:
            self._ord1, self._ord2 = filt_ord, filt_ord

        if isinstance(filt_cut, (tuple, list, ndarray)):
            self._cut1, self._cut2 = filt_cut
        else:
            self._cut1, self._cut2 = filt_cut, filt_cut

        self.datetime = datetime

        # line for plotting the aligned signals
        self.line = None

    def fit(self,
            time1,
            data1,
            time2,
            data2,
            dt1=None,
            dt2=None,
            xlim1=None,
            xlim2=None,
            xnear1=None,
            xnear2=None):
        """
        Align the two time series

        Parameters
        ----------
        time1 : numpy.ndarray
            (N1, ) array of time-stamps for the first series of data. Will be resampled to match the sampling rate of
            the second data series if necessary during the alignment process.
        data1 : numpy.ndarray
            (N1, M1) array of the first series of data
        time2 : numpy.ndarray
            (N2, ) array of time-stamps for the second series of data. Does not have to be the same sampling rate as
            the first series of data. The first series of data will be resampled to match that of the second series
        data2 : numpy.ndarray
            (N2, M2) array of the second series of data
        dt1 : {None, float}, optional
            Sampling time for the first series time stamps, if necessary. Default is None, which will be ignored and
            the mean difference in time-stamps for the first 100 samples of the provided time stamps will be used.
        dt2 : {None, float}, optional
            Sampling time for the second series time stamps, if necessary. Default is None, which will be ignored and
            the mean difference in time-stamps for the first 100 samples of the provided time stamps will be used.
        xlim1 : array_like, optional
            X-limits for plotting series 1 data. Useful if you know approximately where the events to time sync are
            located in the series 1 data.
        xlim2 : array_like, optional
            X-limits for plotting series 2 data. Useful if you now approximately where the events to time sync are
            located in the series 2 data.
        xnear1 : float, optional
            X-value where to search near in the first signal. Will be marked on the graph with a vertical line.
        xnear2 : float, optional
            X-value where to search near in the second signal. Will be marked on the graph with a vertical line.

        Returns
        -------
        dt_1_2 : float
            The time difference between series 2 and series 1, calculated by subtracting the aligned time 2 - time 1

        Attributes
        ----------
        t1_0 : float
            Aligned time int time1 detected by the convolution of the two signals in th regions chosen.
        t2_0 : float
            Aligned time in time2 detected by the convolution of the two signals in the regions chosen.
        """
        # assign the times
        self._t1 = time1
        self._t2 = time2

        # assign the raw data
        self._rx1 = data1
        self._rx2 = data2

        # compute the sampling times
        if dt1 is None:
            if self.datetime:
                self._dt1 = mean(diff(self._t1[:100])) / timedelta64(1, 's')
            else:
                self._dt1 = mean(diff(self._t1[:100]))
        else:
            self._dt1 = dt1
        if dt2 is None:
            if self.datetime:
                self._dt2 = mean(diff(self._t2[:100])) / timedelta64(1, 's')
            else:
                self._dt2 = mean(diff(self._t2[:100]))
        else:
            self._dt2 = dt2

        # filter the data
        if self._filt1:
            fc1 = butter(self._ord1, 2 * self._cut1 * self._dt1, btype='low')
            self._x1 = filtfilt(fc1[0], fc1[1], self._rx1)
        else:
            self._x1 = self._rx1

        if self._filt2:
            fc2 = butter(self._ord2, 2 * self._cut2 * self._dt2, btype='low')
            self._x2 = filtfilt(fc2[0], fc2[1], self._rx2)
        else:
            self._x2 = self._rx2

        # plot the data
        self._f, (self._ax1, self._ax2) = plt.subplots(2, figsize=(20, 10))

        if self._filt1:
            self._ax1.plot(self._t1,
                           self._rx1,
                           color='C0',
                           linewidth=1.5,
                           label='Raw',
                           alpha=0.7)
        if self._filt2:
            self._ax2.plot(self._t2,
                           self._rx2,
                           color='C0',
                           linewidth=1.5,
                           label='Raw',
                           alpha=0.7)
        self._ax1.plot(self._t1,
                       self._x1,
                       color='C1',
                       linewidth=2,
                       label='Filtered')
        self._ax2.plot(self._t2,
                       self._x2,
                       color='C1',
                       linewidth=2,
                       label='Filtered')

        if xlim1 is not None:
            self._ax1.set_xlim(xlim1)
        if xlim2 is not None:
            self._ax2.set_xlim(xlim2)

        if xnear1 is not None:
            self._ax1.axvline(xnear1, color='k', linewidth=3, alpha=0.5)
        if xnear2 is not None:
            self._ax2.axvline(xnear2, color='k', linewidth=3, alpha=0.5)

        self._ax1.legend(loc='best')
        self._ax2.legend(loc='best')

        # create the cursor and span selectors for the first axis
        self._ax1.set_title('Navigate and select the region to check: '
                            )  # let user know what they are doing
        self._cursor1 = Cursor(self._ax1,
                               color='C0',
                               useblit=True,
                               linewidth=1)
        self._span1 = SpanSelector(self._ax1,
                                   self._on_select1,
                                   'horizontal',
                                   useblit=True,
                                   rectprops=dict(alpha=0.5, facecolor='red'),
                                   button=1,
                                   span_stays=True)

        self._ax2.set_title('Navigate and select region to match: ')
        self._cursor2 = Cursor(self._ax2,
                               color='C0',
                               useblit=True,
                               linewidth=1)
        self._span2 = SpanSelector(self._ax2,
                                   self._on_select2,
                                   'horizontal',
                                   useblit=True,
                                   rectprops=dict(alpha=0.5, facecolor='red'),
                                   button=1,
                                   span_stays=True)

        self._cursor2.set_active(False)
        self._span2.set_visible(False)

        self._f.tight_layout()

        plt.show(block=True)

        return self.t_diff

    def _on_select1(self, xmin, xmax):
        self._ax1.set_title(None)
        if self.datetime:
            t1 = date2num(self._t1)
        else:
            t1 = self._t1

        start, stop = searchsorted(t1, (xmin, xmax))

        f = interp1d(t1[start - 10:stop + 10],
                     self._x1[start - 10:stop + 10],
                     kind='cubic')
        if self.datetime:
            self._ta = drange(self._t1[start], self._t1[stop],
                              to_timedelta(self._dt2, unit='s'))
        else:
            self._t1 = arange(self._t1[start], self._t1[stop], self._dt2)

        self._a = f(self._ta)

        self._cursor2.set_active(True)
        self._span2.set_visible(True)

    def _on_select2(self, xmin, xmax):
        self._ax2.set_title(None)
        if self.datetime:
            t2 = date2num(self._t2)
        else:
            t2 = self._t2
        start, stop = searchsorted(t2, (xmin, xmax))

        self._b = zeros(self._a.shape)
        self._tb = t2[stop - self._a.size:stop]

        self._b[-(stop - start):] = self._x2[start:stop]

        A = fftpack.fft(self._a)
        B = fftpack.fft(self._b)

        Ar = -A.conjugate()

        self._ind_shift = argmax(nabs(fftpack.ifft(
            Ar * B)))  # convolve the arrays and find the peak

        self.t1_0 = self._ta[0]  # find the time value in the first signal
        self.t2_0 = self._tb[
            self._ind_shift]  # find the time value in the second signal

        t_pl = self._ta + (self.t2_0 - self.t1_0)
        x_pl = self._a

        if self.line is not None:
            self.line.set_data([], [])
        self.line, = self._ax2.plot(t_pl, x_pl, color='C2', label='Aligned')

        if self.datetime:
            self.t1_0 = to_datetime(num2date(self.t1_0)).tz_localize(None)
            self.t2_0 = to_datetime(num2date(self.t2_0)).tz_localize(None)

        # time difference between the signals
        self.t_diff = self.t2_0 - self.t1_0
Esempio n. 4
0
class Kanvas(FigureCanvas):
    """
    Klasa koja definira kanvas (prostor za crtanje grafova).
    """
    # definiramo signal za promjenu flaga, int->kanal, str->str vrijeme u ISO formatu, bool->OK ili BAD
    signal_flag_change = QtCore.pyqtSignal(int, str, str, bool)
    #definiramo signal za izbor redka (zoom na tablicama sa podacima) -- pandas datetime
    signal_time_pick = QtCore.pyqtSignal('PyQt_PyObject')

    def __init__(self, parent=None, width=12, height=6, dpi=100):
        """
        Konstruktor klase. Sadrži 4 grafa na jednom kanvasu. Redom od gore prema
        dolje : span, zero, satni average koncentracija, koncentracija.
        """
        # definicija figure i kanvasa (inicijalizacija objekata)
        self.fig = Figure(figsize=(width, height), dpi=dpi)
        FigureCanvas.__init__(self, self.fig)
        FigureCanvas.setSizePolicy(self, QtWidgets.QSizePolicy.Expanding,
                                   QtWidgets.QSizePolicy.Expanding)
        # u principu parent će se automatski postaviti prilikom layout managementa
        self.setParent(parent)
        # status ostalih alata sa Navigation toolbara
        self.OTHER_TOOLS_IN_USE = False
        # Session - podaci koje treba crtati
        self.SESSION = None
        # Kanal u fokusu
        self.AKTIVNI_KANAL = None
        # memberi koji drze vrijeme za promjenu flaga -> vrijeme od-do
        self.__lastTimeMin = None
        self.__lastTimeMax = None
        # info koji je graf u fokusu (ostali su smanjeni)
        self.SPAN_IN_FOCUS = False
        self.ZERO_IN_FOCUS = False
        self.CONC_SATNI_IN_FOCUS = False
        self.CONC_IN_FOCUS = True
        # info o statusu legende i grida (da li ih crtamo)
        self.isLegendDrawn = False
        self.isGridDrawn = False
        # STYLE GENERATOR
        self.styleGenerator = semirandom_styles()
        # ostali pomocni plotovi (grafovi ostalih kanala)
        self.ostaliGrafovi = {}
        # gridspec layout subplotova - nacin na koji su grafovi poslagani na kanvasu
        self.gs = gridspec.GridSpec(4, 1, height_ratios=[1, 1, 1, 4])
        # defiincija pojedinih osi subplotova
        self.axesConc = self.fig.add_subplot(self.gs[3, 0])  #koncentracija
        self.axesConcSatni = self.fig.add_subplot(
            self.gs[2,
                    0], sharex=self.axesConc)  #koncentracija - satni aggregate
        self.axesSpan = self.fig.add_subplot(self.gs[0, 0],
                                             sharex=self.axesConc)  #span
        self.axesZero = self.fig.add_subplot(self.gs[1, 0],
                                             sharex=self.axesConc)  #zero
        # custom names TODO! za naziv plotova?
        self.axesConc._CUSTOM_NAME = 'Graf koncentracija'
        self.axesConcSatni._CUSTOM_NAME = 'Graf koncentracija - satni'
        self.axesZero._CUSTOM_NAME = 'Graf zero'
        self.axesSpan._CUSTOM_NAME = 'Graf span'
        # svi grafovi djele x os, skrivanje labela i tickova za sve osim koncentracijskog grafa
        self.axesSpan.xaxis.set_visible(False)
        self.axesZero.xaxis.set_visible(False)
        self.axesConcSatni.xaxis.set_visible(False)
        # prebaci tickove na "staggered" nacin radi citljivosti (prebacivanje tickova desno)
        self.axesSpan.yaxis.tick_right()
        self.axesConcSatni.yaxis.tick_right()
        #prebaci y labele na "staggered" nacin radi citljivosti (prebacivanje labela desno)
        self.axesSpan.yaxis.set_label_position("right")
        self.axesConcSatni.yaxis.set_label_position("right")
        # podesi spacing izmedju axesa (sljepi grafove radi ustede vertikalnog prostora)
        self.fig.subplots_adjust(wspace=0.001, hspace=0.001)
        # update geometriju
        FigureCanvas.updateGeometry(self)
        # definiramo "custom" kontekstni meni na desni klik
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        # span selektori za kanvase - promjena flaga
        self.spanSelector = SpanSelector(self.axesConc,
                                         self.spanSelectorConcCallback,
                                         direction='horizontal',
                                         button=1,
                                         useblit=True,
                                         rectprops=dict(alpha=0.2,
                                                        facecolor='yellow'))
        self.spanSelectorSatni = SpanSelector(
            self.axesConcSatni,
            self.spanSelectorConcSatniCallback,
            direction='horizontal',
            button=1,
            useblit=True,
            rectprops=dict(alpha=0.2, facecolor='yellow'))
        # definiranje callback-a za pick evente
        self.pickCid = self.mpl_connect('button_press_event', self.on_pick)

    def promjeni_aktivni_kanal(self, x, draw=True):
        """
        Promjena aktivnog kanala (x) za crtanje. Dodatna opcija "draw" služi kao parametar koji
        forsira ponovno crtanje grafa.
        """
        # nema smisla mjenjati ako je x već postavljen kao aktivni kanal
        if x != self.AKTIVNI_KANAL:
            self.AKTIVNI_KANAL = x
        # force redraw
        if draw:
            self.crtaj(noviSession=False)

    def nav_tools_in_use(self, x):
        """
        Callback za status aktivnih alata u navigation toolbaru. Ako se koriste
        navigacijski alati za pan i zoom, moram iskljuciti span selector na grafovima.
        """
        self.OTHER_TOOLS_IN_USE = x
        self.spanSelector.set_visible(not x)
        self.spanSelectorSatni.set_visible(not x)

    def on_pick(self, event):
        """
        Pick event za interakciju sa kanvasom. Event sadrži informacije koji graf je 'kliknut',
        koji gumb na mišu je aktivan, poziciju klika...
        """
        # event mora biti unutar osi te alati moraju biti ugaseni
        if ((not self.OTHER_TOOLS_IN_USE) and (event.inaxes in [
                self.axesConc, self.axesConcSatni, self.axesSpan, self.axesZero
        ])):
            # potrebno je odrediti orgin os gdje moramo prikazati menu
            if event.inaxes == self.axesConcSatni:
                origin = 'satni'
            elif event.inaxes == self.axesConc:
                origin = 'koncentracija'
            else:
                origin = 'zero ili span'
            if event.button == 1:
                # left click
                # matplotlib conversion of xdata to pandas.datetime
                if origin in ['satni', 'koncentracija']:
                    # priprema vremena (round i prilagodba za tablicu)
                    tajm = self.num2date_converter(event.xdata, origin=origin)
                    # emit signal za izbor reda (prema vremenu)
                    self.signal_time_pick.emit(tajm)
            elif event.button == 3:
                # right click
                pos = QtGui.QCursor.pos()
                # matplotlib conversion of xdata to pandas.datetime
                if origin == 'satni':
                    # pomak vremena mora biti OK za satno agregirane...
                    tmin = self._mpltime_to_pdtime(
                        event.xdata) - datetime.timedelta(hours=1)
                    tmax = self._mpltime_to_pdtime(event.xdata)
                    self.show_context_menu(pos, tmin, tmax, origin=origin)
                elif origin == 'koncentracija':
                    tmin = self._mpltime_to_pdtime(event.xdata)
                    tmax = self._mpltime_to_pdtime(event.xdata)
                    self.show_context_menu(pos, tmin, tmax, origin=origin)
                else:
                    # zero i span grafovi imaju jednostavniji kontekstni menu, vrijeme im nije potrebno
                    self.show_context_menu(pos, None, None, origin=origin)
            else:
                pass

    def _mpltime_to_pdtime(self, x):
        """Converter iz matplotlib date (broj) u pandas datetime"""
        xpoint = matplotlib.dates.num2date(x)  #datetime.datetime
        #problem.. rounding offset aware i offset naive datetimes..workaround
        xpoint = datetime.datetime(xpoint.year, xpoint.month, xpoint.day,
                                   xpoint.hour, xpoint.minute, xpoint.second)
        #konverzija iz datetime.datetime objekta u pandas.tislib.Timestamp
        xpoint = pd.to_datetime(xpoint)
        return xpoint

    def num2date_converter(self, x, origin='koncentracija'):
        """
        Pretvara matplotlib datum (broj) u pandas timestamp. Zaokruživanje vremena ovisi o
        tipu grafa (satno ili minutno zaokruživanje.)
        """
        # pretvori u timestamp
        tajm = self._mpltime_to_pdtime(x)
        # zaokruži
        if origin == 'koncentracija':
            return tajm.round('Min')
        elif origin == 'satni':
            return tajm.round('H')
        else:
            raise ValueError('wrong interval')

    def spanSelectorConcCallback(self, low, high):
        """
        Span selector callback za koncentracije.
        """
        if low != high:
            pos = QtGui.QCursor.pos()
            self.span_select_context_menu(pos,
                                          low,
                                          high,
                                          origin='koncentracija')

    def spanSelectorConcSatniCallback(self, low, high):
        """
        Span selector callback za satno agregirane koncentracije.
        """
        if low != high:
            pos = QtGui.QCursor.pos()
            self.span_select_context_menu(pos, low, high, origin='satni')

    def set_axes_focus(self, tip='koncentracija'):
        """
        Prebacivanje fokusa izmedju grafova za span-zero-satno agregirane-koncentracije
        """
        if tip == 'span':
            self.gs.set_height_ratios([4, 1, 1, 1])
            self.SPAN_IN_FOCUS = True
            self.ZERO_IN_FOCUS = False
            self.CONC_SATNI_IN_FOCUS = False
            self.CONC_IN_FOCUS = False
        elif tip == 'zero':
            self.gs.set_height_ratios([1, 4, 1, 1])
            self.SPAN_IN_FOCUS = False
            self.ZERO_IN_FOCUS = True
            self.CONC_SATNI_IN_FOCUS = False
            self.CONC_IN_FOCUS = False
        elif tip == 'satni':
            self.gs.set_height_ratios([1, 1, 4, 1])
            self.SPAN_IN_FOCUS = False
            self.ZERO_IN_FOCUS = False
            self.CONC_SATNI_IN_FOCUS = True
            self.CONC_IN_FOCUS = False
        else:
            #koncentracije (minutna rezolucija)
            self.gs.set_height_ratios([1, 1, 1, 4])
            self.SPAN_IN_FOCUS = False
            self.ZERO_IN_FOCUS = False
            self.CONC_SATNI_IN_FOCUS = False
            self.CONC_IN_FOCUS = True
        self.fig.tight_layout()
        # podesi razmak između grafova (sljepi grafove)
        self.fig.subplots_adjust(wspace=0.001, hspace=0.001)
        self.draw()
        self.crtaj_legendu(self.isLegendDrawn, skipDraw=True)
        self.crtaj_grid(self.isGridDrawn, skipDraw=False)

    def promjena_flaga(self, flag=True):
        """
        Metoda sluzi za signaliziranje promjene flaga.
        """
        self.signal_flag_change.emit(self.AKTIVNI_KANAL, self.__lastTimeMin,
                                     self.__lastTimeMax, flag)

    def span_select_context_menu(self, pos, tmin, tmax, origin='satni'):
        """
        Prikaz menua za promjenu flaga uz span select. Za satni origin min vrijeme moramo
        pomaknuti dovoljno unazad da uhvati dobar period agregiranja.

        pos - pozicija gdje prikazujemo menu
        tmin - vrijeme od
        tmax - vrijeme do
        origin - graf koji je pokrenuo event
        """
        # pretvori u pandas timestamp
        tmin = self._mpltime_to_pdtime(tmin)
        tmax = self._mpltime_to_pdtime(tmax)
        # zapamti rubna vremena intervala, trebati ce za druge metode
        if origin == 'satni':
            # makni sat unazad 1 sat
            minsat = pd.to_datetime(tmin - datetime.timedelta(hours=1))
            self.__lastTimeMin = str(minsat)
            self.__lastTimeMax = str(tmax)
        else:
            self.__lastTimeMin = str(tmin)
            self.__lastTimeMax = str(tmax)

        # stvaramo menu za prikaz
        menu = QtWidgets.QMenu(self)
        menu.setTitle('Menu')
        # definiramo akcije
        action1 = QtWidgets.QAction("Flag: dobar", menu)
        action2 = QtWidgets.QAction("Flag: los", menu)
        # dodajemo akcije u menu
        menu.addAction(action1)
        menu.addAction(action2)
        # povezujemo akcije sa callbackovima
        action1.triggered.connect(
            functools.partial(self.promjena_flaga, flag=True))
        action2.triggered.connect(
            functools.partial(self.promjena_flaga, flag=False))
        # prikaz menu-a
        menu.popup(pos)

    def show_context_menu(self, pos, tmin, tmax, origin='satni'):
        """
        Right click kontekstni menu. Za origin == 'satni' vrijeme moramo
        pomaknuti da uhvatimo dobar period za promjenu flaga. Za origin==koncentracija
        vrijeme je dobro definirano.

        pos - pozicija gdje prikazujemo menu
        tmin - vrijeme od
        tmax - vrijeme do
        origin - graf koji je pokrenuo event
        """
        # zapamti vremena... trebati će za flag change
        self.__lastTimeMin = tmin
        self.__lastTimeMax = tmax
        # definiraj menu
        menu = QtWidgets.QMenu(self)
        menu.setTitle('Menu')
        # definiranje akcija
        if origin in ['satni', 'koncentracija']:
            action1 = QtWidgets.QAction("Flag: dobar", menu)
            action2 = QtWidgets.QAction("Flag: los", menu)
        action3 = QtWidgets.QAction("focus: SPAN", menu)
        action3.setCheckable(True)
        action3.setChecked(self.SPAN_IN_FOCUS)
        action4 = QtWidgets.QAction("focus: ZERO", menu)
        action4.setCheckable(True)
        action4.setChecked(self.ZERO_IN_FOCUS)
        action5 = QtWidgets.QAction("focus: satno agregirani", menu)
        action5.setCheckable(True)
        action5.setChecked(self.CONC_SATNI_IN_FOCUS)
        action6 = QtWidgets.QAction("focus: koncentracija", menu)
        action6.setCheckable(True)
        action6.setChecked(self.CONC_IN_FOCUS)
        action7 = QtWidgets.QAction("Legend", menu)
        action7.setCheckable(True)
        action7.setChecked(self.isLegendDrawn)
        action8 = QtWidgets.QAction("Grid", menu)
        action8.setCheckable(True)
        action8.setChecked(self.isGridDrawn)
        # slaganje akcija u menu
        if origin in ['satni', 'koncentracija']:
            menu.addAction(action1)
            menu.addAction(action2)
            menu.addSeparator()
        menu.addAction(action3)
        menu.addAction(action4)
        menu.addAction(action5)
        menu.addAction(action6)
        menu.addSeparator()
        menu.addAction(action7)
        menu.addAction(action8)
        # povezi akcije menua sa callback funkcijama
        if origin in ['satni', 'koncentracija']:
            action1.triggered.connect(
                functools.partial(self.promjena_flaga, flag=True))
            action2.triggered.connect(
                functools.partial(self.promjena_flaga, flag=False))
        action3.triggered.connect(
            functools.partial(self.set_axes_focus, tip='span'))
        action4.triggered.connect(
            functools.partial(self.set_axes_focus, tip='zero'))
        action5.triggered.connect(
            functools.partial(self.set_axes_focus, tip='satni'))
        action6.triggered.connect(
            functools.partial(self.set_axes_focus, tip='koncentracija'))
        action7.triggered.connect(self.crtaj_legendu)
        action8.triggered.connect(self.crtaj_grid)
        # pokazi menu na poziciji pos
        menu.popup(pos)

    def set_session(self, x):
        """
        Connect Sessiona sa kanvasom... cilj je prosljediti pointer na trenutni session
        kanvasu kako bi metode za crtanje mogle doci do trenutnih podataka.
        """
        # postavi novi Session
        self.SESSION = x
        # pozovi crtanje
        self.crtaj(noviSession=True)

    def crtaj(self, noviSession=True):
        """
        Glavna metoda za crtanje podataka na svim grafovima.

        noviSession - ako je True, ponistava se prethodni "zoom" te se slika crta
        zoomirana na granice ucitanog podrucja. To je slucaj kada stavljamo novi
        session u kanvas. Inače ponovno crtanje unutar prethodno zapamćenih granica.
        """
        #TODO! spremanje nekih opcija grafova nakon crtanja?
        # zapamti prethodne limite grafa
        self.zapamti_trenutni_zoom()
        # clear sve osi
        self.clear_graf()
        # delegacija crtanja pojedinim grafovima
        self.crtaj_zero_span(tip='zero')
        self.crtaj_zero_span(tip='span')
        self.crtaj_koncentracija()
        self.crtaj_satne()
        # rotacija labela x osi najnižeg grafa radi čitljivosti
        allXLabels = self.axesConc.get_xticklabels()
        for label in allXLabels:
            label.set_rotation(30)
            label.set_fontsize(8)

        if noviSession:
            # ako je novi session, postavi default granice kao max raspon podataka
            self.default_trenutni_zoom()
        else:
            # ako crtamo već postojeći session, zoomiraj na prethodno zapamćeni zoom
            self.restore_trenutni_zoom()
        # kompresija prostora za crtanje - maksimiziramo prostor koji imamo na raspolaganju
        self.fig.tight_layout()
        # podesi spacing izmedju grafova (sljepi grafove)
        self.fig.subplots_adjust(wspace=0.001, hspace=0.001)
        self.draw()  #naredba kanvasu za render
        self.crtaj_legendu(self.isLegendDrawn, skipDraw=True)
        self.crtaj_grid(self.isGridDrawn, skipDraw=False)
        # zapamti limite grafa nakon sto je nacrtan za buduće crtanje
        self.zapamti_trenutni_zoom()

    def zapamti_trenutni_zoom(self):
        """
        Funkcija pamti x i y raspone na svim grafovima. Cilj je prilikom promjene imati
        isti zoom level.
        """
        # ostali grafovi imaju synced x osi sa grafom koncentracije, nema smisla ih pamtiti
        self.ZOOM_LEVEL_X = self.axesConc.get_xlim()
        self.ZOOM_LEVEL_CONC = self.axesConc.get_ylim()
        self.ZOOM_LEVEL_SATNI = self.axesConcSatni.get_ylim()
        self.ZOOM_LEVEL_ZERO = self.axesZero.get_ylim()
        self.ZOOM_LEVEL_SPAN = self.axesSpan.get_ylim()

    def restore_trenutni_zoom(self):
        """
        Funkcija postavlja x i y raspone na svim grafovima koji su prethodno zapamceni.
        """
        # ostali grafovi imaju synced x osi sa grafom koncentracije, nema smisla ih pamtiti
        self.axesConc.set_xlim(self.ZOOM_LEVEL_X)
        self.axesConc.set_ylim(self.ZOOM_LEVEL_CONC)
        self.axesConcSatni.set_ylim(self.ZOOM_LEVEL_SATNI)
        self.axesZero.set_ylim(self.ZOOM_LEVEL_ZERO)
        self.axesSpan.set_ylim(self.ZOOM_LEVEL_SPAN)

    def default_trenutni_zoom(self):
        """
        Funkcija postavlja defaultne x i y raspone na svim grafovima prilikom prvog crtanja.
        """
        #default x raspon
        datastore = self.SESSION.get_datastore(self.AKTIVNI_KANAL)
        xmin = matplotlib.dates.date2num(datastore.koncentracija.startTime -
                                         datetime.timedelta(hours=1))
        xmax = matplotlib.dates.date2num(datastore.koncentracija.endTime +
                                         datetime.timedelta(hours=1))
        self.ZOOM_LEVEL_X = (xmin, xmax)
        #default y rasponi
        lowKonc = min([
            self.SESSION.get_datastore(kanal).koncentracija.yPlotRange[0]
            for kanal in self.SESSION.sviKanali
        ])
        highKonc = max([
            self.SESSION.get_datastore(kanal).koncentracija.yPlotRange[1]
            for kanal in self.SESSION.sviKanali
        ])
        self.ZOOM_LEVEL_CONC = (lowKonc, highKonc)
        lowSatni = min([
            self.SESSION.get_datastore(kanal).satni.yPlotRange[0]
            for kanal in self.SESSION.sviKanali
        ])
        highSatni = max([
            self.SESSION.get_datastore(kanal).satni.yPlotRange[1]
            for kanal in self.SESSION.sviKanali
        ])
        self.ZOOM_LEVEL_SATNI = (lowSatni, highSatni)
        lowZero = min([
            self.SESSION.get_datastore(kanal).zero.yPlotRange[0]
            for kanal in self.SESSION.sviKanali
        ])
        highZero = max([
            self.SESSION.get_datastore(kanal).zero.yPlotRange[1]
            for kanal in self.SESSION.sviKanali
        ])
        self.ZOOM_LEVEL_ZERO = (lowZero, highZero)
        lowSpan = min([
            self.SESSION.get_datastore(kanal).span.yPlotRange[0]
            for kanal in self.SESSION.sviKanali
        ])
        highSpan = max([
            self.SESSION.get_datastore(kanal).span.yPlotRange[1]
            for kanal in self.SESSION.sviKanali
        ])
        self.ZOOM_LEVEL_SPAN = (lowSpan, highSpan)
        #postavi nove granice zoom-a
        self.restore_trenutni_zoom()

    def clear_graf(self):
        """
        Clear svih grafova. Brišemo mapu sa linijama ostalih kanala, clearamo sve
        grafove.
        """
        #TODO!
        self.ostaliGrafovi = {}
        self.blockSignals(True)
        self.axesConc.clear()
        self.axesConcSatni.clear()
        self.axesSpan.clear()
        self.axesZero.clear()
        self.blockSignals(False)

    def crtaj_legendu(self, toggle, skipDraw=False):
        """
        Funkcija za crtanje / toggle legende. "toggle" je state legende, ako je True onda crtamo.
        "skipDraw" služi da preskočimo korak za render (draw).
        """
        # zapamti status da li se crta legenda
        self.isLegendDrawn = toggle
        # all legends - iterator (axes, da li je fokus na tom axesu)
        ite = zip(
            [self.axesConc, self.axesConcSatni, self.axesSpan, self.axesZero],
            [
                self.CONC_IN_FOCUS, self.CONC_SATNI_IN_FOCUS,
                self.SPAN_IN_FOCUS, self.ZERO_IN_FOCUS
            ])
        # crtamo odvojenu legendu za svaki graf, prikazujemo samo onu na aktivnom grafu
        for i, j in ite:
            i.legend(fontsize=8, loc='center left', bbox_to_anchor=(1, 0.5))
            # switch za draggable opciju legende, ovisi o verziji matplotliba
            if LooseVersion(matplotlib.__version__) >= LooseVersion('3.0.0'):
                i.get_legend().set_draggable(True)
            else:
                i.get_legend().draggable(state=True)
            stejt = toggle and j
            i.get_legend().set_visible(stejt)
        # display legendu
        if not skipDraw:
            self.draw()

    def crtaj_grid(self, toggle, skipDraw=False):
        """
        Funkcija za crtanje / toggle grida. "toggle" je state grida, ako je True onda crtamo.
        "skipDraw" služi da preskočimo korak za render (draw).
        """
        self.isGridDrawn = toggle
        if toggle:
            for i in [
                    self.axesConc, self.axesConcSatni, self.axesSpan,
                    self.axesZero
            ]:
                i.grid(which='major',
                       color='black',
                       linestyle='-',
                       linewidth='0.4',
                       alpha=0.4)
                #minor tick lines?
                #i.grid(which='minor', color='black', linestyle='-',
                #       linewidth='0.2', alpha=0.2)
                #i.minorticks_on()
        else:
            for i in [
                    self.axesConc, self.axesConcSatni, self.axesSpan,
                    self.axesZero
            ]:
                i.grid(False)
                #i.minorticks_off()
        if not skipDraw:
            self.draw()

    def crtaj_zero_span(self, tip='zero'):
        """
        Funkcija koja crta zero ili span graf (ovisno o parametru "tip")
        tip : 'zero', 'span'
        """
        try:
            # potreban nam je datastore aktivnog kanala da bi došli do podataka
            datastore = self.SESSION.get_datastore(self.AKTIVNI_KANAL)
            if tip == 'span':
                plotAxes = self.axesSpan  # dinamički definiramo na koji graf crtamo
                indeks = datastore.span.indeks  # indeks - podaci x osi
                gornjaGranica = datastore.span.maxAllowed  # max dozvoljeno
                donjaGranica = datastore.span.minAllowed  # min dozvoljeno
                spanLinija = datastore.span.baseline  # osnovna linija
                korekcija = datastore.span.korekcija  # osnovna linija korekcije
                okKorekcija = datastore.span.korekcijaOk  # mjesta gdje je korekcija OK
                badKorekcija = datastore.span.korekcijaBad  #mjesta gdje je korekcije loša
            elif tip == 'zero':
                plotAxes = self.axesZero  # dinamički definiramo na koji graf crtamo
                indeks = datastore.zero.indeks  # indeks - podaci x osi
                gornjaGranica = datastore.zero.maxAllowed  # max dozvoljeno
                donjaGranica = datastore.zero.minAllowed  # min dozvoljeno
                spanLinija = datastore.zero.baseline  # osnovna linija
                korekcija = datastore.zero.korekcija  # osnovna linija korekcije
                okKorekcija = datastore.zero.korekcijaOk  # mjesta gdje je korekcija OK
                badKorekcija = datastore.zero.korekcijaBad  #mjesta gdje je korekcije loša
            else:
                # nešto stvarno mora poći po zlu da dođemo do ove linije...
                raise ValueError("Only 'zero' or 'span' allowed")
            # PLOTTING
            # gornja granica
            self.spanTopLimit, = plotAxes.plot(indeks,
                                               gornjaGranica,
                                               label='Gornja granica',
                                               linestyle='--',
                                               linewidth=1.2,
                                               color='red')
            # donja granica
            self.spanLowLimit, = plotAxes.plot(indeks,
                                               donjaGranica,
                                               label='Donja granica',
                                               linestyle='--',
                                               linewidth=1.2,
                                               color='red')
            # originalna linija
            self.spanLine, = plotAxes.plot(indeks,
                                           spanLinija,
                                           label='Span',
                                           linestyle='-.',
                                           linewidth=1.0,
                                           color='blue')
            # linija nakon korekcije
            self.spanKorekcija, = plotAxes.plot(indeks,
                                                korekcija,
                                                label='Korekcija',
                                                linestyle='-',
                                                drawstyle='default',
                                                linewidth=1.2,
                                                color='black')
            #ok tocke (markeri) nakon korekcije
            self.spanGood, = plotAxes.plot(indeks,
                                           okKorekcija,
                                           label='Dobar span',
                                           linestyle='None',
                                           marker='d',
                                           markersize=6,
                                           markerfacecolor='green',
                                           markeredgecolor='green')
            #lose tocke (markeri) nakon korekcije
            self.spanBad, = plotAxes.plot(indeks,
                                          badKorekcija,
                                          label='Los span',
                                          linestyle='None',
                                          marker='d',
                                          markersize=6,
                                          markerfacecolor='red',
                                          markeredgecolor='red')
            # postavi y label - mjerna jedinica aktivne komponente
            if tip == 'zero':
                lab = 'ZERO ' + datastore.zero.jedinica
                plotAxes.set_ylabel(lab)
            else:
                lab = 'SPAN ' + datastore.span.jedinica
                plotAxes.set_ylabel(lab)
        except Exception as err:
            # slučaj kada dođe do pogreške u crtanju?
            logging.error(str(err), exc_info=True)

    def crtaj_koncentracija(self):
        """
        Crtanje podataka sa minutnim koncentracijama.
        """
        try:
            # potreban nam je datastore aktivnog kanala da bi došli do podataka
            datastore = self.SESSION.get_datastore(self.AKTIVNI_KANAL)
            indeks = datastore.koncentracija.indeks  # x os
            lineLDL = datastore.koncentracija.ldl_line  # LDL linija
            lineSviOK = datastore.koncentracija.koncentracijaOk  # linija sa ok sirovim koncentracijama
            lineSviBAD = datastore.koncentracija.koncentracijaBad  # linija sa bad sirovim koncentracijama
            lineOKkorekcija = datastore.koncentracija.korekcijaOk  # linija sa podacima OK korekcije
            lineBadkorekcija = datastore.koncentracija.korekcijaBad  # linija sa podacima loše korekcije
            glavniOpis = datastore.koncentracija.puniOpis  # opis - za labele
            # PLOTTING
            # zero linija
            zeroLinija = self.axesConc.axhline(0.0,
                                               label='0 line',
                                               linewidth=0.9,
                                               color='black')
            # limit podataka -> najmanji vremenski indeks
            leftLimit = self.axesConc.axvline(
                datastore.koncentracija.startTime,
                label='Min vrijeme',
                linestyle='-.',
                linewidth=1.2,
                color='blue')
            # limit podataka -> najveci vremenski indeks
            rightLimit = self.axesConc.axvline(datastore.koncentracija.endTime,
                                               label='Max vrijeme',
                                               linestyle='-.',
                                               linewidth=1.2,
                                               color='blue')
            # LDL vrijednost
            self.koncLDL = self.axesConc.plot(indeks,
                                              lineLDL,
                                              label=glavniOpis + ' - LDL',
                                              linestyle='-',
                                              linewidth=1.2,
                                              color='red')
            # zelena linija za sirove OK podatke
            self.koncLineOK, = self.axesConc.plot(indeks,
                                                  lineSviOK,
                                                  label=glavniOpis +
                                                  ' - sirovi OK',
                                                  linestyle='-',
                                                  linewidth=1.5,
                                                  color='green')
            # crvena linija za sirove OK podatke
            self.koncLineBAD, = self.axesConc.plot(indeks,
                                                   lineSviBAD,
                                                   label=glavniOpis +
                                                   ' - sirovi BAD',
                                                   linestyle='-',
                                                   linewidth=1.5,
                                                   color='red')
            # korekcijska linija OK - CRNA, puna debela linija
            self.koncKorekcijaOK, = self.axesConc.plot(indeks,
                                                       lineOKkorekcija,
                                                       label=glavniOpis +
                                                       ' - korekcija OK',
                                                       linestyle='-',
                                                       linewidth=1.5,
                                                       color='black')
            # korekcijska linija BAD - CRNA, dotted debela linija
            self.koncKorekcijaBad, = self.axesConc.plot(indeks,
                                                        lineBadkorekcija,
                                                        label=glavniOpis +
                                                        ' - korekcija BAD',
                                                        linestyle='-.',
                                                        linewidth=1.5,
                                                        color='black')
            # postavi y label - mjerna jedinica aktivne komponente
            lab = 'Konc. ' + datastore.koncentracija.jedinica
            self.axesConc.set_ylabel(lab)
            # dohvati ostale kanale i plotaj i njih (spremi u pomocne radi on/off opcije)
            ostaliKanali = self.SESSION.get_ostale_kanale(self.AKTIVNI_KANAL)
            for k in ostaliKanali:
                self.crtaj_dodatni_kanal(k)
        except Exception as err:
            #pogreska prilikom crtanja?
            logging.error(str(err), exc_info=True)

    def crtaj_dodatni_kanal(self, tmpKanal):
        """Plot helper za crtanje dodatnog kanala."""
        self.styleGenerator = semirandom_styles()  # reset style generator
        self.ostaliGrafovi[tmpKanal] = {}
        # generiranje seimirandom stila (prvih par je zadano...)
        linestil, tmpboja = self.styleGenerator.__next__(
        )  # manualno advancanje generatora
        # pristup podacima
        datastore = self.SESSION.get_datastore(tmpKanal)
        tmpOpis = datastore.koncentracija.puniOpis
        indeks = datastore.koncentracija.indeks
        fullline_korekcija = datastore.koncentracija.korekcija_line
        self.ostaliGrafovi[tmpKanal]['korekcija'], = self.axesConc.plot(
            indeks,
            fullline_korekcija,
            label=tmpOpis + ' - korekcija',
            linestyle=linestil,
            linewidth=0.8,
            color=tmpboja,
            alpha=0.5)

    def crtaj_dodatni_kanal_satni(self, tmpKanal):
        """Plot helper za crtanje dodatnog kanala"""
        self.styleGenerator = semirandom_styles()  #reset style generator
        self.ostaliGrafovi[tmpKanal] = {}
        #generiranje seimirandom stila (prvih par je zadano...)
        linestil, tmpboja = self.styleGenerator.__next__(
        )  # manualno advancanje generatora
        #pristup podacima
        datastore = self.SESSION.get_datastore(tmpKanal)
        tmpOpis = datastore.satni.puniOpis
        indeks = datastore.satni.indeks
        fullline_korekcija = datastore.satni.korekcija_line
        self.ostaliGrafovi[tmpKanal]['korekcija'], = self.axesConcSatni.plot(
            indeks,
            fullline_korekcija,
            label=tmpOpis + ' - korekcija',
            linestyle=linestil,
            linewidth=0.8,
            color=tmpboja,
            alpha=0.5)

    def crtaj_satne(self):
        """
        Crtanje podataka sa satno agregiranim koncentracijama.
        """
        try:
            # potreban nam je datastore aktivnog kanala da bi došli do podataka
            datastore = self.SESSION.get_datastore(self.AKTIVNI_KANAL)
            indeks = datastore.satni.indeks  # x os
            lineOriginalOK = datastore.satni.koncentracijaOk  # linija sirovih koncentracija OK
            lineOriginalBAD = datastore.satni.koncentracijaBad  # linija sirovih koncentracija BAD
            lineOKkorekcija = datastore.satni.korekcijaOk  # linija gdje je korekcija OK
            lineBadkorekcija = datastore.satni.korekcijaBad  # linija gdje je korekcija loša
            glavniOpis = datastore.satni.puniOpis  # opis kanala za labele
            # vremena za start / kraj moraju odgovarati minutnim podacima
            startline = datastore.koncentracija.startTime
            endline = datastore.koncentracija.endTime
            # PLOTTING
            # zero linija ... orijentir
            zeroLinija = self.axesConcSatni.axhline(0.0,
                                                    label='0 line',
                                                    color='black')
            # limit podataka -> najmanji vremenski indeks
            leftLimit = self.axesConcSatni.axvline(startline,
                                                   label='Min vrijeme',
                                                   linestyle='-.',
                                                   linewidth=1.2,
                                                   color='blue')
            # limit podataka -> najveci vremenski indeks
            rightLimit = self.axesConcSatni.axvline(endline,
                                                    label='Max vrijeme',
                                                    linestyle='-.',
                                                    linewidth=1.2,
                                                    color='blue')
            # sve koncentracije koje su OK
            self.koncSatniGood, = self.axesConcSatni.plot(indeks,
                                                          lineOriginalOK,
                                                          label=glavniOpis +
                                                          ' - OK',
                                                          linestyle='-',
                                                          linewidth=1.5,
                                                          color='green')
            # sve koncentracije koje su BAD
            self.koncSatniGood, = self.axesConcSatni.plot(indeks,
                                                          lineOriginalBAD,
                                                          label=glavniOpis +
                                                          ' - BAD',
                                                          linestyle='-',
                                                          linewidth=1.5,
                                                          color='red')
            # korekcijska linija OK
            self.koncKorekcijaOK, = self.axesConcSatni.plot(indeks,
                                                            lineOKkorekcija,
                                                            label=glavniOpis +
                                                            ' - korekcija OK',
                                                            linestyle='-',
                                                            linewidth=1.5,
                                                            color='black')
            # korekcijska linija Bad
            self.koncKorekcijaBad, = self.axesConcSatni.plot(
                indeks,
                lineBadkorekcija,
                label=glavniOpis + ' - korekcija BAD',
                linestyle='-.',
                linewidth=1.5,
                color='black')
            # postavi y label - mjerna jedinica aktivne komponente
            # postavi y label - mjerna jedinica aktivne komponente
            lab = 'Konc.Satni ' + datastore.koncentracija.jedinica
            self.axesConcSatni.set_ylabel(lab)
            # dohvati ostale kanale i plotaj i njih (spremi u pomocne radi on/off opcije)
            ostaliKanali = self.SESSION.get_ostale_kanale(self.AKTIVNI_KANAL)
            for k in ostaliKanali:
                self.crtaj_dodatni_kanal_satni(k)
        except Exception as err:
            logging.error(str(err), exc_info=True)