""" Premium Gantt-style timeline visualization using Plotly. Features: - Glassmorphic dark theme matching the UI - Color coding by priority and status - Conflict highlighting with red borders - Custom HH:MM time axis - Inbox summary bar chart """ import plotly.graph_objects as go from typing import List, Dict # ─── Premium Color Palette ─────────────────────────────────────────────────── PRIORITY_COLORS = { "high": "#f87171", "medium": "#fbbf24", "low": "#60a5fa", } STATUS_COLORS = { "completed": "#34d399", "scheduled": "#a78bfa", "pending": "#fbbf24", "missed": "#f87171", "deferred": "#94a3b8", "rejected": "#6b7280", } TYPE_LABELS = { "meeting": "Meeting", "work": "Work", "personal": "Personal", } PLOT_BG = "#0f1629" PAPER_BG = "#0a0f1e" GRID_COLOR = "rgba(148, 163, 184, 0.08)" TEXT_COLOR = "#c7d2fe" SUBTLE_TEXT = "#64748b" def _time_to_minutes(time_str: str) -> int: h, m = map(int, time_str.split(":")) return h * 60 + m def _minutes_to_time(minutes: int) -> str: return f"{minutes // 60:02d}:{minutes % 60:02d}" def _add_minutes(time_str: str, minutes: int) -> str: return _minutes_to_time(_time_to_minutes(time_str) + minutes) # ─── Main Timeline ─────────────────────────────────────────────────────────── def create_timeline( tasks: List[Dict], current_time: str = "08:00", title: str = "Executive Schedule Timeline", show_conflicts: bool = True, ) -> go.Figure: """Create a premium Gantt-style timeline.""" fig = go.Figure() if not tasks: fig.add_annotation( text="No tasks scheduled yet", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=18, color=SUBTLE_TEXT, family="Inter"), ) fig.update_layout( template="plotly_dark", paper_bgcolor=PAPER_BG, plot_bgcolor=PLOT_BG, title=dict(text=title, font=dict(size=16, color=TEXT_COLOR, family="Inter")), height=200, ) return fig sorted_tasks = sorted(tasks, key=lambda t: t.get("time", "08:00")) conflict_pairs = set() if show_conflicts: conflict_pairs = _find_conflict_pairs(sorted_tasks) for idx, task in enumerate(sorted_tasks): start_time = task.get("time", "08:00") duration = task.get("duration", 30) end_time = _add_minutes(start_time, duration) status = task.get("status", "pending") priority = task.get("priority", "medium") task_type = task.get("type", "work") task_title = task.get("title", f"Task {task.get('id', idx)}") task_id = task.get("id", idx) color = STATUS_COLORS.get(status, PRIORITY_COLORS.get(priority, "#60a5fa")) is_conflicted = any(task_id in pair for pair in conflict_pairs) border_color = "#ef4444" if is_conflicted else "rgba(255,255,255,0.1)" border_width = 3 if is_conflicted else 1 type_label = TYPE_LABELS.get(task_type, "Task") label = f"{task_title}" conflict_text = ( "CONFLICT DETECTED" if is_conflicted else "No conflict" ) hover_text = ( f"{task_title}
" f"Time: {start_time} - {end_time} ({duration}min)
" f"Priority: {priority.upper()}
" f"Status: {status.upper()}
" f"Type: {type_label}
" f"{conflict_text}" ) start_mins = _time_to_minutes(start_time) fig.add_trace(go.Bar( x=[duration], y=[label], base=[start_mins], orientation="h", marker=dict( color=color, line=dict(color=border_color, width=border_width), opacity=0.88 if status != "missed" else 0.35, cornerradius=6, ), hovertext=hover_text, hoverinfo="text", showlegend=False, text=f" {start_time} - {end_time} ", textposition="inside", textfont=dict(color="white", size=11, family="Inter"), )) # "Now" indicator now_mins = _time_to_minutes(current_time) fig.add_shape( type="line", x0=now_mins, x1=now_mins, y0=-0.5, y1=len(sorted_tasks) - 0.5, line=dict(color="#818cf8", width=2, dash="dot"), ) fig.add_annotation( x=now_mins, y=-0.6, text=f"NOW {current_time}", showarrow=False, font=dict(color="#818cf8", size=11, family="Inter", weight="bold" if hasattr(dict, '__call__') else None), bgcolor="rgba(129,140,248,0.1)", bordercolor="rgba(129,140,248,0.3)", borderwidth=1, borderpad=4, ) # Time axis tick_vals = list(range(480, 1081, 30)) # 08:00 to 18:00 tick_labels = [_minutes_to_time(m) for m in tick_vals] fig.update_layout( title=dict( text=title, font=dict(size=16, color=TEXT_COLOR, family="Inter"), x=0.01, ), xaxis=dict( tickvals=tick_vals, ticktext=tick_labels, range=[460, 1100], gridcolor=GRID_COLOR, tickfont=dict(color=SUBTLE_TEXT, size=10, family="Inter"), title=None, ), yaxis=dict( autorange="reversed", gridcolor=GRID_COLOR, tickfont=dict(color=TEXT_COLOR, size=11, family="Inter"), title=None, ), template="plotly_dark", paper_bgcolor=PAPER_BG, plot_bgcolor=PLOT_BG, height=max(280, len(sorted_tasks) * 48 + 120), margin=dict(l=220, r=40, t=60, b=50), barmode="overlay", hoverlabel=dict( bgcolor="#1e293b", bordercolor="rgba(255,255,255,0.1)", font=dict(color=TEXT_COLOR, size=12, family="Inter"), ), ) # Status legend for status, color in STATUS_COLORS.items(): fig.add_trace(go.Bar( x=[0], y=["_"], marker=dict(color=color), name=status.capitalize(), showlegend=True, visible="legendonly", )) # Conflict warning if conflict_pairs: fig.add_annotation( text=f" {len(conflict_pairs)} conflict(s) detected ", xref="paper", yref="paper", x=0.99, y=1.06, xanchor="right", showarrow=False, font=dict(size=12, color="#fca5a5", family="Inter"), bgcolor="rgba(239,68,68,0.12)", bordercolor="rgba(239,68,68,0.3)", borderwidth=1, borderpad=6, ) fig.update_layout( legend=dict( orientation="h", yanchor="bottom", y=-0.2, xanchor="center", x=0.5, font=dict(size=10, color=SUBTLE_TEXT, family="Inter"), bgcolor="rgba(0,0,0,0)", ) ) return fig def _find_conflict_pairs(tasks: List[Dict]) -> set: conflicts = set() for i, t1 in enumerate(tasks): for t2 in tasks[i + 1:]: s1 = _time_to_minutes(t1.get("time", "08:00")) e1 = s1 + t1.get("duration", 30) s2 = _time_to_minutes(t2.get("time", "08:00")) e2 = s2 + t2.get("duration", 30) if s1 < e2 and s2 < e1: conflicts.add((t1.get("id", i), t2.get("id", i + 1))) return conflicts # ─── Inbox Summary Chart ──────────────────────────────────────────────────── def create_inbox_summary(inbox: List[Dict]) -> go.Figure: """Create a premium inbox status chart.""" fig = go.Figure() if not inbox: fig.add_annotation( text="No messages", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=16, color=SUBTLE_TEXT, family="Inter"), ) fig.update_layout( template="plotly_dark", paper_bgcolor=PAPER_BG, plot_bgcolor=PLOT_BG, height=280, ) return fig total = len(inbox) replied = sum(1 for m in inbox if m.get("replied", False)) urgent = sum(1 for m in inbox if m.get("urgency") == "high") urgent_replied = sum( 1 for m in inbox if m.get("urgency") == "high" and m.get("replied", False) ) categories = ["Total", "Replied", "Urgent", "Urgent\nReplied"] values = [total, replied, urgent, urgent_replied] colors = ["#818cf8", "#34d399", "#f87171", "#fbbf24"] fig.add_trace(go.Bar( x=categories, y=values, marker=dict( color=colors, line=dict(color="rgba(255,255,255,0.05)", width=1), cornerradius=8, opacity=0.85, ), text=values, textposition="outside", textfont=dict(size=16, color=TEXT_COLOR, family="Inter", weight=700), )) fig.update_layout( title=dict( text="Inbox Status", font=dict(size=14, color=TEXT_COLOR, family="Inter"), x=0.01, ), template="plotly_dark", paper_bgcolor=PAPER_BG, plot_bgcolor=PLOT_BG, height=280, yaxis=dict( gridcolor=GRID_COLOR, tickfont=dict(color=SUBTLE_TEXT, size=10, family="Inter"), title=None, ), xaxis=dict( tickfont=dict(color=TEXT_COLOR, size=11, family="Inter"), title=None, ), margin=dict(l=40, r=20, t=50, b=40), hoverlabel=dict( bgcolor="#1e293b", font=dict(color=TEXT_COLOR, family="Inter"), ), ) return fig