Files
BeyondCX_Insights/dashboard/config.py
sujucu70 75e7b9da3d 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>
2026-01-19 16:27:30 +01:00

412 lines
11 KiB
Python

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