Source code for madcubapy.regions.contours

import astropy.units as u

import copy

from IPython import get_ipython

from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk
from matplotlib.contour import ContourSet
import matplotlib.patches as patches
import matplotlib.patheffects as PathEffects
from matplotlib.ticker import MultipleLocator

import numpy as np

import platform

import tkinter as tk

from madcubapy.visualization import add_wcs_axes
from madcubapy.visualization import add_colorbar
from madcubapy.visualization import parse_clabel
from madcubapy.coordinates import pixel_to_world

__all__ = [
    'import_region_contourset',
    'import_region_patch',
    'select_contour_regions',
]


[docs] def import_region_contourset( ax, contour, index=0, ref_fitsmap=None, **kwargs): """ Import one contour region from a contour object into the selected axes. Parameters ---------- ax : `~matplotlib.axes.Axes` or `~astropy.visualization.wcsaxes.WCSAxes` Axes object into which the contour region will be added. contour : `~matplotlib.contour.ContourSet` Contour object from which to take information. index : `int`, default=0 Index of the contour region to be imported. ref_fitsmap : `~madcubapy.io.MadcubaMap` or `~astropy.nddata.CCDData`, \ optional Used to apply the transform if the fitsmap is different from the one plotted in the axes object. Returns ------- new_CS : `~matplotlib.contour.ContourSet` Contour object containing the indexed contour region. Other Parameters ---------------- **kwargs Parameters to pass to `~matplotlib.contour.ContourSet`. """ # Create a contour with only one region. segment = contour.allsegs[0][index] if ref_fitsmap == None: new_CS = ContourSet(ax, [0], [[segment.tolist()]], **kwargs) else: new_CS = ContourSet( ax, [0], [[segment.tolist()]], transform=ax.get_transform(ref_fitsmap.wcs.celestial), **kwargs, ) return new_CS
[docs] def import_region_patch( ax, contour, index=0, ref_fitsmap=None, **kwargs): """ Import one contour region from a contour object into the selected axes as a matplotlib patch. Parameters ---------- ax : `~matplotlib.axes.Axes` or `~astropy.visualization.wcsaxes.WCSAxes` Axes object into which the contour region will be added. contour : `~matplotlib.contour.ContourSet` Contour object from which to take information. index : `int`, default=0 Index of the contour region to be imported. ref_fitsmap : `~madcubapy.io.MadcubaMap` or `~astropy.nddata.CCDData`, \ optional Used to apply the transform if the fitsmap is different from the one plotted in the axes object. Returns ------- poly : `~matplotlib.patches.Polygon` Polygon patch of the the indexed contour region. Other Parameters ---------------- **kwargs Parameters to pass to `~matplotlib.patches.Polygon`. """ # Default polygon params if ('color' not in kwargs and 'ec' not in kwargs and 'edgecolor' not in kwargs): kwargs['ec'] = 'white' if 'linewidth' not in kwargs: kwargs['linewidth'] = 1 if 'linestyle' not in kwargs: kwargs['linestyle'] = 'solid' if 'fill' not in kwargs: kwargs['fill'] = False # Create a contour with only one region. segment = contour.allsegs[0][index] if ref_fitsmap == None: new_CS = ContourSet(ax, [0], [[segment.tolist()]]) else: new_CS = ContourSet( ax, [0], [[segment.tolist()]], transform=ax.get_transform(ref_fitsmap.wcs.celestial), ) # The segments of the contourset are still in the original coordinates v = new_CS.allsegs[0][0] # Manually transform the vertices of the region to the new axes. The # ContourSet always stores coordinates in the original data, the transform # parameter is only for matplotlib to do a transformation and priont it, # not store it. if ref_fitsmap: world_coords = pixel_to_world(points=v, fitsmap=ref_fitsmap) # Finally we can do the same but backwards for the second axes. final_world2screen = ax.get_transform('world') final_screen2pix = ax.get_transform('pixel').inverted() final_screen_coords = final_world2screen.transform(world_coords.value) region_points = final_screen2pix.transform(final_screen_coords) else: region_points = v poly = patches.Polygon(xy=region_points, **kwargs) new_CS.remove() # Remove contour from plot return poly
[docs] def select_contour_regions( contour, fig=None, ax=None, fitsmap=None, **kwargs): """ Shows the regions contained in a contour object in a pop-up window and lets the user select closed contours as regions. Returns the indexes of the regions that have been clicked. Parameters ---------- contour : `~matplotlib.contour.ContourSet` Contour object to plot as polygons with indexes. ax : `~astropy.visualization.wcsaxes.WCSAxes`, optional Ax object to use for displaying the contour. fig : `~matplotlib.figure.Figure`, optional Fig object to use for displaying the contour. fitsmap : `~madcubapy.io.MadcubaMap` or `~astropy.nddata.CCDData`, optional Map object to use for displaying the contour. Returns ------- selected_indexes : `list` List with the indexes of the selected Polygons. Other Parameters ---------------- **kwargs Parameters to pass to the :func:`~matplotlib.pyplot.imshow()` function. """ # Create a Tkinter window root = tk.Tk() root.title("Check contour") # Create a Figure compatible with the input parameters if fitsmap != None: if ax != None or fig != None: raise TypeError( f"Only one or none of 'fitsmap', 'ax', and 'fig' can be set" ) else: # Create a Matplotlib figure. new_fig = Figure(figsize=(7,7)) if kwargs is None: kwargs = {} new_ax, new_img = add_wcs_axes(new_fig, 1, 1, 1, fitsmap=fitsmap, **kwargs) new_cbar = add_colorbar(new_ax) elif ax != None: if fitsmap != None or fig != None: raise TypeError( f"Only one or none of 'fitsmap', 'ax', and 'fig' can be set" ) else: # Create new Figure and plot the image data from input axes new_fig = Figure(figsize=(7, 7)) new_ax = new_fig.add_subplot(1,1,1) og_img = ax.get_images()[0] data = np.array(og_img.get_array()) clim = og_img.get_clim() if kwargs is None: kwargs = {} kwargs['vmin'] = clim[0] kwargs['vmax'] = clim[1] new_img = new_ax.imshow(data, origin='lower', **kwargs) elif fig != None: if fitsmap != None or ax != None: raise TypeError( f"Only one or none of 'fitsmap', 'ax', and 'fig' can be set" ) else: # Create new Figure and plot the image data from input figure new_fig = Figure(figsize=(7, 7)) new_ax = new_fig.add_subplot(1,1,1) ax = fig.get_axes()[0] og_img = ax.get_images()[0] data = np.array(og_img.get_array()) clim = og_img.get_clim() if kwargs is None: kwargs = {} kwargs['vmin'] = clim[0] kwargs['vmax'] = clim[1] new_img = new_ax.imshow(data, origin='lower', **kwargs) else: # Create a new Figure without a map new_fig = Figure(figsize=(7, 7)) new_ax = new_fig.add_subplot(1,1,1) # Arrays to store data for plot limits x_maxs = np.array([]) x_mins = np.array([]) y_maxs = np.array([]) y_mins = np.array([]) # Prettier plots if fitsmap != None: new_ax.set_title('Check contour', fontsize=15, pad=20) new_ax.coords[0].set_axislabel("RA (ICRS)", fontsize=12) new_ax.coords[1].set_axislabel("DEC (ICRS)", fontsize=12) new_ax.coords[0].tick_params(which="major", length=5) new_ax.coords[0].display_minor_ticks(True) new_ax.coords[0].set_ticklabel(size=11, exclude_overlapping=True) new_ax.coords[1].tick_params(which="major", length=5) new_ax.coords[1].display_minor_ticks(True) new_ax.coords[1].set_ticklabel(size=11, exclude_overlapping=True, rotation='vertical') new_cbar.ax.set_ylabel(parse_clabel(fitsmap), fontsize=12) new_cbar.ax.tick_params(labelsize=11) else: new_ax.set_title('Check contour', fontsize=15, pad=20) new_ax.set_xlabel("FITS X", fontsize=12) new_ax.set_ylabel("FITS Y", fontsize=12) new_ax.tick_params(which='major', length=5, labelsize=11, right=True, top=True) new_ax.tick_params(which='minor', right=True, top=True) new_ax.xaxis.set_major_locator(MultipleLocator(25)) new_ax.xaxis.set_minor_locator(MultipleLocator(5)) new_ax.yaxis.set_major_locator(MultipleLocator(25)) new_ax.yaxis.set_minor_locator(MultipleLocator(5)) # Visual options for the contour regions if fitsmap == None and ax == None and fig == None: ec='blue' fc='C0' text_color = '0.7' text_fg = 'black' else: ec='white' fc=(0.7, 0.7, 0.7, 0.5) text_color = 'white' text_fg = '0.5' # Initialize rrays to store polygons, patches, and indexes polygons = [] selected_artists = [] # List of used patches, needed for the if # condition of the mouse hover event. selected_indexes = [] polygon_texts = [] return_indexes = None # Plot the contour as polygons for i in range(len(contour.allsegs[0])): v = contour.allsegs[0][i] poly = patches.Polygon(xy=v, lw=1, ec=ec, fc=fc, fill=False, closed=True, picker=True) new_ax.add_patch(poly) polygons.append(poly) center = v.mean(axis=0) txt = new_ax.text(center[0], center[1], f'{i}', color=text_color, ha = 'center', va='center', visible=False) txt.set_path_effects([PathEffects.withStroke(linewidth=1.5, foreground=text_fg)]) polygon_texts.append(txt) # Store X and Y info for empty image limits if fitsmap == None and ax == None and fig == None: x_maxs = np.append(x_maxs, contour.allsegs[0][i][:,0].max()) x_mins = np.append(x_mins, contour.allsegs[0][i][:,0].min()) y_maxs = np.append(y_maxs, contour.allsegs[0][i][:,1].max()) y_mins = np.append(y_mins, contour.allsegs[0][i][:,1].min()) # Set zoom level at image range +- 5% if no image is provided if fitsmap == None and ax == None and fig == None: x_lim = (x_mins.min() - (x_maxs.max() - x_mins.min()) * 0.05, x_maxs.max() + (x_maxs.max() - x_mins.min()) * 0.05) y_lim = (y_mins.min() - (y_maxs.max() - y_mins.min()) * 0.05, y_maxs.max() + (y_maxs.max() - y_mins.min()) * 0.05) new_ax.set_xlim(x_lim) new_ax.set_ylim(y_lim) ### Interactive functions ### def _save(): nonlocal return_indexes return_indexes = selected_indexes root.quit() root.destroy() def _quit(): root.quit() root.destroy() # Save and Quit shortcuts def onkeypress(event): if event.key == 'q': _quit() if event.key == 's': _save() # Store desired polygons with their indexes def on_pick(event): nonlocal selected_indexes nonlocal selected_artists nonlocal polygons artist = event.artist if isinstance(artist, patches.Polygon): for i in range(len(polygons)): if polygons[i] == artist: artist.set(fill=True) verts = artist.get_xy() center = verts.mean(axis=0) polygon_texts[i].set_position(center) polygon_texts[i].set(visible=True) print(f"Selected the contour with index {i}") # Add the index artist avoiding duplicates if i not in selected_indexes: selected_indexes.append(i) if artist not in selected_artists: selected_artists.append(artist) # Refresh the canvas canvas.draw() # Highlight polygon and show its index on mouseover def on_hover(event): nonlocal polygons nonlocal selected_artists nonlocal polygon_texts for i in range(len(polygons)): # Only highlight non-selected regions if polygons[i] not in selected_artists: # Check if the mouse is over a polygon if polygons[i].contains(event)[0]: polygons[i].set_linewidth(2) text_x = event.xdata + 0.02 \ * (new_ax.get_xlim()[1] - new_ax.get_xlim()[0]) text_y = event.ydata + 0.02 \ * (new_ax.get_ylim()[1] - new_ax.get_ylim()[0]) polygon_texts[i].set_position((text_x, text_y)) polygon_texts[i].set(visible=True) else: polygons[i].set_linewidth(1) polygon_texts[i].set(visible=False) # Redraw the figure canvas.draw() ### tkinter window design ### # Create a Matplotlib canvas embedded within the Tkinter window canvas = FigureCanvasTkAgg(new_fig, master=root) canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1) # Show toolbar. toolbar = NavigationToolbar2Tk(canvas, root) toolbar.update() canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1) # Buttons button_frame = tk.Frame(root) button_frame.pack(side=tk.BOTTOM, pady=3) abort_button = tk.Button(master=button_frame, text="Abort", command=_quit) abort_button.pack(side=tk.RIGHT, padx=5) save_button = tk.Button(master=button_frame, text="Save and exit", command=_save) save_button.pack(side=tk.LEFT, padx=5) # Connect events to the canvas canvas.mpl_connect('key_press_event', onkeypress) canvas.mpl_connect('pick_event', on_pick) canvas.mpl_connect('motion_notify_event', on_hover) # Start the Tkinter event loop tk.mainloop() return return_indexes
def _check_sigma_contours( ax, fitsmap): """ Disclaimer: Test Function Open a window with two main tabs. The first one lets the user select polygons in the map that are used to calculate the noise (sigma). The second one plots the contour of an input level times the desired calculated sigma value. The user can then select the desired polygons to add them to the image of the input ax object. Parameters ---------- ax : `~astropy.visualization.wcsaxes.WCSAxes` Axes object in which to add the ContourSet object(s). fitsmap : `~madcubapy.io.MadcubaMap` or `~astropy.nddata.CCDData`, optional Map object to use for calculating sigma and its contours. """ # OS check and kernel check def in_jupyter_notebook(): try: shell = get_ipython() return shell and "IPKernelApp" in shell.config except Exception: return False if in_jupyter_notebook() and platform.system() == 'Darwin': right_click = 2 middle_click = 3 else: right_click = 3 middle_click = 2 left_click = 1 # Create a Tkinter window root = tk.Tk() root.title("Check contour") ### Functions ### # Calculate sigma def get_sigma(): nonlocal sigma all_data = np.array([]) for inside_data in inside_pixels: all_data = np.append(all_data, inside_data) sigma = np.std(all_data, ddof=0) print(f"sigma: {sigma}") return sigma # Plot a (Quad)ContourSet as polygons def plot_contour_polygons(contour): nonlocal check_ax for i in range(len(contour.allsegs[0])): v = contour.allsegs[0][i] poly = patches.Polygon(xy=v, lw=1, ec='1', fc=(0.7, 0.7, 0.7, 0.5), fill=False, closed=True, picker=True) check_ax.add_patch(poly) polygons.append(poly) center = v.mean(axis=0) txt = check_ax.text(center[0], center[1], f'{i}', color='white', ha = 'center', va='center', visible=False) txt.set_path_effects([PathEffects.withStroke(linewidth=1.5, foreground='0.5')]) polygon_texts.append(txt) # Main call to create and plot contours def plot_contours(sigma, level): nonlocal check_ax nonlocal sigma_contour sigma_contour = check_ax.contour(fitsmap.data[0, 0, :, :], [level*sigma], origin=None, colors=['white'], linewidths=3) sigma_contour.remove() plot_contour_polygons(sigma_contour) # Calculate sigma half def calculate_sigma_part(): nonlocal current_figure if current_figure == 'check': canvas.figure = sigma_fig current_figure = 'sigma' canvas.draw() # Change bottom frame check_frame.pack_forget() sigma_frame.pack(side=tk.TOP, pady=10) check_button.config(relief=tk.RAISED) sigma_button.config(relief=tk.SUNKEN) else: return # Clear all drawn and selected polygons def clear_polygons(): nonlocal polygon_paths nonlocal current_polygon nonlocal inside_pixels nonlocal sigma # Reset sigma lists polygon_paths = [] current_polygon = [] inside_pixels = [] sigma = np.nan # Remove sigma painted objects # Lines and scatter points for line in sigma_ax.lines: line.remove() # Patches for patch in sigma_ax.patches: patch.remove() # Texts for text in sigma_ax.texts: text.remove() canvas.draw() # Create polygons by hand def onclick(event): nonlocal polygon_paths nonlocal current_polygon nonlocal inside_pixels if current_figure == 'sigma': # Left-click to select points if event.button == left_click: # Add the clicked point to the current polygon current_polygon.append((event.xdata, event.ydata)) now_polygon = np.array(current_polygon) # Draw a small point at the first clicked point if len(now_polygon) == 1: sigma_ax.plot(event.xdata, event.ydata, 'white', marker='o', markersize=5) # Draw lines if len(now_polygon) > 1: sigma_ax.plot(now_polygon[-2:, 0], now_polygon[-2:, 1], 'white', lw=2, alpha=0.9) sigma_ax.plot(event.xdata, event.ydata, 'white', marker='o', markersize=5) # Refresh the canvas canvas.draw() # Right-click to finalize the current polygon elif event.button == right_click and current_polygon: polygon = np.array(current_polygon) # Draw the last side of the polygon closed_polygon = np.vstack((polygon, polygon[0])) sigma_ax.plot(closed_polygon[-2:, 0], closed_polygon[-2:, 1], 'white', lw=2, alpha=0.9) # Draw the polygon on the plot poly = patches.Polygon(xy=polygon, linewidth=2, edgecolor='white', facecolor='white', alpha=0.5) new_poly=copy.copy(poly) sigma_ax.add_patch(new_poly) # Get the shape of the CCDData object if fitsmap.header['NAXIS'] == 2: height, width = fitsmap.data.shape elif fitsmap.header['NAXIS'] == 3: freq, height, width = fitsmap.data.shape elif fitsmap.header['NAXIS'] == 4: freq, stokes, height, width = fitsmap.data.shape # Create a meshgrid of coordinates to create mask x, y = np.meshgrid(range(width), range(height)) points = np.vstack((x.flatten(), y.flatten())).T mask = poly.contains_points(points, radius=0) mask = mask.reshape((height, width)) # Calculate std new_data = copy.copy(fitsmap.data[0,0,:,:]) std = new_data[mask].std() # Paint std inside polygon x_text = polygon.T[0].min() \ + (polygon.T[0].max() - polygon.T[0].min()) / 2 y_text = polygon.T[1].min() \ + (polygon.T[1].max() - polygon.T[1].min()) / 2 std_text = sigma_ax.text(x_text, y_text, f'{std:.2f}', va='center', ha='center', color='white', fontsize=13) std_text.set_path_effects( [PathEffects.withStroke(linewidth=1.5, foreground="0.5")] ) # Add info on lists polygon_paths.append(polygon) inside_pixels.append(new_data[mask]) # Reset the current polygon current_polygon = [] # Refresh the canvas canvas.draw() else: return # Select contour regions half def plot_contour_fig(): nonlocal sigma_contour nonlocal polygons nonlocal selected_artists nonlocal selected_indexes nonlocal polygon_texts nonlocal level checked_contour = True # For now, always reset contour plotting # Reset the check contour figure to start over if checked_contour != None: # Reset check lists polygons = [] selected_artists = [] selected_indexes = [] polygon_texts = [] # Remove check polygons for patch in check_ax.patches: patch.remove() for text in check_ax.texts: text.remove() # Reset sigma_contour sigma_contour = None # Plot contours sigma = get_sigma() level = float(level_box.get()) plot_contours(sigma, level) canvas.figure = check_fig canvas.draw() def select_contour_regions_part(): nonlocal current_figure if current_figure == 'sigma': plot_contour_fig() current_figure = 'check' # Reconnect events to current figure, as mpl_connect does not # connect it to the canvas, but to the figure in the canvas. canvas.mpl_connect('key_press_event', onkeypress) canvas.mpl_connect('button_press_event', on_pick_click) canvas.mpl_connect("motion_notify_event", on_hover) # Change bottom frame sigma_frame.pack_forget() check_frame.pack(side=tk.TOP, pady=10, fill=tk.X) # Set button relief states sigma_button.config(relief=tk.RAISED) check_button.config(relief=tk.SUNKEN) else: return # Store desired polygons with their indexes def on_pick_click(event): nonlocal selected_indexes nonlocal selected_artists nonlocal polygons if current_figure == 'check': if event.inaxes: for i in range(len(polygons)): if polygons[i].contains_point((event.x, event.y)): polygons[i].set(fill=True) verts = polygons[i].get_xy() center = verts.mean(axis=0) polygon_texts[i].set_position(center) polygon_texts[i].set(visible=True) print(f"Selected the contour with index {i}") # Add the index artist avoiding duplicates if i not in selected_indexes: selected_indexes.append(i) if polygons[i] not in selected_artists: selected_artists.append(polygons[i]) # Refresh the canvas canvas.draw() else: return # Highlight polygon and show its index on mouseover def on_hover(event): nonlocal polygons nonlocal selected_artists nonlocal polygon_texts if current_figure == 'check': for i in range(len(polygons)): # Highlight non-selected regions on mouseover if polygons[i] not in selected_artists: # Check if the mouse is over a polygon if polygons[i].contains_point((event.x, event.y)): polygons[i].set_linewidth(2) text_x = event.xdata + 0.02 \ * (check_ax.get_xlim()[1] - check_ax.get_xlim()[0]) text_y = event.ydata + 0.02 \ * (check_ax.get_ylim()[1] - check_ax.get_ylim()[0]) polygon_texts[i].set_position((text_x, text_y)) polygon_texts[i].set(visible=True) else: polygons[i].set_linewidth(1) polygon_texts[i].set(visible=False) # Redraw the figure canvas.draw() else: return # Add exit pathways def _quit(): root.quit() root.destroy() # Save and exit def _exit_plotting(): nonlocal ax for i in selected_indexes: new_contour = import_region_contourset( ax=ax, contour=sigma_contour, index=i, colors='white', linewidths=[1], linestyles='-' ) root.quit() root.destroy() # Save and Abort shortcuts def onkeypress(event): if event.key == 'q': _quit() if event.key == 's': _exit_plotting() # Initialize lists to store the polygons, arrays with pixel data... # Calculate sigma polygon_paths = [] current_polygon = [] inside_pixels = [] sigma = np.nan # Check contour polygons = [] selected_artists = [] # List of used patches, needed for the 'if' # condition of the mouse hover event. selected_indexes = [] polygon_texts = [] level = 5 # Track current Figure current_figure = 'sigma' # Default contour sigma_contour = None # Create Matplotlib figures sigma_fig = Figure(figsize=(7,6)) sigma_ax, sigma_img = add_wcs_axes(sigma_fig, 1, 1, 1, fitsmap=fitsmap, vmin=0, vmax=300) sigma_cbar = add_colorbar(sigma_ax) check_fig = Figure(figsize=(7,6)) check_ax, check_img = add_wcs_axes(check_fig, 1, 1, 1, fitsmap=fitsmap, vmin=0, vmax=300) check_cbar = add_colorbar(check_ax) # Prettier plots sigma_ax.set_title('Calculate sigma', fontsize=15, pad=20) check_ax.set_title('Contour check', fontsize=15, pad=20) for axis in [sigma_ax, check_ax]: axis.coords[0].set_axislabel("RA (ICRS)", fontsize=12) axis.coords[1].set_axislabel("DEC (ICRS)", fontsize=12) axis.coords[0].tick_params(which="major", length=5) axis.coords[0].display_minor_ticks(True) axis.coords[0].set_ticklabel(size=11, exclude_overlapping=True) axis.coords[1].tick_params(which="major", length=5) axis.coords[1].display_minor_ticks(True) axis.coords[1].set_ticklabel(size=11, exclude_overlapping=True, rotation='vertical') for colorbar in [sigma_cbar, check_cbar]: colorbar.ax.set_ylabel(parse_clabel(fitsmap), fontsize=12) colorbar.ax.tick_params(labelsize=11) ### tkinter window design ### # Create a frame for the main buttons above the figure canvas tab_frame = tk.Frame(root) tab_frame.pack(side=tk.TOP, pady=10) # Main buttons sigma_button = tk.Button(tab_frame, text="Calculate sigma", relief='sunken', command=calculate_sigma_part) sigma_button.pack(side=tk.LEFT, padx=2) check_button = tk.Button(tab_frame, text="Check contour", command=select_contour_regions_part) check_button.pack(side=tk.LEFT, padx=2) # Create a Matplotlib canvas embedded within the Tkinter window canvas = FigureCanvasTkAgg(sigma_fig, master=root) canvas.draw() canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1) # Show toolbar toolbar = NavigationToolbar2Tk(canvas, root) toolbar.update() # Create a frame for sigma calculation sigma_frame = tk.Frame(root) sigma_frame.pack(side=tk.TOP, pady=10) clear_button = tk.Button(sigma_frame, text="Clear", command=clear_polygons) clear_button.pack(side=tk.LEFT, padx=2) abort_button_sigma = tk.Button(sigma_frame, text="Abort", command=_quit) abort_button_sigma.pack(side=tk.RIGHT, padx=2) # Create a frame for contour checking and do not show it yet check_frame = tk.Frame(root) # Secondary frame for exit buttons # By being in a frame inside the check frame, these two buttons are # placed together in the center of the available space by using # expand=True. If they are not in the second frame, the buttons use # half of the available space each and lay separated. check_exit_frame = tk.Frame(check_frame) check_exit_frame.pack(side=tk.LEFT, expand=True) finish_button = tk.Button(check_exit_frame, text="Save and exit", command=_exit_plotting) finish_button.pack(side=tk.LEFT, padx=2) abort_button_check = tk.Button(check_exit_frame, text="Abort", command=_quit) abort_button_check.pack(side=tk.RIGHT, padx=2) # Select sigma level UI plot_contours_button = tk.Button(check_frame, text="Update contour", command=plot_contour_fig) plot_contours_button.pack(side=tk.RIGHT, padx=5) sigma_symbol = tk.Label(check_frame, text='σ') sigma_symbol.pack(side=tk.RIGHT) level_box = tk.Entry(check_frame, width=2) level_box.insert(0, 5) level_box.pack(side=tk.RIGHT) level_label = tk.Label(check_frame, text='Level:') level_label.pack(side=tk.RIGHT) # Connect shortcut and sigma events to the first canvas canvas.mpl_connect('key_press_event', onkeypress) canvas.mpl_connect('button_press_event', onclick) # Start the Tkinter event loop root.mainloop()