feat: Add Streamlit dashboard with Blueprint compliance (v2.1.0)
Dashboard Features: - 8 navigation sections: Overview, Outcomes, Poor CX, FCR, Churn, Agent, Call Explorer, Export - Beyond Brand Identity styling (colors #6D84E3, Outfit font) - RCA Sankey diagram (Driver → Outcome → Churn Risk flow) - Correlation heatmaps (driver co-occurrence, driver-outcome) - Outcome Deep Dive (root causes, correlation, duration analysis) - Export functionality (Excel, HTML, JSON) Blueprint Compliance: - FCR: 4 categories (Primera Llamada/Rellamada × Sin/Con Riesgo de Fuga) - Churn: Binary view (Sin Riesgo de Fuga / En Riesgo de Fuga) - Agent: Talento Para Replicar / Oportunidades de Mejora - Fixed FCR rate calculation (only FIRST_CALL counts as success) Technical: - Streamlit + Plotly for interactive visualizations - Light theme configuration (.streamlit/config.toml) - Fixed Plotly colorbar titlefont deprecation Documentation: - Updated PROJECT_CONTEXT.md, TODO.md, CHANGELOG.md - Added 4 new technical decisions (TD-014 to TD-017) - Created TROUBLESHOOTING.md with 10 common issues Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
411
dashboard/config.py
Normal file
411
dashboard/config.py
Normal file
@@ -0,0 +1,411 @@
|
||||
"""
|
||||
CXInsights Dashboard - Configuration & Branding
|
||||
Based on Beyond Brand Identity Guidelines v1.0
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
|
||||
# =============================================================================
|
||||
# BEYOND BRAND COLORS
|
||||
# =============================================================================
|
||||
|
||||
COLORS = {
|
||||
# Primary colors
|
||||
"black": "#000000", # Beyond Black - Primary
|
||||
"blue": "#6D84E3", # Beyond Blue - Accent (ONLY accent color)
|
||||
"grey": "#B1B1B0", # Beyond Grey - Secondary
|
||||
"light_grey": "#E4E4E4", # Beyond Light Grey - Backgrounds
|
||||
"white": "#FFFFFF",
|
||||
|
||||
# Derived colors for UI states
|
||||
"blue_hover": "#5A6FD1", # Blue darkened 10%
|
||||
"blue_light": "#DBE2FC", # Light blue for subtle backgrounds
|
||||
|
||||
# Chart colors (ordered by importance) - light theme
|
||||
"chart_primary": "#6D84E3", # Blue - main data
|
||||
"chart_secondary": "#B1B1B0", # Grey - comparison/benchmark
|
||||
"chart_tertiary": "#7A7A7A", # Dark grey - third series
|
||||
"chart_quaternary": "#E4E4E4", # Light grey - fourth series
|
||||
|
||||
# Gradients for charts - light theme
|
||||
"gradient_blue": ["#E4E4E4", "#B1B1B0", "#6D84E3"],
|
||||
"gradient_grey": ["#FFFFFF", "#E4E4E4", "#B1B1B0", "#7A7A7A"],
|
||||
"gradient_red": ["#E4E4E4", "#B1B1B0", "#6D84E3", "#5A6FD1"], # For severity
|
||||
}
|
||||
|
||||
# Chart color sequence (for Plotly) - light theme
|
||||
CHART_COLORS = [
|
||||
COLORS["blue"], # Primary
|
||||
COLORS["grey"], # Secondary
|
||||
COLORS["chart_tertiary"], # Dark grey - Tertiary
|
||||
COLORS["light_grey"], # Quaternary
|
||||
]
|
||||
|
||||
# =============================================================================
|
||||
# TYPOGRAPHY (Outfit font via Google Fonts)
|
||||
# =============================================================================
|
||||
|
||||
FONTS = {
|
||||
"family": "'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
||||
"sizes": {
|
||||
"h1": "40px",
|
||||
"h2": "35px",
|
||||
"h3": "21px",
|
||||
"body": "17px",
|
||||
"small": "12px",
|
||||
"caption": "10px",
|
||||
},
|
||||
"weights": {
|
||||
"black": 900,
|
||||
"bold": 700,
|
||||
"medium": 500,
|
||||
"regular": 400,
|
||||
"light": 300,
|
||||
"thin": 100,
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# THEME CONFIG FOR PLOTLY CHARTS
|
||||
# =============================================================================
|
||||
|
||||
THEME_CONFIG = {
|
||||
"layout": {
|
||||
"font": {
|
||||
"family": FONTS["family"],
|
||||
"color": COLORS["black"],
|
||||
},
|
||||
"paper_bgcolor": COLORS["white"],
|
||||
"plot_bgcolor": COLORS["white"],
|
||||
"title": {
|
||||
"font": {
|
||||
"size": 18,
|
||||
"family": FONTS["family"],
|
||||
"color": COLORS["black"],
|
||||
},
|
||||
"x": 0,
|
||||
"xanchor": "left",
|
||||
},
|
||||
"legend": {
|
||||
"font": {"size": 14},
|
||||
"bgcolor": "rgba(255,255,255,0)",
|
||||
},
|
||||
"xaxis": {
|
||||
"gridcolor": COLORS["light_grey"],
|
||||
"linecolor": COLORS["grey"],
|
||||
"tickfont": {"size": 12, "color": COLORS["grey"]},
|
||||
"title_font": {"size": 14, "color": COLORS["grey"]},
|
||||
},
|
||||
"yaxis": {
|
||||
"gridcolor": COLORS["light_grey"],
|
||||
"linecolor": COLORS["grey"],
|
||||
"tickfont": {"size": 12, "color": COLORS["grey"]},
|
||||
"title_font": {"size": 14, "color": COLORS["grey"]},
|
||||
"rangemode": "tozero", # Always start at 0 (McKinsey standard)
|
||||
},
|
||||
"margin": {"l": 60, "r": 40, "t": 60, "b": 60},
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# STREAMLIT CUSTOM CSS
|
||||
# =============================================================================
|
||||
|
||||
def apply_custom_css():
|
||||
"""Apply Beyond brand CSS to Streamlit app."""
|
||||
|
||||
st.markdown("""
|
||||
<style>
|
||||
/* Import Outfit font from Google Fonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100;300;400;500;700;900&display=swap');
|
||||
|
||||
/* Global font */
|
||||
html, body, [class*="css"] {
|
||||
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
/* Headers */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
font-weight: 700 !important;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
h1 { font-size: 40px !important; }
|
||||
h2 { font-size: 35px !important; }
|
||||
h3 { font-size: 21px !important; }
|
||||
|
||||
/* Body text */
|
||||
p, li, span, div {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-weight: 400;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
/* Sidebar styling */
|
||||
[data-testid="stSidebar"] {
|
||||
background-color: #FFFFFF;
|
||||
border-right: 1px solid #E4E4E4;
|
||||
}
|
||||
|
||||
[data-testid="stSidebar"] h1,
|
||||
[data-testid="stSidebar"] h2,
|
||||
[data-testid="stSidebar"] h3 {
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
/* Main content area */
|
||||
.main .block-container {
|
||||
padding-top: 2rem;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
/* Metric cards - Beyond style */
|
||||
[data-testid="stMetric"] {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E4E4E4;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
[data-testid="stMetric"] label {
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
font-weight: 300 !important;
|
||||
font-size: 14px !important;
|
||||
color: #B1B1B0 !important;
|
||||
}
|
||||
|
||||
[data-testid="stMetric"] [data-testid="stMetricValue"] {
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
font-weight: 700 !important;
|
||||
font-size: 32px !important;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
[data-testid="stMetric"] [data-testid="stMetricDelta"] {
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
/* Buttons - Beyond style (light theme) */
|
||||
.stButton > button {
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
font-weight: 700 !important;
|
||||
background-color: #6D84E3 !important;
|
||||
color: #FFFFFF !important;
|
||||
border: none !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 0.5rem 1.5rem !important;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.stButton > button:hover {
|
||||
background-color: #5A6FD1 !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
/* Secondary buttons */
|
||||
.stButton > button[kind="secondary"] {
|
||||
background-color: #FFFFFF !important;
|
||||
color: #6D84E3 !important;
|
||||
border: 2px solid #6D84E3 !important;
|
||||
}
|
||||
|
||||
.stButton > button[kind="secondary"]:hover {
|
||||
background-color: #6D84E3 !important;
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
/* Selectbox styling */
|
||||
[data-testid="stSelectbox"] label {
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
font-weight: 700 !important;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
/* Radio buttons */
|
||||
[data-testid="stRadio"] label {
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
}
|
||||
|
||||
/* Expander headers */
|
||||
.streamlit-expanderHeader {
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
font-weight: 700 !important;
|
||||
color: #000000 !important;
|
||||
background-color: #F8F8F8 !important;
|
||||
}
|
||||
|
||||
/* Tables - Light theme */
|
||||
[data-testid="stTable"] th {
|
||||
background-color: #F8F8F8 !important;
|
||||
color: #000000 !important;
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
font-weight: 700 !important;
|
||||
border-bottom: 2px solid #6D84E3 !important;
|
||||
}
|
||||
|
||||
[data-testid="stTable"] tr:nth-child(even) {
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
/* Dataframe styling - Light theme */
|
||||
.dataframe th {
|
||||
background-color: #F8F8F8 !important;
|
||||
color: #000000 !important;
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
font-weight: 700 !important;
|
||||
text-align: left !important;
|
||||
border-bottom: 2px solid #6D84E3 !important;
|
||||
}
|
||||
|
||||
.dataframe td {
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
text-align: left !important;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
.dataframe tr:nth-child(even) {
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
/* Info/Warning/Error boxes */
|
||||
.stAlert {
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
/* Links - Beyond Blue */
|
||||
a {
|
||||
color: #6D84E3 !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #5A6FD1 !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
/* Caption/small text */
|
||||
.caption, small, .stCaption {
|
||||
font-family: 'Outfit', sans-serif !important;
|
||||
font-weight: 300 !important;
|
||||
color: #B1B1B0 !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
/* Divider line */
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #E4E4E4;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
/* Custom KPI card class */
|
||||
.kpi-card {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E4E4E4;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.kpi-card .kpi-value {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #000000;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.kpi-card .kpi-label {
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
color: #B1B1B0;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.kpi-card .kpi-delta {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #6D84E3;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Highlight card (with blue accent) */
|
||||
.highlight-card {
|
||||
background: #FFFFFF;
|
||||
border-left: 4px solid #6D84E3;
|
||||
border-radius: 4px;
|
||||
padding: 1rem 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
/* Evidence quote styling */
|
||||
.evidence-quote {
|
||||
background: #F8F8F8;
|
||||
border-left: 3px solid #6D84E3;
|
||||
padding: 1rem;
|
||||
margin: 0.5rem 0;
|
||||
font-style: italic;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.evidence-speaker {
|
||||
font-weight: 700;
|
||||
color: #B1B1B0;
|
||||
font-size: 12px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Footer styling */
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #FFFFFF;
|
||||
border-top: 1px solid #E4E4E4;
|
||||
padding: 0.5rem 2rem;
|
||||
font-size: 12px;
|
||||
color: #B1B1B0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Hide Streamlit branding */
|
||||
#MainMenu {visibility: hidden;}
|
||||
footer {visibility: hidden;}
|
||||
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
|
||||
def get_plotly_layout(title: str = "", height: int = 400) -> dict:
|
||||
"""Get standard Plotly layout with Beyond branding."""
|
||||
layout = THEME_CONFIG["layout"].copy()
|
||||
layout["height"] = height
|
||||
if title:
|
||||
layout["title"]["text"] = title
|
||||
return layout
|
||||
|
||||
|
||||
def format_metric_card(value: str, label: str, delta: str = None) -> str:
|
||||
"""Generate HTML for a branded KPI card."""
|
||||
delta_html = f'<div class="kpi-delta">{delta}</div>' if delta else ""
|
||||
return f"""
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-value">{value}</div>
|
||||
<div class="kpi-label">{label}</div>
|
||||
{delta_html}
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def format_evidence_quote(text: str, speaker: str = None) -> str:
|
||||
"""Format evidence text with Beyond styling."""
|
||||
speaker_html = f'<div class="evidence-speaker">— {speaker}</div>' if speaker else ""
|
||||
return f"""
|
||||
<div class="evidence-quote">
|
||||
"{text}"
|
||||
{speaker_html}
|
||||
</div>
|
||||
"""
|
||||
Reference in New Issue
Block a user