我正在构建一个减少质谱仪数据的程序:它应该向用户提供分析中的原始数据,并允许他们交互式地单击数据以将其作为异常值删除。删除数据后,应自动重新计算数据、趋势线和统计数据。该程序还在数据下方提供了按钮,用于转到下一个或上一个分析以检查异常值。
这对于每个序列中的第一个分析非常有效,但是当我尝试从任何其他分析中删除数据时(例如,单击“下一步”后),绘图将恢复到第一个分析并从该数据集中删除数据,而不是比我正在做的那个要好。看来我一直在与数组中的第一个绘图元素交互,而不是与我正在查看的元素交互(除非我正在查看第一个)。
交互逻辑如下:
from import_raw import get_raw_data
from select_sequences import filter_data
from interactive_plot import setup_interactive_plot, interactive_update
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.backends.backend_tkagg as tkagg
import tkinter as tk
# next button
def on_next(current_plot_index):
current_plot_index = (current_plot_index + 1) % len(filtered_data)
interactive_update(filtered_data[current_plot_index], figure, canvas, stats_frame)
update_buttons()
# previous button
def on_previous(current_plot_index):
current_plot_index = (current_plot_index - 1) % len(filtered_data)
interactive_update(filtered_data[current_plot_index], figure, canvas, stats_frame)
update_buttons()
# finish button (to do)
def on_finish():
window.quit() # replace with call to data reduction script
# exit button
def on_exit():
window.quit()
# remove previous button on first index
# replace next with finish on last index
def update_buttons():
prev_button.pack_forget()
next_button.pack_forget()
finish_button.pack_forget()
if current_plot_index > 0:
prev_button.pack(side=tk.LEFT)
if current_plot_index == len(filtered_data) - 1:
finish_button.pack(side=tk.RIGHT)
else:
next_button.pack(side=tk.RIGHT)
### main code below ###
matplotlib.use('TkAgg') # forces TkAgg backend to matplotlib (for MacOSX development)
# get and filter raw data
global filtered_data
all_data, sequence_data = get_raw_data() # get all raw data and group into sequences
filtered_data = filter_data(all_data, sequence_data) # filter out only the selected sequences
# initiate GUI
window = tk.Tk()
window.title('HeMan - Alphachron Data Reduction')
# define and pack main frame
main_frame = tk.Frame(window)
main_frame.pack(fill=tk.BOTH, expand=True)
# define and pack frame for statistics panel on the left
stats_frame = tk.Frame(main_frame, borderwidth=2, relief=tk.SUNKEN)
stats_frame.pack(side=tk.LEFT, fill=tk.Y)
# define and pack frame for data plot and buttons on the right
right_frame = tk.Frame(main_frame)
right_frame.pack(side=tk.RIGHT, fill=tk.X, expand=True)
# define and pack frame for buttons
button_frame = tk.Frame(right_frame)
button_frame.pack(side=tk.BOTTOM, fill=tk.X)
# define and pack frame for data
data_frame = tk.Frame(right_frame)
data_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
# initialize and pack the data frame
global canvas
figure = plt.figure(figsize=(15,8))
canvas = tkagg.FigureCanvasTkAgg(figure, master=data_frame)
canvas.draw()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
# set current_plot_index
current_plot_index = 0
# initialize and pack buttons
global prev_button, exit_button, next_button, finish_button
button_options = {'width': 10, 'height': 2}
exit_button = tk.Button(button_frame, text="Exit", command=lambda: on_exit(), **button_options)
prev_button = tk.Button(button_frame, text="Previous", command=lambda: on_previous(current_plot_index), **button_options)
next_button = tk.Button(button_frame, text="Next", command=lambda: on_next(current_plot_index), **button_options)
finish_button = tk.Button(button_frame, text="Finish", command=lambda: on_finish(), **button_options)
prev_button.pack(side=tk.LEFT)
exit_button.pack(side=tk.LEFT)
next_button.pack(side=tk.RIGHT)
finish_button.pack(side=tk.RIGHT)
# update the panes
update_buttons()
setup_interactive_plot(filtered_data[current_plot_index], figure, canvas, stats_frame)
# main window loop
window.mainloop()
setup_interactive_plot()
运行 plot_raw_data()
来生成图形,对其进行格式化,将其放置在 tk 画布上,并写入关联的统计窗格。然后,它使用 fig.canvas.mpl_connect()
命令设置 on_click 交互性。def setup_interactive_plot(data_entry, fig, canvas, stats_frame):
plot_raw_data(data_entry, fig, canvas) # draw raw data to data frame
write_stats_frame(data_entry, stats_frame) # write stats to stats frame
# interactivity
fig.canvas.mpl_connect('button_press_event', lambda event: on_click(event, data_entry, fig, canvas, stats_frame))
# draw the raw data plots on the data frame
def plot_raw_data(data_entry, fig, canvas):
update_plot_and_trendlines(data_entry, fig) # generate the figure
plt.tight_layout() # Adjust spacing to prevent labels overlapping
plt.suptitle(f"He {data_entry.helium_number}: {data_entry.analysis_label}")
plt.subplots_adjust(top=0.92)
fig.canvas.draw()
canvas.draw()
on_click()
检查点击是否落在绘图窗口内;如果是这样,它会找到最接近点击的数据,并通过将相应的条目 data_entry.data_status[mass]
从 1 设置为 0 来“停用”它,反之亦然。# what to do when the user clicks on the plot
def on_click(event, data_entry, figure, canvas, stats_frame):
click_coords = (event.xdata, event.ydata) # click coordinates
# check whether the click landed inside a plot and if so, get the plot and closest_index
try:
mass = event.inaxes.get_title() # get axis title of click plot
closest_index = find_closest_index(data_entry.raw_data, click_coords, mass) # closest index to click
except AttributeError:
return # click outside of bounds, do nothing
# toggle the clicked datum between 1 (active) and 0 (inactive)
data_entry.data_status[mass].iloc[closest_index] = (data_entry.data_status[mass].iloc[closest_index] + 1) % 2
if mass in ['3 amu', '4 amu']: # if any datum is excluded from 3 amu or 4 amu, also exclude it from the 4/3 Ratio
data_entry.data_status['4/3 Ratio'].iloc[closest_index] = (data_entry.data_status['4/3 Ratio'].iloc[closest_index] + 1) % 2
interactive_update(data_entry, figure, canvas, stats_frame)
on_click()
以调用 interactive_update()
结束,这将清除图形,使用 plot_raw_data()
绘制一个新图形,并使用新的统计数据更新统计数据框架。# run all interactive update features in one function
def interactive_update(data_entry, fig, canvas, stats_frame):
fig.clf()
plot_raw_data(data_entry, fig, canvas) # draw raw data on data frame canvas
write_stats_frame(data_entry, stats_frame) # write stats to stats frame
# next button
def on_next(current_plot_index):
current_plot_index = (current_plot_index + 1) % len(filtered_data)
interactive_update(filtered_data[current_plot_index], figure, canvas, stats_frame)
update_buttons()
# previous button
def on_previous(current_plot_index):
current_plot_index = (current_plot_index - 1) % len(filtered_data)
interactive_update(filtered_data[current_plot_index], figure, canvas, stats_frame)
update_buttons()
我怀疑我做了一些愚蠢的事情,比如错误地使用了全局变量或其他东西,但我很困惑。
以下是撰写本文时的完整脚本(为了便于阅读,我从上面删除了一些不相关的功能和语句):
还有脚本 write_stats.py、he_stats.py 和 select_sequences.py。我相信这些是无关紧要的,但如果有人有兴趣运行脚本并自己尝试的话,可以将它们与原始数据一起提供。
我正在为第一个图创建一个单击处理程序,并且没有删除它或为每个新图生成一个新的处理程序。这是更正后的代码:
current_click_handler = None
# attach the click handler to the new figure
def attach_click_handler(analysis, figure, canvas, stats_frame):
global current_click_handler
# disconnect the old handler if it exists
if current_click_handler is not None:
figure.canvas.mpl_disconnect(current_click_handler)
click_handler = figure.canvas.mpl_connect('button_press_event', lambda event: on_click(event, analysis, figure, canvas, stats_frame))
current_click_handler = click_handler
# run all interactive update features in one function
def interactive_update(analysis, figure, canvas, stats_frame):
figure.clf()
plot_raw_data(analysis, figure, canvas) # draw raw data on data frame canvas
write_stats_frame(analysis, stats_frame) # write stats to stats frame
attach_click_handler(analysis, figure, canvas, stats_frame)
def setup_interactive_plot(analysis, figure, canvas, stats_frame):
plot_raw_data(analysis, figure, canvas) # draw raw data to data frame
write_stats_frame(analysis, stats_frame) # write stats to stats frame
attach_click_handler(analysis, figure, canvas, stats_frame)