You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
767 lines
23 KiB
767 lines
23 KiB
#!/usr/bin/env python3
|
|
"""
|
|
Graph Core - vzug-e-hinge
|
|
==========================
|
|
Pure plotting functions for telemetry data visualization.
|
|
|
|
Database-agnostic - works with any data source via table_query adapters.
|
|
|
|
Features:
|
|
- Matplotlib plotting functions
|
|
- Overlay, subplots, drift comparison, multi-series
|
|
- XY scatter plots
|
|
- Export PNG and CSV
|
|
- No database dependency (uses TelemetryData from table_query)
|
|
|
|
Author: Kynsight
|
|
Version: 2.0.0
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
from typing import List, Dict, Tuple, Optional, Any
|
|
from dataclasses import dataclass
|
|
|
|
import matplotlib
|
|
matplotlib.use('QtAgg') # For PyQt6 integration
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.figure import Figure
|
|
import numpy as np
|
|
|
|
# Import data structures from table_query
|
|
from graph_table_query import TelemetryData, get_column_label
|
|
|
|
|
|
# =============================================================================
|
|
# Plot Configuration
|
|
# =============================================================================
|
|
|
|
@dataclass
|
|
class PlotConfig:
|
|
"""Plot configuration."""
|
|
title: str = "Telemetry Data"
|
|
xlabel: str = "Time (s)"
|
|
figsize: Tuple[int, int] = (16, 10) # Larger default size for better display
|
|
dpi: int = 100
|
|
grid: bool = True
|
|
legend: bool = True
|
|
style: str = "default" # matplotlib style
|
|
linestyle: str = "-" # Line style
|
|
marker: Optional[str] = None # Marker style
|
|
markersize: int = 3 # Marker size
|
|
# Font sizes (smaller for Raspberry Pi displays)
|
|
title_fontsize: int = 10
|
|
label_fontsize: int = 8
|
|
tick_fontsize: int = 7
|
|
legend_fontsize: int = 7
|
|
|
|
|
|
# =============================================================================
|
|
# Plotting Functions
|
|
# =============================================================================
|
|
|
|
def plot_overlay(
|
|
data_list: List[TelemetryData],
|
|
x_column: str,
|
|
y_column: str,
|
|
xlabel: str,
|
|
ylabel: str,
|
|
config: Optional[PlotConfig] = None
|
|
) -> Figure:
|
|
"""
|
|
Create overlay plot (multiple runs on same axes).
|
|
|
|
Args:
|
|
data_list: List of TelemetryData objects
|
|
x_column: Column name for X-axis (e.g., 'time_ms', 't_ns')
|
|
y_column: Column name for Y-axis (e.g., 'pwm', 'motor_current')
|
|
xlabel: X-axis label
|
|
ylabel: Y-axis label
|
|
config: Plot configuration
|
|
|
|
Returns:
|
|
Matplotlib Figure object
|
|
"""
|
|
if config is None:
|
|
config = PlotConfig()
|
|
|
|
fig, ax = plt.subplots(figsize=config.figsize, dpi=config.dpi)
|
|
|
|
# Set color cycle
|
|
ax.set_prop_cycle(color=plt.cm.tab10.colors)
|
|
|
|
# Plot each run
|
|
for data in data_list:
|
|
x_data = getattr(data, x_column, None)
|
|
y_data = getattr(data, y_column, None)
|
|
|
|
if x_data is None or y_data is None:
|
|
continue
|
|
|
|
# Label format: "session_name (run_no) [command] - Data"
|
|
label = f"{data.session_name} ({data.run_no}) [{data.command_name}] - {ylabel}"
|
|
ax.plot(x_data, y_data, label=label, alpha=0.8,
|
|
linestyle=config.linestyle, marker=config.marker,
|
|
markersize=config.markersize)
|
|
|
|
# Formatting
|
|
ax.set_xlabel(xlabel, fontsize=config.label_fontsize)
|
|
ax.set_ylabel(ylabel, fontsize=config.label_fontsize)
|
|
ax.set_title(config.title, fontsize=config.title_fontsize)
|
|
ax.tick_params(axis='both', labelsize=config.tick_fontsize)
|
|
|
|
if config.grid:
|
|
ax.grid(True, alpha=0.3)
|
|
|
|
if config.legend:
|
|
# Only add legend if there are labeled artists
|
|
handles, labels = ax.get_legend_handles_labels()
|
|
if handles and labels:
|
|
# Legend at bottom, outside plot
|
|
ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.12), ncol=2, fontsize=config.legend_fontsize)
|
|
fig.tight_layout(rect=[0, 0.08, 1, 1]) # Leave space for legend
|
|
else:
|
|
fig.tight_layout()
|
|
else:
|
|
fig.tight_layout()
|
|
|
|
return fig
|
|
|
|
|
|
def plot_subplots(
|
|
data_list: List[TelemetryData],
|
|
x_column: str,
|
|
y_column: str,
|
|
xlabel: str,
|
|
ylabel: str,
|
|
config: Optional[PlotConfig] = None
|
|
) -> Figure:
|
|
"""
|
|
Create subplot grid (one subplot per run).
|
|
|
|
Args:
|
|
data_list: List of TelemetryData objects
|
|
x_column: Column name for X-axis
|
|
y_column: Column name for Y-axis
|
|
xlabel: X-axis label
|
|
ylabel: Y-axis label
|
|
config: Plot configuration
|
|
|
|
Returns:
|
|
Matplotlib Figure object
|
|
"""
|
|
if config is None:
|
|
config = PlotConfig()
|
|
|
|
n_runs = len(data_list)
|
|
|
|
# Calculate grid dimensions
|
|
n_cols = min(2, n_runs)
|
|
n_rows = (n_runs + n_cols - 1) // n_cols
|
|
|
|
fig, axes = plt.subplots(
|
|
n_rows, n_cols,
|
|
figsize=config.figsize,
|
|
dpi=config.dpi,
|
|
squeeze=False
|
|
)
|
|
|
|
# Flatten axes for easy iteration
|
|
axes_flat = axes.flatten()
|
|
|
|
# Plot each run
|
|
for idx, data in enumerate(data_list):
|
|
ax = axes_flat[idx]
|
|
x_data = getattr(data, x_column, None)
|
|
y_data = getattr(data, y_column, None)
|
|
|
|
if x_data is None or y_data is None:
|
|
ax.text(0.5, 0.5, 'No data', ha='center', va='center')
|
|
ax.set_title(f"{data.session_name} ({data.run_no}) [{data.command_name}]")
|
|
continue
|
|
|
|
ax.plot(x_data, y_data, alpha=0.8,
|
|
linestyle=config.linestyle, marker=config.marker,
|
|
markersize=config.markersize)
|
|
ax.set_xlabel(xlabel, fontsize=config.label_fontsize)
|
|
ax.set_ylabel(ylabel, fontsize=config.label_fontsize)
|
|
# Title format: "session_name (run_no) [command]"
|
|
ax.set_title(f"{data.session_name} ({data.run_no}) [{data.command_name}]", fontsize=config.title_fontsize)
|
|
ax.tick_params(axis='both', labelsize=config.tick_fontsize)
|
|
|
|
if config.grid:
|
|
ax.grid(True, alpha=0.3)
|
|
|
|
# Hide unused subplots
|
|
for idx in range(len(data_list), len(axes_flat)):
|
|
axes_flat[idx].set_visible(False)
|
|
|
|
fig.tight_layout()
|
|
|
|
return fig
|
|
|
|
|
|
def plot_comparison(
|
|
data_list: List[TelemetryData],
|
|
x_column: str,
|
|
y_column: str,
|
|
xlabel: str,
|
|
ylabel: str,
|
|
reference_index: int = 0,
|
|
config: Optional[PlotConfig] = None
|
|
) -> Figure:
|
|
"""
|
|
Create drift comparison plot (deviation from reference run).
|
|
|
|
Args:
|
|
data_list: List of TelemetryData objects
|
|
x_column: Column name for X-axis
|
|
y_column: Column name for Y-axis
|
|
xlabel: X-axis label
|
|
ylabel: Y-axis label
|
|
reference_index: Index of reference run (default: 0 = first run)
|
|
config: Plot configuration
|
|
|
|
Returns:
|
|
Matplotlib Figure object
|
|
"""
|
|
if config is None:
|
|
config = PlotConfig()
|
|
|
|
if reference_index >= len(data_list):
|
|
reference_index = 0
|
|
|
|
fig, ax = plt.subplots(figsize=config.figsize, dpi=config.dpi)
|
|
|
|
reference = data_list[reference_index]
|
|
ref_x = getattr(reference, x_column, None)
|
|
ref_y = getattr(reference, y_column, None)
|
|
|
|
if ref_x is None or ref_y is None:
|
|
ax.text(0.5, 0.5, 'No reference data', ha='center', va='center')
|
|
return fig
|
|
|
|
# Plot reference as baseline (zero)
|
|
ax.axhline(y=0, color='black', linestyle='--',
|
|
# Label format: "session_name (run_no) [command] - Data"
|
|
label=f'Reference: {reference.session_name} ({reference.run_no}) [{reference.command_name}]')
|
|
|
|
# Set color cycle
|
|
ax.set_prop_cycle(color=plt.cm.tab10.colors)
|
|
|
|
# Plot deviations
|
|
for idx, data in enumerate(data_list):
|
|
if idx == reference_index:
|
|
continue
|
|
|
|
x_data = getattr(data, x_column, None)
|
|
y_data = getattr(data, y_column, None)
|
|
|
|
if x_data is None or y_data is None:
|
|
continue
|
|
|
|
# Interpolate to match reference x points (for comparison)
|
|
if len(x_data) != len(ref_x) or not np.array_equal(x_data, ref_x):
|
|
y_interp = np.interp(ref_x, x_data, y_data)
|
|
else:
|
|
y_interp = y_data
|
|
|
|
# Calculate deviation
|
|
deviation = y_interp - ref_y
|
|
|
|
# Label format: "session_name (run_no) [command] - Data"
|
|
label = f"{data.session_name} ({data.run_no}) [{data.command_name}] - {ylabel}"
|
|
ax.plot(ref_x, deviation, label=label, alpha=0.8,
|
|
linestyle=config.linestyle, marker=config.marker,
|
|
markersize=config.markersize)
|
|
|
|
# Formatting
|
|
ax.set_xlabel(xlabel, fontsize=config.label_fontsize)
|
|
ax.set_ylabel(f"Deviation in {ylabel}", fontsize=config.label_fontsize)
|
|
ax.set_title(f"{config.title} - Drift Analysis", fontsize=config.title_fontsize)
|
|
ax.tick_params(axis='both', labelsize=config.tick_fontsize)
|
|
|
|
if config.grid:
|
|
ax.grid(True, alpha=0.3)
|
|
|
|
if config.legend:
|
|
# Only add legend if there are labeled artists
|
|
handles, labels = ax.get_legend_handles_labels()
|
|
if handles and labels:
|
|
# Legend at bottom
|
|
ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.12), ncol=2, fontsize=config.legend_fontsize)
|
|
fig.tight_layout(rect=[0, 0.08, 1, 1])
|
|
else:
|
|
fig.tight_layout()
|
|
else:
|
|
fig.tight_layout()
|
|
|
|
return fig
|
|
|
|
|
|
def plot_multi_series(
|
|
data_list: List[TelemetryData],
|
|
x_column: str,
|
|
y_columns: List[str],
|
|
xlabel: str,
|
|
ylabels: List[str],
|
|
config: Optional[PlotConfig] = None
|
|
) -> Figure:
|
|
"""
|
|
Create multi-series plot (multiple data columns, multiple runs).
|
|
|
|
Args:
|
|
data_list: List of TelemetryData objects
|
|
x_column: Column name for X-axis
|
|
y_columns: List of column names to plot
|
|
xlabel: X-axis label
|
|
ylabels: List of Y-axis labels
|
|
config: Plot configuration
|
|
|
|
Returns:
|
|
Matplotlib Figure object
|
|
"""
|
|
if config is None:
|
|
config = PlotConfig()
|
|
|
|
n_series = len(y_columns)
|
|
|
|
fig, axes = plt.subplots(
|
|
n_series, 1,
|
|
figsize=config.figsize,
|
|
dpi=config.dpi,
|
|
sharex=True
|
|
)
|
|
|
|
# Handle single subplot case
|
|
if n_series == 1:
|
|
axes = [axes]
|
|
|
|
# Plot each series
|
|
for idx, (y_col, ylabel) in enumerate(zip(y_columns, ylabels)):
|
|
ax = axes[idx]
|
|
|
|
# Set color cycle
|
|
ax.set_prop_cycle(color=plt.cm.tab10.colors)
|
|
|
|
for data in data_list:
|
|
x_data = getattr(data, x_column, None)
|
|
y_data = getattr(data, y_col, None)
|
|
|
|
if x_data is None or y_data is None:
|
|
continue
|
|
|
|
# Label format: "session_name (run_no) [command] - Data"
|
|
label = f"{data.session_name} ({data.run_no}) [{data.command_name}] - {ylabel}"
|
|
ax.plot(x_data, y_data, label=label, alpha=0.8,
|
|
linestyle=config.linestyle, marker=config.marker,
|
|
markersize=config.markersize)
|
|
|
|
ax.set_ylabel(ylabel, fontsize=config.label_fontsize)
|
|
ax.tick_params(axis='both', labelsize=config.tick_fontsize)
|
|
|
|
if config.grid:
|
|
ax.grid(True, alpha=0.3)
|
|
|
|
if config.legend and idx == 0: # Legend only on first subplot
|
|
# Only add legend if there are labeled artists
|
|
handles, labels = ax.get_legend_handles_labels()
|
|
if handles and labels:
|
|
ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15), ncol=2, fontsize=config.legend_fontsize)
|
|
|
|
# X-label only on bottom subplot
|
|
axes[-1].set_xlabel(xlabel, fontsize=config.label_fontsize)
|
|
|
|
fig.suptitle(config.title, fontsize=config.title_fontsize)
|
|
fig.tight_layout(rect=[0, 0.08, 1, 0.96])
|
|
|
|
return fig
|
|
|
|
|
|
def plot_xy_scatter(
|
|
data_list: List[TelemetryData],
|
|
x_column: str,
|
|
y_column: str,
|
|
xlabel: str,
|
|
ylabel: str,
|
|
config: Optional[PlotConfig] = None
|
|
) -> Figure:
|
|
"""
|
|
Create XY scatter/line plot (any column vs any column).
|
|
|
|
Useful for phase plots, correlation analysis, etc.
|
|
Example: motor_current vs pwm, angle vs encoder_value
|
|
|
|
Args:
|
|
data_list: List of TelemetryData objects
|
|
x_column: Column name for X-axis
|
|
y_column: Column name for Y-axis
|
|
xlabel: X-axis label
|
|
ylabel: Y-axis label
|
|
config: Plot configuration
|
|
|
|
Returns:
|
|
Matplotlib Figure object
|
|
"""
|
|
if config is None:
|
|
config = PlotConfig()
|
|
|
|
fig, ax = plt.subplots(figsize=config.figsize, dpi=config.dpi)
|
|
|
|
# Set color cycle
|
|
ax.set_prop_cycle(color=plt.cm.tab10.colors)
|
|
|
|
# Plot each run
|
|
for data in data_list:
|
|
x_data = getattr(data, x_column, None)
|
|
y_data = getattr(data, y_column, None)
|
|
|
|
if x_data is None or y_data is None:
|
|
continue
|
|
|
|
# Label format: "session_name (run_no) [command] - Data"
|
|
label = f"{data.session_name} ({data.run_no}) [{data.command_name}] - {ylabel}"
|
|
ax.plot(x_data, y_data, label=label, alpha=0.8,
|
|
linestyle=config.linestyle, marker=config.marker or 'o',
|
|
markersize=config.markersize if config.marker else 2,
|
|
linewidth=1)
|
|
|
|
# Formatting
|
|
ax.set_xlabel(xlabel, fontsize=config.label_fontsize)
|
|
ax.set_ylabel(ylabel, fontsize=config.label_fontsize)
|
|
ax.set_title(config.title, fontsize=config.title_fontsize)
|
|
ax.tick_params(axis='both', labelsize=config.tick_fontsize)
|
|
|
|
if config.grid:
|
|
ax.grid(True, alpha=0.3)
|
|
|
|
if config.legend:
|
|
# Only add legend if there are labeled artists
|
|
handles, labels = ax.get_legend_handles_labels()
|
|
if handles and labels:
|
|
# Legend at bottom
|
|
ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.12), ncol=2, fontsize=config.legend_fontsize)
|
|
fig.tight_layout(rect=[0, 0.08, 1, 1])
|
|
else:
|
|
fig.tight_layout()
|
|
else:
|
|
fig.tight_layout()
|
|
|
|
return fig
|
|
|
|
|
|
def plot_envelope(
|
|
data_list: List[TelemetryData],
|
|
x_column: str,
|
|
y_column: str,
|
|
xlabel: str,
|
|
ylabel: str,
|
|
config: Optional[PlotConfig] = None
|
|
) -> Figure:
|
|
"""
|
|
Create envelope plot - shows min/max bounds across all runs.
|
|
|
|
Useful for visualizing acceptable variation ranges.
|
|
|
|
Args:
|
|
data_list: List of TelemetryData objects
|
|
x_column: Column name for X-axis
|
|
y_column: Column name for Y-axis
|
|
xlabel: X-axis label
|
|
ylabel: Y-axis label
|
|
config: Plot configuration
|
|
|
|
Returns:
|
|
Matplotlib Figure object
|
|
"""
|
|
if config is None:
|
|
config = PlotConfig()
|
|
|
|
fig, ax = plt.subplots(figsize=config.figsize, dpi=config.dpi)
|
|
|
|
if not data_list:
|
|
ax.text(0.5, 0.5, 'No data', ha='center', va='center')
|
|
return fig
|
|
|
|
# Find common x-axis (use first run as reference)
|
|
reference = data_list[0]
|
|
ref_x = getattr(reference, x_column, None)
|
|
|
|
if ref_x is None:
|
|
ax.text(0.5, 0.5, 'No reference data', ha='center', va='center')
|
|
return fig
|
|
|
|
# Collect all Y values at each X point
|
|
all_y_values = []
|
|
|
|
for data in data_list:
|
|
x_data = getattr(data, x_column, None)
|
|
y_data = getattr(data, y_column, None)
|
|
|
|
if x_data is None or y_data is None:
|
|
continue
|
|
|
|
# Interpolate to match reference x points
|
|
if len(x_data) != len(ref_x) or not np.array_equal(x_data, ref_x):
|
|
y_interp = np.interp(ref_x, x_data, y_data)
|
|
else:
|
|
y_interp = y_data
|
|
|
|
all_y_values.append(y_interp)
|
|
|
|
if not all_y_values:
|
|
ax.text(0.5, 0.5, 'No data to plot', ha='center', va='center')
|
|
return fig
|
|
|
|
# Calculate statistics
|
|
all_y_values = np.array(all_y_values)
|
|
y_min = np.min(all_y_values, axis=0)
|
|
y_max = np.max(all_y_values, axis=0)
|
|
y_mean = np.mean(all_y_values, axis=0)
|
|
|
|
# Plot envelope
|
|
ax.fill_between(ref_x, y_min, y_max, alpha=0.3, color='lightblue',
|
|
label='Min/Max Range')
|
|
ax.plot(ref_x, y_mean, '-', color='blue', linewidth=2,
|
|
label='Mean', alpha=0.8)
|
|
ax.plot(ref_x, y_min, '--', color='gray', linewidth=1,
|
|
label='Min', alpha=0.6)
|
|
ax.plot(ref_x, y_max, '--', color='gray', linewidth=1,
|
|
label='Max', alpha=0.6)
|
|
|
|
# Formatting
|
|
ax.set_xlabel(xlabel, fontsize=config.label_fontsize)
|
|
ax.set_ylabel(ylabel, fontsize=config.label_fontsize)
|
|
ax.set_title(f"{config.title} - Envelope Analysis", fontsize=config.title_fontsize)
|
|
ax.tick_params(axis='both', labelsize=config.tick_fontsize)
|
|
|
|
if config.grid:
|
|
ax.grid(True, alpha=0.3)
|
|
|
|
if config.legend:
|
|
ax.legend(loc='best', fontsize=config.legend_fontsize)
|
|
|
|
fig.tight_layout()
|
|
return fig
|
|
|
|
|
|
def plot_cumulative_deviation(
|
|
data_list: List[TelemetryData],
|
|
x_column: str,
|
|
y_column: str,
|
|
xlabel: str,
|
|
ylabel: str,
|
|
reference_index: int = 0,
|
|
config: Optional[PlotConfig] = None
|
|
) -> Figure:
|
|
"""
|
|
Create cumulative deviation plot - shows accumulated error over time.
|
|
|
|
Useful for detecting slow, compounding drift.
|
|
|
|
Args:
|
|
data_list: List of TelemetryData objects
|
|
x_column: Column name for X-axis
|
|
y_column: Column name for Y-axis
|
|
xlabel: X-axis label
|
|
ylabel: Y-axis label
|
|
reference_index: Index of reference run (default: 0)
|
|
config: Plot configuration
|
|
|
|
Returns:
|
|
Matplotlib Figure object
|
|
"""
|
|
if config is None:
|
|
config = PlotConfig()
|
|
|
|
if reference_index >= len(data_list):
|
|
reference_index = 0
|
|
|
|
fig, ax = plt.subplots(figsize=config.figsize, dpi=config.dpi)
|
|
|
|
reference = data_list[reference_index]
|
|
ref_x = getattr(reference, x_column, None)
|
|
ref_y = getattr(reference, y_column, None)
|
|
|
|
if ref_x is None or ref_y is None:
|
|
ax.text(0.5, 0.5, 'No reference data', ha='center', va='center')
|
|
return fig
|
|
|
|
# Plot reference as zero line
|
|
ax.axhline(y=0, color='black', linestyle='--',
|
|
label=f'Reference: {reference.session_name} ({reference.run_no}) [{reference.command_name}]')
|
|
|
|
# Set color cycle
|
|
ax.set_prop_cycle(color=plt.cm.tab10.colors)
|
|
|
|
# Plot cumulative deviations
|
|
for idx, data in enumerate(data_list):
|
|
if idx == reference_index:
|
|
continue
|
|
|
|
x_data = getattr(data, x_column, None)
|
|
y_data = getattr(data, y_column, None)
|
|
|
|
if x_data is None or y_data is None:
|
|
continue
|
|
|
|
# Interpolate to match reference x points
|
|
if len(x_data) != len(ref_x) or not np.array_equal(x_data, ref_x):
|
|
y_interp = np.interp(ref_x, x_data, y_data)
|
|
else:
|
|
y_interp = y_data
|
|
|
|
# Calculate deviation
|
|
deviation = y_interp - ref_y
|
|
|
|
# Calculate cumulative sum
|
|
cumulative_deviation = np.cumsum(deviation)
|
|
|
|
# Label format
|
|
label = f"{data.session_name} ({data.run_no}) [{data.command_name}]"
|
|
ax.plot(ref_x, cumulative_deviation, label=label, alpha=0.8,
|
|
linestyle=config.linestyle, marker=config.marker,
|
|
markersize=config.markersize)
|
|
|
|
# Formatting
|
|
ax.set_xlabel(xlabel, fontsize=config.label_fontsize)
|
|
ax.set_ylabel(f"Cumulative Deviation in {ylabel}", fontsize=config.label_fontsize)
|
|
ax.set_title(f"{config.title} - Cumulative Drift Analysis", fontsize=config.title_fontsize)
|
|
ax.tick_params(axis='both', labelsize=config.tick_fontsize)
|
|
|
|
if config.grid:
|
|
ax.grid(True, alpha=0.3)
|
|
|
|
if config.legend:
|
|
handles, labels = ax.get_legend_handles_labels()
|
|
if handles and labels:
|
|
ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.12), ncol=2, fontsize=config.legend_fontsize)
|
|
fig.tight_layout(rect=[0, 0.08, 1, 1])
|
|
else:
|
|
fig.tight_layout()
|
|
else:
|
|
fig.tight_layout()
|
|
|
|
return fig
|
|
|
|
|
|
# =============================================================================
|
|
# Export Functions
|
|
# =============================================================================
|
|
|
|
def export_png(fig: Figure, filepath: str, dpi: int = 300) -> bool:
|
|
"""
|
|
Export figure to PNG.
|
|
|
|
Args:
|
|
fig: Matplotlib figure
|
|
filepath: Output file path
|
|
dpi: Resolution
|
|
|
|
Returns:
|
|
True on success
|
|
"""
|
|
try:
|
|
fig.savefig(filepath, dpi=dpi, bbox_inches='tight')
|
|
return True
|
|
except Exception as e:
|
|
print(f"[EXPORT ERROR] Failed to save PNG: {e}")
|
|
return False
|
|
|
|
|
|
def export_csv(
|
|
data_list: List[TelemetryData],
|
|
filepath: str,
|
|
x_column: str,
|
|
y_columns: List[str]
|
|
) -> bool:
|
|
"""
|
|
Export telemetry data to CSV (only selected columns).
|
|
|
|
Args:
|
|
data_list: List of TelemetryData objects
|
|
filepath: Output file path
|
|
x_column: X-axis column name (e.g., 'time_ms')
|
|
y_columns: List of Y-axis column names (e.g., ['motor_current', 'pwm'])
|
|
|
|
Returns:
|
|
True on success
|
|
"""
|
|
try:
|
|
import csv
|
|
|
|
with open(filepath, 'w', newline='') as f:
|
|
writer = csv.writer(f)
|
|
|
|
# Header: metadata + X column + selected Y columns
|
|
header = ['session_id', 'session_name', 'run_no', 'command_name', x_column] + y_columns
|
|
writer.writerow(header)
|
|
|
|
# Data rows
|
|
for data in data_list:
|
|
# Get X data
|
|
x_data = getattr(data, x_column, None)
|
|
|
|
if x_data is None:
|
|
continue
|
|
|
|
# Get length from X column
|
|
length = len(x_data)
|
|
|
|
# Write each data point
|
|
for i in range(length):
|
|
row = [
|
|
data.session_id,
|
|
data.session_name,
|
|
data.run_no,
|
|
data.command_name,
|
|
x_data[i]
|
|
]
|
|
|
|
# Add selected Y columns
|
|
for y_col in y_columns:
|
|
y_data = getattr(data, y_col, None)
|
|
if y_data is not None and i < len(y_data):
|
|
row.append(y_data[i])
|
|
else:
|
|
row.append('') # Empty if column doesn't exist
|
|
|
|
writer.writerow(row)
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"[EXPORT ERROR] Failed to save CSV: {e}")
|
|
return False
|
|
|
|
|
|
# =============================================================================
|
|
# Demo
|
|
# =============================================================================
|
|
|
|
if __name__ == "__main__":
|
|
print("Graph Core - Pure Plotting Functions")
|
|
print("=" * 50)
|
|
print()
|
|
print("Database-agnostic plotting library!")
|
|
print()
|
|
print("Usage:")
|
|
print(" from table_query import SQLiteAdapter, CSVAdapter")
|
|
print(" from graph_core import plot_overlay, plot_xy_scatter")
|
|
print()
|
|
print(" # Load data via adapter")
|
|
print(" adapter = SQLiteAdapter('./database/ehinge.db')")
|
|
print(" adapter.connect()")
|
|
print(" data = adapter.load_run_data('Session_A', 1)")
|
|
print()
|
|
print(" # Plot (pure function - no database!)")
|
|
print(" fig = plot_overlay([data], 'time_ms', 'pwm', 'Time', 'PWM')")
|
|
print()
|
|
print("Available functions:")
|
|
print(" • plot_overlay() - Multiple runs on same axes")
|
|
print(" • plot_subplots() - One subplot per run")
|
|
print(" • plot_comparison() - Drift analysis")
|
|
print(" • plot_multi_series() - Stacked subplots")
|
|
print(" • plot_xy_scatter() - XY scatter/line plot")
|
|
print(" • export_png() - Save as PNG")
|
|
print(" • export_csv() - Export data as CSV")
|