Source code for frontend.ui_dialogs.data_selection

"""Data selection dialogs: variables and data preview."""

from typing import Any, List, Optional, Tuple
from tkinter import Listbox, Tk, Toplevel, StringVar, Text, ttk

from config import UI_STYLE, apply_hover_to_children, get_entry_font
from frontend.window_utils import place_window_centered
from frontend.keyboard_nav import bind_enter_to_accept
from i18n import t

# Max size for pair-plot image window so it does not resize the desktop
_PAIR_PLOT_MAX_WIDTH = 920
_PAIR_PLOT_MAX_HEIGHT = 720
_PAIR_PLOT_MAX_VARS = 10


def _filter_uncertainty_variables(variable_names: List[str]) -> List[str]:
    """
    Filter out uncertainty-paired variables (e.g. keep 'x', drop 'ux' when 'x' exists).

    Returns a list with at most one of each base/uncertainty pair for cleaner selection.
    """
    filtered: List[str] = []
    excluded: set[str] = set()
    for var in variable_names:
        if var in excluded:
            continue
        if var.startswith("u") and len(var) > 1:
            base_var = var[1:]
            if base_var in variable_names:
                excluded.add(var)
                continue
        u_var = f"u{var}"
        if u_var in variable_names:
            excluded.add(u_var)
        filtered.append(var)
    return filtered if filtered else variable_names


[docs] def ask_variables( parent_window: Any, variable_names: List[str] ) -> Tuple[str, str, str]: """ Dialog to select independent (x) and dependent (y) variables and plot name. Args: parent_window: Parent Tkinter window. variable_names: List of available variable names from the dataset. Returns: Tuple of ``(x_name, y_name, plot_name)``. Returns ``('', '', '')`` if user cancels. """ call_var_level = Toplevel() call_var_level.title(t("dialog.data")) call_var_level.cancelled = False def _on_close_variables() -> None: call_var_level.cancelled = True call_var_level.destroy() call_var_level.protocol("WM_DELETE_WINDOW", _on_close_variables) call_var_level.frame_custom = ttk.Frame( call_var_level, padding=UI_STYLE["border_width"] ) call_var_level.x_name = StringVar() call_var_level.y_name = StringVar() call_var_level.graf_name = StringVar() call_var_level.label_message = ttk.Label( call_var_level.frame_custom, text=t("dialog.variable_names"), style="LargeBold.TLabel", ) call_var_level.label_message_x = ttk.Label( call_var_level.frame_custom, text=t("dialog.independent_variable"), ) variable_names = _filter_uncertainty_variables(variable_names) call_var_level.x_nom = ttk.Combobox( call_var_level.frame_custom, textvariable=call_var_level.x_name, values=variable_names, state="readonly", width=UI_STYLE["spinbox_width"], font=get_entry_font(), ) if variable_names: call_var_level.x_nom.current(0) call_var_level.label_message_y = ttk.Label( call_var_level.frame_custom, text=t("dialog.dependent_variable"), ) call_var_level.y_nom = ttk.Combobox( call_var_level.frame_custom, textvariable=call_var_level.y_name, values=variable_names, state="readonly", width=UI_STYLE["spinbox_width"], font=get_entry_font(), ) if len(variable_names) > 1: call_var_level.y_nom.current(1) elif variable_names: call_var_level.y_nom.current(0) call_var_level.accept_button = ttk.Button( call_var_level.frame_custom, text=t("dialog.accept"), command=call_var_level.destroy, style="Primary.TButton", width=UI_STYLE["button_width"], ) call_var_level.label_message_plot = ttk.Label( call_var_level.frame_custom, text=t("dialog.plot_name"), ) call_var_level.graf_nom = ttk.Entry( call_var_level.frame_custom, textvariable=call_var_level.graf_name, width=UI_STYLE["entry_width"], font=get_entry_font(), ) call_var_level.frame_custom.grid(column=0, row=0) call_var_level.label_message_plot.grid( column=0, row=0, padx=UI_STYLE["padding"], pady=UI_STYLE["padding"] ) call_var_level.graf_nom.grid( column=1, row=0, padx=UI_STYLE["padding"], pady=UI_STYLE["padding"] ) call_var_level.label_message.grid( column=0, row=1, columnspan=2, padx=UI_STYLE["padding"], pady=6 ) call_var_level.label_message_x.grid( column=0, row=2, padx=UI_STYLE["padding"], pady=UI_STYLE["padding"] ) call_var_level.x_nom.grid( column=1, row=2, padx=UI_STYLE["padding"], pady=UI_STYLE["padding"] ) call_var_level.label_message_y.grid( column=0, row=3, padx=UI_STYLE["padding"], pady=UI_STYLE["padding"] ) call_var_level.y_nom.grid( column=1, row=3, padx=UI_STYLE["padding"], pady=UI_STYLE["padding"] ) call_var_level.accept_button.grid( column=1, row=4, padx=UI_STYLE["padding"], pady=UI_STYLE["padding"] ) bind_enter_to_accept( [call_var_level.x_nom, call_var_level.y_nom, call_var_level.graf_nom], call_var_level.destroy, ) apply_hover_to_children(call_var_level.frame_custom) call_var_level.graf_nom.focus_set() call_var_level.resizable(False, False) place_window_centered(call_var_level, preserve_size=True) parent_window.wait_window(call_var_level) if getattr(call_var_level, "cancelled", False): return ("", "", "") return ( call_var_level.x_name.get(), call_var_level.y_name.get(), call_var_level.graf_name.get(), )
[docs] def ask_multiple_x_variables( parent_window: Any, variable_names: List[str], num_vars: int, first_x_name: str ) -> List[str]: """ Dialog to select multiple independent (x) variables for multidimensional fitting. Args: parent_window: Parent Tkinter window. variable_names: List of available variable names from the dataset. num_vars: Number of independent variables to select. first_x_name: Name of the first x variable already selected. Returns: List of x variable names. Returns empty list if user cancels. """ call_var_level = Toplevel() call_var_level.title(t("dialog.data")) call_var_level.cancelled = False def _on_close_variables() -> None: call_var_level.cancelled = True call_var_level.destroy() call_var_level.protocol("WM_DELETE_WINDOW", _on_close_variables) call_var_level.frame_custom = ttk.Frame( call_var_level, padding=UI_STYLE["border_width"] ) call_var_level.label_message = ttk.Label( call_var_level.frame_custom, text=t("dialog.select_multiple_x_variables", num=num_vars), style="LargeBold.TLabel", ) variable_names = _filter_uncertainty_variables(variable_names) # Create comboboxes for each x variable x_vars = [] x_combos = [] for i in range(num_vars): x_var = StringVar() if i == 0: x_var.set(first_x_name) # First one is already selected x_vars.append(x_var) label = ttk.Label( call_var_level.frame_custom, text=t("dialog.independent_variable_index", index=i + 1, index_minus=i), ) combo = ttk.Combobox( call_var_level.frame_custom, textvariable=x_var, values=variable_names, state="readonly", width=UI_STYLE["spinbox_width"], font=get_entry_font(), ) if variable_names and i == 0: try: combo.current(variable_names.index(first_x_name)) except ValueError: combo.current(0) elif variable_names: combo.current(min(i, len(variable_names) - 1)) x_combos.append((label, combo)) call_var_level.accept_button = ttk.Button( call_var_level.frame_custom, text=t("dialog.accept"), command=call_var_level.destroy, style="Primary.TButton", width=UI_STYLE["button_width"], ) call_var_level.frame_custom.grid(column=0, row=0) call_var_level.label_message.grid( column=0, row=0, columnspan=2, padx=UI_STYLE["padding"], pady=UI_STYLE["padding"], ) for i, (label, combo) in enumerate(x_combos): label.grid( column=0, row=i + 1, padx=UI_STYLE["padding"], pady=UI_STYLE["padding"] ) combo.grid( column=1, row=i + 1, padx=UI_STYLE["padding"], pady=UI_STYLE["padding"] ) call_var_level.accept_button.grid( column=1, row=num_vars + 1, padx=UI_STYLE["padding"], pady=UI_STYLE["padding"] ) bind_enter_to_accept([combo for _, combo in x_combos], call_var_level.destroy) apply_hover_to_children(call_var_level.frame_custom) if x_combos: x_combos[0][1].focus_set() call_var_level.resizable(False, False) place_window_centered(call_var_level, preserve_size=True) parent_window.wait_window(call_var_level) if getattr(call_var_level, "cancelled", False): return [] result = [x_var.get() for x_var in x_vars] return result if all(result) else []
def _ask_pair_plot_variables( parent: Tk | Toplevel, variables: List[str], max_select: int = _PAIR_PLOT_MAX_VARS, ) -> Optional[List[str]]: """ Ask user to select variables for pair plot when there are many. Returns selected variable names (up to max_select), or None if cancelled. """ if len(variables) <= max_select: return variables dlg = Toplevel(parent) dlg.title(t("dialog.pair_plots_select_variables")) dlg.configure(background=UI_STYLE["bg"]) dlg.transient(parent) dlg.grab_set() frame = ttk.Frame(dlg, padding=UI_STYLE["padding"]) frame.pack(fill="both", expand=True) ttk.Label( frame, text=t("dialog.pair_plots_select_variables"), wraplength=400, ).pack(anchor="w", pady=(0, 4)) listbox = Listbox( frame, selectmode="extended", height=min(12, len(variables)), font=get_entry_font(), exportselection=False, ) scrollbar = ttk.Scrollbar(frame, orient="vertical", command=listbox.yview) for v in variables: listbox.insert("end", v) listbox.config(yscrollcommand=scrollbar.set) # Default select first max_select for i in range(min(max_select, len(variables))): listbox.selection_set(i) listbox.pack(side="left", fill="both", expand=True, pady=4) scrollbar.pack(side="right", fill="y", pady=4) result: List[str] = [] def _on_accept() -> None: nonlocal result sel = listbox.curselection() result = [variables[i] for i in sel][:max_select] dlg.destroy() def _on_cancel() -> None: dlg.destroy() btn_frame = ttk.Frame(frame) btn_frame.pack(fill="x", pady=8) ttk.Button( btn_frame, text=t("dialog.accept"), command=_on_accept, style="Primary.TButton", width=UI_STYLE["button_width"], ).pack(side="left", padx=2) ttk.Button( btn_frame, text=t("dialog.cancel"), command=_on_cancel, width=UI_STYLE["button_width"], ).pack(side="left", padx=2) place_window_centered(dlg, preserve_size=True) dlg.wait_window() return result if result else None def _show_image_toplevel( parent: Tk | Toplevel, image_path: str, title: str, *, existing_win: Toplevel | None = None, existing_label: ttk.Label | None = None, ) -> tuple[Toplevel | None, ttk.Label | None]: """ Open or update a Toplevel window showing an image from file. If existing_win/existing_label are provided and the window still exists, updates the image in place. Otherwise creates a new window. Returns: Tuple (window, label) for the displayed image, or (None, None) on failure. """ from pathlib import Path from frontend.image_utils import ( load_image_scaled, plot_display_path, preview_path_to_remove_after_display, ) display_path = plot_display_path(image_path) preview_to_remove = preview_path_to_remove_after_display(display_path, image_path) photo = load_image_scaled(display_path, _PAIR_PLOT_MAX_WIDTH, _PAIR_PLOT_MAX_HEIGHT) if not photo: return (existing_win, existing_label) # Update existing window if it still exists if ( existing_win is not None and existing_label is not None and existing_win.winfo_exists() ): existing_label.configure(image=photo) existing_label.image = photo return (existing_win, existing_label) # Create new window win = Toplevel(parent) win.title(title) win.configure(background=UI_STYLE["bg"]) win.resizable(False, False) def _on_close() -> None: if preview_to_remove: try: Path(preview_to_remove).unlink(missing_ok=True) except OSError: pass win.destroy() label = ttk.Label(win, image=photo) label.image = photo label.pack(padx=UI_STYLE["padding"], pady=UI_STYLE["padding"]) close_btn = ttk.Button( win, text=t("dialog.accept"), command=_on_close, style="Primary.TButton", width=UI_STYLE["button_width"], ) close_btn.pack(padx=UI_STYLE["padding"], pady=UI_STYLE["padding"]) close_btn.focus_set() bind_enter_to_accept([close_btn, win], _on_close) win.protocol("WM_DELETE_WINDOW", _on_close) place_window_centered(win, preserve_size=True) return (win, label)
[docs] def show_data_dialog(parent_window: Tk | Toplevel, data: Any) -> None: """ Dialog to display loaded data. Provides transform, cleaning, save, and pair-plot options. Transform/clean dialogs are non-modal so focus stays on the data window for multiple ops. Pair plots auto-update when data is transformed (if already open). Args: parent_window: Parent Tkinter window (``Tk`` or ``Toplevel``). data: DataFrame to display (or string). If DataFrame, converted to string for display using ``to_string()`` method. """ import pandas as pd from config import get_output_path from loaders import get_variable_names from plotting import create_pair_plots from data_analysis import ( CLEAN_OPTIONS, TRANSFORM_OPTIONS, apply_cleaning, apply_transform, ) from frontend.ui_dialogs import open_save_dialog, show_data_view_help_dialog # Mutable container so transforms/cleaning can update the displayed data current_data: list[Any] = [data] def _content_from(d: Any) -> str: if hasattr(d, "to_string"): return d.to_string() return str(d) watch_data_level = Toplevel() watch_data_level.title(t("dialog.show_data_title")) watch_data_level.configure(background=UI_STYLE["bg"]) watch_data_level.minsize(800, 260) watch_data_level.resizable(False, False) # Pair plot window reference for auto-refresh when data changes watch_data_level._pair_plot_win: Toplevel | None = None watch_data_level._pair_plot_label: ttk.Label | None = None watch_data_level._pair_plot_vars: Optional[List[str]] = ( None # Selected vars when many ) # Data display area: reduced height for compact window _data_display_lines = 12 text_frame = ttk.Frame(watch_data_level) text_frame.pack(padx=UI_STYLE["padding"], pady=6, fill="both") scrollbar_y = ttk.Scrollbar(text_frame, orient="vertical") scrollbar_y.pack(side="right", fill="y") scrollbar_x = ttk.Scrollbar(text_frame, orient="horizontal") scrollbar_x.pack(side="bottom", fill="x") text_widget = Text( text_frame, height=_data_display_lines, bg=UI_STYLE["text_bg"], fg=UI_STYLE["text_fg"], font=(UI_STYLE["text_font_family"], UI_STYLE["text_font_size"]), wrap="none", yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set, relief="sunken", borderwidth=UI_STYLE["border_width"], padx=UI_STYLE["padding"], pady=UI_STYLE["padding"], insertbackground=UI_STYLE["text_insert_bg"], selectbackground=UI_STYLE["text_select_bg"], selectforeground=UI_STYLE["text_select_fg"], ) text_widget.pack(side="left", fill="both", expand=True) scrollbar_y.config(command=text_widget.yview) scrollbar_x.config(command=text_widget.xview) text_widget.insert("1.0", _content_from(current_data[0])) text_widget.config(state="disabled") def _refresh_display() -> None: text_widget.config(state="normal") text_widget.delete("1.0", "end") text_widget.insert("1.0", _content_from(current_data[0])) text_widget.config(state="disabled") def _refresh_pair_plot_if_open() -> None: if not isinstance(current_data[0], pd.DataFrame): return pw = watch_data_level._pair_plot_win pl = watch_data_level._pair_plot_label if pw is None or pl is None or not pw.winfo_exists(): return try: variables = get_variable_names(current_data[0], filter_uncertainty=True) if not variables: return # Use stored selection if still valid; otherwise use all (or first N) stored = getattr(watch_data_level, "_pair_plot_vars", None) if stored: plot_vars = [v for v in stored if v in variables][:_PAIR_PLOT_MAX_VARS] if not plot_vars: plot_vars = variables[:_PAIR_PLOT_MAX_VARS] else: plot_vars = ( variables[:_PAIR_PLOT_MAX_VARS] if len(variables) > _PAIR_PLOT_MAX_VARS else variables ) output_path = get_output_path("pair_plot") create_pair_plots(current_data[0], plot_vars, output_path=output_path) _show_image_toplevel( watch_data_level, output_path, t("dialog.pair_plots_title"), existing_win=pw, existing_label=pl, ) except Exception: pass def _on_data_updated(new_data: Any) -> None: current_data[0] = new_data _refresh_display() _refresh_pair_plot_if_open() watch_data_level.focus_set() def _open_pair_plots_window() -> None: try: variables = get_variable_names(current_data[0], filter_uncertainty=True) if not variables: return plot_vars = _ask_pair_plot_variables(watch_data_level, variables) if not plot_vars: return watch_data_level._pair_plot_vars = ( plot_vars if len(variables) > _PAIR_PLOT_MAX_VARS else None ) output_path = get_output_path("pair_plot") create_pair_plots(current_data[0], plot_vars, output_path=output_path) win, label = _show_image_toplevel( watch_data_level, output_path, t("dialog.pair_plots_title") ) if win is not None and label is not None: watch_data_level._pair_plot_win = win watch_data_level._pair_plot_label = label except Exception: pass def _apply_transform() -> None: if not isinstance(current_data[0], pd.DataFrame): return label = transform_var.get() tid = next((k for k, v in translated_transforms.items() if v == label), None) if not tid: return try: new_data = apply_transform(current_data[0], tid) _on_data_updated(new_data) except Exception as e: from tkinter import messagebox messagebox.showerror(t("dialog.data"), str(e)) def _apply_clean() -> None: if not isinstance(current_data[0], pd.DataFrame): return label = clean_var.get() cid = next((k for k, v in translated_cleans.items() if v == label), None) if not cid: return try: new_data = apply_cleaning(current_data[0], cid) _on_data_updated(new_data) except Exception as e: from tkinter import messagebox messagebox.showerror(t("dialog.data"), str(e)) def _open_save() -> None: if not isinstance(current_data[0], pd.DataFrame): return open_save_dialog( watch_data_level, current_data[0], on_focus_data=watch_data_level.focus_set, ) opts_frame = ttk.Frame(watch_data_level) opts_frame.pack(padx=UI_STYLE["padding"], pady=4, fill="x") is_dataframe = isinstance(current_data[0], pd.DataFrame) can_show_pairs = is_dataframe and len(getattr(current_data[0], "columns", [])) > 0 transform_var = StringVar() clean_var = StringVar() translated_transforms = { tid: t(f"data_analysis.transform_label_{tid}") for tid in TRANSFORM_OPTIONS } translated_cleans = { cid: t(f"data_analysis.clean_label_{cid}") for cid in CLEAN_OPTIONS } transform_values = list(translated_transforms.values()) clean_values = list(translated_cleans.values()) if transform_values: transform_var.set(transform_values[0]) if clean_values: clean_var.set(clean_values[0]) # Row 1: Show pair plots | Save updated data | Help row1 = ttk.Frame(opts_frame) row1.pack(anchor="w") if can_show_pairs: ttk.Button( row1, text=t("dialog.show_pair_plots"), command=_open_pair_plots_window, style="Primary.TButton", width=min(42, max(36, UI_STYLE["button_width_wide"] + 10)), ).pack(side="left", padx=(0, 4), pady=2) if is_dataframe: ttk.Button( row1, text=t("data_analysis.save_updated"), command=_open_save, width=26, ).pack(side="left", padx=2, pady=2) ttk.Button( row1, text=t("dialog.help_title"), command=lambda: show_data_view_help_dialog(watch_data_level), width=10, ).pack(side="left", padx=2, pady=2) # Row 2: Transform combobox | Transformar if is_dataframe: row2 = ttk.Frame(opts_frame) row2.pack(anchor="w") ttk.Combobox( row2, textvariable=transform_var, values=transform_values, state="readonly", width=28, font=get_entry_font(), ).pack(side="left", padx=(0, 4), pady=2) ttk.Button( row2, text=t("data_analysis.transform"), command=_apply_transform, style="Equation.TButton", width=12, ).pack(side="left", padx=2, pady=2) # Row 3: Clean combobox | Limpiar if is_dataframe: row3 = ttk.Frame(opts_frame) row3.pack(anchor="w") ttk.Combobox( row3, textvariable=clean_var, values=clean_values, state="readonly", width=28, font=get_entry_font(), ).pack(side="left", padx=(0, 4), pady=2) ttk.Button( row3, text=t("data_analysis.clean"), command=_apply_clean, style="Equation.TButton", width=12, ).pack(side="left", padx=2, pady=2) watch_data_level.accept_button = ttk.Button( watch_data_level, text=t("dialog.accept"), command=watch_data_level.destroy, style="Primary.TButton", width=UI_STYLE["button_width"], ) watch_data_level.accept_button.pack( padx=UI_STYLE["padding"], pady=UI_STYLE["padding"] ) bind_enter_to_accept( [text_widget, watch_data_level.accept_button], watch_data_level.destroy, ) watch_data_level.accept_button.focus_set() place_window_centered(watch_data_level, preserve_size=True) parent_window.wait_window(watch_data_level)