From 452ec10f85ef4751fa682900c591f4da312b2c70 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 26 May 2025 20:45:26 +0200 Subject: [PATCH] =?UTF-8?q?CSVVisualizer.py=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deepseek --- CSVVisualizer.py | 215 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 CSVVisualizer.py diff --git a/CSVVisualizer.py b/CSVVisualizer.py new file mode 100644 index 0000000..bc981a4 --- /dev/null +++ b/CSVVisualizer.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +import os +import csv +import pandas as pd +import matplotlib.pyplot as plt +import matplotlib.patches as mpatches +from tkinter import Tk, filedialog, messagebox +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import tkinter as tk +from tkinter import ttk + +class CSVVisualizer: + def __init__(self, root): + self.root = root + self.root.protocol("WM_DELETE_WINDOW", self.cleanup) + self.root.title("CSV to Graph Converter") + self.root.geometry("1000x700") + + # Farben für die Phasen + self.phase_colors = { + "Charge": "#4E79A7", # Blau + "Discharge": "#E15759", # Rot + "Resting (Post-Charge)": "#59A14F", # Grün + "Resting (Post-Discharge)": "#EDC948", # Gelb + "Resting Between Cycles": "#B07AA1", # Lila + "Initial Discharge": "#FF9DA7" # Rosa + } + + self.setup_ui() + + def setup_ui(self): + """Erstellt die Benutzeroberfläche""" + main_frame = ttk.Frame(self.root) + main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # Steuerleiste oben + control_frame = ttk.Frame(main_frame) + control_frame.pack(fill=tk.X, pady=(0, 10)) + + ttk.Button(control_frame, text="CSV auswählen", command=self.load_csv).pack(side=tk.LEFT, padx=5) + ttk.Button(control_frame, text="Grafik speichern", command=self.save_plot).pack(side=tk.LEFT, padx=5) + + # Anzeige des aktuellen Dateipfads + self.file_label = ttk.Label(control_frame, text="Keine Datei ausgewählt") + self.file_label.pack(side=tk.LEFT, padx=10, expand=True) + + # Plot-Bereich + self.plot_frame = ttk.Frame(main_frame) + self.plot_frame.pack(fill=tk.BOTH, expand=True) + + # Platzhalter für den Plot + self.fig, self.ax = plt.subplots(figsize=(10, 5)) + self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_frame) + self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) + + # Statusleiste + self.status_var = tk.StringVar() + self.status_var.set("Bereit") + ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN).pack(fill=tk.X, pady=(5, 0)) + + def load_csv(self): + """Load CSV file with robust error handling for Raspberry Pi""" + filepath = filedialog.askopenfilename( + title="Select CSV File", + filetypes=[("CSV Files", "*.csv"), ("All Files", "*.*")] + ) + + if not filepath: + return + + try: + # First detect problematic lines + good_lines = [] + with open(filepath, 'r') as f: + reader = csv.reader(f) + headers = next(reader) # Keep header + + for i, row in enumerate(reader): + # Skip summary lines and malformed rows + if not row or len(row) < 4 or row[0].startswith('Cycle'): + continue + + # Validate numeric columns + try: + float(row[0]) # Time(s) + float(row[1]) # Voltage(V) + float(row[2]) # Current(A) + good_lines.append(i+1) # +1 to account for header + except ValueError: + continue + if not good_lines: + messagebox.showwarning("Warnung", "Keine gültigen Datenzeilen gefunden.") + self.status_var.set("Keine gültigen Daten") + return + + # Now read only valid lines + self.df = pd.read_csv( + filepath, + skiprows=lambda x: x not in good_lines and x != 0, # keep header + dtype={ + 'Time(s)': 'float32', + 'Voltage(V)': 'float32', + 'Current(A)': 'float32', + 'Phase': 'category', + 'Discharge_Capacity(Ah)': 'float32', + 'Charge_Capacity(Ah)': 'float32', + 'Coulomb_Eff(%)': 'float32', + 'Cycle': 'int32' + }, + engine='c', + memory_map=True + ) + + self.file_label.config(text=os.path.basename(filepath)) + self.status_var.set(f"Loaded: {len(self.df)} valid measurements") + self.update_plot() + + except Exception as e: + messagebox.showerror("Error", f"Failed to load file:\n{str(e)}") + self.status_var.set("Error loading file") + + def update_plot(self): + """Aktualisiert den Plot mit den aktuellen Daten""" + if not hasattr(self, 'df'): + return + + df_clean = self.df.copy() + + # Zeit relativ zum Start berechnen + start_time = df_clean["Time(s)"].min() + df_clean["Relative_Time(s)"] = df_clean["Time(s)"] - start_time + + # Plot zurücksetzen + self.ax.clear() + + # Spannung plotten + self.ax.plot(df_clean["Relative_Time(s)"], df_clean["Voltage(V)"], + label="Voltage (V)", color="black", linewidth=1.5) + + # Phasen als farbige Hintergründe + start_idx = 0 + for i in range(1, len(df_clean)): + if df_clean.iloc[i]["Phase"] != df_clean.iloc[i-1]["Phase"] or i == len(df_clean) - 1: + end_idx = i + start_time_rel = df_clean.iloc[start_idx]["Relative_Time(s)"] + end_time_rel = df_clean.iloc[end_idx]["Relative_Time(s)"] + phase = df_clean.iloc[start_idx]["Phase"] + + # Verwende Standardfarbe falls Phase nicht definiert ist + color = self.phase_colors.get(phase, "#CCCCCC") + self.ax.axvspan(start_time_rel, end_time_rel, facecolor=color, alpha=0.3) + start_idx = i + + # Legende erstellen + patches = [mpatches.Patch(color=self.phase_colors[phase], label=phase) + for phase in self.phase_colors if phase in df_clean["Phase"].unique()] + + # Füge Spannungs-Linie zur Legende hinzu + patches.append(plt.Line2D([0], [0], color='black', label='Voltage (V)')) + + self.ax.legend(handles=patches, loc="upper right") + self.ax.set_xlabel("Time (s) since start") + self.ax.set_ylabel("Voltage (V)") + self.ax.set_title("Battery Test Analysis") + self.ax.grid(True) + + # Aktualisiere den Canvas + self.canvas.draw() + self.status_var.set("Grafik aktualisiert") + + def save_plot(self): + """Speichert den aktuellen Plot als Bilddatei""" + if not hasattr(self, 'df'): + messagebox.showwarning("Warnung", "Keine Daten zum Speichern vorhanden") + return + + filetypes = [ + ('PNG Image', '*.png'), + ('JPEG Image', '*.jpg'), + ('PDF Document', '*.pdf'), + ('SVG Vector', '*.svg') + ] + + default_filename = "battery_test_plot.png" + if hasattr(self, 'file_label'): + base = os.path.splitext(self.file_label.cget("text"))[0] + if base: + default_filename = f"{base}_plot.png" + + filepath = filedialog.asksaveasfilename( + title="Grafik speichern", + initialfile=default_filename, + filetypes=filetypes, + defaultextension=".png" + ) + + if filepath: + try: + self.fig.savefig(filepath, dpi=300, bbox_inches='tight') + self.status_var.set(f"Grafik gespeichert als: {os.path.basename(filepath)}") + messagebox.showinfo("Erfolg", "Grafik wurde erfolgreich gespeichert") + except Exception as e: + messagebox.showerror("Fehler", f"Konnte Grafik nicht speichern:\n{str(e)}") + self.status_var.set("Fehler beim Speichern") + + def cleanup(self): + """Perform cleanup before closing""" + if hasattr(self, 'fig'): + plt.close(self.fig) + self.root.destroy() + +if __name__ == "__main__": + root = Tk() + app = CSVVisualizer(root) + root.mainloop() \ No newline at end of file