gauravlochab
feat: enable auto-ranging for x-axis and y-axis in APR and ROI charts
9bcd921
"""
APR chart implementations.
"""
import plotly.graph_objects as go
import pandas as pd
import logging
from datetime import datetime
from typing import Dict, Any, Optional, Tuple
from ..config.constants import DATE_RANGES, Y_AXIS_RANGES, FILE_PATHS
from .base_chart import BaseChart
logger = logging.getLogger(__name__)
class APRChart(BaseChart):
"""Chart for APR visualizations."""
def create_chart(self, df: pd.DataFrame, **kwargs) -> go.Figure:
"""Create APR time series chart."""
if df.empty:
return self._create_empty_chart("No APR data available")
# Filter for APR data only
apr_data = df[df['metric_type'] == 'APR'].copy()
if apr_data.empty:
return self._create_empty_chart("No APR data available")
# Apply daily median aggregation to reduce outliers
apr_data = self.data_processor.aggregate_daily_medians(apr_data, ['apr', 'adjusted_apr'])
if apr_data.empty:
return self._create_empty_chart("No APR data available after aggregation")
# Apply high APR filtering (400% threshold) with forward filling
apr_data, _ = self.data_processor.filter_high_apr_values(apr_data)
if apr_data.empty:
return self._create_empty_chart("No APR data available after high APR filtering")
# Save processed APR data for verification (after all processing steps)
processed_csv_path = self.data_processor.save_to_csv(apr_data, FILE_PATHS['apr_processed_csv'])
if processed_csv_path:
logger.info(f"Saved processed APR data to {processed_csv_path} for verification")
logger.info(f"Processed APR data contains {len(apr_data)} rows after agent exclusion, zero filtering, daily median aggregation, and high APR filtering")
# Filter outliers (disabled but keeping for compatibility)
apr_data = self._filter_outliers(apr_data, 'apr')
# Get time range
min_time = apr_data['timestamp'].min()
max_time = apr_data['timestamp'].max()
x_start_date = DATE_RANGES['apr_start']
# Create figure
fig = self._create_base_figure()
# Add background shapes
y_range = Y_AXIS_RANGES['apr']
self._add_background_shapes(fig, min_time, max_time, y_range['min'], y_range['max'])
self._add_zero_line(fig, min_time, max_time)
# Calculate moving averages
avg_apr_data = self._calculate_moving_average(apr_data, 'apr')
# Add individual agent data points
unique_agents = apr_data['agent_name'].unique()
color_map = self._get_color_map(unique_agents)
self._add_agent_data_points(fig, apr_data, 'apr', color_map)
# Add APR moving average line
self._add_moving_average_line(
fig, avg_apr_data, 'apr',
'Average APR (7d window)',
self.colors['apr'],
width=3
)
# Add adjusted APR moving average if available
if 'adjusted_apr' in apr_data.columns and apr_data['adjusted_apr'].notna().any():
# Calculate adjusted APR moving average
adjusted_avg_data = self._calculate_moving_average(apr_data, 'adjusted_apr')
# Handle missing values with forward fill (fix the column name)
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore", FutureWarning)
# Fix: Use the correct column name 'moving_avg' not 'adjusted_moving_avg'
adjusted_avg_data['moving_avg'] = adjusted_avg_data['moving_avg'].ffill()
self._add_moving_average_line(
fig, adjusted_avg_data, 'adjusted_apr',
'Average ETH Adjusted APR (7d window)',
self.colors['adjusted_apr'],
width=3
)
# Update layout and axes
self._update_layout(
fig,
title="Modius Agents",
y_axis_title=None, # Remove single y-axis title to use region-specific labels
y_range=[y_range['min'], y_range['max']]
)
# Use auto-range for both x-axis and y-axis to show all available data
self._update_axes(
fig,
x_range=None, # Let plotly auto-determine the best x-axis range
y_auto=True # Let plotly auto-determine the best y-axis range
)
# Add region-specific annotations for positive and negative areas
self._add_region_annotations(fig, y_range)
# Save chart
self._save_chart(
fig,
FILE_PATHS['apr_graph_html'],
FILE_PATHS['apr_graph_png']
)
return fig
def _add_region_annotations(self, fig: go.Figure, y_range: Dict[str, float]) -> None:
"""Add annotations for positive and negative regions."""
# Annotation for negative region
fig.add_annotation(
x=-0.08,
y=-25,
xref="paper",
yref="y",
text="Percent drawdown [%]",
showarrow=False,
font=dict(
size=16,
family=self.config['font_family'],
color="black",
weight="bold"
),
textangle=-90,
align="center"
)
# Annotation for positive region
fig.add_annotation(
x=-0.08,
y=50,
xref="paper",
yref="y",
text="Agent APR [%]",
showarrow=False,
font=dict(
size=16,
family=self.config['font_family'],
color="black",
weight="bold"
),
textangle=-90,
align="center"
)
class APRHashChart(BaseChart):
"""Chart for APR vs Agent Hash visualizations."""
def create_chart(self, df: pd.DataFrame, **kwargs) -> go.Figure:
"""Create APR vs agent hash bar chart."""
if df.empty:
return self._create_empty_chart("No agent hash data available")
# Data is already filtered and processed by the calling function
# Just filter for data with agent hash
apr_data = df[df['agent_hash'].notna()].copy()
if apr_data.empty:
return self._create_empty_chart("No valid APR data with agent_hash found")
logger.info(f"APR Hash Chart: Using {len(apr_data)} processed data points")
# Create figure
fig = self._create_base_figure()
# Get unique hashes and create version mapping
unique_hashes = apr_data['agent_hash'].unique()
version_map = self._create_version_map(unique_hashes)
# Sort hashes by version
sorted_hashes = sorted(unique_hashes, key=lambda h: "1" if h.endswith("tby") else "2" if h.endswith("vq") else h)
# Add zero line for bar chart
self._add_zero_line(fig, -0.5, len(version_map) - 0.5)
# Version colors
version_colors = {
"v0.4.1": "rgba(31, 119, 180, 0.7)",
"v0.4.2": "rgba(44, 160, 44, 0.7)",
}
default_color = "rgba(214, 39, 40, 0.7)"
# Aggregate data by version for bar chart
version_data = {}
version_stats = {}
# Aggregate data by version
for agent_hash in sorted_hashes:
hash_data = apr_data[apr_data['agent_hash'] == agent_hash]
version = version_map[agent_hash]
# Calculate statistics
apr_values = hash_data['apr'].tolist()
# Store statistics for version comparison
if version not in version_stats:
version_stats[version] = {'apr_values': [], 'count': 0, 'hashes': []}
version_stats[version]['apr_values'].extend(apr_values)
version_stats[version]['count'] += len(apr_values)
version_stats[version]['hashes'].append(agent_hash)
# Create bar chart data
versions = list(version_stats.keys())
medians = []
colors = []
hover_texts = []
for version in versions:
# Calculate median APR for this version
all_values = version_stats[version]['apr_values']
median_apr = pd.Series(all_values).median()
medians.append(median_apr)
# Choose color
color = version_colors.get(version, default_color)
colors.append(color)
# Create hover text
count = version_stats[version]['count']
mean_apr = pd.Series(all_values).mean()
min_apr = pd.Series(all_values).min()
max_apr = pd.Series(all_values).max()
hover_text = (
f"Version: {version}<br>"
f"Median APR: {median_apr:.2f}%<br>"
f"Mean APR: {mean_apr:.2f}%<br>"
f"Min APR: {min_apr:.2f}%<br>"
f"Max APR: {max_apr:.2f}%<br>"
f"Data points: {count}"
)
hover_texts.append(hover_text)
# Add bar chart
fig.add_trace(
go.Bar(
x=versions,
y=medians,
marker=dict(
color=colors,
line=dict(width=1, color='black')
),
hoverinfo='text',
hovertext=hover_texts,
showlegend=False,
name="Median APR"
)
)
# Add median value annotations on top of bars
for i, (version, median_apr) in enumerate(zip(versions, medians)):
fig.add_annotation(
x=version,
y=median_apr + (max(medians) * 0.05), # 5% above the bar
text=f"{median_apr:.1f}%",
showarrow=False,
font=dict(
family=self.config['font_family'],
size=14,
color="black",
weight="bold"
)
)
# Add version comparison annotation
self._add_version_comparison(fig, version_stats, len(versions))
# Update layout
self._update_layout(
fig,
title="Performance Graph",
height=900
)
# Update axes
self._update_axes(fig, y_auto=True)
# Update x-axis for bar chart
fig.update_xaxes(
tickangle=-45
)
# Save chart
self._save_chart(
fig,
FILE_PATHS['apr_hash_graph_html'],
FILE_PATHS['apr_hash_graph_png']
)
return fig
def _create_version_map(self, hashes: list) -> Dict[str, str]:
"""Create version mapping for agent hashes."""
version_map = {}
for hash_val in hashes:
if hash_val.endswith("tby"):
version_map[hash_val] = "v0.4.1"
elif hash_val.endswith("vq"):
version_map[hash_val] = "v0.4.2"
else:
version_map[hash_val] = f"Hash: {hash_val[-6:]}"
return version_map
def _add_version_comparison(self, fig: go.Figure, version_stats: Dict, num_hashes: int) -> None:
"""Add version comparison annotation."""
if "v0.4.1" in version_stats and "v0.4.2" in version_stats:
v041_values = version_stats["v0.4.1"]["apr_values"]
v042_values = version_stats["v0.4.2"]["apr_values"]
v041_median = pd.Series(v041_values).median()
v042_median = pd.Series(v042_values).median()
improvement = v042_median - v041_median
change_text = "improvement" if improvement > 0 else "decrease"
fig.add_annotation(
x=(num_hashes - 1) / 2,
y=90,
text=f"<b>Version Comparison:</b> {abs(improvement):.2f}% {change_text} from v0.4.1 to v0.4.2",
showarrow=False,
font=dict(
family=self.config['font_family'],
size=16,
color="black",
weight="bold"
),
bgcolor="rgba(255, 255, 255, 0.9)",
bordercolor="black",
borderwidth=2,
borderpad=6,
opacity=0.9
)
def generate_apr_visualizations(data_processor=None) -> Tuple[go.Figure, Optional[str]]:
"""Generate APR visualizations."""
from ..data.data_processor import DataProcessor
if data_processor is None:
data_processor = DataProcessor()
# Fetch data
apr_df, _ = data_processor.fetch_apr_data_from_db()
# Create chart
apr_chart = APRChart(data_processor)
fig, csv_path = apr_chart.generate_visualization(
apr_df,
csv_filename=FILE_PATHS['apr_csv']
)
return fig, csv_path
def generate_apr_vs_agent_hash_visualizations(df: pd.DataFrame) -> Tuple[go.Figure, Optional[str]]:
"""Generate APR vs agent hash visualizations."""
from ..data.data_processor import DataProcessor
data_processor = DataProcessor()
# Use the same processed data as the APR time series chart
# First apply the same filtering pipeline to get consistent data
apr_data = df[df['metric_type'] == 'APR'].copy()
if not apr_data.empty:
# Apply daily median aggregation
apr_data = data_processor.aggregate_daily_medians(apr_data, ['apr', 'adjusted_apr'])
# Apply high APR filtering
apr_data, _ = data_processor.filter_high_apr_values(apr_data)
# Create chart using the processed data
apr_hash_chart = APRHashChart(data_processor)
fig, csv_path = apr_hash_chart.generate_visualization(
apr_data,
csv_filename=FILE_PATHS['apr_hash_csv']
)
return fig, csv_path