"""
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