344 lines
10 KiB
Python
344 lines
10 KiB
Python
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()
|
||
|
||
|