csv_2_plot: add support for plotting with relative time on the x-axis

Signed-off-by: YoungSoo Shin <shinys000114@gmail.com>
This commit is contained in:
2025-12-11 12:10:25 +09:00
parent 5a505a5205
commit c98e735410

View File

@@ -3,11 +3,12 @@ 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 from matplotlib.ticker import MultipleLocator, FuncFormatter
def plot_power_data(csv_path, output_path, plot_types, sources, def plot_power_data(csv_path, output_path, plot_types, sources,
voltage_y_max=None, current_y_max=None, power_y_max=None): voltage_y_max=None, current_y_max=None, power_y_max=None,
relative_time=False):
""" """
Reads power data from a CSV file and generates a plot image. Reads power data from a CSV file and generates a plot image.
@@ -21,16 +22,29 @@ def plot_power_data(csv_path, output_path, plot_types, sources,
voltage_y_max (float, optional): Maximum value for the voltage plot's Y-axis. voltage_y_max (float, optional): Maximum value for the voltage plot's Y-axis.
current_y_max (float, optional): Maximum value for the current plot's Y-axis. current_y_max (float, optional): Maximum value for the current plot's Y-axis.
power_y_max (float, optional): Maximum value for the power plot's Y-axis. power_y_max (float, optional): Maximum value for the power plot's Y-axis.
relative_time (bool): If True, the x-axis will show elapsed time from the start.
""" """
try: try:
# Read the CSV file into a pandas DataFrame # Read the CSV file into a pandas DataFrame
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 --- if df.empty:
local_tz = gettz() print("CSV file is empty. Exiting.")
df['timestamp'] = df['timestamp'].dt.tz_convert(local_tz) return
print(f"Timestamp converted to local timezone: {local_tz}")
# --- Time Handling ---
x_axis_data = df['timestamp']
if relative_time:
start_time = df['timestamp'].iloc[0]
df['elapsed_seconds'] = (df['timestamp'] - start_time).dt.total_seconds()
x_axis_data = df['elapsed_seconds']
print("X-axis set to relative time (elapsed seconds).")
else:
# --- Timezone Conversion for absolute time ---
local_tz = gettz()
df['timestamp'] = df['timestamp'].dt.tz_convert(local_tz)
print(f"Timestamp converted to local timezone: {local_tz}")
except FileNotFoundError: except FileNotFoundError:
print(f"Error: The file '{csv_path}' was not found.") print(f"Error: The file '{csv_path}' was not found.")
@@ -88,7 +102,7 @@ def plot_power_data(csv_path, output_path, plot_types, sources,
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], zorder=2) ax.plot(x_axis_data, df[col_name], label=channel_labels[j], color=channel_colors[j], zorder=2)
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
@@ -97,7 +111,6 @@ def plot_power_data(csv_path, output_path, plot_types, sources,
# --- Dynamic Y-axis Scaling --- # --- Dynamic Y-axis Scaling ---
ax.set_ylim(bottom=0) ax.set_ylim(bottom=0)
# Set y-axis max from options if provided
y_max_option = y_max_options.get(plot_type) y_max_option = y_max_options.get(plot_type)
if y_max_option is not None: if y_max_option is not None:
ax.set_ylim(top=y_max_option) ax.set_ylim(top=y_max_option)
@@ -112,25 +125,16 @@ def plot_power_data(csv_path, output_path, plot_types, sources,
# --- Grid and Tick Configuration --- # --- Grid and Tick Configuration ---
y_min, y_max = ax.get_ylim() y_min, y_max = ax.get_ylim()
if plot_type == 'current' and y_max <= 2.5: major_interval = 0.5
# Keep the dynamic major_interval logic for tick LABELS elif y_max <= 10: major_interval = 2
if plot_type == 'current' and y_max <= 2.5: elif y_max <= 25: major_interval = 5
major_interval = 0.5 else: major_interval = y_max / 5.0
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_major_locator(MultipleLocator(major_interval))
ax.yaxis.set_minor_locator(MultipleLocator(1)) 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(False, which='major')
ax.yaxis.grid(True, which='minor', linestyle='--', linewidth=0.6, zorder=0) 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): for y_val in range(int(y_min), int(y_max) + 1):
if y_val == 0: continue if y_val == 0: continue
if y_val % 10 == 0: if y_val % 10 == 0:
@@ -138,43 +142,49 @@ def plot_power_data(csv_path, output_path, plot_types, sources,
elif y_val % 5 == 0: elif y_val % 5 == 0:
ax.axhline(y=y_val, color='midnightblue', linestyle='--', linewidth=1.2, zorder=1) ax.axhline(y=y_val, color='midnightblue', linestyle='--', linewidth=1.2, zorder=1)
# Keep the x-axis grid
ax.xaxis.grid(True, which='major', linestyle='--', linewidth=0.8) ax.xaxis.grid(True, which='major', linestyle='--', linewidth=0.8)
# --- Formatting the x-axis (Time) --- # --- Formatting the x-axis ---
local_tz = gettz()
last_ax = axes[-1] last_ax = axes[-1]
if not df.empty: if not df.empty:
last_ax.set_xlim(df['timestamp'].iloc[0], df['timestamp'].iloc[-1]) last_ax.set_xlim(x_axis_data.iloc[0], x_axis_data.iloc[-1])
if relative_time:
last_ax.xaxis.set_major_locator(plt.MaxNLocator(15))
# Optional: Format to M:S if needed
# formatter = FuncFormatter(lambda s, x: f'{int(s//60)}:{int(s%60):02d}')
# last_ax.xaxis.set_major_formatter(formatter)
plt.xlabel('Elapsed Time (seconds)')
else:
local_tz = gettz()
last_ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S', tz=local_tz))
last_ax.xaxis.set_major_locator(plt.MaxNLocator(15))
plt.xlabel(f'Time ({local_tz.tzname(df["timestamp"].iloc[-1])})')
last_ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S', tz=local_tz))
last_ax.xaxis.set_major_locator(plt.MaxNLocator(15))
plt.xlabel(f'Time ({local_tz.tzname(df["timestamp"].iloc[-1])})')
plt.xticks(rotation=45) plt.xticks(rotation=45)
# --- Add a main title and subtitle --- # --- Add a main title and subtitle ---
start_time = df['timestamp'].iloc[0].strftime('%Y-%m-%d %H:%M:%S') if relative_time:
end_time = df['timestamp'].iloc[-1].strftime('%H:%M:%S') main_title = 'PowerMate Log'
main_title = f'PowerMate Log ({start_time} to {end_time})' else:
start_time_str = df['timestamp'].iloc[0].strftime('%Y-%m-%d %H:%M:%S')
end_time_str = df['timestamp'].iloc[-1].strftime('%H:%M:%S')
main_title = f'PowerMate Log ({start_time_str} to {end_time_str})'
subtitle_parts = [] subtitle_parts = []
if avg_interval_ms > 0: if avg_interval_ms > 0:
subtitle_parts.append(f'Avg. Interval: {avg_interval_ms:.2f} ms') 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()] voltage_strings = [f'{source.upper()} Avg: {avg_v:.2f} V' for source, avg_v in avg_voltages.items()]
if voltage_strings: if voltage_strings:
subtitle_parts.extend(voltage_strings) subtitle_parts.extend(voltage_strings)
subtitle = ' | '.join(subtitle_parts) subtitle = ' | '.join(subtitle_parts)
full_title = main_title full_title = main_title
if subtitle: if subtitle:
full_title += f'\n{subtitle}' full_title += f'\n{subtitle}'
fig.suptitle(full_title, fontsize=14) fig.suptitle(full_title, fontsize=14)
# Adjust layout to make space for the subtitle
plt.tight_layout(rect=[0, 0, 1, 0.98]) plt.tight_layout(rect=[0, 0, 1, 0.98])
# --- Save the plot to a file --- # --- Save the plot to a file ---
@@ -208,6 +218,11 @@ def main():
parser.add_argument("--voltage_y_max", type=float, help="Maximum value for the voltage plot's Y-axis.") parser.add_argument("--voltage_y_max", type=float, help="Maximum value for the voltage plot's Y-axis.")
parser.add_argument("--current_y_max", type=float, help="Maximum value for the current plot's Y-axis.") parser.add_argument("--current_y_max", type=float, help="Maximum value for the current plot's Y-axis.")
parser.add_argument("--power_y_max", type=float, help="Maximum value for the power plot's Y-axis.") parser.add_argument("--power_y_max", type=float, help="Maximum value for the power plot's Y-axis.")
parser.add_argument(
"-r", "--relative-time",
action='store_true',
help="Display the x-axis as elapsed time from the start (in seconds) instead of absolute time."
)
args = parser.parse_args() args = parser.parse_args()
@@ -218,7 +233,8 @@ def main():
args.source, args.source,
voltage_y_max=args.voltage_y_max, voltage_y_max=args.voltage_y_max,
current_y_max=args.current_y_max, current_y_max=args.current_y_max,
power_y_max=args.power_y_max power_y_max=args.power_y_max,
relative_time=args.relative_time
) )