""" FocusTrack - Page 2: Analytics & Reports Date-range charts, heatmaps, export. """ import streamlit as st import plotly.express as px import plotly.graph_objects as go import pandas as pd import json import io from datetime import datetime, timedelta, date from ui_utils import ( fmt_duration, fmt_duration_long, get_focus_score, privacy_badge, page_header, CATEGORY_COLORS, get_db, get_date_range ) def render(): db = get_db() privacy_badge() page_header("Analytics & Reports", "Visualize your productivity trends") # ── Date Range Selector ─────────────────────────────────────────────────── col_range, col_start, col_end = st.columns([1.5, 1, 1]) with col_range: range_sel = st.selectbox( "Date range", ["Today", "Yesterday", "Last 7 days", "Last 30 days", "Custom"], index=2, ) start, end = get_date_range(range_sel) if range_sel == "Custom": with col_start: start_d = st.date_input("From", value=date.today() - timedelta(days=6)) with col_end: end_d = st.date_input("To", value=date.today()) start = datetime.combine(start_d, datetime.min.time()) end = datetime.combine(end_d, datetime.max.time()) # ── Summary Metrics ─────────────────────────────────────────────────────── summary = db.get_summary(start, end) focus_sec = summary.get("focus_seconds") or 0 idle_sec = summary.get("idle_seconds") or 0 total_sec = summary.get("total_seconds") or 0 c1, c2, c3, c4 = st.columns(4) with c1: st.metric("Total Focus Time", fmt_duration_long(focus_sec)) with c2: st.metric("Total Idle Time", fmt_duration_long(idle_sec)) with c3: st.metric("Focus Score", f"{get_focus_score(focus_sec, total_sec)}%") with c4: st.metric("Tracked Time", fmt_duration_long(total_sec)) st.markdown("
", unsafe_allow_html=True) # ── Daily Activity Bar Chart ────────────────────────────────────────────── daily = db.get_daily_totals(start, end) if daily: st.markdown("#### Daily Activity") df_daily = pd.DataFrame(daily) df_daily["focus_min"] = df_daily["focus_seconds"] / 60 df_daily["idle_min"] = df_daily["idle_seconds"] / 60 fig = go.Figure() fig.add_bar( x=df_daily["day"], y=df_daily["focus_min"], name="Focus", marker_color="#6366f1", hovertemplate="%{x}
Focus: %{y:.0f} min" ) fig.add_bar( x=df_daily["day"], y=df_daily["idle_min"], name="Idle", marker_color="#374151", hovertemplate="%{x}
Idle: %{y:.0f} min" ) fig.update_layout( barmode="stack", height=280, paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", font=dict(color="#9090b0", family="Inter"), margin=dict(t=10, b=30, l=0, r=0), xaxis=dict(showgrid=False), yaxis=dict(showgrid=True, gridcolor="#2a2a3a", title="Minutes"), legend=dict(orientation="h", yanchor="bottom", y=1, xanchor="left", x=0), ) st.plotly_chart(fig, use_container_width=True, config={"displayModeBar": False}) # ── Two-column: Category Breakdown + Top Apps ───────────────────────────── col_l, col_r = st.columns(2) with col_l: st.markdown("#### Time by Category") cat_data = db.get_by_category(start, end) if cat_data: df_cat = pd.DataFrame(cat_data) df_cat["minutes"] = df_cat["total_seconds"] / 60 colors = [CATEGORY_COLORS.get(c, "#6366f1") for c in df_cat["category"]] fig_cat = px.bar( df_cat, x="minutes", y="category", orientation="h", color="category", color_discrete_map=CATEGORY_COLORS, labels={"minutes": "Minutes", "category": ""}, ) fig_cat.update_layout( height=300, showlegend=False, paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", font=dict(color="#9090b0", family="Inter"), margin=dict(t=10, b=30, l=0, r=0), xaxis=dict(showgrid=True, gridcolor="#2a2a3a"), yaxis=dict(showgrid=False), ) st.plotly_chart(fig_cat, use_container_width=True, config={"displayModeBar": False}) else: st.markdown("

No data for this range.

", unsafe_allow_html=True) with col_r: st.markdown("#### Top Apps Leaderboard") apps_data = db.get_by_app(start, end, limit=10) if apps_data: df_apps = pd.DataFrame(apps_data) df_apps["minutes"] = df_apps["total_seconds"] / 60 html = "" max_min = df_apps["minutes"].max() or 1 for i, row in df_apps.iterrows(): cat = row.get("category", "uncategorized") color = CATEGORY_COLORS.get(cat, "#6366f1") pct = int((row["minutes"] / max_min) * 100) rank = i + 1 html += f"""
#{rank}
{row['app_name'][:30]}
{fmt_duration(row['total_seconds'])}
""" st.markdown(f'
{html}
', unsafe_allow_html=True) else: st.markdown("

No data for this range.

", unsafe_allow_html=True) # ── Weekly Heatmap ──────────────────────────────────────────────────────── if range_sel in ("Last 7 days", "Last 30 days", "Custom"): st.markdown("#### Weekly Activity Heatmap (hours × days)") acts = db.get_activities(start, end, limit=50000) if acts: df_all = pd.DataFrame(acts) df_all["dt"] = pd.to_datetime(df_all["timestamp"]) df_all["hour"] = df_all["dt"].dt.hour df_all["day"] = df_all["dt"].dt.strftime("%a %b %d") heatmap = df_all.groupby(["day", "hour"])["duration_seconds"].sum().reset_index() heatmap["minutes"] = heatmap["duration_seconds"] / 60 pivot = heatmap.pivot_table(index="day", columns="hour", values="minutes", fill_value=0) fig_hm = go.Figure(go.Heatmap( z=pivot.values, x=[f"{h:02d}:00" for h in pivot.columns], y=pivot.index.tolist(), colorscale=[[0, "#0a0a0f"], [0.3, "#1a1a3a"], [0.7, "#4040a0"], [1, "#6366f1"]], showscale=True, hovertemplate="%{y} at %{x}
%{z:.0f} minutes", )) fig_hm.update_layout( height=max(200, len(pivot) * 30 + 80), paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", font=dict(color="#9090b0", family="Inter"), margin=dict(t=10, b=30, l=80, r=0), xaxis=dict(tickfont=dict(size=9)), yaxis=dict(tickfont=dict(size=9)), ) st.plotly_chart(fig_hm, use_container_width=True, config={"displayModeBar": False}) # ── Export ──────────────────────────────────────────────────────────────── st.markdown("---") st.markdown("#### Export Data") col_ex1, col_ex2, col_ex3 = st.columns([1, 1, 4]) acts_for_export = db.get_activities(start, end, limit=100000) if acts_for_export: df_export = pd.DataFrame(acts_for_export) with col_ex1: csv_buf = df_export.to_csv(index=False).encode("utf-8") st.download_button( "📥 Export CSV", csv_buf, file_name=f"focustrack_export_{start.date()}_{end.date()}.csv", mime="text/csv", use_container_width=True ) with col_ex2: json_buf = df_export.to_json(orient="records", indent=2).encode("utf-8") st.download_button( "📥 Export JSON", json_buf, file_name=f"focustrack_export_{start.date()}_{end.date()}.json", mime="application/json", use_container_width=True ) else: st.markdown("

No data to export for selected range.

", unsafe_allow_html=True)