From 2be825d6a2fbcbd8aba3f265ab5d238cec577446 Mon Sep 17 00:00:00 2001 From: Andre Schwarb Date: Mon, 7 Sep 2026 12:58:43 +0200 Subject: [PATCH] initial commit --- Python/plotScopeReworked.py | 343 ++++++++++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 Python/plotScopeReworked.py diff --git a/Python/plotScopeReworked.py b/Python/plotScopeReworked.py new file mode 100644 index 0000000..e04f2a8 --- /dev/null +++ b/Python/plotScopeReworked.py @@ -0,0 +1,343 @@ +import os +import sys + +import pandas as pd +import plotly.graph_objects as go +import tkinter as tk +from plotly.subplots import make_subplots +from tkinter import filedialog, messagebox + +# -------------------------------------------------- +# Konstanten +# -------------------------------------------------- +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +REPO_ROOT = os.path.dirname(SCRIPT_DIR) +DEFAULT_CSV = os.path.join( + REPO_ROOT, "ScopeData", "Scope YT NC Project-10_Platten_reworked.csv" +) + +TIME_COL = "Time [ms]" + + +# -------------------------------------------------- +# CSV einlesen +# -------------------------------------------------- +def load_reworked_csv(path): + """Liest den 'reworked' Scope-Export (Semikolon-getrennt) ein. + + Gibt (kurzer_name, DataFrame) zurueck. Zeilen mit EOF-Markern und + Zeilen ohne Zeitwert werden verworfen, leere Signalzellen werden NaN. + """ + df = pd.read_csv(path, sep=";", low_memory=False) + df.columns = [str(c).strip() for c in df.columns] + + if TIME_COL not in df.columns: + raise ValueError( + f"Spalte '{TIME_COL}' nicht gefunden in {os.path.basename(path)}" + ) + + # Nur Zeilen mit numerischem Zeitwert behalten (filtert z.B. 'EOF'-Zeile) + df[TIME_COL] = pd.to_numeric(df[TIME_COL], errors="coerce") + df = df.dropna(subset=[TIME_COL]).sort_values(TIME_COL).reset_index(drop=True) + + # Signale numerisch konvertieren (leere Zellen -> NaN) + for col in df.columns: + if col != TIME_COL: + df[col] = pd.to_numeric(df[col], errors="coerce") + + if df.empty: + raise ValueError(f"Keine Daten in {path}") + + short_name = os.path.basename(path).replace(".csv", "") + return short_name, df + + +def signal_columns(df): + """Alle Spalten, die als Signal geplotet werden koennen. + + Zeitspalten (z.B. 'Time [ms]' oder 'Current Time [ms]') werden + als Signale ausgeschlossen. + """ + return [ + col + for col in df.columns + if col != TIME_COL and "time [" not in col.lower() + ] + +# -------------------------------------------------- +# Plot erstellen +# -------------------------------------------------- +def create_plot(): + files = file_list.get(0, "end") + if not files: + messagebox.showwarning("Hinweis", "Mindestens eine Datei laden.") + return + + selected = [name for name, var in signal_vars.items() if var.get()] + if not selected: + messagebox.showwarning("Hinweis", "Mindestens ein Signal auswählen.") + return + + # X-Range (Zoom) + x_range = None + try: + xmin = xmin_entry.get().strip() + xmax = xmax_entry.get().strip() + if xmin and xmax: + x_range = [float(xmin), float(xmax)] + elif xmax: + x_range = [0.0, float(xmax)] + except ValueError: + messagebox.showerror("Fehler", "Ungültige X-Achsen Werte") + return + + # Y-Range + if auto_scale_var.get(): + y_range = None + else: + try: + y_range = [float(ymin_entry.get()), float(ymax_entry.get())] + except ValueError: + messagebox.showerror("Fehler", "Ungültige Y-Achsen Werte") + return + + subplot_mode = layout_var.get() == "subplots" + n_rows = len(selected) if subplot_mode else 1 + + fig = make_subplots( + rows=n_rows, + cols=1, + shared_xaxes=True, + vertical_spacing=0.06 if n_rows > 1 else None, + subplot_titles=selected if subplot_mode else None, + ) + + for i, signal in enumerate(selected): + row = i + 1 if subplot_mode else 1 + + for path in files: + short_name, df = file_data[path] + fig.add_trace( + go.Scatter( + x=df[TIME_COL], + y=df[signal], + mode="lines", + name=f"{short_name}: {signal}", + legendgroup=signal, + showlegend=True, + ), + row=row, + col=1, + ) + + if y_range is not None: + fig.update_yaxes(range=y_range, row=row, col=1) + + fig.update_yaxes(title_text=signal, row=row, col=1) + + fig.update_layout( + legend=dict(orientation="h", yanchor="bottom", y=1.02) + if subplot_mode + else dict(orientation="v", yanchor="top"), + title="Scope YT NC – Platten (reworked)", + xaxis_title="Zeit [ms]", + xaxis_range=x_range, + height=360 * n_rows + 120, + hovermode="x unified", + template="plotly_white", + ) + + fig.show() +# -------------------------------------------------- +# Datei-Liste (GUI) +# -------------------------------------------------- +def add_file(): + paths = filedialog.askopenfilenames( + title="Scope CSV(s) auswählen", + initialdir=os.path.dirname(DEFAULT_CSV), + filetypes=[("CSV Dateien", "*.csv"), ("Alle Dateien", "*.*")], + ) + for path in paths: + path = os.path.abspath(path) + if path not in file_list.get(0, "end"): + try: + short_name, df = load_reworked_csv(path) + file_data[path] = (short_name, df) + file_list.insert(tk.END, path) + update_signal_checkboxes() + except Exception as e: + messagebox.showerror("Fehler", f"{path}\n\n{e}") + + +def remove_file(): + selection = file_list.curselection() + if not selection: + return + # von unten nach oben entfernen, damit Indizes stabil bleiben + for idx in reversed(selection): + path = file_list.get(idx) + file_data.pop(path, None) + file_list.delete(idx) + if not file_list.get(0, "end"): + for var in signal_vars.values(): + var.set(False) + + +def update_signal_checkboxes(): + """Signalliste aus allen geladenen Dateien aktualisieren (Vereinigung).""" + union = [] + for _, df in file_data.values(): + for col in signal_columns(df): + if col not in union: + union.append(col) + rebuild_signal_checkboxes(union) + + +def rebuild_signal_checkboxes(columns): + for widget in signal_frame.winfo_children(): + widget.destroy() + + signal_vars.clear() + for col in columns: + var = tk.BooleanVar(value=True) + tk.Checkbutton(signal_frame, text=col, variable=var).pack(anchor="w") + signal_vars[col] = var +# -------------------------------------------------- +# Hauptprogramm +# -------------------------------------------------- +def main(): + global file_list, file_data, signal_frame, signal_vars + global layout_var, auto_scale_var + global ymin_entry, ymax_entry, xmin_entry, xmax_entry + + # Kommandozeilen-Dateipfade (optional) + cli_files = sys.argv[1:] + + root = tk.Tk() + root.title("Scope Visualizer – YT NC Project (reworked)") + root.geometry("480x560") + + # --- Dateien --- + tk.Label(root, text="Dateien:").pack(anchor="w", padx=8, pady=(8, 0)) + + file_frame = tk.Frame(root) + file_frame.pack(fill="x", padx=8) + + file_list = tk.Listbox(file_frame, height=4) + file_list.pack(side="left", fill="both", expand=True) + + file_buttons = tk.Frame(file_frame) + file_buttons.pack(side="left", padx=(6, 0)) + + tk.Button( + file_buttons, text="Hinzufügen", width=12, command=add_file + ).pack(pady=2) + tk.Button( + file_buttons, text="Entfernen", width=12, command=remove_file + ).pack(pady=2) + + # --- Signale --- + tk.Label(root, text="Signale:").pack(anchor="w", padx=8, pady=(8, 0)) + + signal_scroll = tk.Frame(root) + signal_scroll.pack(fill="both", expand=True, padx=8) + + signal_canvas = tk.Canvas(signal_scroll, height=140, highlightthickness=0) + signal_scrollbar = tk.Scrollbar( + signal_scroll, orient="vertical", command=signal_canvas.yview + ) + signal_canvas.configure(yscrollcommand=signal_scrollbar.set) + + signal_scrollbar.pack(side="right", fill="y") + signal_canvas.pack(side="left", fill="both", expand=True) + + signal_frame = tk.Frame(signal_canvas) + canvas_window = signal_canvas.create_window( + (0, 0), window=signal_frame, anchor="nw" + ) + signal_frame.bind( + "", + lambda e: signal_canvas.configure(scrollregion=signal_canvas.bbox("all")), + ) + signal_canvas.bind( + "", + lambda e: signal_canvas.itemconfigure(canvas_window, width=e.width), + ) + signal_canvas.bind_all( + "", + lambda e: signal_canvas.yview_scroll(-1 * (e.delta // 120), "units"), + ) + + # --- Anzeige-Optionen --- + options_frame = tk.LabelFrame(root, text="Anzeige") + options_frame.pack(fill="x", padx=8, pady=8) + + layout_var = tk.StringVar(value="subplots") + tk.Radiobutton( + options_frame, + text="Ein Plot pro Signal (Subplots)", + variable=layout_var, + value="subplots", + ).pack(anchor="w", padx=6, pady=2) + tk.Radiobutton( + options_frame, + text="Alle Signale überlagern", + variable=layout_var, + value="overlay", + ).pack(anchor="w", padx=6, pady=2) + + auto_scale_var = tk.BooleanVar(value=True) + tk.Checkbutton( + options_frame, text="Automatische Y-Skalierung", variable=auto_scale_var + ).pack(anchor="w", padx=6) + + y_frame = tk.Frame(options_frame) + y_frame.pack(fill="x", padx=6, pady=2) + tk.Label(y_frame, text="Y-Min").pack(side="left") + ymin_entry = tk.Entry(y_frame, width=10) + ymin_entry.pack(side="left", padx=(2, 12)) + tk.Label(y_frame, text="Y-Max").pack(side="left") + ymax_entry = tk.Entry(y_frame, width=10) + ymax_entry.pack(side="left", padx=2) + + x_frame = tk.Frame(options_frame) + x_frame.pack(fill="x", padx=6, pady=2) + tk.Label(x_frame, text="X-Min [ms]").pack(side="left") + xmin_entry = tk.Entry(x_frame, width=10) + xmin_entry.pack(side="left", padx=(2, 12)) + tk.Label(x_frame, text="X-Max [ms]").pack(side="left") + xmax_entry = tk.Entry(x_frame, width=10) + xmax_entry.pack(side="left", padx=2) + + tk.Button( + root, text="Plot anzeigen", command=create_plot, padx=20 + ).pack(pady=8) + + # --- Startdaten laden --- + file_data = {} + + initial = ( + cli_files + if cli_files + else ([DEFAULT_CSV] if os.path.exists(DEFAULT_CSV) else []) + ) + for path in initial: + path = os.path.abspath(path) + try: + short_name, df = load_reworked_csv(path) + file_data[path] = (short_name, df) + file_list.insert(tk.END, path) + except Exception as e: + messagebox.showerror("Fehler", f"{path}\n\n{e}") + + signal_vars = {} + if file_data: + update_signal_checkboxes() + + root.mainloop() + + +if __name__ == "__main__": + main() + +