csv_2_plot: improve plot configuration and add average voltage/interval calculations

Signed-off-by: YoungSoo Shin <shinys000114@gmail.com>
This commit is contained in:
2025-12-09 17:39:38 +09:00
parent 194474fdff
commit 8930a36eaf

View File

@@ -3,6 +3,7 @@ import matplotlib.dates as mdates
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pandas as pd import pandas as pd
from dateutil.tz import gettz from dateutil.tz import gettz
from matplotlib.ticker import MultipleLocator
def plot_power_data(csv_path, output_path, plot_types, sources): def plot_power_data(csv_path, output_path, plot_types, sources):
@@ -19,16 +20,11 @@ def plot_power_data(csv_path, output_path, plot_types, sources):
""" """
try: try:
# Read the CSV file into a pandas DataFrame # Read the CSV file into a pandas DataFrame
# The 'timestamp' column is parsed as dates. Pandas automatically recognizes
# the ISO format (with 'Z') as UTC.
df = pd.read_csv(csv_path, parse_dates=['timestamp']) df = pd.read_csv(csv_path, parse_dates=['timestamp'])
print(f"Successfully loaded {len(df)} records from '{csv_path}'") print(f"Successfully loaded {len(df)} records from '{csv_path}'")
# --- Timezone Conversion --- # --- Timezone Conversion ---
# Get the system's local timezone
local_tz = gettz() local_tz = gettz()
# The timestamp from CSV is already UTC-aware.
# Convert it to the system's local timezone for plotting.
df['timestamp'] = df['timestamp'].dt.tz_convert(local_tz) df['timestamp'] = df['timestamp'].dt.tz_convert(local_tz)
print(f"Timestamp converted to local timezone: {local_tz}") print(f"Timestamp converted to local timezone: {local_tz}")
@@ -39,24 +35,32 @@ def plot_power_data(csv_path, output_path, plot_types, sources):
print(f"An error occurred while reading the CSV file: {e}") print(f"An error occurred while reading the CSV file: {e}")
return return
# --- Calculate Average Interval ---
avg_interval_ms = 0
if len(df) > 1:
avg_interval = df['timestamp'].diff().mean()
avg_interval_ms = avg_interval.total_seconds() * 1000
# --- Calculate Average Voltages ---
avg_voltages = {}
for source in sources:
voltage_col = f'{source}_voltage'
if voltage_col in df.columns:
avg_voltages[source] = df[voltage_col].mean()
# --- Plotting Configuration --- # --- Plotting Configuration ---
# Y-axis scale settings from chart.js
scale_config = { scale_config = {
'power': {'steps': [5, 20, 50, 160]}, 'power': {'steps': [5, 20, 50, 160]},
'voltage': {'steps': [5, 10, 15, 25]}, 'voltage': {'steps': [5, 10, 15, 25]},
'current': {'steps': [1, 2.5, 5, 10]} 'current': {'steps': [1, 2.5, 5, 10]}
} }
plot_configs = { plot_configs = {
'power': {'title': 'Power Consumption', 'ylabel': 'Power (W)', 'power': {'title': 'Power Consumption', 'ylabel': 'Power (W)', 'cols': [f'{s}_power' for s in sources]},
'cols': [f'{s}_power' for s in sources]}, 'voltage': {'title': 'Voltage', 'ylabel': 'Voltage (V)', 'cols': [f'{s}_voltage' for s in sources]},
'voltage': {'title': 'Voltage', 'ylabel': 'Voltage (V)', 'current': {'title': 'Current', 'ylabel': 'Current (A)', 'cols': [f'{s}_current' for s in sources]}
'cols': [f'{s}_voltage' for s in sources]},
'current': {'title': 'Current', 'ylabel': 'Current (A)',
'cols': [f'{s}_current' for s in sources]}
} }
channel_labels = [s.upper() for s in sources] channel_labels = [s.upper() for s in sources]
# Define a color map for all possible sources
color_map = {'vin': 'red', 'main': 'green', 'usb': 'blue'} color_map = {'vin': 'red', 'main': 'green', 'usb': 'blue'}
channel_colors = [color_map[s] for s in sources] channel_colors = [color_map[s] for s in sources]
@@ -65,20 +69,17 @@ def plot_power_data(csv_path, output_path, plot_types, sources):
print("No plot types selected. Exiting.") print("No plot types selected. Exiting.")
return return
# Create a figure and a set of subplots based on the number of selected plot types. fig, axes = plt.subplots(num_plots, 1, figsize=(15, 9 * num_plots), sharex=True, squeeze=False)
fig, axes = plt.subplots(num_plots, 1, figsize=(15, 6 * num_plots), sharex=True, squeeze=False) axes = axes.flatten()
axes = axes.flatten() # Flatten the 2D array to 1D for easier iteration
# --- Loop through selected plot types and generate plots --- # --- Loop through selected plot types and generate plots ---
for i, plot_type in enumerate(plot_types): for i, plot_type in enumerate(plot_types):
ax = axes[i] ax = axes[i]
config = plot_configs[plot_type] config = plot_configs[plot_type]
max_data_value = 0 max_data_value = 0
for j, col_name in enumerate(config['cols']): for j, col_name in enumerate(config['cols']):
if col_name in df.columns: if col_name in df.columns:
ax.plot(df['timestamp'], df[col_name], label=channel_labels[j], color=channel_colors[j]) ax.plot(df['timestamp'], df[col_name], label=channel_labels[j], color=channel_colors[j], zorder=2)
# Find the maximum value in the current column to set the y-axis limit
max_col_value = df[col_name].max() max_col_value = df[col_name].max()
if max_col_value > max_data_value: if max_col_value > max_data_value:
max_data_value = max_col_value max_data_value = max_col_value
@@ -86,34 +87,82 @@ def plot_power_data(csv_path, output_path, plot_types, sources):
print(f"Warning: Column '{col_name}' not found in CSV. Skipping.") print(f"Warning: Column '{col_name}' not found in CSV. Skipping.")
# --- Dynamic Y-axis Scaling --- # --- Dynamic Y-axis Scaling ---
ax.set_ylim(bottom=0) # Set y-axis minimum to 0 ax.set_ylim(bottom=0)
if plot_type in scale_config: if plot_type in scale_config:
steps = scale_config[plot_type]['steps'] steps = scale_config[plot_type]['steps']
# Find the smallest step that is >= max_data_value
new_max = next((step for step in steps if step >= max_data_value), steps[-1]) new_max = next((step for step in steps if step >= max_data_value), steps[-1])
ax.set_ylim(top=new_max) ax.set_ylim(top=new_max)
ax.set_title(config['title']) ax.set_title(config['title'])
ax.set_ylabel(config['ylabel']) ax.set_ylabel(config['ylabel'])
ax.legend() ax.legend()
ax.grid(True, which='both', linestyle='--', linewidth=0.5)
# --- Grid and Tick Configuration ---
y_min, y_max = ax.get_ylim()
# Keep the dynamic major_interval logic for tick LABELS
if plot_type == 'current' and y_max <= 2.5:
major_interval = 0.5
elif y_max <= 10:
major_interval = 2
elif y_max <= 25:
major_interval = 5
else:
major_interval = y_max / 5.0
ax.yaxis.set_major_locator(MultipleLocator(major_interval))
ax.yaxis.set_minor_locator(MultipleLocator(1))
# Disable the default major grid, but keep the minor one
ax.yaxis.grid(False, which='major')
ax.yaxis.grid(True, which='minor', linestyle='--', linewidth=0.6, zorder=0)
# Draw custom lines for 5 and 10 multiples, which are now the only major grid lines
for y_val in range(int(y_min), int(y_max) + 1):
if y_val == 0: continue
if y_val % 10 == 0:
ax.axhline(y=y_val, color='green', linestyle='-', linewidth=1.0, zorder=1)
elif y_val % 5 == 0:
ax.axhline(y=y_val, color='red', linestyle='-', linewidth=1.0, zorder=1)
# Keep the x-axis grid
ax.xaxis.grid(True, which='major', linestyle='--', linewidth=0.8)
# --- Formatting the x-axis (Time) --- # --- Formatting the x-axis (Time) ---
local_tz = gettz() local_tz = gettz()
last_ax = axes[-1] last_ax = axes[-1]
# Pass the timezone to the formatter
if not df.empty:
last_ax.set_xlim(df['timestamp'].iloc[0], df['timestamp'].iloc[-1])
last_ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S', tz=local_tz)) last_ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S', tz=local_tz))
last_ax.xaxis.set_major_locator(plt.MaxNLocator(15)) # Limit the number of ticks last_ax.xaxis.set_major_locator(plt.MaxNLocator(15))
plt.xlabel(f'Time ({local_tz.tzname(df["timestamp"].iloc[-1])})') # Display timezone name plt.xlabel(f'Time ({local_tz.tzname(df["timestamp"].iloc[-1])})')
plt.xticks(rotation=45) plt.xticks(rotation=45)
# Add a main title to the figure # --- Add a main title and subtitle ---
start_time = df['timestamp'].iloc[0].strftime('%Y-%m-%d %H:%M:%S') start_time = df['timestamp'].iloc[0].strftime('%Y-%m-%d %H:%M:%S')
end_time = df['timestamp'].iloc[-1].strftime('%H:%M:%S') end_time = df['timestamp'].iloc[-1].strftime('%H:%M:%S')
fig.suptitle(f'PowerMate Log ({start_time} to {end_time})', fontsize=16, y=0.95) main_title = f'PowerMate Log ({start_time} to {end_time})'
# Adjust layout to prevent titles/labels from overlapping subtitle_parts = []
plt.tight_layout(rect=[0, 0, 1, 0.94]) if avg_interval_ms > 0:
subtitle_parts.append(f'Avg. Interval: {avg_interval_ms:.2f} ms')
voltage_strings = [f'{source.upper()} Avg: {avg_v:.2f} V' for source, avg_v in avg_voltages.items()]
if voltage_strings:
subtitle_parts.extend(voltage_strings)
subtitle = ' | '.join(subtitle_parts)
full_title = main_title
if subtitle:
full_title += f'\n{subtitle}'
fig.suptitle(full_title, fontsize=14)
# Adjust layout to make space for the subtitle
plt.tight_layout(rect=[0, 0, 1, 0.93])
# --- Save the plot to a file --- # --- Save the plot to a file ---
try: try: