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()
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()
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
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)