|
import pandas as pd |
|
import numpy as np |
|
import plotly.express as px |
|
import plotly.graph_objects as go |
|
from plotly.subplots import make_subplots |
|
import datetime |
|
from typing import Dict, List, Any, Union, Optional, Tuple |
|
import calendar |
|
from collections import Counter, defaultdict |
|
|
|
from utils.logging import setup_logger |
|
from utils.error_handling import handle_exceptions |
|
from utils.storage import load_data, safe_get |
|
from utils.config import FILE_PATHS, UI_COLORS |
|
|
|
|
|
logger = setup_logger(__name__) |
|
|
|
@handle_exceptions |
|
def filter_data_by_time_period(data: List[Dict[str, Any]], time_period: str, timestamp_key: str = "created_at") -> List[Dict[str, Any]]: |
|
""" |
|
Filter data based on the selected time period |
|
|
|
Args: |
|
data: List of data items (tasks, notes, goals, etc.) |
|
time_period: Time period to filter by ("Last 7 Days", "Last 30 Days", "Last 90 Days", "All Time") |
|
timestamp_key: Key in the data items that contains the timestamp |
|
|
|
Returns: |
|
Filtered data items |
|
""" |
|
logger.debug(f"Filtering data by time period: {time_period}") |
|
|
|
if time_period == "All Time" or not data: |
|
return data |
|
|
|
now = datetime.datetime.now() |
|
|
|
if time_period == "Last 7 Days": |
|
cutoff = now - datetime.timedelta(days=7) |
|
elif time_period == "Last 30 Days": |
|
cutoff = now - datetime.timedelta(days=30) |
|
elif time_period == "Last 90 Days": |
|
cutoff = now - datetime.timedelta(days=90) |
|
else: |
|
logger.warning(f"Unknown time period: {time_period}, returning all data") |
|
return data |
|
|
|
|
|
cutoff_timestamp = cutoff.timestamp() |
|
|
|
filtered_data = [] |
|
for item in data: |
|
item_timestamp = item.get(timestamp_key) |
|
if not item_timestamp: |
|
continue |
|
|
|
|
|
if isinstance(item_timestamp, str): |
|
try: |
|
item_datetime = datetime.datetime.fromisoformat(item_timestamp.replace('Z', '+00:00')) |
|
item_timestamp = item_datetime.timestamp() |
|
except ValueError: |
|
logger.warning(f"Could not parse timestamp: {item_timestamp}") |
|
continue |
|
|
|
if item_timestamp >= cutoff_timestamp: |
|
filtered_data.append(item) |
|
|
|
logger.debug(f"Filtered data from {len(data)} to {len(filtered_data)} items") |
|
return filtered_data |
|
|
|
@handle_exceptions |
|
def create_completion_rate_chart(data: List[Dict[str, Any]], title: str = "Completion Rate", |
|
completed_key: str = "completed") -> go.Figure: |
|
""" |
|
Create a pie chart showing completion rate |
|
|
|
Args: |
|
data: List of data items (tasks, goals, etc.) |
|
title: Chart title |
|
completed_key: Key in the data items that indicates completion status |
|
|
|
Returns: |
|
Plotly figure object |
|
""" |
|
logger.debug(f"Creating completion rate chart with {len(data)} items") |
|
|
|
|
|
completed = sum(1 for item in data if safe_get(item, completed_key, False)) |
|
incomplete = len(data) - completed |
|
|
|
|
|
if data: |
|
completed_pct = completed / len(data) * 100 |
|
incomplete_pct = 100 - completed_pct |
|
else: |
|
completed_pct = 0 |
|
incomplete_pct = 0 |
|
|
|
|
|
labels = [f"Completed ({completed_pct:.1f}%)", f"Incomplete ({incomplete_pct:.1f}%)"] |
|
values = [completed, incomplete] |
|
|
|
|
|
fig = go.Figure(data=[go.Pie( |
|
labels=labels, |
|
values=values, |
|
hole=0.4, |
|
marker_colors=[UI_COLORS["success"], UI_COLORS["warning"]] |
|
)]) |
|
|
|
fig.update_layout( |
|
title=title, |
|
showlegend=True, |
|
legend=dict(orientation="h", yanchor="bottom", y=-0.2, xanchor="center", x=0.5), |
|
margin=dict(l=20, r=20, t=40, b=20), |
|
height=300 |
|
) |
|
|
|
return fig |
|
|
|
@handle_exceptions |
|
def create_status_distribution_chart(tasks: List[Dict[str, Any]], title: str = "Task Status Distribution") -> go.Figure: |
|
""" |
|
Create a bar chart showing the distribution of task statuses |
|
|
|
Args: |
|
tasks: List of task items |
|
title: Chart title |
|
|
|
Returns: |
|
Plotly figure object |
|
""" |
|
logger.debug(f"Creating status distribution chart with {len(tasks)} tasks") |
|
|
|
|
|
status_counts = Counter(safe_get(task, "status", "To Do") for task in tasks) |
|
|
|
|
|
status_order = ["To Do", "In Progress", "Done"] |
|
status_colors = [UI_COLORS["warning"], UI_COLORS["info"], UI_COLORS["success"]] |
|
|
|
|
|
statuses = [] |
|
counts = [] |
|
colors = [] |
|
|
|
for status in status_order: |
|
if status in status_counts: |
|
statuses.append(status) |
|
counts.append(status_counts[status]) |
|
colors.append(status_colors[status_order.index(status)]) |
|
|
|
|
|
for status, count in status_counts.items(): |
|
if status not in status_order: |
|
statuses.append(status) |
|
counts.append(count) |
|
colors.append(UI_COLORS["secondary"]) |
|
|
|
|
|
fig = go.Figure(data=[go.Bar( |
|
x=statuses, |
|
y=counts, |
|
marker_color=colors, |
|
text=counts, |
|
textposition='auto', |
|
)]) |
|
|
|
fig.update_layout( |
|
title=title, |
|
xaxis_title="Status", |
|
yaxis_title="Number of Tasks", |
|
margin=dict(l=20, r=20, t=40, b=20), |
|
height=300 |
|
) |
|
|
|
return fig |
|
|
|
@handle_exceptions |
|
def create_priority_distribution_chart(tasks: List[Dict[str, Any]], title: str = "Task Priority Distribution") -> go.Figure: |
|
""" |
|
Create a bar chart showing the distribution of task priorities |
|
|
|
Args: |
|
tasks: List of task items |
|
title: Chart title |
|
|
|
Returns: |
|
Plotly figure object |
|
""" |
|
logger.debug(f"Creating priority distribution chart with {len(tasks)} tasks") |
|
|
|
|
|
priority_counts = Counter(safe_get(task, "priority", "Medium") for task in tasks) |
|
|
|
|
|
priority_order = ["Low", "Medium", "High", "Urgent"] |
|
priority_colors = [UI_COLORS["success"], UI_COLORS["info"], UI_COLORS["warning"], UI_COLORS["danger"]] |
|
|
|
|
|
priorities = [] |
|
counts = [] |
|
colors = [] |
|
|
|
for priority in priority_order: |
|
if priority in priority_counts: |
|
priorities.append(priority) |
|
counts.append(priority_counts[priority]) |
|
colors.append(priority_colors[priority_order.index(priority)]) |
|
|
|
|
|
for priority, count in priority_counts.items(): |
|
if priority not in priority_order: |
|
priorities.append(priority) |
|
counts.append(count) |
|
colors.append(UI_COLORS["secondary"]) |
|
|
|
|
|
fig = go.Figure(data=[go.Bar( |
|
x=priorities, |
|
y=counts, |
|
marker_color=colors, |
|
text=counts, |
|
textposition='auto', |
|
)]) |
|
|
|
fig.update_layout( |
|
title=title, |
|
xaxis_title="Priority", |
|
yaxis_title="Number of Tasks", |
|
margin=dict(l=20, r=20, t=40, b=20), |
|
height=300 |
|
) |
|
|
|
return fig |
|
|
|
@handle_exceptions |
|
def create_time_series_chart(data: List[Dict[str, Any]], title: str = "Activity Over Time", |
|
timestamp_key: str = "created_at", group_by: str = "day") -> go.Figure: |
|
""" |
|
Create a time series chart showing activity over time |
|
|
|
Args: |
|
data: List of data items (tasks, notes, goals, activity, etc.) |
|
title: Chart title |
|
timestamp_key: Key in the data items that contains the timestamp |
|
group_by: Time grouping ("day", "week", "month") |
|
|
|
Returns: |
|
Plotly figure object |
|
""" |
|
logger.debug(f"Creating time series chart with {len(data)} items, grouped by {group_by}") |
|
|
|
if not data: |
|
|
|
fig = go.Figure() |
|
fig.update_layout(title=title, height=300) |
|
return fig |
|
|
|
|
|
dates = [] |
|
for item in data: |
|
timestamp = item.get(timestamp_key) |
|
if not timestamp: |
|
continue |
|
|
|
|
|
if isinstance(timestamp, str): |
|
try: |
|
date = datetime.datetime.fromisoformat(timestamp.replace('Z', '+00:00')) |
|
except ValueError: |
|
logger.warning(f"Could not parse timestamp: {timestamp}") |
|
continue |
|
else: |
|
date = datetime.datetime.fromtimestamp(timestamp) |
|
|
|
dates.append(date) |
|
|
|
if not dates: |
|
|
|
fig = go.Figure() |
|
fig.update_layout(title=title, height=300) |
|
return fig |
|
|
|
|
|
date_counts = defaultdict(int) |
|
|
|
for date in dates: |
|
if group_by == "day": |
|
key = date.strftime("%Y-%m-%d") |
|
elif group_by == "week": |
|
|
|
start_of_week = date - datetime.timedelta(days=date.weekday()) |
|
key = start_of_week.strftime("%Y-%m-%d") |
|
elif group_by == "month": |
|
key = date.strftime("%Y-%m") |
|
else: |
|
logger.warning(f"Unknown group_by value: {group_by}, using day") |
|
key = date.strftime("%Y-%m-%d") |
|
|
|
date_counts[key] += 1 |
|
|
|
|
|
sorted_dates = sorted(date_counts.keys()) |
|
counts = [date_counts[date] for date in sorted_dates] |
|
|
|
|
|
if group_by == "day": |
|
x_labels = [datetime.datetime.strptime(d, "%Y-%m-%d").strftime("%b %d") for d in sorted_dates] |
|
elif group_by == "week": |
|
x_labels = [f"Week of {datetime.datetime.strptime(d, '%Y-%m-%d').strftime('%b %d')}" for d in sorted_dates] |
|
elif group_by == "month": |
|
x_labels = [datetime.datetime.strptime(d, "%Y-%m").strftime("%b %Y") for d in sorted_dates] |
|
else: |
|
x_labels = sorted_dates |
|
|
|
|
|
fig = go.Figure(data=go.Scatter( |
|
x=x_labels, |
|
y=counts, |
|
mode='lines+markers', |
|
line=dict(color=UI_COLORS["primary"], width=2), |
|
marker=dict(size=8, color=UI_COLORS["primary"]), |
|
fill='tozeroy', |
|
fillcolor=f"rgba({int(UI_COLORS['primary'][1:3], 16)}, {int(UI_COLORS['primary'][3:5], 16)}, {int(UI_COLORS['primary'][5:7], 16)}, 0.2)" |
|
)) |
|
|
|
fig.update_layout( |
|
title=title, |
|
xaxis_title="Date", |
|
yaxis_title="Count", |
|
margin=dict(l=20, r=20, t=40, b=20), |
|
height=300 |
|
) |
|
|
|
return fig |
|
|
|
@handle_exceptions |
|
def create_activity_heatmap(data: List[Dict[str, Any]], title: str = "Activity Heatmap", |
|
timestamp_key: str = "created_at") -> go.Figure: |
|
""" |
|
Create a heatmap showing activity by day of week and hour of day |
|
|
|
Args: |
|
data: List of data items (tasks, notes, goals, activity, etc.) |
|
title: Chart title |
|
timestamp_key: Key in the data items that contains the timestamp |
|
|
|
Returns: |
|
Plotly figure object |
|
""" |
|
logger.debug(f"Creating activity heatmap with {len(data)} items") |
|
|
|
if not data: |
|
|
|
fig = go.Figure() |
|
fig.update_layout(title=title, height=400) |
|
return fig |
|
|
|
|
|
day_hour_counts = np.zeros((7, 24)) |
|
|
|
for item in data: |
|
timestamp = item.get(timestamp_key) |
|
if not timestamp: |
|
continue |
|
|
|
|
|
if isinstance(timestamp, str): |
|
try: |
|
date = datetime.datetime.fromisoformat(timestamp.replace('Z', '+00:00')) |
|
except ValueError: |
|
logger.warning(f"Could not parse timestamp: {timestamp}") |
|
continue |
|
else: |
|
date = datetime.datetime.fromtimestamp(timestamp) |
|
|
|
|
|
day_of_week = date.weekday() |
|
hour = date.hour |
|
|
|
|
|
day_hour_counts[day_of_week, hour] += 1 |
|
|
|
|
|
days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] |
|
hours = [f"{h:02d}:00" for h in range(24)] |
|
|
|
|
|
fig = go.Figure(data=go.Heatmap( |
|
z=day_hour_counts, |
|
x=hours, |
|
y=days, |
|
colorscale='Viridis', |
|
hoverongaps=False, |
|
colorbar=dict(title="Count") |
|
)) |
|
|
|
fig.update_layout( |
|
title=title, |
|
xaxis_title="Hour of Day", |
|
yaxis_title="Day of Week", |
|
margin=dict(l=20, r=20, t=40, b=20), |
|
height=400 |
|
) |
|
|
|
return fig |
|
|
|
@handle_exceptions |
|
def create_calendar_heatmap(data: List[Dict[str, Any]], title: str = "Calendar Heatmap", |
|
timestamp_key: str = "created_at", year: Optional[int] = None) -> go.Figure: |
|
""" |
|
Create a calendar heatmap showing activity by day of year |
|
|
|
Args: |
|
data: List of data items (tasks, notes, goals, activity, etc.) |
|
title: Chart title |
|
timestamp_key: Key in the data items that contains the timestamp |
|
year: Year to display (defaults to current year) |
|
|
|
Returns: |
|
Plotly figure object |
|
""" |
|
logger.debug(f"Creating calendar heatmap with {len(data)} items") |
|
|
|
if not data: |
|
|
|
fig = go.Figure() |
|
fig.update_layout(title=title, height=500) |
|
return fig |
|
|
|
|
|
if year is None: |
|
year = datetime.datetime.now().year |
|
|
|
|
|
date_counts = defaultdict(int) |
|
|
|
for item in data: |
|
timestamp = item.get(timestamp_key) |
|
if not timestamp: |
|
continue |
|
|
|
|
|
if isinstance(timestamp, str): |
|
try: |
|
date = datetime.datetime.fromisoformat(timestamp.replace('Z', '+00:00')) |
|
except ValueError: |
|
logger.warning(f"Could not parse timestamp: {timestamp}") |
|
continue |
|
else: |
|
date = datetime.datetime.fromtimestamp(timestamp) |
|
|
|
|
|
if date.year == year: |
|
date_key = date.strftime("%Y-%m-%d") |
|
date_counts[date_key] += 1 |
|
|
|
|
|
start_date = datetime.date(year, 1, 1) |
|
end_date = datetime.date(year, 12, 31) |
|
all_dates = [start_date + datetime.timedelta(days=i) for i in range((end_date - start_date).days + 1)] |
|
|
|
dates = [d.strftime("%Y-%m-%d") for d in all_dates] |
|
counts = [date_counts.get(d, 0) for d in dates] |
|
weekdays = [d.weekday() for d in all_dates] |
|
months = [d.month for d in all_dates] |
|
days = [d.day for d in all_dates] |
|
|
|
|
|
df = pd.DataFrame({ |
|
'date': dates, |
|
'count': counts, |
|
'weekday': weekdays, |
|
'month': months, |
|
'day': days |
|
}) |
|
|
|
|
|
|
|
fig = make_subplots(rows=4, cols=3, subplot_titles=[calendar.month_name[i] for i in range(1, 13)]) |
|
|
|
|
|
max_count = max(counts) if counts else 1 |
|
colorscale = px.colors.sequential.Viridis |
|
|
|
|
|
for month in range(1, 13): |
|
month_data = df[df['month'] == month] |
|
|
|
|
|
month_grid = np.zeros((7, 6)) |
|
month_grid.fill(np.nan) |
|
|
|
|
|
first_day = month_data.iloc[0] |
|
first_weekday = first_day['weekday'] |
|
|
|
|
|
for _, row in month_data.iterrows(): |
|
day = row['day'] - 1 |
|
weekday = row['weekday'] |
|
week = (day + first_weekday) // 7 |
|
if week < 6: |
|
month_grid[weekday, week] = row['count'] |
|
|
|
|
|
row_idx = (month - 1) // 3 + 1 |
|
col_idx = (month - 1) % 3 + 1 |
|
|
|
fig.add_trace( |
|
go.Heatmap( |
|
z=month_grid, |
|
x=list(range(6)), |
|
y=['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], |
|
colorscale=colorscale, |
|
showscale=month == 12, |
|
zmin=0, |
|
zmax=max_count |
|
), |
|
row=row_idx, |
|
col=col_idx |
|
) |
|
|
|
|
|
fig.update_layout( |
|
title=title, |
|
height=800, |
|
margin=dict(l=20, r=20, t=40, b=20), |
|
) |
|
|
|
|
|
fig.update_xaxes(showticklabels=False, showgrid=False) |
|
|
|
return fig |
|
|
|
@handle_exceptions |
|
def create_tags_distribution_chart(data: List[Dict[str, Any]], title: str = "Tags Distribution", |
|
tags_key: str = "tags", top_n: int = 10) -> go.Figure: |
|
""" |
|
Create a bar chart showing the distribution of tags |
|
|
|
Args: |
|
data: List of data items (tasks, notes, goals, etc.) |
|
title: Chart title |
|
tags_key: Key in the data items that contains the tags |
|
top_n: Number of top tags to display |
|
|
|
Returns: |
|
Plotly figure object |
|
""" |
|
logger.debug(f"Creating tags distribution chart with {len(data)} items") |
|
|
|
|
|
all_tags = [] |
|
for item in data: |
|
tags = safe_get(item, tags_key, []) |
|
if tags and isinstance(tags, list): |
|
all_tags.extend(tags) |
|
|
|
if not all_tags: |
|
|
|
fig = go.Figure() |
|
fig.update_layout(title=title, height=300) |
|
return fig |
|
|
|
|
|
tag_counts = Counter(all_tags) |
|
|
|
|
|
top_tags = tag_counts.most_common(top_n) |
|
tags = [tag for tag, _ in top_tags] |
|
counts = [count for _, count in top_tags] |
|
|
|
|
|
fig = go.Figure(data=[go.Bar( |
|
x=tags, |
|
y=counts, |
|
marker_color=UI_COLORS["primary"], |
|
text=counts, |
|
textposition='auto', |
|
)]) |
|
|
|
fig.update_layout( |
|
title=title, |
|
xaxis_title="Tag", |
|
yaxis_title="Count", |
|
margin=dict(l=20, r=20, t=40, b=20), |
|
height=300 |
|
) |
|
|
|
return fig |
|
|
|
@handle_exceptions |
|
def create_sentiment_chart(notes: List[Dict[str, Any]], title: str = "Notes Sentiment Analysis", |
|
content_key: str = "content", timestamp_key: str = "created_at") -> go.Figure: |
|
""" |
|
Create a line chart showing sentiment analysis of notes over time |
|
|
|
Args: |
|
notes: List of note items |
|
title: Chart title |
|
content_key: Key in the note items that contains the content |
|
timestamp_key: Key in the note items that contains the timestamp |
|
|
|
Returns: |
|
Plotly figure object |
|
""" |
|
logger.debug(f"Creating sentiment chart with {len(notes)} notes") |
|
|
|
if not notes: |
|
|
|
fig = go.Figure() |
|
fig.update_layout(title=title, height=300) |
|
return fig |
|
|
|
|
|
sorted_notes = sorted(notes, key=lambda x: safe_get(x, timestamp_key, 0)) |
|
|
|
|
|
dates = [] |
|
sentiments = [] |
|
texts = [] |
|
|
|
for note in sorted_notes: |
|
content = safe_get(note, content_key, "") |
|
timestamp = safe_get(note, timestamp_key, None) |
|
title_text = safe_get(note, "title", "Untitled Note") |
|
|
|
if not content or not timestamp: |
|
continue |
|
|
|
|
|
try: |
|
from utils.ai_models import analyze_sentiment |
|
sentiment = analyze_sentiment(content) |
|
except Exception as e: |
|
logger.warning(f"Error analyzing sentiment: {str(e)}") |
|
|
|
sentiment = (np.random.random() - 0.5) |
|
|
|
|
|
if isinstance(timestamp, str): |
|
try: |
|
date = datetime.datetime.fromisoformat(timestamp.replace('Z', '+00:00')) |
|
except ValueError: |
|
logger.warning(f"Could not parse timestamp: {timestamp}") |
|
continue |
|
else: |
|
date = datetime.datetime.fromtimestamp(timestamp) |
|
|
|
dates.append(date.strftime("%Y-%m-%d")) |
|
sentiments.append(sentiment) |
|
texts.append(f"<b>{title_text}</b><br>{content[:100]}..." if len(content) > 100 else f"<b>{title_text}</b><br>{content}") |
|
|
|
if not dates: |
|
|
|
fig = go.Figure() |
|
fig.update_layout(title=title, height=300) |
|
return fig |
|
|
|
|
|
fig = go.Figure(data=go.Scatter( |
|
x=dates, |
|
y=sentiments, |
|
mode='lines+markers', |
|
line=dict(color=UI_COLORS["primary"], width=2), |
|
marker=dict( |
|
size=10, |
|
color=sentiments, |
|
colorscale=[[0, UI_COLORS["danger"]], [0.5, UI_COLORS["warning"]], [1, UI_COLORS["success"]]], |
|
cmin=-1, |
|
cmax=1, |
|
showscale=True, |
|
colorbar=dict(title="Sentiment") |
|
), |
|
text=texts, |
|
hoverinfo="text+x+y" |
|
)) |
|
|
|
fig.update_layout( |
|
title=title, |
|
xaxis_title="Date", |
|
yaxis_title="Sentiment Score", |
|
yaxis=dict(range=[-1, 1]), |
|
margin=dict(l=20, r=20, t=40, b=20), |
|
height=400 |
|
) |
|
|
|
|
|
fig.add_shape( |
|
type="line", |
|
x0=dates[0], |
|
y0=0, |
|
x1=dates[-1], |
|
y1=0, |
|
line=dict(color="gray", width=1, dash="dash") |
|
) |
|
|
|
return fig |
|
|
|
@handle_exceptions |
|
def create_model_usage_distribution(activities: List[Dict[str, Any]], title: str = "AI Model Usage Distribution") -> go.Figure: |
|
""" |
|
Create a pie chart showing the distribution of AI model usage |
|
|
|
Args: |
|
activities: List of activity items |
|
title: Chart title |
|
|
|
Returns: |
|
Plotly figure object |
|
""" |
|
logger.debug(f"Creating model usage distribution chart with {len(activities)} activities") |
|
|
|
|
|
ai_activities = [] |
|
for activity in activities: |
|
activity_type = safe_get(activity, "type", "") |
|
if "ai_" in activity_type or "model_" in activity_type: |
|
ai_activities.append(activity) |
|
|
|
if not ai_activities: |
|
|
|
fig = go.Figure() |
|
fig.update_layout(title=title, height=300) |
|
return fig |
|
|
|
|
|
model_counts = defaultdict(int) |
|
for activity in ai_activities: |
|
activity_type = safe_get(activity, "type", "unknown") |
|
|
|
model_type = activity_type.replace("ai_", "").replace("model_", "").replace("_", " ").title() |
|
model_counts[model_type] += 1 |
|
|
|
|
|
models = list(model_counts.keys()) |
|
counts = list(model_counts.values()) |
|
|
|
|
|
colors = px.colors.qualitative.Plotly[:len(models)] if len(models) <= 10 else px.colors.qualitative.Alphabet |
|
|
|
|
|
fig = go.Figure(data=[go.Pie( |
|
labels=models, |
|
values=counts, |
|
hole=0.4, |
|
marker=dict(colors=colors), |
|
textinfo="label+percent", |
|
insidetextorientation="radial" |
|
)]) |
|
|
|
fig.update_layout( |
|
title=title, |
|
margin=dict(l=20, r=20, t=40, b=20), |
|
height=300, |
|
legend=dict(orientation="h", yanchor="bottom", y=-0.2, xanchor="center", x=0.5) |
|
) |
|
|
|
return fig |
|
|
|
@handle_exceptions |
|
def create_model_usage_over_time(activities: List[Dict[str, Any]], title: str = "AI Model Usage Over Time", |
|
timestamp_key: str = "timestamp", group_by: str = "day") -> go.Figure: |
|
""" |
|
Create a stacked area chart showing AI model usage over time |
|
|
|
Args: |
|
activities: List of activity items |
|
title: Chart title |
|
timestamp_key: Key in the activity items that contains the timestamp |
|
group_by: Time grouping ("day", "week", "month") |
|
|
|
Returns: |
|
Plotly figure object |
|
""" |
|
logger.debug(f"Creating model usage over time chart with {len(activities)} activities") |
|
|
|
|
|
ai_activities = [] |
|
for activity in activities: |
|
activity_type = safe_get(activity, "type", "") |
|
if "ai_" in activity_type or "model_" in activity_type: |
|
ai_activities.append(activity) |
|
|
|
if not ai_activities: |
|
|
|
fig = go.Figure() |
|
fig.update_layout(title=title, height=400) |
|
return fig |
|
|
|
|
|
date_model_counts = defaultdict(lambda: defaultdict(int)) |
|
model_types = set() |
|
|
|
for activity in ai_activities: |
|
timestamp = safe_get(activity, timestamp_key, None) |
|
if not timestamp: |
|
continue |
|
|
|
|
|
if isinstance(timestamp, str): |
|
try: |
|
date = datetime.datetime.fromisoformat(timestamp.replace('Z', '+00:00')) |
|
except ValueError: |
|
logger.warning(f"Could not parse timestamp: {timestamp}") |
|
continue |
|
else: |
|
date = datetime.datetime.fromtimestamp(timestamp) |
|
|
|
|
|
if group_by == "day": |
|
date_key = date.strftime("%Y-%m-%d") |
|
elif group_by == "week": |
|
|
|
start_of_week = date - datetime.timedelta(days=date.weekday()) |
|
date_key = start_of_week.strftime("%Y-%m-%d") |
|
elif group_by == "month": |
|
date_key = date.strftime("%Y-%m") |
|
else: |
|
logger.warning(f"Unknown group_by value: {group_by}, using day") |
|
date_key = date.strftime("%Y-%m-%d") |
|
|
|
|
|
activity_type = safe_get(activity, "type", "unknown") |
|
model_type = activity_type.replace("ai_", "").replace("model_", "").replace("_", " ").title() |
|
model_types.add(model_type) |
|
|
|
|
|
date_model_counts[date_key][model_type] += 1 |
|
|
|
if not date_model_counts: |
|
|
|
fig = go.Figure() |
|
fig.update_layout(title=title, height=400) |
|
return fig |
|
|
|
|
|
sorted_dates = sorted(date_model_counts.keys()) |
|
|
|
|
|
if group_by == "day": |
|
x_labels = [datetime.datetime.strptime(d, "%Y-%m-%d").strftime("%b %d") for d in sorted_dates] |
|
elif group_by == "week": |
|
x_labels = [f"Week of {datetime.datetime.strptime(d, '%Y-%m-%d').strftime('%b %d')}" for d in sorted_dates] |
|
elif group_by == "month": |
|
x_labels = [datetime.datetime.strptime(d, "%Y-%m").strftime("%b %Y") for d in sorted_dates] |
|
else: |
|
x_labels = sorted_dates |
|
|
|
|
|
model_types = sorted(model_types) |
|
|
|
|
|
colors = px.colors.qualitative.Plotly[:len(model_types)] if len(model_types) <= 10 else px.colors.qualitative.Alphabet |
|
|
|
|
|
fig = go.Figure() |
|
|
|
for i, model_type in enumerate(model_types): |
|
y_values = [date_model_counts[date].get(model_type, 0) for date in sorted_dates] |
|
|
|
fig.add_trace(go.Scatter( |
|
x=x_labels, |
|
y=y_values, |
|
mode='lines', |
|
stackgroup='one', |
|
name=model_type, |
|
line=dict(width=0.5, color=colors[i % len(colors)]), |
|
fillcolor=colors[i % len(colors)] |
|
)) |
|
|
|
fig.update_layout( |
|
title=title, |
|
xaxis_title="Date", |
|
yaxis_title="Usage Count", |
|
margin=dict(l=20, r=20, t=40, b=20), |
|
height=400, |
|
legend=dict(orientation="h", yanchor="bottom", y=-0.3, xanchor="center", x=0.5) |
|
) |
|
|
|
return fig |
|
|
|
@handle_exceptions |
|
def create_completion_time_chart(data: List[Dict[str, Any]], title: str = "Completion Time Distribution", |
|
created_key: str = "created_at", completed_key: str = "completed_at") -> go.Figure: |
|
""" |
|
Create a histogram showing the distribution of completion times |
|
|
|
Args: |
|
data: List of data items (tasks, goals, etc.) |
|
title: Chart title |
|
created_key: Key in the data items that contains the creation timestamp |
|
completed_key: Key in the data items that contains the completion timestamp |
|
|
|
Returns: |
|
Plotly figure object |
|
""" |
|
logger.debug(f"Creating completion time chart with {len(data)} items") |
|
|
|
|
|
completed_items = [] |
|
for item in data: |
|
created_at = safe_get(item, created_key, None) |
|
completed_at = safe_get(item, completed_key, None) |
|
|
|
if not created_at or not completed_at: |
|
continue |
|
|
|
|
|
if isinstance(created_at, str): |
|
try: |
|
created_date = datetime.datetime.fromisoformat(created_at.replace('Z', '+00:00')) |
|
except ValueError: |
|
logger.warning(f"Could not parse timestamp: {created_at}") |
|
continue |
|
else: |
|
created_date = datetime.datetime.fromtimestamp(created_at) |
|
|
|
if isinstance(completed_at, str): |
|
try: |
|
completed_date = datetime.datetime.fromisoformat(completed_at.replace('Z', '+00:00')) |
|
except ValueError: |
|
logger.warning(f"Could not parse timestamp: {completed_at}") |
|
continue |
|
else: |
|
completed_date = datetime.datetime.fromtimestamp(completed_at) |
|
|
|
|
|
completion_time = (completed_date - created_date).total_seconds() / (60 * 60 * 24) |
|
|
|
|
|
if completion_time > 0: |
|
completed_items.append({ |
|
"item": item, |
|
"completion_time": completion_time |
|
}) |
|
|
|
if not completed_items: |
|
|
|
fig = go.Figure() |
|
fig.update_layout(title=title, height=300) |
|
return fig |
|
|
|
|
|
completion_times = [item["completion_time"] for item in completed_items] |
|
|
|
|
|
fig = go.Figure(data=go.Histogram( |
|
x=completion_times, |
|
nbinsx=20, |
|
marker_color=UI_COLORS["primary"], |
|
opacity=0.7 |
|
)) |
|
|
|
|
|
avg_time = np.mean(completion_times) |
|
median_time = np.median(completion_times) |
|
|
|
|
|
fig.add_shape( |
|
type="line", |
|
x0=avg_time, |
|
y0=0, |
|
x1=avg_time, |
|
y1=1, |
|
yref="paper", |
|
line=dict(color=UI_COLORS["success"], width=2, dash="dash"), |
|
) |
|
|
|
fig.add_shape( |
|
type="line", |
|
x0=median_time, |
|
y0=0, |
|
x1=median_time, |
|
y1=1, |
|
yref="paper", |
|
line=dict(color=UI_COLORS["warning"], width=2, dash="dash"), |
|
) |
|
|
|
|
|
fig.add_annotation( |
|
x=avg_time, |
|
y=0.95, |
|
yref="paper", |
|
text=f"Average: {avg_time:.1f} days", |
|
showarrow=True, |
|
arrowhead=1, |
|
ax=40, |
|
ay=-40, |
|
bgcolor=UI_COLORS["success"], |
|
font=dict(color="white") |
|
) |
|
|
|
fig.add_annotation( |
|
x=median_time, |
|
y=0.85, |
|
yref="paper", |
|
text=f"Median: {median_time:.1f} days", |
|
showarrow=True, |
|
arrowhead=1, |
|
ax=-40, |
|
ay=-40, |
|
bgcolor=UI_COLORS["warning"], |
|
font=dict(color="white") |
|
) |
|
|
|
fig.update_layout( |
|
title=title, |
|
xaxis_title="Completion Time (days)", |
|
yaxis_title="Count", |
|
margin=dict(l=20, r=20, t=40, b=20), |
|
height=300 |
|
) |
|
|
|
return fig |