# Vizro is an open-source toolkit for creating modular data visualization applications.
# check out https://github.com/mckinsey/vizro for more info about Vizro
# and checkout https://vizro.readthedocs.io/en/stable/ for documentation.
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "vizro>=0.1.52",
# "pandas",
# "numpy",
# ]
# ///
"""Visual demo: 3 chart types with customer support ticket routing context."""
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import vizro.models as vm
import vizro.plotly.express as px
from vizro import Vizro
from vizro.managers import data_manager
from vizro.models.types import capture
# =============================================================================
# VIZRO PALETTE COLORS
# =============================================================================
COLOR_ROUTED = "#097DFE" # blue
COLOR_ESCALATED = "#6F39E3" # dark_purple
# =============================================================================
# MOCK DATA: Customer Support Ticket Routing
# =============================================================================
np.random.seed(99)
TEAMS = ["Billing", "Technical", "Account", "Shipping", "Returns", "Onboarding", "Loyalty", "Fraud"]
AGENTS = {
"Billing": ["Amy Chen", "Ben Torres"],
"Technical": ["Fiona Lee", "Greg Hall"],
"Account": ["Kay Brown", "Leo Scott"],
"Shipping": ["Pat Wood", "Quinn Price"],
"Returns": ["Uma Nash", "Vic Stone"],
"Onboarding": ["Ava Mills", "Blake Hunt"],
"Loyalty": ["Finn Hayes", "Gwen Sharp"],
"Fraud": ["Kira Walsh", "Luca Vega"],
}
TICKET_CATEGORIES = ["Billing Dispute", "Password Reset", "Refund Request", "Delivery Issue",
"Product Defect", "Subscription Change", "Account Lockout"]
ROUTING_ACTIONS = ["Resolved", "Escalated"]
ESCALATION_REASONS = [
"Requires manager approval",
"Customer requested supervisor",
"Policy exception needed",
"Technical limitation",
"Repeated contact (3+ times)",
"High-value account",
"Cross-department handoff required",
"Compliance review needed",
# "Awaiting vendor response",
# "SLA breach risk",
]
WEEKS = pd.date_range("2026-01-05", periods=16, freq="W-MON").strftime("%Y-%m-%d").tolist()
rows = []
for _ in range(3000):
team = np.random.choice(TEAMS, p=[0.18, 0.15, 0.14, 0.13, 0.10, 0.12, 0.10, 0.08])
agent = np.random.choice(AGENTS[team])
category = np.random.choice(TICKET_CATEGORIES)
week = np.random.choice(WEEKS)
is_escalated = np.random.random() < 0.30
action = "Escalated" if is_escalated else "Resolved"
reason = np.random.choice(ESCALATION_REASONS) if is_escalated else None
rows.append({
"team": team,
"agent": agent,
"category": category,
"week": week,
"action": action,
"escalation_reason": reason,
"is_resolved": int(not is_escalated),
"is_escalated": int(is_escalated),
})
df = pd.DataFrame(rows)
# =============================================================================
# CHARTS
# =============================================================================
@capture("graph")
def escalation_reason_breakdown(data_frame: pd.DataFrame) -> go.Figure:
"""Horizontal stacked bar: escalation reason breakdown by agent."""
escalated = data_frame[data_frame["action"] == "Escalated"]
if len(escalated) == 0:
fig = go.Figure()
fig.update_layout(annotations=[dict(text="No escalated tickets", showarrow=False, font=dict(size=16))])
return fig
agg = escalated.groupby(["agent", "escalation_reason"]).size().reset_index(name="count")
fig = px.bar(agg, y="agent", x="count", color="escalation_reason", barmode="stack", orientation="h")
fig.update_layout(yaxis_title="", xaxis_title="Count", legend_title="Reason", height=540)
return fig
@capture("graph")
def ticket_routing_sankey(data_frame: pd.DataFrame) -> go.Figure:
"""Sankey: ticket category -> Resolved/Escalated -> escalation reason."""
categories = sorted(data_frame["category"].unique())
actions = ["Resolved", "Escalated"]
reasons = sorted(data_frame["escalation_reason"].dropna().unique())
labels = list(categories) + actions + list(reasons)
n_cat = len(categories)
idx_resolved = n_cat
idx_escalated = n_cat + 1
idx_reason_start = n_cat + 2
node_colors = (
["#05D0F0"] * n_cat
+ [COLOR_ROUTED, COLOR_ESCALATED]
+ ["#97A1B0"] * len(reasons)
)
sources, targets, values, link_colors = [], [], [], []
for i, cat in enumerate(categories):
cat_data = data_frame[data_frame["category"] == cat]
resolved_count = int(cat_data["is_resolved"].sum())
escalated_count = int(cat_data["is_escalated"].sum())
if resolved_count > 0:
sources.append(i)
targets.append(idx_resolved)
values.append(resolved_count)
link_colors.append("rgba(9, 125, 254, 0.4)")
if escalated_count > 0:
sources.append(i)
targets.append(idx_escalated)
values.append(escalated_count)
link_colors.append("rgba(111, 57, 227, 0.4)")
escalated = data_frame[data_frame["action"] == "Escalated"]
if len(escalated) > 0:
reason_counts = escalated.groupby("escalation_reason").size()
for reason, count in reason_counts.items():
if reason in reasons:
sources.append(idx_escalated)
targets.append(idx_reason_start + reasons.index(reason))
values.append(int(count))
link_colors.append("rgba(111, 57, 227, 0.25)")
fig = go.Figure(go.Sankey(
node=dict(label=labels, color=node_colors, pad=15, thickness=20),
link=dict(source=sources, target=targets, value=values, color=link_colors),
))
fig.update_layout(margin=dict(l=10, r=10, t=10, b=10))
return fig
@capture("graph")
def weekly_volume_by_team(data_frame: pd.DataFrame) -> go.Figure:
"""Multi-line: weekly ticket volume trend by team."""
agg = data_frame.groupby(["week", "team"]).size().reset_index(name="ticket_count").sort_values("week")
fig = px.line(agg, x="week", y="ticket_count", color="team", markers=True)
fig.update_layout(xaxis_title="Week", yaxis_title="Ticket Count")
return fig
# =============================================================================
# DASHBOARD
# =============================================================================
data_manager["tickets"] = df
demo_page = vm.Page(
title="Visual Demo",
layout=vm.Grid(
grid=[
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
],
row_min_height="140px",
),
components=[
vm.Graph(title="Ticket Routing Flow", figure=ticket_routing_sankey(data_frame="tickets")),
vm.Graph(title="Escalation Reason Breakdown by Agent", figure=escalation_reason_breakdown(data_frame="tickets")),
vm.Graph(title="Weekly Ticket Volume by Team", figure=weekly_volume_by_team(data_frame="tickets")),
],
controls=[
vm.Filter(column="team", selector=vm.Dropdown(title="Team")),
vm.Filter(column="category", selector=vm.Dropdown(title="Ticket Category")),
],
)
dashboard = vm.Dashboard(
title="Customer Support Analytics",
pages=[demo_page],
)
Vizro().build(dashboard).run()