From a1dbb4f3acb3e4c45e54e7bb04240712bb8f0afa Mon Sep 17 00:00:00 2001 From: Wojciech Wojnowski <wojciech.wojnowski@pg.edu.pl> Date: Fri, 14 Feb 2025 06:59:01 +0000 Subject: [PATCH] Update main.py after journal review --- main.py | 836 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 487 insertions(+), 349 deletions(-) diff --git a/main.py b/main.py index 4ed6b1b..4be8d32 100644 --- a/main.py +++ b/main.py @@ -1,416 +1,554 @@ import tkinter as tk -from tkinter import ttk +from tkinter import ttk, filedialog import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg -from tkinter import filedialog from matplotlib import colormaps -# Start the main app: -root = tk.Tk() -root.title('RAPI beta 0.7') -root.geometry('1000x600') -root.iconbitmap('Icon.ico') +# ============================================================================= +# GLOBAL SETTINGS, COLORS, AND STYLE +# ============================================================================= -# Define the color palette. This can be based on https://materialui.co/colors colors = { 'foreground': '#ffffff', 'background': '#EEEEEE', - 'accent': '#FFCC80', + 'accent': '#CBE8ED', 'text': 'black', - 'inactive': '#AAAAAA' # Grayed-out color for inactive elements + 'inactive': '#AAAAAA' # For grayed-out elements } -# Configure the background: -root.configure(bg=colors['background'], padx=0, pady=0) +root = tk.Tk() +root.title('RAPI beta v.0.9') +root.geometry('1000x630') +root.resizable(False, False) +root.iconbitmap('Icon.ico') +root.configure(bg=colors['background']) -# Create and configure a style to be used in tabs, labels, and other elements +# Create and apply a custom style s = ttk.Style() s.theme_create("RAPI_theme", parent="alt", settings={ - 'TLabel': { - 'configure': { - 'background': colors['foreground'], - 'foreground': colors['text'], - 'padding': [0, 5, 7, 0]}}, - 'TFrame': { - 'configure': { - 'background': colors['foreground'], - 'borderwidth': 0, - 'highlightcolor': 'red'}}, - 'TRadiobutton': { - 'configure': { - 'background': colors['foreground'], - 'foreground': colors['text'], - 'padding': [5, 0, 0, 0], - }}, - 'TMenubutton': { - 'configure': { - 'background': colors['accent'], - 'foreground': colors['text'], - 'padding': [5, 0, 0, 0], - 'width': '30' - }}, - 'TMenu': { - 'configure': { - 'background': colors['accent'], - 'foreground': colors['text'], - 'width': '20', - 'arrowcolor': colors['text'], - }}, + 'TLabel': {'configure': {'background': colors['foreground'], + 'foreground': colors['text'], + 'padding': [0, 5, 7, 0]}}, + 'TFrame': {'configure': {'background': colors['foreground'], + 'borderwidth': 0}}, + 'TRadiobutton': {'configure': {'background': colors['foreground'], + 'foreground': colors['text'], + 'padding': [5, 0, 0, 0]}}, + 'TMenubutton': {'configure': {'background': colors['accent'], + 'foreground': colors['text'], + 'padding': [5, 0, 0, 0], + 'width': '30'}}, + 'TMenu': {'configure': {'background': colors['accent'], + 'foreground': colors['text'], + 'width': '20', + 'arrowcolor': colors['text']}}, }) - s.configure('RAPI_theme', relief='flat', background=colors['background'], foreground=colors['text'], insertcolor=colors['text'], - selectbackground=colors['accent'] - ) - + selectbackground=colors['accent']) s.theme_use("RAPI_theme") +# Map disabled menubutton appearance so that inactive dropdowns use pale grey (#DDDDDD) +s.map("Disabled.TMenubutton", + foreground=[("disabled", colors["inactive"])], + background=[("disabled", "#DDDDDD")]) -def save_image(): - ftypes = [('PNG file', '.png'), ('SVG file', '.svg'), ('All files', '*')] - filename = filedialog.asksaveasfilename(filetypes=ftypes, defaultextension='.png') - plt.savefig(filename, bbox_inches='tight', dpi=300) - - -def reset_scores(): - for criterion in criteria: - criterion.valuevar.set(0.0) - criterion.color.set('white') - for dropdown in dropdowns: - dropdown.reset_option() - - chromatography_var.set('no') - toggle_chromatography() # Resets state according to radio button - create_plot() +# Mapping from color names to numerical values. +color_values = { + 'white': 0, + 'white_Kedge': 0, + '#fff5f0': 2.5, + '#fca183': 5, + '#e53228': 7.5, + '#67000d': 10 +} +# ============================================================================= +# COORDINATE DATA (modify later if needed) +# ============================================================================= -def call_info_popup(): - win = tk.Toplevel() - win_frame = tk.Frame(win, width=500, background=colors['foreground']) - win_frame.pack() - win.iconbitmap('Icon.ico') - win.wm_title('About and citation info') - text0 = ttk.Label(win_frame, text='RAPI is a metric tool for ...', - wraplength=380, justify='left') - text0.grid(column=0, row=0, padx=8, pady=8, sticky='w') - text1 = ttk.Label(win_frame, text='If you use RAPI, please cite:', wraplength=280, justify='left') - text1.grid(column=0, row=1, padx=8, pady=8, sticky='w') - text2 = tk.Text(win_frame, width=50, height=6, wrap='word', bg=colors['background']) - citation = 'PaweĹ Mateusz Nowak, Wojciech Wojnowski, Natalia Manousi, Victoria Samanidou, Justyna PĹotka-Wasylka,' \ - 'Red Analytical Performance Index (RAPI) and software: the missing tool for assessing methods' \ - 'in terms of analytical effectiveness, doi.org/XX.XXXX/XXXXXXXXXX' - text2.grid(column=0, row=2, padx=8, pady=8, sticky='w') - text2.insert(tk.END, citation) - text3 = ttk.Label(win_frame, text='Most recent version of the source code can be found at:', wraplength=320, - justify='left') - text3.grid(column=0, row=3, padx=8, pady=8, sticky='w') - text4 = tk.Text(win_frame, width=50, height=1, wrap='word', bg=colors['background']) - source_link = 'git.pg.edu.pl/p174235/rapi' - text4.grid(column=0, row=4, padx=8, pady=8, sticky='w') - text4.insert(tk.END, source_link) - text5 = ttk.Label(win_frame, text='Š 2024 P. Nowak, W. Wojnowski, N. Manousi, V. Samanidou, J. PĹotka-Waskylka, '\ - 'available under the MIT License', wraplength=380, - justify='left') - text5.grid(column=0, row=5, padx=8, pady=8, sticky='w') +coordinates = { + '0': [(-8.719, 12), (-14.107, -4.584), (0, -14.833), (14.107, -4.584), (8.719, 12)], + '1': [(8.719, 12), (21.796, 30), (-21.796, 30), (-8.719, 12)], + '2': [(-8.719, 12), (-21.796, 30), (-35.267, -11.459), (-14.107, -4.584)], + '3': [(-14.107, -4.584), (-35.267, -11.459), (0, -37.082), (0, -14.833)], + '4': [(0, -14.833), (0, -37.082), (35.267, -11.459), (14.107, -4.584)], + '5': [(14.107, -4.584), (35.267, -11.459), (21.796, 30), (8.719, 12)], + '6': [(20.497, 34), (8.199, 50.927), (-8.199, 50.927), (-20.497, 34)], + '7': [(-26.002, 30), (-45.901, 23.535), (-50.968, 7.94), (-38.67, -8.987)], + '8': [(-36.567, -15.459), (-36.567, -36.382), (-23.301, -46.02), (-3.402, -39.554)], + '9': [(36.567, -15.459), (36.567, -36.382), (23.301, -46.02), (3.402, -39.554)], + '10': [(38.67, -8.987), (50.968, 7.94), (45.901, 23.535), (26.002, 30)], +} +# Use a simple mapping (adjust later if needed) +coord_keys = {i: str(i) for i in range(11)} + +# ============================================================================= +# OPTION DICTIONARIES +# ============================================================================= + +crit1_table = { + "c ⤠100%": ['⼠2.0', '< 2.0', '< 1.5', '< 1', '< 0.5'], + "c ⤠10%": ['⼠2.8', '< 2.8', '< 2.1', '< 1.4', '< 0.7'], + "c ⤠1%": ['⼠4.0', '< 4.0', '< 3', '< 2', '< 1.0'], + "c ⤠0.1%": ['⼠5.7', '< 5.7', '< 4.275', '< 2.85', '< 1.425'], + "c ⤠0.01%": ['⼠8.0', '< 8.0', '< 6.0', '< 4.0', '< 2.0'], + "c ⤠0.001%": ['⼠11.3', '< 11.3', '< 8.475', '< 5.65', '< 2.825'], + "c ⤠0.0001% (1 ppm)": ['⼠16.0', '< 16.0', '< 12.0', '< 8.0', '< 4.0'], + "c ⤠0.00001%": ['⼠22.6', '< 22.6', '< 16.95', '< 11.3', '< 5.65'], + "c ⤠0.000001%": ['⼠32.0', '< 32.0', '< 24.0', '< 16.0', '< 8.0'], + "c ⤠0.0000001% (1 ppb)": ['⼠45.3', '< 45.3', '< 33.975', '< 22.65', '< 11.325'] +} +crit2_table = { + "c ⤠100%": ['⼠2.5', '< 2.5', '< 2.0', '< 1.5', '< 1.0'], + "c ⤠10%": ['⼠3.5', '< 3.5', '< 2.8', '< 2.1', '< 1.4'], + "c ⤠1%": ['⼠5.0', '< 5.0', '< 4.0', '< 3.0', '< 2.0'], + "c ⤠0.1%": ['⼠7.1', '< 7.1', '< 5.7', '< 4.3', '< 2.9'], + "c ⤠0.01%": ['⼠10.0', '< 10.0', '< 8.0', '< 6.0', '< 4.0'], + "c ⤠0.001%": ['⼠14.1', '< 14.1', '< 11.3', '< 8.5', '< 5.7'], + "c ⤠0.0001% (1 ppm)": ['⼠20.0', '< 20.0', '< 12.0', '< 16.0', '< 8.0'], + "c ⤠0.00001%": ['⼠28.3', '< 28.3', '< 22.6', '< 17', '< 11.3'], + "c ⤠0.000001%": ['⼠40.0', '< 40.0', '< 24.0', '< 32.0', '< 16.0'], + "c ⤠0.0000001% (1 ppb)": ['⼠56.6', '< 56.6', '< 45.3', '< 34.0', '< 22.7'] +} -def create_menu(): - menu = tk.Menu() - root.config(menu=menu) - file_menu = tk.Menu(menu) - file_menu.config(background=colors['background'], - activeborderwidth=5, - activeforeground=colors['text'], - foreground=colors['text'], - tearoff=0) +crit3_table = { + "c ⤠100%": ['⼠3.0', '< 3.0', '< 2.5', '< 2.0', '< 1.5'], + "c ⤠10%": ['⼠4.2', '< 4.2', '< 3.5', '< 2.8', '< 2.1'], + "c ⤠1%": ['⼠6.0', '< 6.0', '< 5.0', '< 4.0', '< 3.0'], + "c ⤠0.1%": ['⼠8.6', '< 8.6', '< 7.1', '< 5.7', '< 4.3'], + "c ⤠0.01%": ['⼠12.0', '< 12.0', '< 10.0', '< 8.0', '< 6.0'], + "c ⤠0.001%": ['⼠17.0', '< 17.0', '< 14.1', '< 11.3', '< 8.5'], + "c ⤠0.0001% (1 ppm)": ['⼠24.0', '< 24.0', '< 20.0', '< 16.0', '< 12.0'], + "c ⤠0.00001%": ['⼠33.9', '< 33.9', '< 28.3', '< 22.6', '< 17.0'], + "c ⤠0.000001%": ['⼠48.0', '< 48.0', '< 40.0', '< 32.0', '< 24.0'], + "c ⤠0.0000001% (1 ppb)": ['⼠68.0', '< 68.0', '< 56.6', '< 45.3', '< 34.0'] +} - file_menu.add_command(label='Save the image', command=save_image) - file_menu.add_command(label='Re-set', command=reset_scores) - about_menu = tk.Menu(menu) - about_menu.config(background=colors['background'], - activeborderwidth=5, - activeforeground=colors['text'], - foreground=colors['text'], - tearoff=0) - menu.add_cascade(label='File', menu=file_menu) - menu.add_cascade(label='About', menu=about_menu) - about_menu.add_command(label='Info', command=call_info_popup) +crit4_table = { + "c ⤠100%": ['⼠3.0', '< 3.0', '< 2.0', '< 1.0', '< 1.0, confirmed with CRMs'], + "c ⤠10%": ['⼠3.0', '< 3.0', '< 2.0', '< 1.0', '< 1.0, confirmed with CRMs'], + "c ⤠1%": ['⼠5.0', '< 5.0', '< 3.0', '< 2.0', '< 2.0, confirmed with CRMs'], + "c ⤠0.1%": ['⼠10.0', '< 10.0', '< 5.0', '< 3.0', '< 3.0, confirmed with CRMs'], + "c ⤠0.01%": ['⼠20.0', '< 20.0', '< 10.0', '< 5.0', '< 5.0, confirmed with CRMs'], + "c ⤠0.001%": ['⼠40.0', '< 40.0', '< 20.0', '< 10.0', '< 10.0, confirmed with CRMs'], + "c ⤠0.0001% (1 ppm)": ['⼠40.0', '< 40.0', '< 20.0', '< 10.0', '< 10.0, confirmed with CRMs'], + "c ⤠0.00001%": ['⼠40.0', '< 40.0', '< 20.0', '< 10.0', '< 10.0, confirmed with CRMs'], + "c ⤠0.000001%": ['⼠60.0', '< 60.0', '< 40.0', '< 20.0', '< 20.0, confirmed with CRMs'], + "c ⤠0.0000001% (1 ppb)": ['⼠80.0', '< 80.0', '< 60.0', '< 40.0', '< 40.0, confirmed with CRMs'] +} +crit5_table = { + "c ⤠100%": ['⤠97, ⼠103', '> 97, < 103; ME study optional', '> 98, < 102; ME study optional', + '> 99, < 101; acceptable ME', '> 99.5, < 100.5; weak ME'], + "c ⤠10%": ['⤠97, ⼠103', '> 97, < 103; ME study optional', '> 98, < 102; ME study optional', + '> 99, < 101; acceptable ME', '> 99.5, < 100.5; weak ME'], + "c ⤠1%": ['⤠95, ⼠105', '> 95, < 105; ME study optional', '> 97, < 103; ME study optional', + '> 98, < 102; acceptable ME', '> 99, < 101; weak ME'], + "c ⤠0.1%": ['⤠90, ⼠107', '> 90, < 107; ME study optional', '> 95, < 105; ME study optional', + '> 97, < 103; acceptable ME', '> 98, < 102; weak ME'], + "c ⤠0.01%": ['⤠80, ⼠110', '> 80, < 110; ME study optional', '> 90, < 107; ME study optional', + '> 95, < 105; acceptable ME', '> 97, < 103; weak ME'], + "c ⤠0.001%": ['⤠60, ⼠115', '> 60, < 115; ME study optional', '> 80, < 110; ME study optional', + '> 90, < 107; acceptable ME', '> 95, < 105; weak ME'], + "c ⤠0.0001% (1 ppm)": ['⤠60, ⼠115', '> 60, < 115; ME study optional', '> 80, < 110; ME study optional', + '> 90, < 107; acceptable ME', '> 95, < 105; weak ME'], + "c ⤠0.00001%": ['⤠60, ⼠115', '> 60, < 115; ME study optional', '> 80, < 110; ME study optional', + '> 90, < 107; acceptable ME', '> 95, < 105; weak ME'], + "c ⤠0.000001%": ['⤠40, ⼠120', '> 40, < 120; ME study optional', '> 60, < 115; ME study optional', + '> 80, < 110; acceptable ME', '> 90, < 107; weak ME'], + "c ⤠0.0000001% (1 ppb)": ['⤠20, ⼠130', '> 20, < 130; ME study optional', '> 40, < 120; ME study optional', + '> 60, < 115; acceptable ME', '> 80, < 110; weak ME'] +} -create_menu() +crit6_options = { + 'not tested': 'white_Kedge', + 'LOQ ⼠25%': 'white_Kedge', + 'LOQ < 25%': '#fff5f0', + 'LOQ < 10%': '#fca183', + 'LOQ < 3%': '#e53228', + 'LOQ < 1%': '#67000d' +} -coordinates = { - 0: [(-8.719, 12), (-14.107, -4.584), (0, -14.833), (14.107, -4.584), (8.719, 12)], - 1: [(8.719, 12), (21.796, 30), (-21.796, 30), (-8.719, 12)], - 2: [(20.497, 34), (8.199, 50.927), (-8.199, 50.927), (-20.497, 34)], - 3: [(-14.107, -4.584), (-35.267, -11.459), (0, -37.082), (0, -14.833)], - 4: [(0, -14.833), (0, -37.082), (35.267, -11.459), (14.107, -4.584)], - 5: [(14.107, -4.584), (35.267, -11.459), (21.796, 30), (8.719, 12)], - 6: [(-8.719, 12), (-21.796, 30), (-35.267, -11.459), (-14.107, -4.584)], - 7: [(-26.002, 30), (-45.901, 23.535), (-50.968, 7.94), (-38.67, -8.987)], - 8: [(38.67, -8.987), (50.968, 7.94), (45.901, 23.535), (26.002, 30)], - 9: [(-36.567, -15.459), (-36.567, -36.382), (-23.301, -46.02), (-3.402, -39.554)], - 10: [(36.567, -15.459), (36.567, -36.382), (23.301, -46.02), (3.402, -39.554)], +crit7_options = { + 'not tested': 'white_Kedge', + 'narrower than 5ĂLOQ': 'white_Kedge', + 'wider than 5ĂLOQ': '#fff5f0', + 'wider than 10ĂLOQ': '#fca183', + 'wider than 30ĂLOQ': '#e53228', + 'wider than 100ĂLOQ': '#67000d' } -color_values = { - 'white': 0, - '#fff5f0': 2.5, - '#fca183': 5, - '#e53228': 7.5, - '#67000d': 10} +crit8_options = { + 'not tested': 'white_Kedge', + 'R^2 ⤠0.90': 'white_Kedge', + 'R^2 > 0.90': '#fff5f0', + 'R^2 > 0.94': '#fca183', + 'R^2 > 0.97': '#e53228', + 'R^2 > 0.99': '#67000d' +} -# Create the two main frames of the GUI -left_frame = tk.Frame(root, bd=0, padx=2, pady=0, width=500, height=500, bg=colors['foreground'], - highlightbackground=colors['background'], highlightthickness=8) -left_frame.pack(side='left', anchor='n', fill='both', expand=True) +crit9_options = { + 'not demonstrated': 'white_Kedge', + 'demonstrated for 1 factor': '#fff5f0', + 'demonstrated for 2 factors': '#fca183', + 'demonstrated for 3 factors': '#e53228', + 'demonstrated for 5 or more factors': '#67000d' +} -right_frame = tk.Frame(root, bd=0, padx=2, pady=0, width=500, height=500, bg=colors['background'], - highlightbackground=colors['background'], highlightthickness=8, - highlightcolor=colors['background']) -right_frame.pack(side='right', anchor='n') +crit10_options = { + 'not demonstrated': 'white_Kedge', + 'demonstrated for 1 potential interferent': '#fff5f0', + 'demonstrated for 2 potential interferents': '#fca183', + 'demonstrated for 3 potential interferents': '#e53228', + 'demonstrated for 5 or more potential interferents': '#67000d' +} +# ============================================================================= +# CRITERION CLASS +# ============================================================================= class Criterion: def __init__(self, number): self.number = number - self.valuevar = tk.DoubleVar() - self.valuevar.set(0.0) self.color = tk.StringVar() - self.color.set('white') - self.optionvar = tk.StringVar() - self.optionvar.set('select') - self.coordinates = coordinates[number] + self.color.set('white' if number == 0 else 'white_Kedge') +# Create 11 Criterion objects (0 is central) +criteria = [Criterion(i) for i in range(11)] -criteria = [] -for i in range(0, 11): - criteria.append(Criterion(i)) +# ============================================================================= +# LAYOUT: MAIN FRAMES +# ============================================================================= +# Create padding/grey border around the criteria and plot areas. +left_frame = tk.Frame(root, bd=0, padx=2, pady=0, width=500, height=500, bg=colors['foreground'], + highlightbackground=colors['background'], highlightthickness=8) +left_frame.pack(side='left', anchor='n', fill='both', expand=True) -def clear_frame(frame): - frame.destroy() - global right_frame - right_frame = tk.Frame(root, bd=0, padx=2, pady=0, width=500, height=500, bg=colors['foreground'], - highlightbackground=colors['background'], highlightthickness=8, - highlightcolor=colors['background']) - right_frame.pack(side='right', anchor='n') +right_frame = tk.Frame(root, bd=0, padx=2, pady=0, width=500, height=500, bg=colors['background'], + highlightbackground=colors['background'], highlightthickness=8, + highlightcolor=colors['background']) +right_frame.pack(side='right', anchor='n') +# ============================================================================= +# TWO-STAGE DROPDOWN WIDGET (for criteria 1â5) +# ============================================================================= + +class TwoStageDropdown: + def __init__(self, parent, criterion, label_text, row, table_mapping): + self.criterion = criterion + self.table_mapping = table_mapping + self.concentration_items = [ + "not tested", "c ⤠100%", "c ⤠10%", "c ⤠1%", "c ⤠0.1%", + "c ⤠0.01%", "c ⤠0.001%", "c ⤠0.0001% (1 ppm)", + "c ⤠0.00001%", "c ⤠0.000001%", "c ⤠0.0000001% (1 ppb)" + ] + self.point_colors = {0: 'white_Kedge', 25: '#fff5f0', 50: '#fca183', 75: '#e53228', 100: '#67000d'} + + self.container = tk.Frame(parent, bg=colors['foreground'], + highlightbackground='lightgrey', highlightthickness=1) + self.container.grid(row=row, column=0, padx=8, pady=4, sticky='EW') + self.label = ttk.Label(self.container, text=label_text, background=colors['foreground']) + self.label.grid(row=0, column=0, sticky='W', padx=4, pady=(0,2)) + self.concentration_var = tk.StringVar(value="not tested") + self.concentration_menu = ttk.OptionMenu( + self.container, self.concentration_var, "not tested", *self.concentration_items, + command=self.concentration_changed) + self.concentration_menu.config(width=25) + self.concentration_menu.grid(row=1, column=0, sticky='W', padx=4, pady=2) + self.threshold_var = tk.StringVar(value="") + self.threshold_menu = ttk.OptionMenu(self.container, self.threshold_var, "") + self.threshold_menu.config(width=25, state='disabled', style='Disabled.TMenubutton') + self.threshold_menu.grid(row=1, column=1, padx=(8,4), pady=2, sticky='W') + def concentration_changed(self, value): + if value == "not tested": + self.threshold_var.set("") + self.threshold_menu.config(state='disabled', style='Disabled.TMenubutton') + self.criterion.color.set('white_Kedge') + create_plot() + else: + options = self.table_mapping.get(value, []) + self.threshold_options = {} + for point, opt in zip([0, 25, 50, 75, 100], options): + self.threshold_options[opt] = self.point_colors[point] + menu = self.threshold_menu["menu"] + menu.delete(0, "end") + for opt in self.threshold_options: + menu.add_command(label=opt, command=lambda opt=opt: self.threshold_selected(opt)) + first_opt = list(self.threshold_options.keys())[0] + self.threshold_var.set(first_opt) + self.threshold_menu.config(state='normal', style='TMenubutton') + self.criterion.color.set(self.threshold_options[first_opt]) + create_plot() + def threshold_selected(self, selection): + self.threshold_var.set(selection) + self.criterion.color.set(self.threshold_options[selection]) + create_plot() + def reset(self): + self.concentration_var.set("not tested") + self.threshold_var.set("") + self.threshold_menu.config(state='disabled', style='Disabled.TMenubutton') + self.criterion.color.set('white_Kedge') + +# ============================================================================= +# SINGLE DROPDOWN WIDGET (for criteria 6â10) +# ============================================================================= + +class SingleDropdown: + def __init__(self, parent, criterion, options, label_text, row): + self.criterion = criterion + self.options = options + self.var = tk.StringVar(value=list(options.keys())[0]) + self.var.trace('w', self.change_dropdown) + self.container = tk.Frame(parent, bg=colors['foreground'], + highlightbackground='lightgrey', highlightthickness=1) + self.container.grid(row=row, column=0, padx=8, pady=(4,0), sticky='EW') + self.label = ttk.Label(self.container, text=label_text, background=colors['foreground']) + self.label.pack(anchor='w', padx=4, pady=(2,0)) + self.dropdown = ttk.OptionMenu(self.container, self.var, self.var.get(), *options.keys()) + self.dropdown.config(width=45) + self.dropdown.pack(anchor='w', padx=4, pady=(0,4)) + def change_dropdown(self, *args): + self.criterion.color.set(self.options[self.var.get()]) + create_plot() + def reset(self): + default = list(self.options.keys())[0] + self.var.set(default) + self.criterion.color.set(self.options[default]) -def calculate_score(): - active_criteria = criteria[1:9] if chromatography_var.get() == 'no' else criteria[1:11] - score = sum(color_values[c.color.get()] for c in active_criteria) - return round(score / len(active_criteria) * 10, 1) +# ============================================================================= +# PLOTTING FUNCTION (Unannotated) +# ============================================================================= +current_fig = None # Global reference to the current figure -def create_plot(event=None): +def calculate_score(): + active_criteria = criteria[1:] + total = 0 + count = 0 + for crit in active_criteria: + col = crit.color.get() + if col != 'white': + total += color_values.get(col, 0) + count += 1 + score = 0 if count == 0 else total / count * 10 + return 100 if score == 100 else round(score, 1) + +def create_plot(): + global current_fig plt.close() - clear_frame(right_frame) - + for widget in right_frame.winfo_children(): + widget.destroy() fig, ax = plt.subplots(figsize=(10, 10), dpi=150, facecolor=colors['foreground']) - - for i, criterion in enumerate(criteria[1:], start=1): - face_color = criterion.color.get() - edge_color = 'black' - if chromatography_var.get() == 'no' and i in (9, 10): - edge_color = 'white' - face_color = 'white' # Hide polygons for criteria 9 and 10 - - polygon = plt.Polygon(criterion.coordinates, facecolor=face_color, edgecolor=edge_color, - joinstyle='round') + for crit in criteria[1:]: + key = coord_keys.get(crit.number, str(crit.number)) + pts = coordinates.get(key, []) + col = crit.color.get() + if col == 'white': + polygon = plt.Polygon(pts, facecolor=col, edgecolor='white', linewidth=1, joinstyle='round') + elif col == 'white_Kedge': + polygon = plt.Polygon(pts, facecolor='white', edgecolor='black', linewidth=1, joinstyle='round') + else: + polygon = plt.Polygon(pts, facecolor=col, edgecolor='black', linewidth=1, joinstyle='round') ax.add_patch(polygon) - + center_score = calculate_score() + center_facecolor = 'white' if center_score == 0 else colormaps['Reds'](int(255 * (center_score / 100))) + center_polygon = plt.Polygon(coordinates['0'], facecolor=center_facecolor, edgecolor='black', linewidth=1, joinstyle='round') + ax.add_patch(center_polygon) ax.autoscale() ax.set_aspect('equal') - center_score = calculate_score() - - central_polygon_color = 'white' if center_score == 0 else colormaps['Reds'](int(255 * (center_score / 100))) - central_polygon = plt.Polygon(criteria[0].coordinates, facecolor=central_polygon_color, edgecolor='black') - ax.add_patch(central_polygon) - text_color = 'white' if center_score >= 80 else 'black' ax.text(0, 0, str(center_score), ha='center', va='center', fontsize=14, color=text_color) - ax.axis('off') + canvas = FigureCanvasTkAgg(fig, master=right_frame) + canvas.draw() + canvas.get_tk_widget().pack(side='top', fill='both', expand=True) + current_fig = fig + return fig + +# ============================================================================= +# ANNOTATED PLOT FUNCTION +# ============================================================================= + +def create_annotated_plot(): + global current_fig + plt.close() + for widget in right_frame.winfo_children(): + widget.destroy() + fig, ax = plt.subplots(figsize=(10, 17), dpi=150, facecolor=colors['foreground']) + for crit in criteria[1:]: + key = coord_keys.get(crit.number, str(crit.number)) + pts = coordinates.get(key, []) + col = crit.color.get() + if col == 'white': + polygon = plt.Polygon(pts, facecolor=col, edgecolor='white', linewidth=0.6, joinstyle='round') + elif col == 'white_Kedge': + polygon = plt.Polygon(pts, facecolor='white', edgecolor='black', linewidth=0.6, joinstyle='round') + else: + polygon = plt.Polygon(pts, facecolor=col, edgecolor='black', linewidth=0.6, joinstyle='round') + ax.add_patch(polygon) + center_score = calculate_score() + center_facecolor = 'white' if center_score == 0 else colormaps['Reds'](int(255 * (center_score / 100))) + center_polygon = plt.Polygon(coordinates['0'], facecolor=center_facecolor, edgecolor='black', linewidth=0.6, joinstyle='round') + ax.add_patch(center_polygon) + ax.autoscale() + ax.set_aspect('equal') + text_color = 'white' if center_score >= 80 else 'black' + ax.text(0, 0, str(center_score), ha='center', va='center', fontsize=10, color=text_color) + # Add field number annotations. + annotations = { + '1': (0, 21), + '5': (19.972, 6.489), + '4': (12.343, -16.989), + '3': (-12.343, -16.989), + '2': (-19.972, 6.489), + '6': (0, 42.463), + '10': (40.385, 13.122), + '7': (-40.385, 13.122), + '9': (24.959, -34.354), + '8': (-24.959, -34.354) + } + for key, pos in annotations.items(): + try: + crit_index = int(key) + field_color = criteria[crit_index].color.get() + except Exception: + field_color = 'white_Kedge' + ann_color = 'white' if field_color == '#67000d' else 'black' + ax.text(pos[0], pos[1], key, ha='center', va='center', fontsize=6, color=ann_color) + caption = ( + "1. Repeatability\n" + "2. Intermediate precision\n" + "3. Reproducibility\n" + "4. Trueness\n" + "5. Recovery, matrix effect\n" + "6. LOQ\n" + "7. Working range\n" + "8. Linearity\n" + "9. Ruggedness/robustness\n" + "10. Selectivity" + ) + ax.text(56, 0, caption, fontsize=8, va='center', ha='left') + current_xlim = ax.get_xlim() + ax.set_xlim(current_xlim[0], current_xlim[1] + 60) + ax.axis('off') + canvas = FigureCanvasTkAgg(fig, master=right_frame) + canvas.draw() + canvas.get_tk_widget().pack(side='top', fill='both', expand=True) + current_fig = fig + return fig - canvas = FigureCanvasTkAgg(figure=fig, master=right_frame) - plot_widget = canvas.get_tk_widget() - plot_widget.pack(side='top', padx=8, pady=8) - - -class Dropdown: - def __init__(self, criterion, options, label_text, row): - self.var = criterion.color - self.options = options - self.row = row - self.dropdown_var = tk.StringVar(value='not confirmed') - - # Create the dropdown menu - self.dropdown_menu = ttk.OptionMenu(left_frame, self.dropdown_var, self.dropdown_var.get(), - *self.options.keys()) - self.dropdown_menu.config(width=50) # Adjust the width here - self.dropdown_menu.grid(row=self.row + 1, column=0, padx=8, sticky='W') - - # Initialize label - self.label = ttk.Label(left_frame, text=label_text) - self.label.grid(row=self.row, column=0, padx=8, sticky='W') - - # Overlay label for inactive state - # Using a lighter gray to ensure text remains visible - self.overlay = tk.Label(left_frame, bg='#DDDDDD', width=20, height=1) - self.overlay.place_forget() # Initially hidden - - # Trace changes - self.dropdown_var.trace('w', self.change_dropdown) - - def change_dropdown(self, *args): - self.var.set(self.options[self.dropdown_var.get()]) - create_plot() +# ============================================================================= +# IMAGE SAVING AND MENU FUNCTIONS +# ============================================================================= - def reset_option(self): - self.dropdown_var.set('not confirmed') - self.var.set('white') +def save_image(): + ftypes = [('PNG file', '.png'), ('SVG file', '.svg'), ('All files', '*')] + filename = filedialog.asksaveasfilename(filetypes=ftypes, defaultextension='.png') + if filename: + # Save the current figure exactly as displayed. + current_fig.savefig(filename, bbox_inches='tight', dpi=300) - def set_inactive(self, inactive=True): - if inactive: - self.overlay.place(in_=self.dropdown_menu, relwidth=1, relheight=1) - else: - self.overlay.place_forget() - - -def toggle_chromatography(*args): - if chromatography_var.get() == 'yes': - dropdown9.label.config(foreground=colors['text']) # Active state color - dropdown10.label.config(foreground=colors['text']) - dropdown9.dropdown_menu.config(state='normal') - dropdown10.dropdown_menu.config(state='normal') - dropdown9.set_inactive(False) - dropdown10.set_inactive(False) - else: - dropdown9.reset_option() - dropdown10.reset_option() - dropdown9.dropdown_menu.config(state='disabled') - dropdown10.dropdown_menu.config(state='disabled') - dropdown9.label.config(foreground=colors['inactive']) # Inactive state color - dropdown10.label.config(foreground=colors['inactive']) - dropdown9.set_inactive(True) - dropdown10.set_inactive(True) +def save_annotated_image(): + fig = create_annotated_plot() + ftypes = [('PNG file', '.png'), ('SVG file', '.svg'), ('All files', '*')] + filename = filedialog.asksaveasfilename(filetypes=ftypes, defaultextension='.png') + if filename: + fig.savefig(filename, bbox_inches='tight', dpi=300) create_plot() +def reset_scores(): + for crit in criteria[1:]: + crit.color.set('white_Kedge') + for ts in two_stage_dropdowns: + ts.reset() + dropdown6.reset() + dropdown7.reset() + dropdown8.reset() + dropdown9.reset() + dropdown10.reset() + create_plot() +def call_info_popup(): + win = tk.Toplevel() + win_frame = tk.Frame(win, width=500, background=colors['foreground']) + win_frame.pack() + win.wm_title('About and citation info') + text0 = ttk.Label(win_frame, + text='The Red Analytical Performance Index (RAPI) is a tool for evaluating analytical methods in terms of their performance. It is inspired by the concept of White Analytical Chemistry (WAC).', + wraplength=380, justify='left') + text0.grid(column=0, row=0, padx=8, pady=8, sticky='w') + text1 = ttk.Label(win_frame, text='If you use RAPI, please cite:', wraplength=280, justify='left') + text1.grid(column=0, row=1, padx=8, pady=8, sticky='w') + text2 = tk.Text(win_frame, width=50, height=6, wrap='word', bg=colors['background']) + citation = ('PaweĹ Mateusz Nowak, Wojciech Wojnowski, Natalia Manousi, Victoria Samanidou, ' + 'Justyna PĹotka-Wasylka, Red Analytical Performance Index (RAPI) and software: ' + 'the missing tool for assessing methods in terms of analytical effectiveness.') + text2.grid(column=0, row=2, padx=8, pady=8, sticky='w') + text2.insert(tk.END, citation) + text3 = ttk.Label(win_frame, text='Most recent version of the source code can be found at:', wraplength=320, + justify='left') + text3.grid(column=0, row=3, padx=8, pady=8, sticky='w') + text4 = tk.Text(win_frame, width=50, height=1, wrap='word', bg=colors['background']) + source_link = 'git.pg.edu.pl/p174235/rapi' + text4.grid(column=0, row=4, padx=8, pady=8, sticky='w') + text4.insert(tk.END, source_link) + text5 = ttk.Label(win_frame, text='Š 2024 Authors, available under the MIT License', wraplength=380, + justify='left') + text5.grid(column=0, row=5, padx=8, pady=8, sticky='w') -# Dropdowns -dropdown1 = Dropdown(criterion=criteria[1], options={ - 'not confirmed': 'white', - 'error > 20%': '#fff5f0', - '10% < error ⤠20%': '#fca183', - '5% < error ⤠10%': '#e53228', - 'error ⤠5%': '#67000d'}, - row=1, - label_text='1. Trueness (relative error, %)') - -dropdown2 = Dropdown(criterion=criteria[2], options={ - 'not confirmed': 'white', - 'recovery < 80%, recovery > 120%': '#fff5f0', - 'recovery â (80% : 90%âŠ, â¨110% : 120%)': '#fca183', - 'recovery â (90% : 95%âŠ, â¨105% : 110%)': '#e53228', - 'recovery > 95%, recovery < 105%': '#67000d'}, - row=3, - label_text='2. Recovery (%)') - -dropdown3 = Dropdown(criterion=criteria[3], options={ - 'not confirmed': 'white', - 'RSD > 20%': '#fff5f0', - '10% < RSD ⤠20%': '#fca183', - '5% < RSD ⤠10%': '#e53228', - 'RSD ⤠5%': '#67000d'}, - row=5, label_text='3. Intra-day precision (RSD, %)') - -dropdown4 = Dropdown(criterion=criteria[4], options={ - 'not confirmed': 'white', - 'RSD > 25%': '#fff5f0', - '15% < RSD ⤠25%': '#fca183', - '7% < RSD ⤠15%': '#e53228', - 'RSD ⤠7%': '#67000d'}, - row=7, label_text='4. Inter-day precision (RSD, %)') - -dropdown5 = Dropdown(criterion=criteria[5], options={ - 'not confirmed': 'white', - 'LOQ > 50% e.c.': '#fff5f0', - '10% e.c. < LOQ ⤠50% e.c.': '#fca183', - '1% e.c. < LOQ ⤠10% e.c.': '#e53228', - 'LOQ ⤠1% e.c.': '#67000d'}, - row=9, label_text='5. LOQ (%)') - -dropdown6 = Dropdown(criterion=criteria[6], options={ - 'not confirmed': 'white', - '< (LOQ : 10ĂLOQ)': '#fff5f0', - '⼠(LOQ : 10ĂLOQ), ⤠(LOQ : 30ĂLOQ)': '#fca183', - '⼠(LOQ : 30ĂLOQ), ⤠(LOQ : 100ĂLOQ)': '#e53228', - '> (LOQ : 100ĂLOQ)': '#67000d'}, - row=11, label_text='6. Calibration (linearity) range') - -dropdown7 = Dropdown(criterion=criteria[7], options={ - 'not confirmed': 'white', - 'linearity < 0.900': '#fff5f0', - '0.990 > linearity ⼠0.900': '#fca183', - '0.999 > linearity ⼠0.990': '#e53228', - 'linearity ⼠0.999': '#67000d'}, - row=13, label_text='7. Linearity (R2)') - -dropdown8 = Dropdown(criterion=criteria[8], options={ - 'not confirmed': 'white', - 'confirmed against 1 factor': '#fff5f0', - 'confirmed against 2 factors': '#fca183', - 'confirmed against 3 factors': '#e53228', - 'confirmed against > 3 factors': '#67000d'}, - row=15, label_text='8. Robustness') - -chrom_label = ttk.Label(left_frame, text='Does the method involve chromatographic separation?') -chrom_label.grid(row=17, column=0, padx=8, sticky='W') - -chromatography_var = tk.StringVar(value='no') -chrom_radio1 = ttk.Radiobutton(left_frame, text='Yes', variable=chromatography_var, value='yes', style='TRadiobutton') -chrom_radio2 = ttk.Radiobutton(left_frame, text='No', variable=chromatography_var, value='no', style='TRadiobutton') -chrom_radio1.grid(row=18, column=0, sticky='W', padx=8) -chrom_radio2.grid(row=18, column=0, sticky='W', padx=58) - -# Configure the radio buttons to avoid dark frame highlight on selection -chrom_radio1.configure(takefocus=False) -chrom_radio2.configure(takefocus=False) - -chromatography_var.trace('w', toggle_chromatography) - -dropdown9 = Dropdown(criterion=criteria[9], options={ - 'not confirmed': 'white', - '⤠1 000': '#fff5f0', - 'â (1 000 : 10 000âŠ': '#fca183', - 'â (10 000 : 100 000âŠ': '#e53228', - '> 100 000': '#67000d'}, - row=19, label_text='9. Mean no. of theoretical plates') - -dropdown10 = Dropdown(criterion=criteria[10], options={ - 'not confirmed': 'white', - 'resolution < 0.5': '#fff5f0', - '1.0 > resolution ⼠0.5': '#fca183', - '1.5 > resolution ⼠1.0': '#e53228', - 'resolution ⼠1.5': '#67000d'}, - row=21, label_text='10. Resolution of the worst-separated peaks') - -# Set initial states for dropdown labels and states -toggle_chromatography() - -dropdowns = [dropdown1, dropdown2, dropdown3, dropdown4, dropdown5, dropdown6, dropdown7, dropdown8, dropdown9, - dropdown10] - -create_plot() - +def create_menu(): + menu = tk.Menu(root) + root.config(menu=menu) + file_menu = tk.Menu(menu, tearoff=0) + file_menu.add_command(label='Save the image', command=save_image) + file_menu.add_command(label='Save annotated image', command=save_annotated_image) + file_menu.add_command(label='Reset', command=reset_scores) + about_menu = tk.Menu(menu, tearoff=0) + menu.add_cascade(label='File', menu=file_menu) + menu.add_cascade(label='About', menu=about_menu) + about_menu.add_command(label='Info', command=call_info_popup) -def main(): - while True: - root.mainloop() +create_menu() +# ============================================================================= +# CREATE DROPDOWNS IN SEQUENTIAL ORDER (Fields 1 to 10) +# ============================================================================= + +two_stage_dropdowns = [] +two_stage_dropdowns.append(TwoStageDropdown(left_frame, criteria[1], + "1. Repeatability", + row=1, table_mapping=crit1_table)) +two_stage_dropdowns.append(TwoStageDropdown(left_frame, criteria[2], + "2. Intermediate precision", + row=2, table_mapping=crit2_table)) +two_stage_dropdowns.append(TwoStageDropdown(left_frame, criteria[3], + "3. Reproducibility", + row=3, table_mapping=crit3_table)) +two_stage_dropdowns.append(TwoStageDropdown(left_frame, criteria[4], + "4. Trueness", + row=4, table_mapping=crit4_table)) +two_stage_dropdowns.append(TwoStageDropdown(left_frame, criteria[5], + "5. Recovery & matrix effect", + row=5, table_mapping=crit5_table)) +dropdown6 = SingleDropdown(left_frame, criteria[6], crit6_options, + "6. LOQ (% of expected mean analyte concentraton in the matrix)", row=6) +dropdown7 = SingleDropdown(left_frame, criteria[7], crit7_options, + "7. Working range", row=7) +dropdown8 = SingleDropdown(left_frame, criteria[8], crit8_options, + "8. Simplified linearity estimation", row=8) +dropdown9 = SingleDropdown(left_frame, criteria[9], crit9_options, + "9. Ruggedness/robustness", row=9) +dropdown10 = SingleDropdown(left_frame, criteria[10], crit10_options, + "10. Selectivity", row=10) + +# ============================================================================= +# MAIN LOOP +# ============================================================================= -main() +create_plot() +root.mainloop() -- GitLab