| """ |
| 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 |
|
|
|
|
| |
|
|
| 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) |
|
|
|
|
| |
|
|
| 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 = ( |
| "<span style='color:#ef4444'><b>CONFLICT DETECTED</b></span>" |
| if is_conflicted |
| else "<span style='color:#34d399'>No conflict</span>" |
| ) |
|
|
| hover_text = ( |
| f"<b>{task_title}</b><br>" |
| f"<span style='color:#94a3b8'>Time:</span> {start_time} - {end_time} ({duration}min)<br>" |
| f"<span style='color:#94a3b8'>Priority:</span> {priority.upper()}<br>" |
| f"<span style='color:#94a3b8'>Status:</span> {status.upper()}<br>" |
| f"<span style='color:#94a3b8'>Type:</span> {type_label}<br>" |
| 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_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, |
| ) |
|
|
| |
| tick_vals = list(range(480, 1081, 30)) |
| 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"), |
| ), |
| ) |
|
|
| |
| 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", |
| )) |
|
|
| |
| 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 |
|
|
|
|
| |
|
|
| 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 |
|
|