Spaces:
Sleeping
Sleeping
| """ | |
| 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("<div style='height:0.75rem'></div>", 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="<b>%{x}</b><br>Focus: %{y:.0f} min<extra></extra>" | |
| ) | |
| fig.add_bar( | |
| x=df_daily["day"], y=df_daily["idle_min"], | |
| name="Idle", marker_color="#374151", | |
| hovertemplate="<b>%{x}</b><br>Idle: %{y:.0f} min<extra></extra>" | |
| ) | |
| 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("<p style='color:#5a5a7a'>No data for this range.</p>", 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""" | |
| <div style="display:flex; align-items:center; gap:10px; padding:8px 0; border-bottom:1px solid #1e1e2e;"> | |
| <div style="font-family:'DM Mono',monospace; font-size:0.8rem; color:#5a5a7a; min-width:20px;">#{rank}</div> | |
| <div style="flex:1; min-width:0;"> | |
| <div style="font-size:0.85rem; font-weight:500; color:#f0f0ff; margin-bottom:3px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{row['app_name'][:30]}</div> | |
| <div style="background:#1e1e2e; border-radius:4px; height:4px;"> | |
| <div style="background:{color}; width:{pct}%; height:100%; border-radius:4px;"></div> | |
| </div> | |
| </div> | |
| <div style="font-family:'DM Mono',monospace; font-size:0.8rem; color:#9090b0; min-width:55px; text-align:right;">{fmt_duration(row['total_seconds'])}</div> | |
| </div>""" | |
| st.markdown(f'<div style="font-family:Inter,sans-serif;">{html}</div>', unsafe_allow_html=True) | |
| else: | |
| st.markdown("<p style='color:#5a5a7a'>No data for this range.</p>", 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="<b>%{y}</b> at <b>%{x}</b><br>%{z:.0f} minutes<extra></extra>", | |
| )) | |
| 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("<p style='color:#5a5a7a; font-size:0.85rem;'>No data to export for selected range.</p>", | |
| unsafe_allow_html=True) | |