class GraphFrame(AppWidget): def __init__(self, parent, controller, config, dbvar=None, wd=14, ht=7.5): self.widget_name = "graphframe" self.lines_list = [] self.bars_list = [] self.event_polygon_list = [] self.prime_graph_pack = None self.secondary_graph_pack = None super().__init__(parent, controller, config, dbvar=None) self.my_figure = Figure(figsize=(wd, ht), dpi=85) self.my_legend = self.my_figure.legend() self.axis_prime = self.my_figure.add_subplot(1, 1, 1) # self.axis_prime.set_title("No Query Selected") self.axis_secondary = self.my_figure.add_subplot( 111, sharex=self.axis_prime, frameon=False) self.axis_secondary.xaxis.set_visible(False) self.axis_secondary.yaxis.set_label_position("right") self.axis_secondary.yaxis.tick_right() self.canvas = FigureCanvas(self.my_figure, self) self.canvas.draw() # # self.canvas.manager.toolbar.add_tool('zoom', 'foo') # self.toolbar = NavigationToolbar2TkAgg(self.canvas, self) # self.toolbar.update() self.canvas._tkcanvas.pack() def update_annot(self, hover_obj, ind, kind="line"): text = "DEFAULT" if kind == "line": x, y = hover_obj.get_data() coords = (x[ind["ind"][0]], y[ind["ind"][0]]) yshort = '%.2f' % coords[1] self.annot.xy = coords text = "{}\n{},{}".format(hover_obj.get_label(), coords[0], yshort) self.annot.set_text(text) self.annot.get_bbox_patch().set_alpha(0.4) elif kind == "bar": coords = hover_obj.get_xy() ynew = coords[1] + hover_obj.get_height() self.annot.xy = coords[0] + hover_obj.get_width() / 2, ynew text = "{}-{}".format( self.axis_prime.get_xticklabels()[ind].get_text(), ynew) self.annot.set_text(text) self.annot.get_bbox_patch().set_alpha(0.4) def hover(self, event): vis = self.annot.get_visible() if str(event.inaxes) == str(self.axis_prime): for line2d in self.lines_list: cont, ind = line2d.contains(event) if cont: self.update_annot(line2d, ind) self.annot.set_visible(True) self.canvas.draw_idle() return for index, rec in enumerate(self.bars_list): cont, ind = rec.contains(event) if cont: self.update_annot(rec, index, kind="bar") self.annot.set_visible(True) self.canvas.draw_idle() return if vis: self.annot.set_visible(False) self.canvas.draw_idle() def update_graph(self, gr_pk): # PRETTYPRINT(gr_pk) """ {'axis': 'left', 'breakdown_colors': ['firebrick', 'dodgerblue', 'seagreen', 'darkorchid', 'gray', 'yellow', 'salmon', 'deeppink'], 'color': '#a38bb6', 'end': datetime.date(2018, 4, 1), 'event_dates': [('04/29 Sale', '20180429', '20180429'), ('05/01-05/06 Event', '20180501', '20180506'), ('05/01-05/06 Event', '20180517', '20180520')], 'force_y': (True, 0, 4), 'gtype': 'date_series', 'line_labels': ['PC', '모바일', 'm_PC', 'm_모바일'], 'linestyles': '-', 'met': 'Order Size', 'start': datetime.date(2018, 3, 18), 'str_x': 'Date (Average of 7-Day Periods)', 'str_y': 'Order Size (Average)', 'title': 'Average of Order Size (Exclude Cancelled Items) By Gen. Platform', 'x_data': [datetime.date(2018, 3, 18), datetime.date(2018, 3, 25), datetime.date(2018, 4, 1)], 'y_data': [[3.371877804372405, 3.330132851297275, 3.3963963963963963], [2.5645786328057234, 2.5824528794692228, 2.647798742138365], [3.6697965620085644, 3.371877804372405, 3.433628318584071], [2.5361031401007033, 2.5645786328057234, 2.6683168316831685]]} """ # CLEAN UP AND RESET self.log("Update graph call.") self.clear_graph() # DETERMINE TARGET AXIS if gr_pk["axis"] == "left": target_axis = self.axis_prime elif gr_pk["axis"] == "right": target_axis = self.axis_secondary self.axis_prime.xaxis.set_visible(True) self.axis_prime.yaxis.set_visible(True) self.axis_prime.grid(b=True, which="major") self.prime_graph_pack = gr_pk other_pack = self.secondary_graph_pack self.log("target_axis: {} => self.axis_prime".format(gr_pk["axis"])) # TITLING if not other_pack: target_axis.set_title(gr_pk["title"]) target_axis.xaxis.set_label_text(gr_pk["str_x"]) elif other_pack: new_title = "{} vs. {}".format(self.prime_graph_pack["title"], self.secondary_graph_pack["title"]) target_axis.set_title(new_title) target_axis.yaxis.set_label_text(gr_pk["str_y"]) # ## GRAPH PRP gr_pk_colors = [gr_pk["color"]] + gr_pk["breakdown_colors"] # PRETTYPRINT(gr_pk_colors) start = gr_pk["start"] end = gr_pk["end"] color_c = 0 if gr_pk["gtype"] == "date_series": for x in range(0, len(gr_pk["y_data"])): try: gr_pk_colors[x] except IndexError: color_to_use = gr_pk_colors[x - 10] else: color_to_use = gr_pk_colors[x] if "m_" in gr_pk["line_labels"][x]: linewidth = 0.75 else: linewidth = 1.45 self.lines_list.append( target_axis.plot_date(gr_pk["x_data"], gr_pk["y_data"][x], color=color_to_use, ls=gr_pk["linestyles"], lw=linewidth, label=gr_pk["line_labels"][x])[0]) # print(self.line) color_c += 1 target_axis.set_xlim(gr_pk["x_data"][0], gr_pk["x_data"][-1]) elif gr_pk["gtype"] == "string-bar" or gr_pk["gtype"] == "bar": self.bars_list = target_axis.bar( gr_pk["x_data"], gr_pk["y_data"][0], color=gr_pk_colors[color_c]).patches color_c += 1 elif gr_pk["gtype"] == "pie": self.axis_prime.pie(gr_pk["y_data"][0], labels=gr_pk["x_data"]) # # POST PLOTTING FORMATTING if not gr_pk["gtype"] == "pie": # Y-AXIS FORMATTING if gr_pk["force_y"][0]: target_axis.set_ylim(bottom=gr_pk["force_y"][1], top=gr_pk["force_y"][2]) elif not gr_pk["force_y"][0]: if target_axis.get_ylim()[1] >= 1000: target_axis.get_yaxis().set_major_formatter( ticker.FuncFormatter(lambda x, p: format(int(x), ','))) if target_axis.get_ylim()[0] <= 0: target_axis.set_ylim(bottom=0) for tick in target_axis.get_xticklabels(): tick.set_fontsize(8) tick.set_rotation(30) # EVENT HIGHLIGHTS if gr_pk["str_x"] == "Date": # event_colors = self.get_cfg_val("event_colors").split("-") invDisToAxFrac = self.axis_prime.transAxes.inverted() axis_left_lim = matplotlib.dates.num2date( self.axis_prime.get_xlim()[0]).date() for e_index, event_tuple in enumerate(gr_pk["event_dates"]): name_str = event_tuple[0] start_str = event_tuple[1] end_str = event_tuple[2] self.log("{} -> {}".format(start_str, end_str)) start_date = datetime.date(int(start_str[0:4]), int(start_str[4:6]), int(start_str[6:8])) end_date = datetime.date(int(end_str[0:4]), int(end_str[4:6]), int(end_str[6:8])) axis_left_lim = matplotlib.dates.num2date( self.axis_prime.get_xlim()[0]).date() # if start_date < axis_left_lim: # start_date = axis_left_lim event_polygon = self.axis_prime.axvspan(start_date, end_date, alpha=0.08, color="red", lw=7, linestyle=(115, (20, 2))) self.event_polygon_list.append(event_polygon) left_top = event_polygon.get_verts()[1] right_top = event_polygon.get_verts()[2] # from display to axis decimal-fraction ax_left_top = invDisToAxFrac.transform(left_top) ax_right_top = invDisToAxFrac.transform(right_top) final_left_top = list(ax_left_top) if ax_left_top[0] <= 0: if ax_right_top[0] < 0: continue else: final_left_top[0] = 0 elif ax_left_top[0] >= 1: self.log("Too far in future, skipping.") continue self.log("LEFT: {} -> {} -> {}".format( left_top, ax_left_top, final_left_top)) self.log("RIGHT: {} -> {}".format(right_top, ax_right_top)) stagger_y = 0.95 - (0.10 * e_index) self.axis_prime.text(final_left_top[0] + 0.01, stagger_y, s=name_str, horizontalalignment="left", transform=self.axis_prime.transAxes, fontproperties=propF) # # # GENERAL FORMATTING self.my_legend = self.axis_prime.legend( ncol=2, fontsize="x-small", # borderaxespad=-0.1, markerscale=0.6, framealpha=0.85, prop=propF) # self.canvas.draw() # self.annot = self.axis_prime.annotate("", xy=(0, 0), xytext=(-20, 20), textcoords="offset points", bbox=dict(boxstyle="round", fc="w"), arrowprops=dict(arrowstyle="->"), fontproperties=propF) self.annot.set_visible(False) self.canvas.mpl_connect("motion_notify_event", self.hover) self.log("Completed Graphing.") def clear_graph(self, axis="both"): try: self.my_legend.remove() except BaseException: self.bug( "My legend doesn't exist yet. Likely first time calling _update_plot()" ) self.lines_list = [] self.bars_list = [] self.event_polygon_list = [] if axis == "both": self.axis_prime.cla() self.axis_prime.yaxis.set_visible(False) self.axis_prime.grid(b=False) self.axis_secondary.cla() self.axis_secondary.yaxis.set_visible(False)
class PageThree(tk.Frame): """Frame showing Data View and chop capabilities """ def __init__(self, parent, controller, **kwargs): tk.Frame.__init__(self, parent) # call class parent """ Initialize data members """ self.DA = DA self.isSafeToUpdate = False self.numViews = 2 self.deactList = list() self.DVconfig = controller.DVconfig """ Create label for frame """ #label = tk.Label(self, text="Data View", font=LARGE_FONT) #label.pack(pady=10,padx=10) """ Create button to go back to other frame """ #button1 = ttk.Button(self, text="Back to Home", # command=lambda: controller.show_frame(StartPage)) #button1.pack() """ Add menu item to load data """ filemenu = controller.filemenu filemenu.insert_command(index=1,label="Load", command=self.load) self.addMainFigure() # Add main figure area to frame self.addDataWindow() # Add widgets for setting data window self.addDataView() # Add widgets for setting data view self.addChop() # Add chop button self.addStat() # Add stat button def addDeactivateList(self, listLike) : """ Keep a list of deactivate-able widgets """ if isinstance(listLike, list) or isinstance(listLike, tuple) : for item in listLike : self.deactList.append(item) else : self.deactList.append(listLike) def deactivateWidgets(self) : for widget in self.deactList : widget.configure(state='disabled') def activateWidgets(self) : if not self.DA.isLoaded : return DataNotLoaded() for widget in self.deactList : widget.configure(state='normal') def addDataView(self) : """ Add widgets that control the view """ self.viewList = list(('No Data','No Data')) """ Add View Widget Sub-Frames """ self.dataViewSubFrameList = list() # list of "subframes" for frameNum in range(0,self.numViews) : subFrame = viewWidgetGroupFrame(self, label="Data View "+str(frameNum)) subFrame.setEventHandler(self.viewChangeTrace) subFrame.pack() self.dataViewSubFrameList.append(subFrame) """ Data View Index Selection """ self.altIdxSel = tk.StringVar(self) self.altIdxSel.set("No data") # default value """ self.altIdxSelW = tk.OptionMenu(self, self.altIdxSel, "No Data" ) self.altIdxSelW.configure(state="disabled") self.altIdxSelW.pack() self.altIdxSel.trace('w', self.viewChangeTrace) # set up event self.addDeactivateList(self.altIdxSelW) """ def addDataWindow(self) : """ Data Window Size Widget """ self.dataWindowSizeWidgetLabel = tk.Label(self, text='View Size') self.dataWindowSizeWidgetLabel.pack() self.dataWindowSizeWidget = tk.Scale(self, from_=1, to=10, resolution=1, orient="horizontal") self.dataWindowSizeWidget.bind("<ButtonRelease-1>", self.updateEvent) self.dataWindowSizeWidget.bind("<Button1-Motion>", self.updateEvent) self.dataWindowSizeWidget.pack(fill=tk.X,expand=1) """ Data Window Start Widget """ self.dataWindowStartWidgetLabel = tk.Label(self, text='View Start') self.dataWindowStartWidgetLabel.pack() self.dataWindowStartWidget = tk.Scale(self, from_=0, to=10, resolution=1, orient="horizontal") self.dataWindowStartWidget.bind("<ButtonRelease-1>", self.updateEvent) self.dataWindowStartWidget.bind("<Button1-Motion>", self.updateEvent) self.dataWindowStartWidget.pack(fill=tk.X,expand=1) def addMainFigure(self) : """ Add main figure area """ self.fig = plt.figure(figsize=(5,4), dpi=100) self.ax = self.fig.add_subplot(111) self.ax.set_title('No data') self.fig.canvas.draw() self.canvas = FigureCanvas(self.fig, self) """ Set up callback from canvas draw events, i.e. pan/zoom """ #self.cid1 = self.fig.canvas.mpl_connect('draw_event', self.updateFromCavas) #self.cid1 = self.fig.canvas.mpl_connect('button_release_event', self.updateFromCavas) #TODO animation self.canvas.draw() self.canvas.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True) self.canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True) def addChop(self) : chopBW = tk.Button(self, text="Chop", command=self.doChop) chopBW.pack() self.chopButtonW = chopBW def addStat(self) : statBW = tk.Button(self, text="Stats", command=self.doStat) statBW.pack() self.statButtonW = statBW def postLoad(self) : """ Things to run after loading a new DataAnalyser object. """ debug("postLoad: get current view from DA...") viewList = self.DA.getView() # retrieve the default view after load debug("postLoad: get labels from DA...") self.updateLabels() # get new data types from data loaded self.setAltIndex('index') self.setView(viewList) # configure GUI to reflect new data self.isSafeToUpdate = True debug("postLoad: isSafeToUpdate:"+str(self.isSafeToUpdate)) """ now, set the window """ #TODO use data values instead of index values limitDict=self.DA.getIndexLimits() maxSize, minVal = ( limitDict['max'] , limitDict['min'] ) #print( "DEBUG: postLoad: maxSize: "+str(maxSize) ) #print( "DEBUG: postLoad: minValue: "+str(minVal) ) self.setWindow( minVal=minVal, start=0, maxSize=maxSize, size=int(maxSize/2)) # reset the GUI window self.updateEvent(None) # update GUI def setWindow(self, minVal=0, start=0, maxSize=10, size=10) : """ set the GUI values that correspond to the window """ debug("DEBUG: setWindow: %s, start: %s, maxSize: %s, size: %s" ,minVal, start, maxSize, size ) self.dataWindowSizeWidget.config(from_=1, to=maxSize-start+1) self.dataWindowSizeWidget.set(size) self.dataWindowStartWidget.config(from_=minVal, to=maxSize-size+1) self.dataWindowStartWidget.set(start) def load(self, *args, **kwargs) : """ catch all load method Currently, the only implemented mode is CSV file data. """ self.isSafeToUpdate = False name = filedialog.askopenfilename() self.loadFileData(path=name, *args, **kwargs) def loadFileData(self, path=None, *args, **kwargs) : """ Show a dialog to select a file and load it. """ if path is None: raise TypeError("loadFileData: path option is required") print("Got filename:" +path) loadData(path, *args, **kwargs) self.postLoad() def updateLabels(self) : """ Update labels from the DataAnalyser object Call whenever DA is loaded/changed. """ #print("DEBUG: updateLabels: isSafeToUpdate:", self.isSafeToUpdate) if not self.DA.isLoaded : return DataNotLoaded() newLabels = DA.getLabels() #debug("DEBUG: got label list from DA: "+str(newLabels)) for viewSubFrame in self.dataViewSubFrameList: viewSubFrame.setOptionsList(newLabels) """ re-enable widgets """ self.activateWidgets() def updateFromCavas(self, event) : """ Called when view is changed using figure canvas area. """ """ Get the figure axis limits """ figMinX, figMaxX = self.ax.get_xlim() debug('Got interval from MPL axis:'+str((figMinX,figMaxX))) """ Set the current window to match """ #TODO def viewChangeTrace(self, *args): """View is changed, make proper updates downward.""" debug("viewChange: isSafeToUpdate:"+str(self.isSafeToUpdate)) if not self.isSafeToUpdate : #print("DEBUG: viewChangeTrace: not safe, returning") return """Do other updates""" self.updateEvent(None) def setView(self, viewList) : """ Set the GUI representation of the current data view Takes a "view" object and sets the GUI so that it matches. """ self.isSafeToUpdate = False self.viewList = viewList for view,subFrame in zip(viewList,self.dataViewSubFrameList) : debug("DataViewApp: setView: "+str(view)) subFrame.disable() subFrame.setView(view) subFrame.enable() def setAltIndex(self, newIdx): """ Set the GUI presentation of alt index """ self.isSafeToUpdate = False self.altIdxSel.set(newIdx) def updateEvent(self, event): """ Change/Update data view when user requests a change. Call this whenever the user makes a change to view values. """ debug("updateEvent: called, event:"+str(event)) if not self.DA.isLoaded : return DataNotLoaded() DA = self.DA """ Set window from interface settings """ newWinSize = self.dataWindowSizeWidget.get() newWinStart = self.dataWindowStartWidget.get() limitDict=self.DA.getIndexLimits() debug("DEBUG: updateEvent: got limits: "+str(limitDict)) maxSize, minVal = ( limitDict['max'] , limitDict['min'] ) self.setWindow( minVal=minVal, start=newWinStart, maxSize=maxSize, size=newWinSize ) """ Set views from interface settings """ newViewList = list() for subFrame in self.dataViewSubFrameList : newView = subFrame.getView() debug("updateEvent: newView: "+str(newView)) if newView is not None : """If view frame returns None, don't add to list.""" newViewList.append(newView) """ set index """ try : pass #DA.setAltIndexColumn(self.altIdxSel.get()) #TODO except Exception as e: warn('updateEvent: Failed to set altnernate index, ignoring selection') print(e) else : """ just leave it set to index """ pass DA.setView( viewList=newViewList, windowStart=newWinStart, windowSize=newWinSize, windowType='index' ) """Redraw the plot""" dfList = self.DA.getViewData() #print("DEBUG: updateEvent: got updated data:", df.colums.tolist()) ax = self.ax ax.clear() xlabelList, ylabelList = list(), list() for df in dfList : """draw all df data as x,y data""" (xlabel, ylabel) = df.columns.tolist() ax.plot(df[xlabel].values, df[ylabel].values, 'o-') """store labels""" xlabelList.append(xlabel) ylabelList.append(ylabel) """Set labels""" newXlabel, newYlabel = str(), str() xlabelOverride = self.DVconfig.get('xlabel') ylabelOverride = self.DVconfig.get('ylabel') """Remember, configer doesn't hold native objects""" debug("Plotting: xlabelOverride: %s" % xlabelOverride) debug("Plotting: xlabelOverride eq None: %s" % (xlabelOverride == None)) if (xlabelOverride == None) or (ylabelOverride == None) : """Write the labels for all data""" debug("Plotting: Label overrides OFF") sep = "\n" newXlabel = sep.join( xlabelList ) newYlabel = sep.join( ylabelList ) debug("new xlabel: %s" % newXlabel) else : """the labels""" debug("Plotting: Label overrides ON") newXlabel = xlabelOverride newYlabel = ylabelOverride ax.set_xlabel(newXlabel) ax.set_ylabel(newYlabel) self.fig.canvas.draw() """ Show Statistics """ #self.showStats() def showStats(self): """ Get and report statsistics for the current view """ quantiles = self.DVconfig.get('statQuantiles') statsList = self.DA.getStats( quantiles ) dirpath = self.DVconfig.get('saveCDFDir') prefix = self.DVconfig.get('statFilePrefix') for statDF in statsList : fmt = self.DVconfig.get('statFileFmt') cols = ','.join(map(str,statDF.columns)) start,end = self.DA.getStartEnd() filename = prefix + "_{},{}-{}".format(cols,start,end) + fmt pathname = os.path.join( dirpath , filename ) statDF.to_csv(pathname) for stats in statsList: print("Data View Statistics:") print(stats) def doChop(self) : self.saveViewPlot() directory=pathlib.PurePath(os.path.curdir) chopConf = self.DVconfig.get('chopOpts') debug('chopConf:'+str(chopConf)) debug('dict(chopConf):'+str(dict(chopConf))) self.DA.chop(dirpath=directory, **dict(chopConf)) def saveViewPlot(self) : fig = self.fig # Get figure start,end = self.DA.getStartEnd() # Get view range dirpath = self.DVconfig.get('savePlotDir') prefix = self.DVconfig.get('savePlotPrefix') viewList = self.DA.getView() viewStr = ','.join(map(lambda x: ','.join(map(str,x)) if isinstance(x,tuple) else str(x), viewList)) filename = prefix + "_{vl},{start}-{end}".format( start=start,end=end, vl=viewStr ) + ".pdf" pathname = os.path.join( dirpath , filename ) fig.savefig(pathname) # Save plot def doStat(self) : """ Do actions for "stats" button """ # First, show stats on STDOUT self.showStats() # Then, save the current view plot self.saveViewPlot() # Next, make a CDF plot for all the visible data num_bins = self.DVconfig.get('saveCDFbins') cdfInfoLst = self.DA.getCDFall(num_bins=num_bins) # get CDF info from DA prefix = self.DVconfig.get('saveCDFprefix') dirpath = self.DVconfig.get('saveCDFDir') for cdfInfoT in cdfInfoLst : i = cdfInfoLst.index(cdfInfoT) label, cdf, counts, bin_edges = cdfInfoT start,end = self.DA.getStartEnd() filename = prefix +"_{},{}-{},{}.pdf".format(label,start,end,i) pathname = os.path.join( dirpath , filename ) fig = plt.figure() # mk new fig plt.plot(bin_edges[1:], cdf/cdf[-1]) plt.xlabel("{} values".format(label)) plt.ylabel("Normalized Cumulative Sum (CDF)".format()) fig.savefig(pathname)
class CanvasForGraphs: def __init__(self, root, frame, master_class, figure, axis_id=None, poppedout_id=None, direction="x"): # Save the variables self.root = root self.frame = frame self.master_class = master_class self.figure = figure self.axis_id = axis_id self.poppedout_id = poppedout_id self.direction = direction # Create the frame, canvas and toolbar self.initiate_frameGraph() # Make the canvas available to the master class so we can draw_idle() it if poppedout_id == None: self.master_class.Canvas[axis_id] = self.Canvas if poppedout_id != None: self.root.canvasPoppedOut.append(self.Canvas) #----------- Frame ---------- def initiate_frameGraph(self): # Configure frame that holds the canvas tk.Grid.rowconfigure(self.frame, 0, weight=1) tk.Grid.columnconfigure(self.frame, 0, weight=1) # Create frame for graph elements self.frame_canvas = ttk.Frame(self.frame) self.frame_canvas.grid(row=0, column=0, padx=(0, 0), pady=(0, 0), stick='NSEW') # Configure the frame that holds the figure and toolbar if self.direction == "x": tk.Grid.rowconfigure(self.frame_canvas, 0, weight=1) tk.Grid.rowconfigure(self.frame_canvas, 1, weight=0) tk.Grid.columnconfigure(self.frame_canvas, 0, weight=1) if self.direction == "y": tk.Grid.columnconfigure(self.frame_canvas, 0, weight=0) tk.Grid.columnconfigure(self.frame_canvas, 1, weight=1) tk.Grid.rowconfigure(self.frame_canvas, 0, weight=1) # Add the canvas and the toolbar self.initiate_canvas() self.initiate_toolbar() #----------- Canvas ----------- def initiate_canvas(self): # Add the figure to a Canvas self.Canvas = FigureCanvas(self.figure, self.frame_canvas) self.Canvas.get_tk_widget().configure(background=self.root.color['bg']) self.figure.patch.set_facecolor(self.root.color['bg']) self.Canvas.draw() self.widget = self.Canvas.get_tk_widget() if self.direction == "x": self.widget.grid(row=0, column=0, stick='NSEW') if self.direction == "y": self.widget.grid(row=0, column=1, stick='NSEW') #----------- Toolbar ----------- def initiate_toolbar(self): # Add a toolbar to the canvas self.toolbar = CustomToolbar(self.root, self.Canvas, self.frame_canvas, self.master_class, self.axis_id, self.poppedout_id, self.direction) self.toolbar.config(background=self.root.color['bg']) self.toolbar.update()