initial commit

This commit is contained in:
2026-09-07 12:58:43 +02:00
parent 69b4e899e5
commit 2be825d6a2
+343
View File
@@ -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(
"<Configure>",
lambda e: signal_canvas.configure(scrollregion=signal_canvas.bbox("all")),
)
signal_canvas.bind(
"<Configure>",
lambda e: signal_canvas.itemconfigure(canvas_window, width=e.width),
)
signal_canvas.bind_all(
"<MouseWheel>",
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()