Open the CSV Find the first line where the first cell starts with "Cycle" Use that entire line as the plot title (Chat)
230 lines
8.8 KiB
Python
230 lines
8.8 KiB
Python
# -*- 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
|
||
|
||
self.graph_title = "Battery Test Analysis" # Default title
|
||
with open(filepath, 'r') as f:
|
||
reader = csv.reader(f)
|
||
for row in reader:
|
||
if row and row[0].startswith("Cycle"):
|
||
self.graph_title = ", ".join(row).strip()
|
||
break
|
||
|
||
try:
|
||
# First detect problematic lines
|
||
self.status_var.set("Datei geladen – prüfe Zeilen…")
|
||
self.root.update_idletasks()
|
||
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")
|
||
self.root.update_idletasks()
|
||
return
|
||
|
||
# Now read only valid lines
|
||
self.status_var.set("Verarbeite gültige Zeile...")
|
||
self.root.update_idletasks()
|
||
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"Working with: {len(self.df)} valid measurements...")
|
||
self.root.update_idletasks()
|
||
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)
|
||
#self.ax.margins(x=0)
|
||
|
||
# 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(getattr(self, 'graph_title'))
|
||
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() |