diff --git a/.streamlit/config.toml b/.streamlit/config.toml new file mode 100644 index 0000000..82b5671 --- /dev/null +++ b/.streamlit/config.toml @@ -0,0 +1,6 @@ +[theme] +primaryColor = "#1E5BA8" +backgroundColor = "#F4F7FA" +secondaryBackgroundColor = "#E8EDF3" +textColor = "#0F1419" +font = "sans serif" diff --git a/assets/bouncing_penguin.png b/assets/bouncing_penguin.png new file mode 100644 index 0000000..99d5b9a Binary files /dev/null and b/assets/bouncing_penguin.png differ diff --git a/assets/sipa_logo.png b/assets/sipa_logo.png new file mode 100644 index 0000000..64bd161 Binary files /dev/null and b/assets/sipa_logo.png differ diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..0657247 --- /dev/null +++ b/assets/style.css @@ -0,0 +1,276 @@ +/* ===== Bouncing Penguin · Columbia SIPA — Dashboard Styles ===== */ + +@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap'); + +/* ----- Global typography ----- */ +html, body, [class*="css"], .stApp, .main, [data-testid="stAppViewContainer"] { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; + color: #0F1419; +} + +h1, h2, h3, h4, h5, h6, +[data-testid="stHeader"] h1, +.stMarkdown h1, .stMarkdown h2, .stMarkdown h3, .stMarkdown h4 { + font-family: 'Space Grotesk', 'Inter', sans-serif !important; + font-weight: 600 !important; + color: #1F3464 !important; + letter-spacing: -0.01em; +} + +code, pre, kbd, samp, +[data-testid="stCodeBlock"], .stCodeBlock { + font-family: 'JetBrains Mono', 'Menlo', monospace !important; +} + +/* ----- Page title (h1) accent ----- */ +.stApp h1:first-of-type { + border-bottom: 3px solid #1E5BA8; + padding-bottom: 8px; + display: inline-block; + margin-bottom: 8px; +} + +/* ----- Section headings (h2) — blue dot bullet ----- */ +.stMarkdown h2, +.stApp [data-testid="stMarkdownContainer"] h2, +[data-testid="stHeading"] h2 { + position: relative; + padding-left: 18px; +} +.stMarkdown h2::before, +[data-testid="stMarkdownContainer"] h2::before, +[data-testid="stHeading"] h2::before { + content: ""; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 8px; + height: 8px; + border-radius: 50%; + background: #1E5BA8; + box-shadow: 0 0 0 3px rgba(30, 91, 168, 0.15); +} + +/* ----- Subheaders (h3) — softer accent ----- */ +.stMarkdown h3, +[data-testid="stMarkdownContainer"] h3 { + color: #1F3464 !important; + font-weight: 600 !important; +} + +/* ----- KPI / Metric cards ----- */ +[data-testid="stMetric"] { + background-color: #FFFFFF; + border-radius: 12px; + padding: 16px 20px; + border-left: 4px solid #1E5BA8; + box-shadow: 0 1px 3px rgba(15, 20, 25, 0.06), + 0 1px 2px rgba(15, 20, 25, 0.04); + transition: box-shadow 0.2s ease, transform 0.2s ease; +} + +[data-testid="stMetric"]:hover { + box-shadow: 0 4px 12px rgba(30, 91, 168, 0.10), + 0 2px 4px rgba(15, 20, 25, 0.06); + transform: translateY(-1px); +} + +[data-testid="stMetricValue"] { + font-family: 'JetBrains Mono', monospace !important; + font-weight: 700 !important; + font-size: 1.85rem !important; + color: #1F3464 !important; +} + +[data-testid="stMetricLabel"] { + font-size: 0.72rem !important; + color: #6B7280 !important; + text-transform: uppercase; + letter-spacing: 0.08em; + font-weight: 600 !important; +} + +[data-testid="stMetricDelta"] { + font-family: 'JetBrains Mono', monospace !important; + font-size: 0.85rem !important; + font-weight: 500 !important; +} + +/* ----- Sidebar ----- */ +[data-testid="stSidebar"] { + background-color: #E8EDF3; + border-right: 1px solid #B9D9EB; +} + +[data-testid="stSidebar"] .stMarkdown, +[data-testid="stSidebar"] label, +[data-testid="stSidebar"] [data-testid="stWidgetLabel"] p { + color: #1F3464 !important; + font-weight: 600 !important; +} + +[data-testid="stSidebar"] h1, +[data-testid="stSidebar"] h2, +[data-testid="stSidebar"] h3 { + color: #1F3464 !important; + font-family: 'Space Grotesk', sans-serif !important; +} + +[data-testid="stSidebar"] h2::before, +[data-testid="stSidebar"] h3::before { + display: none; +} + +/* Sidebar widgets */ +[data-testid="stSidebar"] [data-baseweb="select"] > div, +[data-testid="stSidebar"] input, +[data-testid="stSidebar"] textarea { + background-color: #FFFFFF !important; + border-color: #B9D9EB !important; +} + +[data-testid="stSidebar"] [data-baseweb="slider"] [role="slider"] { + background-color: #1E5BA8 !important; +} + +/* ----- Buttons ----- */ +.stButton > button, +[data-testid="stBaseButton-secondary"] { + background-color: #1E5BA8; + color: #FFFFFF !important; + border: none; + border-radius: 8px; + padding: 8px 16px; + font-weight: 500; + transition: background-color 0.15s ease, transform 0.15s ease; +} + +.stButton > button:hover { + background-color: #1F3464; + transform: translateY(-1px); +} + +/* ----- Tabs ----- */ +[data-testid="stTabs"] [data-baseweb="tab-list"] { + gap: 4px; + border-bottom: 2px solid #B9D9EB; +} + +[data-testid="stTabs"] [data-baseweb="tab"] { + background-color: transparent; + color: #6B7280; + font-weight: 500; + font-family: 'Space Grotesk', sans-serif; + border-radius: 8px 8px 0 0; + padding: 8px 16px; +} + +[data-testid="stTabs"] [data-baseweb="tab"][aria-selected="true"] { + color: #1E5BA8; + background-color: rgba(30, 91, 168, 0.08); + border-bottom: 3px solid #1E5BA8; +} + +/* ----- Info / Warning / Success / Error boxes ----- */ +[data-testid="stAlert"], +[data-testid="stNotification"] { + border-radius: 10px; + border-left-width: 4px; +} + +/* Info — Ocean Blue */ +[data-testid="stAlert"][kind="info"], +div[data-baseweb="notification"][kind="info"], +.stAlert.st-info { + background-color: rgba(30, 91, 168, 0.08) !important; + border-left-color: #1E5BA8 !important; + color: #1F3464 !important; +} + +/* Warning — Penguin Orange */ +[data-testid="stAlert"][kind="warning"] { + background-color: rgba(243, 156, 92, 0.10) !important; + border-left-color: #F39C5C !important; + color: #7C4A1E !important; +} + +/* Success — Sky Blue */ +[data-testid="stAlert"][kind="success"] { + background-color: rgba(91, 169, 221, 0.10) !important; + border-left-color: #5BA9DD !important; +} + +/* ----- Radio / Multiselect / Selectbox ----- */ +[data-baseweb="select"] > div, +.stSelectbox > div > div, +.stMultiSelect > div > div { + border-color: #B9D9EB !important; + border-radius: 8px; +} + +[data-baseweb="tag"] { + background-color: rgba(30, 91, 168, 0.10) !important; + color: #1F3464 !important; +} + +/* ----- Dividers ----- */ +hr, [data-testid="stDivider"] { + border-top: 1px solid #B9D9EB !important; + background: transparent !important; +} + +/* ----- Captions ----- */ +[data-testid="stCaptionContainer"], .stCaption, +small, .small { + color: #6B7280 !important; + font-style: italic; +} + +/* ----- Expander ----- */ +[data-testid="stExpander"] { + border: 1px solid #B9D9EB !important; + border-radius: 10px !important; + background-color: #FFFFFF; +} + +[data-testid="stExpander"] summary { + color: #1F3464 !important; + font-weight: 600; + font-family: 'Space Grotesk', sans-serif; +} + +/* ----- DataFrame ----- */ +[data-testid="stDataFrame"] { + border-radius: 10px; + overflow: hidden; + border: 1px solid #B9D9EB; +} + +/* ----- Hide Streamlit chrome ----- */ +#MainMenu { visibility: hidden; } +footer { visibility: hidden; } +header [data-testid="stToolbar"] { visibility: hidden; } +[data-testid="stDecoration"] { display: none; } + +/* ----- Scrollbar (WebKit) ----- */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} +::-webkit-scrollbar-track { + background: #F4F7FA; +} +::-webkit-scrollbar-thumb { + background: #B9D9EB; + border-radius: 6px; + border: 2px solid #F4F7FA; +} +::-webkit-scrollbar-thumb:hover { + background: #5BA9DD; +} + +/* ----- Sidebar image polish ----- */ +[data-testid="stSidebar"] [data-testid="stImage"] img { + border-radius: 8px; +} diff --git a/chart_theme.py b/chart_theme.py new file mode 100644 index 0000000..53334a5 --- /dev/null +++ b/chart_theme.py @@ -0,0 +1,28 @@ +"""Unified Plotly theme for Bouncing Penguin dashboard.""" + +PENGUIN_PALETTE = [ + "#1E5BA8", + "#F39C5C", + "#5BA9DD", + "#1F3464", + "#B9D9EB", + "#0F8B8D", + "#9B6B9E", +] + +PENGUIN_LAYOUT = dict( + plot_bgcolor="rgba(0,0,0,0)", + paper_bgcolor="rgba(0,0,0,0)", + font=dict(family="Inter, sans-serif", color="#0F1419", size=13), + colorway=PENGUIN_PALETTE, + xaxis=dict(gridcolor="#E8EDF3", zerolinecolor="#E8EDF3"), + yaxis=dict(gridcolor="#E8EDF3", zerolinecolor="#E8EDF3"), + legend=dict(bgcolor="rgba(0,0,0,0)"), + hoverlabel=dict(bgcolor="#1F3464", font=dict(color="#FFFFFF", family="Inter")), +) + + +def apply_penguin_theme(fig): + """Apply Bouncing Penguin theme to a Plotly figure (in-place).""" + fig.update_layout(**PENGUIN_LAYOUT) + return fig diff --git a/pages/2_Second_Dataset.py b/pages/2_Second_Dataset.py index 6d74619..b8e3525 100644 --- a/pages/2_Second_Dataset.py +++ b/pages/2_Second_Dataset.py @@ -1,4 +1,5 @@ from datetime import date, timedelta +from pathlib import Path import streamlit as st @@ -6,6 +7,12 @@ st.set_page_config(page_title="NYC COVID Data", layout="wide") +# ===== UI Style Sync ===== +_css = Path(__file__).parent.parent / "assets" / "style.css" +if _css.exists(): + st.markdown(f"", unsafe_allow_html=True) +# ======================== + def main() -> None: st.title("NYC COVID-19 Cases") diff --git a/requirements.txt b/requirements.txt index ae4b34f..f307a28 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ streamlit pandas plotly +Pillow requests ruff pytest diff --git a/streamlit_app.py b/streamlit_app.py index 6274b4b..cecb75d 100644 --- a/streamlit_app.py +++ b/streamlit_app.py @@ -1,11 +1,14 @@ from datetime import date, timedelta +from pathlib import Path import pandas as pd import plotly.express as px import plotly.graph_objects as go import streamlit as st +from PIL import Image from plotly.subplots import make_subplots +from chart_theme import PENGUIN_PALETTE, apply_penguin_theme from utils import ( MODE_COLORS, MTA_MIN_DATE, @@ -19,7 +22,49 @@ load_mta_data, ) -st.set_page_config(page_title="MTA Ridership Dashboard", layout="wide") +_PENGUIN_ICON = Path(__file__).parent / "assets" / "bouncing_penguin.png" +st.set_page_config( + page_title="MTA Ridership Dashboard", + layout="wide", + page_icon=Image.open(_PENGUIN_ICON) if _PENGUIN_ICON.exists() else "🐧", +) + +# ===== UI Customization Block - START ===== +_css_path = Path(__file__).parent / "assets" / "style.css" +if _css_path.exists(): + st.markdown(f"", unsafe_allow_html=True) + +with st.sidebar: + _sipa_logo = Path(__file__).parent / "assets" / "sipa_logo.png" + if _sipa_logo.exists(): + st.image(str(_sipa_logo), width="stretch") + + _penguin = Path(__file__).parent / "assets" / "bouncing_penguin.png" + if _penguin.exists(): + _c1, _c2, _c3 = st.columns([1, 3, 1]) + with _c2: + st.image(str(_penguin), width="stretch") + + st.markdown( + """ +
+
+ Bouncing Penguin +
+
+ Advanced Computing · Columbia SIPA +
+
+
+ """, + unsafe_allow_html=True, + ) +# ===== UI Customization Block - END ===== def get_dashboard_columns(selected_modes: list[str]) -> tuple[str, ...]: @@ -222,6 +267,7 @@ def render_recovery_chart( legend_title_text="", ) fig.update_yaxes(ticksuffix="%", rangemode="tozero") + apply_penguin_theme(fig) st.plotly_chart(fig, width="stretch", config={"displayModeBar": False}) insight_lines = [] @@ -266,6 +312,7 @@ def render_total_chart( yaxis_title="Daily Ridership / Traffic", legend_title_text="", ) + apply_penguin_theme(fig) st.plotly_chart(fig, width="stretch", config={"displayModeBar": False}) st.caption( "Even where recovery percentages plateau, raw ridership still trends up — " @@ -293,7 +340,7 @@ def render_subway_day_type_summary(filtered: pd.DataFrame) -> None: x="Day Type", y=subway_column, color="Day Type", - color_discrete_map={"Weekday": "#2563eb", "Weekend": "#f97316"}, + color_discrete_map={"Weekday": "#1E5BA8", "Weekend": "#F39C5C"}, ) fig.update_layout( height=260, @@ -302,6 +349,7 @@ def render_subway_day_type_summary(filtered: pd.DataFrame) -> None: yaxis_title="Average Subway Ridership", showlegend=False, ) + apply_penguin_theme(fig) st.plotly_chart(fig, width="stretch", config={"displayModeBar": False}) if not averages.empty and len(averages) == 2: weekday_val = averages.loc[averages["Day Type"] == "Weekday", subway_column].mean() @@ -345,6 +393,7 @@ def render_mode_recovery_summary(filtered: pd.DataFrame) -> None: showlegend=False, ) fig.update_yaxes(ticksuffix="%", rangemode="tozero") + apply_penguin_theme(fig) st.plotly_chart(fig, width="stretch", config={"displayModeBar": False}) @@ -383,7 +432,7 @@ def render_weekday_weekend(filtered: pd.DataFrame) -> None: color="Day Type", barmode="group", category_orders={"Day Type": ["Weekday", "Weekend"]}, - color_discrete_sequence=["#7dd3fc", "#2563eb"], + color_discrete_sequence=["#5BA9DD", "#1E5BA8"], ) comparison_fig.update_layout( height=320, @@ -392,6 +441,7 @@ def render_weekday_weekend(filtered: pd.DataFrame) -> None: legend_title_text="", ) comparison_fig.update_yaxes(ticksuffix="%", rangemode="tozero") + apply_penguin_theme(comparison_fig) st.plotly_chart(comparison_fig, width="stretch", config={"displayModeBar": False}) st.markdown("**Monthly Weekend Minus Weekday Gap (Subway)**") @@ -425,6 +475,7 @@ def render_weekday_weekend(filtered: pd.DataFrame) -> None: coloraxis_showscale=False, ) gap_fig.update_yaxes(ticksuffix="%", zeroline=True, zerolinewidth=1) + apply_penguin_theme(gap_fig) st.plotly_chart(gap_fig, width="stretch", config={"displayModeBar": False}) @@ -487,6 +538,7 @@ def render_holiday_impact(filtered: pd.DataFrame) -> None: showlegend=False, ) fig.update_yaxes(ticksuffix="%", rangemode="tozero") + apply_penguin_theme(fig) st.plotly_chart(fig, width="stretch", config={"displayModeBar": False}) impact_rows = [] @@ -548,7 +600,7 @@ def render_yearly_recovery(filtered: pd.DataFrame) -> None: y="Avg Recovery Percent", color="Transit Mode", barmode="group", - color_discrete_sequence=["#60a5fa", "#f97316", "#ef4444", "#34d399", "#a78bfa"], + color_discrete_sequence=PENGUIN_PALETTE, ) yearly_fig.update_layout( height=340, @@ -557,6 +609,7 @@ def render_yearly_recovery(filtered: pd.DataFrame) -> None: legend_title_text="", ) yearly_fig.update_yaxes(ticksuffix="%", rangemode="tozero") + apply_penguin_theme(yearly_fig) st.plotly_chart(yearly_fig, width="stretch", config={"displayModeBar": False}) @@ -669,7 +722,7 @@ def render_covid_context( y=combined["Subway Recovery Percent"], name="Subway recovery", mode="lines", - line=dict(color=MODE_COLORS["Subway"], width=2), + line=dict(color="#1E5BA8", width=2), ), secondary_y=False, ) @@ -679,7 +732,7 @@ def render_covid_context( y=combined["COVID Cases"], name="COVID cases", mode="lines", - line=dict(color="#dc2626", width=2), + line=dict(color="#F39C5C", width=2), ), secondary_y=True, ) @@ -691,6 +744,7 @@ def render_covid_context( fig.update_xaxes(title_text="Date") fig.update_yaxes(title_text="Subway Recovery", ticksuffix="%", secondary_y=False) fig.update_yaxes(title_text="COVID Cases", secondary_y=True) + apply_penguin_theme(fig) st.plotly_chart(fig, width="stretch", config={"displayModeBar": False}) st.caption(