class VisualizerWindow:
    root = []
    dataMgr = []
    
    #graphs
    fig = []
    ax1 = []
    ax2 = []
    ax3 = []
    ax4 = []
    
    ylim_ax1 = []
    ylim_ax2 = []
    ylim_ax3 = []
    
    #data
    x = []
    usage_manual = []   
    
    #debug
    show_debug_msg = False
    
    #autosave (-1 for disable)
    autosave_period = 30
    load_autosave_file = False

    
    def __init__(self, filename):        
        self.root = Tk.Tk()
        self.root.wm_title("EMG-Visualization-Tool")
        self.root.protocol("WM_DELETE_WINDOW", self._quit)
     
        if not filename:
             filename = askopenfilename()
             if not filename:
                 sys.exit(0)
    
        while not os.path.isfile(filename):
             showerror("File not found", "Sorry, file '{}' was not found, try again".format(filename))
             filename = askopenfilename()
             if not filename:
                 sys.exit(0)
        
        if filename[-19:] == '_usage_autosave.pkl':
            self.autosave_file = filename
            print('Input file is an Auto-Save file "{}"'.format(self.autosave_file))
            filename = filename[:-19] + '.csv'
            if not os.path.isfile(filename):
                showerror('Could not find file "{}" (matching to provided auto-save file "{}")'.format(filename, self.autosave_file))
                print('Error: matching file not found (expected "{}")'.format(filename))
                print('EXIT')
                sys.exit(0)
            else:
                self.load_autosave_file = True
        else:
            self.autosave_file = filename.rsplit(".", 1)[0] + "_usage_autosave.pkl"
            if os.path.isfile(self.autosave_file):
                print('Auto-Save file "{}" already exists'.format(self.autosave_file))
                if askyesno('Auto-Save file found', 'Auto-Save file "{}" found. Load it instead? ("No" will result in automatical overwriting of the file when auto-saving)'.format(self.autosave_file)):
                    self.load_autosave_file = True
        
        
        self.root.wm_title("EMG-Visualization-Tool: {}".format(filename))
        self.dataMgr = DataManager(filename)
                
        
        self.csv_out_file = filename.rsplit(".", 1)[0] + "_usage.csv"
        
        while os.path.isfile(self.csv_out_file):
            print('File "{}" already exists'.format(self.csv_out_file))
            if askyesno('Overwrite File?', 'File "{}" already exists. Overwrite?'.format(self.csv_out_file)):
                print('overwriting "{}"'.format(self.csv_out_file))
                break
            else:
                new_out_file = asksaveasfilename(initialfile=self.csv_out_file)
                if new_out_file:
                    self.csv_out_file = new_out_file
                    print('New Output file "{}"'.format(self.csv_out_file))
                else:
                    sys.exit(0)
                    
        print("csv-out-file: {}".format(self.csv_out_file))
        
        self.configFrame = Tk.Frame(self.root, height=500, width=400)

        Tk.Label(self.configFrame, text="\r\nPlot 1").pack()
        self.plot1_select = ttk.Combobox(self.configFrame, values=self.dataMgr.plot_columns, state="readonly")
        self.plot1_select.pack()
        
        if '"f"' in self.dataMgr.plot_columns:
            self.plot1_select.current(self.dataMgr.plot_columns.index('"f"'))
        else:
            self.plot1_select.current(0)
        

        Tk.Label(self.configFrame, text="Plot 2").pack()
        self.plot2_select = ttk.Combobox(self.configFrame, values=self.dataMgr.plot_columns, state="readonly")
        self.plot2_select.pack()

        if '"rmsd"' in self.dataMgr.plot_columns:
            self.plot2_select.current(self.dataMgr.plot_columns.index('"rmsd"'))
        else:
            self.plot2_select.current(0)

        
        Tk.Label(self.configFrame, text="Plot 3").pack()
        self.plot3_select = ttk.Combobox(self.configFrame, values=self.dataMgr.plot_columns, state="readonly")
        self.plot3_select.pack()

        if '"beat"' in self.dataMgr.plot_columns:
            self.plot3_select.current(self.dataMgr.plot_columns.index('"beat"'))
        else:
            self.plot3_select.current(0)
        
        
        Tk.Label(self.configFrame, text="\r\nUsage Plot").pack()

        self.usage_plots = {}

        for usg in self.dataMgr.usage_columns:
            if not usg == '"usage_manual"':
                chkbx_var = Tk.IntVar()
                chkbx_var.set(1)
                usg_check = ttk.Checkbutton(self.configFrame, text=usg, variable=chkbx_var)
                usg_check.pack()
                self.usage_plots[usg] = chkbx_var

        Tk.Label(self.configFrame, text="\r\nLoad/copy \"usage_manual\" from").pack()
        if self.load_autosave_file:
            Tk.Label(self.configFrame, text="provided Auto-Save file").pack()
        else:
            self.usg_man_select = ttk.Combobox(self.configFrame, values=self.dataMgr.usage_columns, state="readonly")
            self.usg_man_select.pack()
            
            if '"usage_manual"' in self.dataMgr.usage_columns:
                self.usg_man_select.current(self.dataMgr.usage_columns.index('"usage_manual"'))
            elif '"usage_total"' in self.dataMgr.usage_columns:
                self.usg_man_select.current(self.dataMgr.usage_columns.index('"usage_total"'))
            else:
                if len(self.dataMgr.usage_columns) > 0:
                    self.usg_man_select.current(0)


        Tk.Label(self.configFrame, text="\r\nJump Column").pack()

        if len(self.dataMgr.jump_columns) == 0:
            self.jmp_select = ttk.Combobox(self.configFrame, values=['--none--'], state="readonly")
            self.jmp_select.current(0)
        else: 
            self.jmp_select = ttk.Combobox(self.configFrame, values=self.dataMgr.jump_columns, state="readonly")
            if '"jump_ibi"' in self.dataMgr.jump_columns:
                self.jmp_select.current(self.dataMgr.jump_columns.index('"jump_ibi"'))
            else:
                self.jmp_select.current(0)        
        self.jmp_select.pack()

        
        Tk.Label(self.configFrame, text="").pack()

        button_continue = Tk.Button(self.configFrame, text='Continue', command=self._CfgContinue)
        button_continue.pack()
        
        Tk.Label(self.configFrame, text="").pack()
        self.loading_label = Tk.Label(self.configFrame, text="")
        self.loading_label.pack()
        
        self.configFrame.pack()

        self.visualizerFrame = Tk.Frame(self.root)
        
        
        # Figure with Subplots
        self.fig = plt.figure(figsize=(17,8), dpi=80, tight_layout=True)
        gs = gridspec.GridSpec(4,1, height_ratios=[3,2,2,1])
        self.ax1 = plt.subplot(gs[0])
        self.ax2 = plt.subplot(gs[1], sharex=self.ax1)
        self.ax3 = plt.subplot(gs[2], sharex=self.ax1)
        self.ax4 = plt.subplot(gs[3], sharex=self.ax1)   
        
        canvas = FigureCanvasTkAgg(self.fig, master=self.visualizerFrame)
        canvas.show()
        canvas.mpl_connect('key_press_event', self.on_key_event)
        canvas.mpl_connect('button_press_event', self.on_button_event)
    
        # GUI Elements
        self.mode_label = Tk.Label(self.visualizerFrame, text="Mode: ADD", background="green")
    
        self.progress_label = Tk.Label(self.visualizerFrame, text="Page {}/{}".format(self.dataMgr.current_page, self.dataMgr.num_pages))
    
        button_prev = Tk.Button(master=self.visualizerFrame, text='Prev', command=self._prev)
        button_next = Tk.Button(master=self.visualizerFrame, text='Next', command=self._next)
    
        button_zoom_in = Tk.Button(master=self.visualizerFrame, text='Zoom In', command=self._zoom_in)
        button_zoom_out = Tk.Button(master=self.visualizerFrame, text='Zoom Out', command=self._zoom_out)    
    
        button_add = Tk.Button(master=self.visualizerFrame, text='Add', command=self._add)
        button_del = Tk.Button(master=self.visualizerFrame, text='Del', command=self._del)
        
        saveFrame = Tk.Frame(self.visualizerFrame)
        button_autosave = Tk.Button(master=saveFrame, text='Auto-Save', command=self._autosave)
        button_save = Tk.Button(master=saveFrame, text='Save', command=self._save)
        button_autosave.grid(column=0, row=0)
        button_save.grid(column=1, row=0)
        
        button_quit = Tk.Button(master=self.visualizerFrame, text='Quit', command=self._quit)
        
        # Selection
        self.multi_cursor = MultiCursor(self.fig.canvas, (self.ax1, self.ax2, self.ax3, self.ax4), useblit=True, horizOn=False, vertOn=True, color='g', lw=1)
        self.span1 = SpanSelector(self.ax1, self.onselectAdd, 'horizontal', useblit=True,
                        rectprops=dict(alpha=0.5, facecolor='green') )
        self.span2 = SpanSelector(self.ax2, self.onselectAdd, 'horizontal', useblit=True,
                        rectprops=dict(alpha=0.5, facecolor='green') )
        self.span3 = SpanSelector(self.ax3, self.onselectAdd, 'horizontal', useblit=True,
                        rectprops=dict(alpha=0.5, facecolor='green') )
        self.span4 = SpanSelector(self.ax4, self.onselectAdd, 'horizontal', useblit=True,
                        rectprops=dict(alpha=0.5, facecolor='green') )

        # GUI Layout
        button_zoom_in.grid(column=0, row=0)
        button_zoom_out.grid(column=1, row=0)
        button_prev.grid(column=3, row=0)
        self.progress_label.grid(column=4, row=0)
        button_next.grid(column=5, row=0)
        canvas.get_tk_widget().grid(column=0, row=1, columnspan=6)
        canvas._tkcanvas.grid(column=0, row=1, columnspan=6)
        button_add.grid(column=0, row=2)
        button_del.grid(column=1, row=2)
        self.mode_label.grid(column=2, row=2, columnspan=2)
        saveFrame.grid(column=4, row=2)
        button_quit.grid(column=5, row=2)
    
        Tk.mainloop()       


    def _CfgContinue(self):
        self.loading_label['text'] = 'loading ...'
        self.loading_label.update()
        
        self.plot_names = [self.plot1_select.get(),
                      self.plot2_select.get(),
                      self.plot3_select.get()]
        
        self.plot_cols = []              
        for pn in self.plot_names:
            self.plot_cols.append(self.dataMgr.header_columns.index(pn))

        self.usage_names = []
        self.usage_cols = []
        for k,v in self.usage_plots.items():
            if v.get() == 1:
                self.usage_names.append(k)
                self.usage_cols.append(self.dataMgr.header_columns.index(k))
        
        if self.load_autosave_file:
            usg_manual_col = -1
        else:
            if self.usg_man_select.get() in self.dataMgr.header_columns:
                usg_manual_col = self.dataMgr.header_columns.index(self.usg_man_select.get())
            else:
                usg_manual_col = -1
            
        if self.jmp_select.get() in self.dataMgr.jump_columns:
            jmp_col = self.dataMgr.header_columns.index(self.jmp_select.get())
        else:
            jmp_col = -1
        
        [self.x, self.plot_data, self.usage_data, self.usage_manual, self.jump_positions] = self.dataMgr.readData(self.plot_cols,self.usage_cols,self.loading_label, usg_manual_col, jmp_col)

        if self.load_autosave_file:
            infile = open(self.autosave_file, 'rb')
            self.usage_manual = pickle.load(infile)
            infile.close()

        self.configFrame.pack_forget()
        self.visualizerFrame.pack()

        print("displaying plots")
        
        self.ylim_ax1 = self.calc_ylims(self.plot_data[0])
        self.ylim_ax2 = self.calc_ylims(self.plot_data[1])
        self.ylim_ax3 = self.calc_ylims(self.plot_data[2])
        
        self.loadPage(1)


    def HMS(seconds, pos):
        """Customized x-axis ticks 
            
        Keyword arguments:
        seconds -- input in secs
        pos -- somehow required argument (matplotlib)
        """
        seconds = int(seconds)
        hours = seconds / 3600
        seconds -= 3600 * hours
        minutes = seconds / 60
        seconds -= 60 * minutes
        if hours == 0:
            if minutes == 0:
                return "%ds" % (seconds)
            return "%dm%02ds" % (minutes, seconds)
        return "%dh%02dm" % (hours, minutes)
        
        
    def calc_ylims(self, data):
        [cnt, edge] = np.histogram(data, 25)
        s = len(data)
        thres = 0.975*s
        i = 0
        j = len(cnt)-1
        while True:
            if cnt[i] < cnt[j]:
                if s-cnt[i] < thres:
                    break
                else:
                    s -= cnt[i]
                    i += 1
            else:
                if s-cnt[j] < thres:
                    break
                else:
                    s -= cnt[j]
                    j -= 1
        
        return [min(0, edge[i]), max(1, edge[j+1])]


    def plotPage(self):
        index_low = self.dataMgr.low_Idx()
        index_high = self.dataMgr.high_Idx()
        
        if self.show_debug_msg:
            print("index_low: {} | index_high: {}".format(index_low, index_high))
    
        self.ax1.clear()
        self.ax1.xaxis.set_major_formatter(plt.FuncFormatter(self.HMS))
        self.ax1.plot(self.x[index_low:index_high], np.array(self.plot_data[0][index_low:index_high]))
        self.ax1.set_ylim(self.ylim_ax1)
        self.ax1.set_title(self.plot_names[0])
        
        self.ax2.clear()
        self.ax2.plot(self.x[index_low:index_high], self.plot_data[1][index_low:index_high])
        self.ax2.set_ylim(self.ylim_ax2)
        self.ax2.set_title(self.plot_names[1])
        
        self.ax3.clear()
        self.ax3.plot(self.x[index_low:index_high], self.plot_data[2][index_low:index_high])
        self.ax3.set_ylim(self.ylim_ax3)
        self.ax3.set_title(self.plot_names[2])
        
        self.plotUsages()
        
        self.fig.canvas.draw()
        
    
    def plotUsages(self):
        index_low = self.dataMgr.low_Idx()
        index_high = self.dataMgr.high_Idx()
        
        self.ax4.clear()
        self.ax4.set_ylim(0,len(self.usage_names)+2)
        self.ax4.set_yticks([], minor=False)     
        
        colors = ['#483D8B', '#228B22', '#B22222', '#8A2BE2', '#808000', '#FF4500', '#DA70D6', '#FFA500']
                
        self.ax4.fill_between(self.x[index_low:index_high], 0, (len(self.usage_names)+2)*np.array(self.usage_manual[index_low:index_high]), facecolor='#7fbf7f', edgecolor='None')
        
        self.ax4.plot(self.jump_positions, [len(self.usage_names)+1]*len(self.jump_positions), 'r*')        
        
        for u in range(0,len(self.usage_data)):
            self.ax4.fill_between(self.x[index_low:index_high], u, u+np.array(self.usage_data[u][index_low:index_high]), facecolor=colors[u], edgecolor='None')

        patches = [mpatches.Patch(color='green', alpha=0.5, label='usage_manual')]
        
        for i in range(0,len(self.usage_names)):
            patches.append(mpatches.Patch(color=colors[i], label=self.usage_names[i]))

        plt.legend(bbox_to_anchor=(0., 1.0, 1., .102), loc=3,
           ncol=5, mode="expand", borderaxespad=0., handles=patches)

        self.ax1.set_xlim(self.dataMgr.low_Xlimit(),self.dataMgr.high_Xlimit())

    
    def onselectAdd(self, xmin, xmax):
        minIdx = max(0, round(xmin*500))
        maxIdx = min(self.dataMgr.file_length-1, round(xmax*500))
        if self.show_debug_msg:
            print("ADD: xmin: {} | xmax: {}".format(xmin, xmax))
            print("ADD: minIdx: {} | maxIdx: {}".format(minIdx, maxIdx))

        self.usage_manual[minIdx:maxIdx] = 1
        self.plotUsages()
        self.fig.canvas.draw()


    def onselectDel(self, xmin, xmax):
        minIdx = max(0, round(xmin*500))
        maxIdx = min(self.dataMgr.file_length-1, round(xmax*500))
        if self.show_debug_msg:
            print("DEL: xmin: {} | xmax: {}".format(xmin, xmax))
            print("DEL: minIdx: {} | maxIdx: {}".format(minIdx, maxIdx))
        
        self.usage_manual[minIdx:maxIdx] = 0
        self.plotUsages()
        self.fig.canvas.draw()


    def onselectZoom(self, xmin, xmax):
        if self.show_debug_msg:
            print("ZOOM: xmin: {} | xmax: {}".format(xmin, xmax))
        
        self.plotUsages()
        self.ax1.set_xlim(xmin,xmax)
        self.fig.canvas.draw()
    
    
    def on_key_event(self, event):
        if self.show_debug_msg:
            print('you pressed %s'%event.key)
        if event.key == 'a':
            self._prev()
        elif event.key == 'd':
            self._next()
        elif event.key == 'w':
            self._add()
        elif event.key == 's':
            self._del()
        elif event.key == 'q':
            self._zoom_in()
        elif event.key == 'e':
            self._zoom_out()
        elif event.key == 'r':
            self._save()
        elif event.key == 'f':
            self._autosave()
        elif event.key == 'x':
            self._prevJump()
        elif event.key == 'c':
            self._nextJump()
        elif event.key == 'left':
            self._prev()
        elif event.key == 'right':
            self._next()
        elif event.key == 'up':
            self._add()
        elif event.key == 'down':
            self._del()
            

    def on_button_event(self, event):
        if self.show_debug_msg:
            print('you clicked %s'%event.button)
        if event.button == 3:   #right mouse button
            self._zoom_out()
        elif event.button == 2: #middle mouse button (scroll wheel)
            self._zoom_in()


    def _quit(self):
        self.root.quit()     # stops mainloop
        self.root.destroy()  # this is necessary on Windows to prevent
                        # Fatal Python Error: PyEval_RestoreThread: NULL tstate
    
    def _prev(self):
        if self.show_debug_msg:
            print('_prev()')
        if self.dataMgr.current_page > 1:
            self.loadPage(self.dataMgr.current_page-1)

    def _next(self):
        if self.show_debug_msg:
            print('next()')
        if self.dataMgr.current_page < self.dataMgr.num_pages:
            self.loadPage(self.dataMgr.current_page+1)


    def _prevJump(self):
        if self.show_debug_msg:
            print('_prevJump()')
        if self.dataMgr.current_page > 1:
            for p in reversed(self.jump_positions):
                num = self.dataMgr.getPageNumByX(p)
                if num < self.dataMgr.current_page:
                    self.loadPage(num)
                    break


    def _nextJump(self):
        if self.show_debug_msg:
            print('nextJump()')
        if self.dataMgr.current_page < self.dataMgr.num_pages:
            for p in self.jump_positions:
                num = self.dataMgr.getPageNumByX(p)
                if num > self.dataMgr.current_page:
                    self.loadPage(num)
                    break
            

    def loadPage(self, page_num):
        if self.autosave_period > -1 and page_num % self.autosave_period == 0:
            if self.show_debug_msg:
                print('autosaving on page {}'.format(page_num))
            self._autosave()
        self.dataMgr.current_page = min(max(1, page_num), self.dataMgr.num_pages)
        if self.show_debug_msg:
            print('loadPage(): {}'.format(self.dataMgr.current_page))
        self.plotPage()
        self.progress_label["text"] ="Page {}/{}".format(self.dataMgr.current_page, self.dataMgr.num_pages)


    def _add(self):
        if self.show_debug_msg:
            print('_add()')
        if float(matplotlib.__version__[0:3])>=1.4:
            self.multi_cursor.disconnect()
        self.multi_cursor = MultiCursor(self.fig.canvas, (self.ax1, self.ax2, self.ax3, self.ax4), useblit=True, horizOn=False, vertOn=True, color='g', lw=1)
        self.span1.disconnect_events()
        self.span2.disconnect_events()
        self.span3.disconnect_events()
        self.span4.disconnect_events()
        self.span1 = SpanSelector(self.ax1, self.onselectAdd, 'horizontal', useblit=True,
                        rectprops=dict(alpha=0.5, facecolor='green') )
        self.span2 = SpanSelector(self.ax2, self.onselectAdd, 'horizontal', useblit=True,
                        rectprops=dict(alpha=0.5, facecolor='green') )
        self.span3 = SpanSelector(self.ax3, self.onselectAdd, 'horizontal', useblit=True,
                        rectprops=dict(alpha=0.5, facecolor='green') )
        self.span4 = SpanSelector(self.ax4, self.onselectAdd, 'horizontal', useblit=True,
                        rectprops=dict(alpha=0.5, facecolor='green') )

        self.mode_label['text'] = 'Mode: ADD'
        self.mode_label['bg'] = 'green'
        self.fig.canvas.draw()


    def _del(self):
        if self.show_debug_msg:
            print('_del()')
        if float(matplotlib.__version__[0:3])>=1.4:
            self.multi_cursor.disconnect()
        self.multi_cursor = MultiCursor(self.fig.canvas, (self.ax1, self.ax2, self.ax3, self.ax4), useblit=True, horizOn=False, vertOn=True, color='r', lw=1)
        self.span1.disconnect_events()
        self.span1.disconnect_events()
        self.span2.disconnect_events()
        self.span3.disconnect_events()
        self.span4.disconnect_events()
        self.span1 = SpanSelector(self.ax1, self.onselectDel, 'horizontal', useblit=True,
                        rectprops=dict(alpha=0.5, facecolor='red') )
        self.span2 = SpanSelector(self.ax2, self.onselectDel, 'horizontal', useblit=True,
                        rectprops=dict(alpha=0.5, facecolor='red') )
        self.span3 = SpanSelector(self.ax3, self.onselectDel, 'horizontal', useblit=True,
                        rectprops=dict(alpha=0.5, facecolor='red') )
        self.span4 = SpanSelector(self.ax4, self.onselectDel, 'horizontal', useblit=True,
                        rectprops=dict(alpha=0.5, facecolor='red') )

        self.mode_label['text'] = 'Mode: DEL'
        self.mode_label['bg'] = 'red'
        self.fig.canvas.draw()

  
    def _zoom_in(self):
        if self.show_debug_msg:
            print('_zoom_in()')
        if float(matplotlib.__version__[0:3])>=1.4:
            self.multi_cursor.disconnect()
        self.multi_cursor = MultiCursor(self.fig.canvas, (self.ax1, self.ax2, self.ax3, self.ax4), useblit=True, horizOn=False, vertOn=True, color='b', lw=1)
        self.span1.disconnect_events()
        self.span1.disconnect_events()
        self.span2.disconnect_events()
        self.span3.disconnect_events()
        self.span4.disconnect_events()
        self.span1 = SpanSelector(self.ax1, self.onselectZoom, 'horizontal', useblit=True,
                        rectprops=dict(alpha=0.5, facecolor='blue') )
        self.span2 = SpanSelector(self.ax2, self.onselectZoom, 'horizontal', useblit=True,
                        rectprops=dict(alpha=0.5, facecolor='blue') )
        self.span3 = SpanSelector(self.ax3, self.onselectZoom, 'horizontal', useblit=True,
                        rectprops=dict(alpha=0.5, facecolor='blue') )
        self.span4 = SpanSelector(self.ax4, self.onselectZoom, 'horizontal', useblit=True,
                        rectprops=dict(alpha=0.5, facecolor='blue') )

        self.mode_label['text'] = 'Mode: ZOOM'
        self.mode_label['bg'] = 'blue'
        self.fig.canvas.draw()


    def _zoom_out(self):
        if self.show_debug_msg:
            print('_zoom_out()')

        self.plotUsages()
        self.fig.canvas.draw()


    def _save(self):
        if self.show_debug_msg:
            print('_save()')
        savetxt = plt.text(20, 20, 'saving...', fontsize=46, color='r', weight='bold', ha='center', va='top')
        self.fig.canvas.draw()
        self.dataMgr.writeCSV(self.csv_out_file, self.usage_manual)
        savetxt.remove()
        self.fig.canvas.draw()
        if os.path.isfile(self.autosave_file):
            archive_filename = self.autosave_file.rsplit(".", 1)[0] + "_old.pkl"
            print('renaming autosave file from "{}" to "{}"'.format(self.autosave_file, archive_filename))
            if os.path.isfile(archive_filename):            
                os.unlink(archive_filename)
            os.rename(self.autosave_file, archive_filename)
        
        
    def _autosave(self):
        if self.show_debug_msg:
            print('_autosave()')
        savetxt = plt.text(20, 20, 'autosaving...', fontsize=46, color='r', weight='bold', ha='center', va='top')
        self.fig.canvas.draw()
        self.dataMgr.writeAutoSave(self.autosave_file, self.usage_manual)
        savetxt.remove()
        self.fig.canvas.draw()
Example #2
0
class MyFrame(matplotlib_GUI.MyFrame):
    # set up datastructure used by get_axes() method -------------------------------------------------------------------
    # where to place plots? map user choice to subplot: (row, col, position)
    # e.g. "top left" for 2x2 plots -> (2,2,1)
    # this will be the argument to self.canvas.figure.add_subplot(...)
    _SUBPLOT_CHOICES = [
        ((1, 1), ("center", )), ((1, 2), ("left", "right")),
        ((2, 1), ("top", "bottom")),
        ((2, 2), ("top left", "top right", "bottom left", "bottom right")),
        ((2, 3), ("top left", "top center", "top right", "bottom left",
                  "bottom center", "bottom right"))
    ]
    _SUBPLOT_POSITIONS = {}
    for ((rows, cols), choices) in _SUBPLOT_CHOICES:
        _SUBPLOT_POSITIONS[rows, cols] = {}
        i = 0
        for row in range(1, rows + 1):
            for col in range(1, cols + 1):
                if choices:
                    _SUBPLOT_POSITIONS[rows,
                                       cols][choices[i]] = (rows, cols, i + 1)
                i += 1
    del rows, cols, choices, i

    # ------------------------------------------------------------------------------------------------------------------

    def __init__(self, *args, **kwargs):
        matplotlib_GUI.MyFrame.__init__(self, *args, **kwargs)
        self.figure = self.canvas.figure

        self.init_toolmanager()
        self.init_events()
        # where to save figures by default?
        matplotlib.rcParams[
            'savefig.directory'] = ""  # defaults to current directory

        self.Bind(wx.EVT_CLOSE, self.OnClose)

        # initialize values from control values; so the .wxg file needs to have correct initialization values!
        self.on_choice_line_style()
        self.on_combo_colour()
        self.on_combo_line_width()

        self.on_choice_subplots()
        self.set_history_buttons()
        self.on_button_plot()

        self.multicursor = None

    def init_toolmanager(self):
        self.toolmanager = matplotlib.backend_managers.ToolManager(
            self.canvas.figure)

        self.toolmanager.add_tool(
            'viewpos',
            'ToolViewsPositions')  # required for pan/zoom/home/back/forward
        self.toolmanager.add_tool(
            'pan', 'ToolPan')  # pan w. mouse and zoom w. wheel with 'p' key
        self.toolmanager.add_tool('zoom',
                                  'ToolZoom')  # zoom to rect with 'o' key
        self.toolmanager.add_tool('home', 'ToolHome')  # 'h', 'r', 'home'
        self.toolmanager.add_tool('back',
                                  'ToolBack')  # 'left', 'c', 'backspace'
        self.toolmanager.add_tool('forward', 'ToolForward')  # 'right', 'v'
        self.toolmanager.add_tool('save', 'ToolSaveFigure')  # 's', 'ctrl+s'
        self.toolmanager.add_tool(
            'grid', 'ToolGrid')  # toggle throug major h/v grids with 'g' key
        self.toolmanager.add_tool(
            'grid_minor',
            'ToolMinorGrid')  # toggle throug major/minor grids with 'G' key

        self.toolmanager.add_tool(
            'yscale', 'ToolYScale')  # toggle lin/log scaling with 'l' key
        self.toolmanager.add_tool(
            'xscale', 'ToolXScale')  # toggle lin/log scaling with 'k','L' keys

        # some tools will only be available with matplotlib 3.0:
        if hasattr(matplotlib.backend_managers, "ToolCopyToClipboard"):
            self.toolmanager.add_tool('copy', 'ToolCopyToClipboard')

        self.toolmanager.add_tool('rubberband', 'ToolRubberband')
        self.toolmanager.add_tool('setcursor', 'ToolSetCursor')

        self.toolmanager.toolmanager_connect("tool_trigger_home",
                                             self.set_history_buttons)
        self.toolmanager.toolmanager_connect("tool_trigger_back",
                                             self.set_history_buttons)
        self.toolmanager.toolmanager_connect("tool_trigger_forward",
                                             self.set_history_buttons)

    def init_events(self):
        # connect to matplotlib events

        # for picking elements
        self.canvas.mpl_connect('pick_event', self.on_pick)
        self._last_pick_mouseevent = None  # store info, as we will act only once per pick event

        # for displaying cursor position
        self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)

        # button_release_event may be the end of a zoom; use CallAfter as we may receive the event too early
        self.canvas.mpl_connect(
            "button_release_event",
            lambda event: wx.CallAfter(self.set_history_buttons))

        # register a handler for all canvas events except 'motion_notify_event', which would cause too much traffic
        for event in ('button_press_event', 'button_release_event',
                      'key_press_event', 'key_release_event', 'draw_event',
                      'pick_event', 'resize_event', 'scroll_event',
                      'figure_enter_event', 'figure_leave_event',
                      'axes_enter_event', 'axes_leave_event'):
            self.canvas.mpl_connect(event, self.on_canvas_event)
        # in addition, there are events from the axes: 'xlim_changed', 'ylim_changed' (see self.get_axes())

    ###################################################################################################################
    # print messages
    def on_canvas_event(self, event):
        print("canvas event:", event)

    ###################################################################################################################
    # global actions: add plot or clear plots or all
    def get_axes(self):
        # return the axes that are selected acc. to "Subplots" and "Subplot position"
        subplots = self._SUBPLOT_POSITIONS[self.subplots][self.subplot]
        # subplots = (row, col, position), e.g. (2,2,1) for the top left of 2x2 plots
        ret = self.figure.add_subplot(*subplots)
        # we could register event handlers when x or y limits are changing:
        #ret.callbacks.connect( 'xlim_changed', lambda evt: print("callback xlim_changed:", evt) )
        return ret

    def on_button_clear(self, types):
        if self.multicursor:
            self.multicursor.disconnect()
            self.multicursor = None
            self.button_multicursor.SetValue(False)

        if types == "plots":
            for axes in self.figure.axes:
                axes.clear()
        elif types in ("figures", "all"):
            self.figure.clear()
        self.canvas.draw()

        if types in ("plots", "all"):
            # reset zoom history
            self.toolmanager.get_tool("viewpos").clear(self.canvas.figure)
            self.set_history_buttons()

    def on_button_multicursor(self, event):
        from matplotlib.widgets import MultiCursor

        axes = self.canvas.figure.axes
        if event.IsChecked() and not self.multicursor and axes:
            # add multicursor
            self.multicursor = MultiCursor(self.canvas,
                                           axes,
                                           color='r',
                                           lw=1,
                                           horizOn=True,
                                           vertOn=True)
        elif self.multicursor:
            # remove multicursor
            self.multicursor.disconnect()
            self.multicursor = None
        self.canvas.draw()
        event.Skip()

    ####################################################################################################################
    # draw elements on canvas plot areas / axes (by coordinates)
    def on_button_plot(self, event=None):
        # plot a function
        xmin, xmax, step = self._get_floats("text_plot_",
                                            ("xmin", "xmax", "xstep"))
        if None in (xmin, xmax, step):
            # one of the values is invalid
            return
        x = numpy.arange(xmin, xmax, step)

        # build globals with some functions for eval(...)
        g = {}
        for name in ["sin", "cos", "tan", "ufunc", "square"]:
            g[name] = getattr(numpy, name)
        y = eval(self.text_function.GetValue(), g, {"numpy": numpy, "x": x})

        axes = self.get_axes()
        colour, width, style = self._get_styles()

        axes.plot(
            x,
            y,
            picker=
            5,  # enable picking, i.e. the user can select this with the mouse
            color=colour,
            linewidth=width,
            linestyle=style)

        # show the updates
        self.canvas.draw()
        self.set_history_buttons()

    def on_button_plot_line(self, event):
        # draw a line; axis coordinates
        x0, y0, x1, y1 = self._get_floats("text_plot_line_",
                                          ("x0", "y0", "x1", "y1"))
        if None in (x0, y0, x1, y1):
            return None

        axes = self.get_axes()
        colour, width, style = self._get_styles()
        line = matplotlib.lines.Line2D((x0, x1), (y0, y1),
                                       pickradius=5,
                                       color=colour,
                                       linewidth=width,
                                       linestyle=style)
        line.set_picker(
            5)  # enable picking, i.e. the user can select this with the mouse
        axes.add_line(line)
        axes.autoscale(True, 'both', True)  # ensure that it is visible
        # show the updates
        self.canvas.draw()
        self.set_history_buttons()

    def on_button_plot_rect(self, event):
        # draw a rectangle; axis coordinates
        x, y, width, height, angle = self._get_floats(
            "text_plot_rect_", ("x", "y", "width", "height", "angle"))
        if None in (x, y, width, height, angle):
            return None

        axes = self.get_axes()
        colour, width, style = self._get_styles()
        patch = matplotlib.patches.Rectangle((x, y),
                                             width,
                                             height,
                                             angle,
                                             figure=self.canvas.figure,
                                             color=colour,
                                             linewidth=width,
                                             linestyle=style,
                                             fill=False)
        patch.set_picker(
            5)  # enable picking, i.e. the user can select this with the mouse
        axes.add_patch(patch)
        axes.autoscale(True, 'both', True)  # ensure that it is visible
        # show the updates
        self.canvas.draw()
        self.set_history_buttons()

    def on_button_plot_circle(self, event):
        # draw a circle; canvas coordinates / pixels
        # https://matplotlib.org/api/_as_gen/matplotlib.patches.Circle.html
        x, y, radius = self._get_floats("text_plot_circle_",
                                        ("x", "y", "radius"))
        if None in (x, y, radius):
            return None

        axes = self.get_axes()
        colour, width, style = self._get_styles()
        circle = matplotlib.patches.Circle((x, y),
                                           radius,
                                           figure=self.canvas.figure,
                                           color=colour,
                                           linewidth=width,
                                           linestyle=style)
        circle.set_picker(
            5)  # enable picking, i.e. the user can select this with the mouse
        axes.add_patch(circle)
        axes.autoscale(True, 'both', True)  # ensure that it is visible
        # show the updates
        self.canvas.draw()
        self.set_history_buttons()

    ####################################################################################################################
    # draw elements on canvas (by pixels)
    # see https://matplotlib.org/api/artist_api.html
    # rectangles, arrows, circles: https://matplotlib.org/api/patches_api.html
    def on_button_draw_line(self, event):
        # draw a line; canvas coordinates / pixels
        x0, y0, x1, y1 = self._get_floats("text_line_",
                                          ("x0", "y0", "x1", "y1"))
        if None in (x0, y0, x1, y1):
            return None

        colour, width, style = self._get_styles()
        # both figure and pickradius are required here; otherwise picking will not work
        line = matplotlib.lines.Line2D((x0, x1), (y0, y1),
                                       figure=self.canvas.figure,
                                       pickradius=5,
                                       color=colour,
                                       linewidth=width,
                                       linestyle=style)
        line.set_picker(
            5)  # enable picking, i.e. the user can select this with the mouse
        self.canvas.figure.lines.append(line)
        # show the updates
        self.canvas.draw()
        self.set_history_buttons()

    def on_button_draw_rect(self, event):
        # draw a rectangle; canvas coordinates / pixels
        x, y, w, h, angle = self._get_floats(
            "text_rect_", ("x", "y", "width", "height", "angle"))
        if None in (x, y, w, h, angle):
            return None

        colour, width, style = self._get_styles()
        # implement method? self.matplotlib_canvas.draw_rectangle( (x,y), width, height, angle )
        patch = matplotlib.patches.Rectangle((x, y),
                                             w,
                                             h,
                                             angle,
                                             figure=self.canvas.figure,
                                             color=colour,
                                             linewidth=width,
                                             linestyle=style)
        patch.set_picker(
            5)  # enable picking, i.e. the user can select this with the mouse
        self.canvas.figure.patches.append(patch)
        # show the updates
        self.canvas.draw()
        self.set_history_buttons()

    def on_button_draw_circle(self, event):
        # draw a circle; canvas coordinates / pixels
        # https://matplotlib.org/api/_as_gen/matplotlib.patches.Circle.html
        x, y, radius = self._get_floats("text_circle_", ("x", "y", "radius"))
        if None in (x, y, radius):
            return None

        colour, width, style = self._get_styles()
        # implement method? self.matplotlib_canvas.draw_rectangle( (x,y), width, height, angle )
        patch = matplotlib.patches.Circle((x, y),
                                          radius,
                                          figure=self.canvas.figure,
                                          color=colour,
                                          linewidth=width,
                                          linestyle=style)
        patch.set_picker(
            5)  # enable picking, i.e. the user can select this with the mouse
        self.canvas.figure.patches.append(patch)
        # show the updates
        self.canvas.draw()
        self.set_history_buttons()

    ####################################################################################################################
    # helpers
    def _get_floats(self, prefix, names):
        ret = []
        for name in names:
            full_name = prefix + name
            f = self._get_float(getattr(self, full_name))
            ret.append(f)
        return ret

    def _get_float(self, control):
        # returns a float or None if not a valid float
        try:
            ret = float(control.GetValue())
            colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
            control.SetBackgroundColour(colour)
        except:
            control.SetBackgroundColour(wx.RED)
            wx.Bell()
            ret = None
        control.Refresh()
        return ret

    ####################################################################################################################
    # mouse actions
    def on_pick(self, event):
        # pick: when the user clicked an item on the canvas that has picking enabled ( see above: line.set_picker(5) )

        if id(event.mouseevent) == self._last_pick_mouseevent:
            # it's reasonable that we have received this one already for another element
            print("skipping pick event")
            return
        self._last_pick_mouseevent = id(event.mouseevent)

        delete = self.checkbox_pick_delete.GetValue()

        artist = event.artist

        s = ""
        if isinstance(artist, matplotlib.lines.Line2D):
            s = "line"
        elif isinstance(artist, matplotlib.patches.Rectangle):
            s = "rectangle"
        elif isinstance(artist, matplotlib.patches.Circle):
            s = "circle"

        if artist.axes:
            # data plotted on axes
            if hasattr(artist, "get_data"):
                xdata, ydata = artist.get_data()
                ind = event.ind[0]
                s = 'plot: %s, %s' % (xdata[ind], ydata[ind])
            elif hasattr(artist, "get_xy"):
                xdata, ydata = artist.get_xy()
            if delete: artist.remove()
        else:
            # no axes -> drawn directly on figure by pixel coordinates
            if delete: self.canvas.figure.patches.remove(artist)
        self.text_picked.SetValue(s)
        self.canvas.draw()

    def on_mouse_move(self, event):
        # display mouse pointer coordinates; based on code from NavigationToolbar2
        s = ""
        if event.inaxes and event.inaxes.get_navigate():
            try:
                xs = event.inaxes.format_xdata(event.xdata).strip()
                ys = event.inaxes.format_ydata(event.ydata).strip()
                s = "%s / %s" % (xs, ys)
            except (ValueError, OverflowError):
                pass
            else:
                artists = [
                    a for a in event.inaxes.mouseover_set
                    if a.contains(event) and a.get_visible()
                ]

                if artists:
                    a = cbook._topmost_artist(artists)
                    if a is not event.inaxes.patch:
                        data = a.get_cursor_data(event)
                        if data is not None:
                            s += ' [%s]' % a.format_cursor_data(data)

        self.text_cursor_xy_value.SetValue(s)
        self.text_cursor_xy_pixel.SetValue("%s / %s px" % (event.x, event.y))

    ####################################################################################################################
    # canvas size and layout (number of plots)
    def on_choice_canvas_size(self, event):
        # this partially relies on a modification of FigureCanvas in matplotlib_compat.py
        value = event.GetString()
        sz = canvas.GetParent().Sizer
        si = sz.GetItem(canvas)
        if "x" in value:
            si.SetProportion(0)
            si.SetFlag(si.GetFlag() & ~wx.EXPAND)  # clear the expand bit
            size = value.split("x")
            size = int(size[0]), int(size[1])
            si.SetMinSize(size)
            canvas.SetSize(size)
            canvas.SetMinSize(size)
        else:
            # variable size
            si.SetProportion(3)
            si.SetFlag(si.GetFlag() | wx.EXPAND)  # clear the expand bit
            si.SetMinSize((100, 100))
        sz.Layout()

    def on_choice_subplots(self, event=None):
        # clear the canvas if required; set the "Subplot" choices
        if event is None:
            rows_cols = self.choice_subplots.GetStringSelection()
            update_subplot_choices = True
        else:
            rows_cols = event.GetString()
            update_subplot_choices = False
        rows, cols = rows_cols.split("x")  # e.g. "2x1"
        subplots = int(rows), int(cols)
        if hasattr(self, "subplots") and subplots != self.subplots:
            # changed -> clear existing plots
            #self.on_button_clear("plots")
            self.on_button_clear("all")
            update_subplot_choices = True
        if update_subplot_choices:
            for SUBPLOTS, CHOICES in self._SUBPLOT_CHOICES:
                if SUBPLOTS == subplots:
                    if CHOICES is None:
                        self.choice_subplot.Clear()
                        self.choice_subplot.Disable()
                        self.subplot = (1, 1)
                    else:
                        self.choice_subplot.Set(CHOICES)
                        self.choice_subplot.SetSelection(0)
                        self.choice_subplot.Enable(len(CHOICES) > 1)
                        self.subplot = CHOICES[0]

        self.subplots = subplots

    def on_choice_subplot(self, event):
        self.subplot = event.GetString()

    ####################################################################################################################
    # plot styles: maintain properties for colour, line width, line style
    def _get_styles(self):
        ret = (self._line_colour, self._line_width, self._line_style)
        n = self.combo_box_colour.GetSelection() + 1
        n = n % self.combo_box_colour.GetCount()
        self.combo_box_colour.Select(n)
        self.on_combo_colour()
        return ret

    def on_combo_colour(self, event=None):
        self._line_colour = self.combo_box_colour.GetValue() or "black"

    def on_combo_line_width(self, event=None):
        self._line_width = float(self.combo_box_line_width.GetValue())

    def on_choice_line_style(self, event=None):
        line_style = self.choice_line_style.GetItems()[
            self.choice_line_style.GetSelection()]
        self._line_style = line_style.split()[0].strip()

    ####################################################################################################################
    # some actions to be forwarded to the tool manager
    def on_button_zoom_history(self, action):
        if action == "home":
            self.toolmanager.trigger_tool("home")
        elif action == "back":
            self.toolmanager.trigger_tool("back")
        elif action == "forward":
            self.toolmanager.trigger_tool("forward")
        self.canvas.draw()
        self.set_history_buttons()

    def on_button_autoscale(self, event):
        if not self.canvas.figure.axes: return
        for axes in self.canvas.figure.axes:
            axes.autoscale(True, 'both', True)
        self.canvas.draw()
        viewpos_tool = self.toolmanager.get_tool("viewpos")
        viewpos_tool.clear(self.canvas.figure)
        if self.canvas.figure in viewpos_tool.views:
            self.toolmanager.get_tool("viewpos").push_current()
        self.set_history_buttons()

    def set_history_buttons(self, event=None):
        # enable or disable buttons: zoom/scale, clear
        vp = self.toolmanager.get_tool("viewpos")
        if vp.views:
            view_stack = vp.views[self.canvas.figure]
            view_stack._pos

            can_backward = view_stack._pos > 0
            can_forward = view_stack._pos < len(view_stack._elements) - 1
        else:
            can_backward = can_forward = False
        self.button_zoom_hist_back.Enable(can_backward)
        self.button_zoom_hist_forward.Enable(can_forward)
        # enable the autoscale button as well
        can_autoscale = bool(self.canvas.figure.axes)
        self.button_autoscale.Enable(can_autoscale)
        # and the clear buttons
        can_clear_plots = False
        for axes in self.canvas.figure.axes:
            if axes.has_data():
                can_clear_plots = True
                break
        can_clear_all = len(self.canvas.figure.get_children()) > 1
        self.button_clear_plots.Enable(can_clear_plots)
        self.button_clear_all.Enable(can_clear_all)

    def on_choice_mouse_action(self, event):
        selection = self.choice_mouse_action.GetStringSelection()
        if selection == "Pan/Zoom":
            self.toolmanager.trigger_tool("pan")
        elif selection == "Zoom":
            self.toolmanager.trigger_tool("zoom")
        else:
            # deactivate by triggering the currently selected tool
            # when pan and zoom are inactive, the mouse will pick elements
            toggled = self.toolmanager.active_toggle.get("default")
            if toggled == "pan":
                self.toolmanager.trigger_tool("pan")
            elif toggled == "zoom":
                self.toolmanager.trigger_tool("zoom")

    def on_file_save(self, event):
        # save figure as bitmap or PDF
        self.toolmanager.trigger_tool("save")

    ####################################################################################################################

    def on_file_exit(self, event):
        self.Close()

    def OnClose(self, event):
        # delete graphics context to avoid crash
        self.canvas.cleanup()
        event.Skip()
Example #3
0
class MyFrame(matplotlib_GUI.MyFrame):
    # set up datastructure used by get_axes() method -------------------------------------------------------------------
    # where to place plots? map user choice to subplot: (row, col, position)
    # e.g. "top left" for 2x2 plots -> (2,2,1)
    # this will be the argument to self.canvas.figure.add_subplot(...)
    _SUBPLOT_CHOICES = [
        ((1, 1), ("center", )), ((1, 2), ("left", "right")),
        ((2, 1), ("top", "bottom")),
        ((2, 2), ("top left", "top right", "bottom left", "bottom right")),
        ((2, 3), ("top left", "top center", "top right", "bottom left",
                  "bottom center", "bottom right"))
    ]
    _SUBPLOT_POSITIONS = {}
    for ((rows, cols), choices) in _SUBPLOT_CHOICES:
        _SUBPLOT_POSITIONS[rows, cols] = {}
        i = 0
        for row in range(1, rows + 1):
            for col in range(1, cols + 1):
                if choices:
                    _SUBPLOT_POSITIONS[rows,
                                       cols][choices[i]] = (rows, cols, i + 1)
                i += 1
    del rows, cols, choices, i

    # ------------------------------------------------------------------------------------------------------------------

    def __init__(self, *args, **kwargs):
        self.result = {}

        matplotlib_GUI.MyFrame.__init__(self, *args, **kwargs)
        self.figure = self.canvas.figure

        self.init_toolmanager()
        self.init_events()
        # where to save figures by default?
        matplotlib.rcParams[
            'savefig.directory'] = ""  # defaults to current directory

        self.Bind(wx.EVT_CLOSE, self.OnClose)

        # initialize values from control values; so the .wxg file needs to have correct initialization values!
        self.on_choice_line_style()
        self.on_combo_colour()
        self.on_combo_line_width()

        self.on_choice_subplots()
        self.set_history_buttons()
        self.on_button_plot()

        self.multicursor = None

    def init_toolmanager(self):
        self.toolmanager = matplotlib.backend_managers.ToolManager(
            self.canvas.figure)

        self.toolmanager.add_tool(
            'viewpos',
            'ToolViewsPositions')  # required for pan/zoom/home/back/forward
        self.toolmanager.add_tool(
            'pan', 'ToolPan')  # pan w. mouse and zoom w. wheel with 'p' key
        self.toolmanager.add_tool('zoom',
                                  'ToolZoom')  # zoom to rect with 'o' key
        self.toolmanager.add_tool('home', 'ToolHome')  # 'h', 'r', 'home'
        if hasattr(matplotlib.backend_managers, "ToolHelp"):
            self.toolmanager.add_tool('help', 'ToolHelp')  # 'F1'
        self.toolmanager.add_tool('back',
                                  'ToolBack')  # 'left', 'c', 'backspace'
        self.toolmanager.add_tool('forward', 'ToolForward')  # 'right', 'v'
        self.toolmanager.add_tool('save', 'ToolSaveFigure')  # 's', 'ctrl+s'
        self.toolmanager.add_tool(
            'grid', 'ToolGrid')  # toggle throug major h/v grids with 'g' key
        self.toolmanager.add_tool(
            'grid_minor',
            'ToolMinorGrid')  # toggle throug major/minor grids with 'G' key

        self.toolmanager.add_tool(
            'yscale', 'ToolYScale')  # toggle lin/log scaling with 'l' key
        self.toolmanager.add_tool(
            'xscale', 'ToolXScale')  # toggle lin/log scaling with 'k','L' keys

        # some tools will only be available with matplotlib 3.0:
        if hasattr(matplotlib.backend_managers, "ToolCopyToClipboard"):
            self.toolmanager.add_tool('copy', 'ToolCopyToClipboard')

        self.toolmanager.add_tool('rubberband', 'ToolRubberband')
        self.toolmanager.add_tool('setcursor', 'ToolSetCursor')

        self.toolmanager.toolmanager_connect("tool_trigger_home",
                                             self.set_history_buttons)
        self.toolmanager.toolmanager_connect("tool_trigger_back",
                                             self.set_history_buttons)
        self.toolmanager.toolmanager_connect("tool_trigger_forward",
                                             self.set_history_buttons)

    def init_events(self):
        # connect to matplotlib events

        # for picking elements
        self.canvas.mpl_connect('pick_event', self.on_pick)
        self._last_pick_mouseevent = None  # store info, as we will act only once per pick event

        # for displaying cursor position
        self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)

        # button_release_event may be the end of a zoom; use CallAfter as we may receive the event too early
        self.canvas.mpl_connect(
            "button_release_event",
            lambda event: wx.CallAfter(self.set_history_buttons))

        # register a handler for all canvas events except 'motion_notify_event', which would cause too much traffic
        for event in ('button_press_event', 'button_release_event',
                      'key_press_event', 'key_release_event', 'draw_event',
                      'pick_event', 'resize_event', 'scroll_event',
                      'figure_enter_event', 'figure_leave_event',
                      'axes_enter_event', 'axes_leave_event'):
            self.canvas.mpl_connect(event, self.on_canvas_event)
        # in addition, there are events from the axes: 'xlim_changed', 'ylim_changed' (see self.get_axes())

    ###################################################################################################################
    # print messages
    def on_canvas_event(self, event):
        print("canvas event:", event)

    ###################################################################################################################
    # global actions: add plot or clear plots or all
    def get_axes(self):
        # return the axes that are selected acc. to "Subplots" and "Subplot position"
        subplots = self._SUBPLOT_POSITIONS[self.subplots][self.subplot]
        # subplots = (row, col, position), e.g. (2,2,1) for the top left of 2x2 plots
        ret = self.figure.add_subplot(*subplots)
        # we could register event handlers when x or y limits are changing:
        #ret.callbacks.connect( 'xlim_changed', lambda evt: print("callback xlim_changed:", evt) )
        return ret

    def on_button_clear(self, types):
        if self.multicursor:
            self.multicursor.disconnect()
            self.multicursor = None
            self.button_multicursor.SetValue(False)

        if types == "plots":
            for axes in self.figure.axes:
                axes.clear()
        elif types in ("figures", "all"):
            self.figure.clear()
        self.canvas.draw()

        if types in ("plots", "all"):
            # reset zoom history
            self.toolmanager.get_tool("viewpos").clear(self.canvas.figure)
            self.set_history_buttons()

    def on_button_multicursor(self, event):
        from matplotlib.widgets import MultiCursor

        axes = self.canvas.figure.axes
        if event.IsChecked() and not self.multicursor and axes:
            # add multicursor
            self.multicursor = MultiCursor(self.canvas,
                                           axes,
                                           color='r',
                                           lw=1,
                                           horizOn=True,
                                           vertOn=True)
        elif self.multicursor:
            # remove multicursor
            self.multicursor.disconnect()
            self.multicursor = None
        self.canvas.draw()
        event.Skip()

    ####################################################################################################################
    # draw elements on canvas plot areas / axes (by coordinates)
    def on_button_plot(self, event=None):
        # plot a function
        xmin, xmax, step = self._get_floats("text_plot_",
                                            ("xmin", "xmax", "xstep"))
        if None in (xmin, xmax, step):
            # one of the values is invalid
            return
        x = numpy.arange(xmin, xmax, step)

        # build globals with some functions for eval(...)
        g = {}
        for name in ["sin", "cos", "tan", "ufunc", "square"]:
            g[name] = getattr(numpy, name)
        y = eval(self.text_function.GetValue(), g, {"numpy": numpy, "x": x})

        axes = self.get_axes()
        colour, width, style = self._get_styles()

        axes.plot(
            x,
            y,
            picker=
            5,  # enable picking, i.e. the user can select this with the mouse
            color=colour,
            linewidth=width,
            linestyle=style)

        # show the updates
        self.canvas.draw()
        self.set_history_buttons()

    ####################################################################################################################
    # helpers
    def _get_floats(self, prefix, names):
        ret = []
        for name in names:
            full_name = prefix + name
            f = self._get_float(getattr(self, full_name))
            ret.append(f)
        return ret

    def _get_float(self, control):
        # returns a float or None if not a valid float
        try:
            ret = float(control.GetValue())
            colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
            control.SetBackgroundColour(colour)
        except:
            control.SetBackgroundColour(wx.RED)
            wx.Bell()
            ret = None
        control.Refresh()
        return ret

    ####################################################################################################################
    # mouse actions
    def on_pick(self, event):
        # pick: when the user clicked an item on the canvas that has picking enabled ( see above: line.set_picker(5) )

        if id(event.mouseevent) == self._last_pick_mouseevent:
            # it's reasonable that we have received this one already for another element
            print("skipping pick event")
            return
        self._last_pick_mouseevent = id(event.mouseevent)

        delete = self.checkbox_pick_delete.GetValue()

        artist = event.artist

        s = ""
        if isinstance(artist, matplotlib.lines.Line2D):
            s = "line"
        elif isinstance(artist, matplotlib.patches.Rectangle):
            s = "rectangle"
        elif isinstance(artist, matplotlib.patches.Circle):
            s = "circle"

        if artist.axes:
            # data plotted on axes
            if hasattr(artist, "get_data"):
                xdata, ydata = artist.get_data()
                ind = event.ind[0]
                s = 'plot: %s, %s' % (xdata[ind], ydata[ind])
            elif hasattr(artist, "get_xy"):
                xdata, ydata = artist.get_xy()
            if delete: artist.remove()
        else:
            # no axes -> drawn directly on figure by pixel coordinates
            if delete: self.canvas.figure.patches.remove(artist)
        self.text_picked.SetValue(s)
        self.canvas.draw()

    def on_mouse_move(self, event):
        # display mouse pointer coordinates; based on code from NavigationToolbar2
        s = ""
        if event.inaxes and event.inaxes.get_navigate():
            try:
                xs = event.inaxes.format_xdata(event.xdata).strip()
                ys = event.inaxes.format_ydata(event.ydata).strip()
                s = "%s / %s" % (xs, ys)
            except (ValueError, OverflowError):
                pass
            else:
                artists = [
                    a for a in event.inaxes.mouseover_set
                    if a.contains(event) and a.get_visible()
                ]

                if artists:
                    a = cbook._topmost_artist(artists)
                    if a is not event.inaxes.patch:
                        data = a.get_cursor_data(event)
                        if data is not None:
                            s += ' [%s]' % a.format_cursor_data(data)

        self.text_cursor_xy_value.SetValue(s)
        self.text_cursor_xy_pixel.SetValue("%s / %s px" % (event.x, event.y))

    ####################################################################################################################
    # canvas size and layout (number of plots)
    def on_choice_canvas_size(self, event):
        # this partially relies on a modification of FigureCanvas in matplotlib_compat.py
        value = event.GetString()
        sz = canvas.GetParent().Sizer
        si = sz.GetItem(canvas)
        if "x" in value:
            si.SetProportion(0)
            si.SetFlag(si.GetFlag() & ~wx.EXPAND)  # clear the expand bit
            size = value.split("x")
            size = int(size[0]), int(size[1])
            si.SetMinSize(size)
            canvas.SetSize(size)
            canvas.SetMinSize(size)
        else:
            # variable size
            si.SetProportion(3)
            si.SetFlag(si.GetFlag() | wx.EXPAND)  # clear the expand bit
            si.SetMinSize((100, 100))
        sz.Layout()

    def on_choice_subplots(self, event=None):
        # clear the canvas if required; set the "Subplot" choices
        if event is None:
            rows_cols = self.choice_subplots.GetStringSelection()
            update_subplot_choices = True
        else:
            rows_cols = event.GetString()
            update_subplot_choices = False
        rows, cols = rows_cols.split("x")  # e.g. "2x1"
        subplots = int(rows), int(cols)
        if hasattr(self, "subplots") and subplots != self.subplots:
            # changed -> clear existing plots
            #self.on_button_clear("plots")
            self.on_button_clear("all")
            update_subplot_choices = True
        if update_subplot_choices:
            for SUBPLOTS, CHOICES in self._SUBPLOT_CHOICES:
                if SUBPLOTS == subplots:
                    if CHOICES is None:
                        self.choice_subplot.Clear()
                        self.choice_subplot.Disable()
                        self.subplot = (1, 1)
                    else:
                        self.choice_subplot.Set(CHOICES)
                        self.choice_subplot.SetSelection(0)
                        self.choice_subplot.Enable(len(CHOICES) > 1)
                        self.subplot = CHOICES[0]

        self.subplots = subplots

    def on_choice_subplot(self, event):
        self.subplot = event.GetString()

    ####################################################################################################################
    # plot styles: maintain properties for colour, line width, line style
    def _get_styles(self):
        ret = (self._line_colour, self._line_width, self._line_style)
        n = self.combo_box_colour.GetSelection() + 1
        n = n % self.combo_box_colour.GetCount()
        self.combo_box_colour.Select(n)
        self.on_combo_colour()
        return ret

    def on_combo_colour(self, event=None):
        self._line_colour = self.combo_box_colour.GetValue() or "black"

    def on_combo_line_width(self, event=None):
        self._line_width = float(self.combo_box_line_width.GetValue())

    def on_choice_line_style(self, event=None):
        line_style = self.choice_line_style.GetItems()[
            self.choice_line_style.GetSelection()]
        self._line_style = line_style.split()[0].strip()

    ####################################################################################################################
    # some actions to be forwarded to the tool manager
    def on_button_zoom_history(self, action):
        if action == "home":
            self.toolmanager.trigger_tool("home")
        elif action == "back":
            self.toolmanager.trigger_tool("back")
        elif action == "forward":
            self.toolmanager.trigger_tool("forward")
        self.canvas.draw()
        self.set_history_buttons()

    def on_button_autoscale(self, event):
        if not self.canvas.figure.axes: return
        for axes in self.canvas.figure.axes:
            axes.autoscale(True, 'both', True)
        self.canvas.draw()
        viewpos_tool = self.toolmanager.get_tool("viewpos")
        viewpos_tool.clear(self.canvas.figure)
        if self.canvas.figure in viewpos_tool.views:
            self.toolmanager.get_tool("viewpos").push_current()
        self.set_history_buttons()

    def set_history_buttons(self, event=None):
        # enable or disable buttons: zoom/scale, clear
        vp = self.toolmanager.get_tool("viewpos")
        if vp.views:
            view_stack = vp.views[self.canvas.figure]
            view_stack._pos

            can_backward = view_stack._pos > 0
            can_forward = view_stack._pos < len(view_stack._elements) - 1
        else:
            can_backward = can_forward = False
        self.button_zoom_hist_back.Enable(can_backward)
        self.button_zoom_hist_forward.Enable(can_forward)
        # enable the autoscale button as well
        can_autoscale = bool(self.canvas.figure.axes)
        self.button_autoscale.Enable(can_autoscale)
        # and the clear buttons
        can_clear_plots = False
        for axes in self.canvas.figure.axes:
            if axes.has_data():
                can_clear_plots = True
                break
        can_clear_all = len(self.canvas.figure.get_children()) > 1
        self.button_clear_plots.Enable(can_clear_plots)
        self.button_clear_all.Enable(can_clear_all)

    def on_choice_mouse_action(self, event):
        selection = self.choice_mouse_action.GetStringSelection()
        if selection == "Pan/Zoom":
            self.toolmanager.trigger_tool("pan")
        elif selection == "Zoom":
            self.toolmanager.trigger_tool("zoom")
        else:
            # deactivate by triggering the currently selected tool
            # when pan and zoom are inactive, the mouse will pick elements
            toggled = self.toolmanager.active_toggle.get("default")
            if toggled == "pan":
                self.toolmanager.trigger_tool("pan")
            elif toggled == "zoom":
                self.toolmanager.trigger_tool("zoom")

    ####################################################################################################################
    ####################################################################################################################
    ####################################################################################################################

    def readfun(self, f, bar):
        global prog_i
        bar.update(prog_i)
        prog_i = prog_i + 1
        return pandas.read_csv(f, index_col=False)

    def on_button_adddata(self, event):
        global prog_i
        prog_i = 0
        self.list_box_currentdata.Append(
            self.list_box_data_folder.GetStringSelection())
        self.list_box_currentdata.SetSelection(
            self.list_box_currentdata.FindString(
                self.list_box_data_folder.GetStringSelection()))
        picklefolder = "F:\\A7data\\p\\"
        folder = self.list_box_data_folder.GetStringSelection()
        files = glob.glob("D:\Project\ODEA7\PTrun\\" + folder +
                          '\\a\\allMyNumbers*.csv')
        bar = progressbar.ProgressBar(max_value=len(files))

        if os.path.exists(picklefolder + folder):
            print("read pickle")
            self.result[folder] = pandas.read_pickle(picklefolder + folder)
        else:
            print("read raw csv")
            self.result[folder] = pandas.concat(
                [self.readfun(f, bar) for f in files], ignore_index=True)
            self.result[folder].to_pickle(picklefolder + folder)

        self.slider_length.SetRange(
            1, self.result[
                self.list_box_data_folder.GetStringSelection()].shape[0])
        self.slider_length.SetValue(self.result[
            self.list_box_data_folder.GetStringSelection()].shape[0])
        self.slider_startpoint.SetRange(
            1, self.result[
                self.list_box_data_folder.GetStringSelection()].shape[0])
        self.slider_startpoint.SetValue(1)
        winsound.Beep(2500, 300)
        print("_add data'!")

    def on_button_update_folder(self, event):
        runlist = os.listdir('D:\Project\ODEA7\PTrun')
        self.list_box_data_folder.Set(runlist)
        # self.listBox.SetSelection(2)
        print("update'!")

    def on_button_removedata(self, event):
        self.result.pop(
            self.list_box_currentdata.GetSelection()
        )  # obvious logic error in delete an item added more than once but can be avoided by restricted ops
        self.list_box_currentdata.Delete(
            self.list_box_currentdata.GetSelection())
        print("_rmdata'!")

    def on_button_draw(self, event):

        x = self.result[self.list_box_currentdata.GetStringSelection(
        )].loc[:, self.list_box_parax.GetStringSelection()]
        y = self.result[self.list_box_currentdata.GetStringSelection(
        )].loc[:, self.list_box_paray.GetStringSelection()]
        endpoint = self.slider_startpoint.GetValue(
        ) + self.slider_length.GetValue() - 1
        if endpoint > self.slider_startpoint.GetMax() - 1:
            endpoint = self.slider_startpoint.GetMax() - 1
        x = x.iloc[self.slider_startpoint.GetValue():endpoint]
        y = y.iloc[self.slider_startpoint.GetValue():endpoint]
        axes = self.get_axes()
        colour, width, style = self._get_styles()

        axes.plot(
            x,
            y,
            picker=
            5,  # enable picking, i.e. the user can select this with the mouse
            color=colour,
            linewidth=width,
            linestyle=style)

        # show the updates
        self.canvas.draw()

    def on_button_popupdraw1(self, event):
        startPer = self.slider_startpoint.GetValue(
        ) / self.slider_startpoint.GetMax()
        endPer = (self.slider_startpoint.GetValue() +
                  self.slider_length.GetValue() -
                  1) / self.slider_startpoint.GetMax()
        paraList = [
            self.list_box_parax.GetStringSelection(),
            self.list_box_paray.GetStringSelection()
        ]
        fig = plt.figure()
        axes = fig.add_subplot(1, 1, 1)
        self.draw_on_request(ax=axes,
                             startPer=startPer,
                             endPer=endPer,
                             paraList=paraList)
        plt.show()

    def on_button_popupdraw2(self, event):
        fig = plt.figure()
        axes = fig.add_subplot(2, 3, 1)
        self.draw_on_request(ax=axes, paraList=['time', 'xb1'])
        axes = fig.add_subplot(2, 3, 2)
        self.draw_on_request(ax=axes, paraList=['time', 'yb1'])
        axes = fig.add_subplot(2, 3, 3)
        self.draw_on_request(ax=axes, paraList=['time', 'zb1'])
        axes = fig.add_subplot(2, 3, 4, projection='3d')
        self.draw_on_request(ax=axes, paraList=['time', 'xb1', 'yb1'], dim=3)
        axes = fig.add_subplot(2, 3, 5, projection='3d')
        self.draw_on_request(ax=axes, paraList=['xb1', 'yb1', 'zb1'], dim=3)
        axes = fig.add_subplot(2, 3, 6)
        #self.draw_on_request(ax=axes, paraList = ['time','xb1'])
        plt.show()

    def on_button_popupdraw3(self, event):
        fig = plt.figure()
        paraLists = [['time', 'xb1'], ['time', 'yb1'], ['time', 'zb1']]
        index = 1
        for paraList in paraLists:
            axes = fig.add_subplot(2, 2, index)
            self.draw_on_request(ax=axes, paraList=paraList)
            index = index + 1

        fig = plt.figure()
        paraLists = [['time', 'xb2'], ['time', 'yb2'], ['time', 'zb2']]
        index = 1
        for paraList in paraLists:
            axes = fig.add_subplot(2, 2, index)
            self.draw_on_request(ax=axes, paraList=paraList)
            index = index + 1

        fig = plt.figure()
        paraLists = [['time', 'xb3'], ['time', 'yb3'], ['time', 'zb3']]
        index = 1
        for paraList in paraLists:
            axes = fig.add_subplot(2, 2, index)
            self.draw_on_request(ax=axes, paraList=paraList)
            index = index + 1

        fig = plt.figure()
        paraLists = [['time', 'xb4'], ['time', 'yb4'], ['time', 'zb4']]
        index = 1
        for paraList in paraLists:
            axes = fig.add_subplot(2, 2, index)
            self.draw_on_request(ax=axes, paraList=paraList)
            index = index + 1

        fig = plt.figure()
        paraLists = [['time', 'x1'], ['time', 'y1'], ['time', 'z1'],
                     ['time', 'theta1x'], ['time', 'theta1y'],
                     ['time', 'theta1z']]
        index = 1
        for paraList in paraLists:
            axes = fig.add_subplot(2, 3, index)
            self.draw_on_request(ax=axes, paraList=paraList)
            index = index + 1
        fig = plt.figure()
        paraLists = [['time', 'x2'], ['time', 'y2'], ['time', 'z2'],
                     ['time', 'theta2x'], ['time', 'theta2y'],
                     ['time', 'theta2z']]
        index = 1
        for paraList in paraLists:
            axes = fig.add_subplot(2, 3, index)
            self.draw_on_request(ax=axes, paraList=paraList)
            index = index + 1
        fig = plt.figure()
        paraLists = [['time', 'thetap'], ['time', 'thetag'],
                     ['time', 'theta1z'], ['time', 'theta2z']]
        index = 1
        for paraList in paraLists:
            axes = fig.add_subplot(2, 2, index)
            self.draw_on_request(ax=axes, paraList=paraList)
            index = index + 1

        plt.show()

    def draw_on_request(self,
                        ax,
                        startPer=0,
                        endPer=1,
                        dim=2,
                        paraList=['time', 'thetap']):
        drawdata = {}
        for obj in self.result.items():
            #firstPara = self.list_box_parax.GetStringSelection()
            # = self.list_box_paray.GetStringSelection()
            key, data = obj
            drawdata[key] = self.getdrawdata(data, paraList)
            total_len = drawdata[key].shape[0]  # length in dim 0
            startPoint = int(total_len * startPer)
            endPoint = int(total_len * endPer)
            drawdata[key] = drawdata[key][startPoint:endPoint, :]
            if dim == 2:
                drawn = self.drawPlot2d(ax,
                                        drawdata[key][:, 0],
                                        drawdata[key][:, 1],
                                        label=key)
                ax.set_xlabel(paraList[0])
                ax.set_ylabel(paraList[1])
            if dim == 3:
                drawn = self.drawPlot3d(ax,
                                        drawdata[key][:, 0],
                                        drawdata[key][:, 1],
                                        drawdata[key][:, 2],
                                        label=key)
                ax.set_xlabel(paraList[0])
                ax.set_ylabel(paraList[1])
                ax.set_zlabel(paraList[2])
            ax.legend()

    def getdrawdata(self, data, paraList):
        nd = numpy.array(data[paraList])
        return nd

    def drawPlot2d(self, ax, x, y, label):
        drawn = ax.plot(
            x,
            y,
            picker=
            5,  # enable picking, i.e. the user can select this with the mouse
            label=label)
        return drawn

    def drawPlot3d(self, ax, x, y, z, label):
        drawn = ax.plot(
            x,
            y,
            z,
            picker=
            5,  # enable picking, i.e. the user can select this with the mouse
            label=label)
        return drawn

    def on_button_openfolder(self, event):
        start_directory = r'D:\Project\ODEA7\PTrun\\' + self.list_box_data_folder.GetStringSelection(
        )
        os.startfile(start_directory)

    def on_button_opencfg(self, event):
        start_directory = r'D:\Project\ODEA7\PTrun\\' + self.list_box_data_folder.GetStringSelection(
        ) + '\cfg.ini'
        os.startfile(start_directory)

    def on_file_save(self, event):
        # save figure as bitmap or PDF
        self.toolmanager.trigger_tool("save")

    ####################################################################################################################
    ####################################################################################################################
    ####################################################################################################################

    def on_file_exit(self, event):
        self.Close()

    def OnClose(self, event):
        # delete graphics context to avoid crash
        self.canvas.cleanup()
        event.Skip()