| """Shared visual style: page config, dark-mode CSS, palette.""" |
| from __future__ import annotations |
|
|
| import streamlit as st |
|
|
| |
| PALETTE = { |
| "primary": "#E15A63", |
| "secondary": "#5BA3DA", |
| "accent": "#E0B458", |
| "good": "#3FAE7B", |
| "warn": "#E0B458", |
| "bad": "#D9534F", |
| "muted": "#9CA3AF", |
| "bg": "#0B0E13", |
| "surface": "#141821", |
| "surface2": "#1A1F2A", |
| "border": "#262C38", |
| "text": "#E6E8EB", |
| "text_dim": "#9CA3AF", |
| "treated": "#5BA3DA", |
| "control": "#E15A63", |
| } |
|
|
| INITIATIVE_COLOR = { |
| 1: "#E0B458", |
| 2: "#5BA3DA", |
| 3: "#3FAE7B", |
| 4: "#D679A4", |
| 5: "#A487E0", |
| } |
|
|
|
|
| def page_setup(title: str, layout: str = "wide") -> None: |
| st.set_page_config( |
| page_title=f"RHTP Evaluation — {title}", |
| layout=layout, |
| initial_sidebar_state="expanded", |
| ) |
| st.markdown(_CSS, unsafe_allow_html=True) |
|
|
|
|
| _CSS = """ |
| <style> |
| :root { |
| --primary: #E15A63; |
| --secondary: #5BA3DA; |
| --accent: #E0B458; |
| --good: #3FAE7B; |
| --warn: #E0B458; |
| --bad: #D9534F; |
| --bg: #0B0E13; |
| --surface: #141821; |
| --surface2: #1A1F2A; |
| --border: #262C38; |
| --text: #E6E8EB; |
| --text-dim: #9CA3AF; |
| } |
| |
| /* Page chrome */ |
| .stApp {background-color: var(--bg);} |
| .main .block-container {padding-top: 2.0rem; padding-bottom: 4rem; |
| max-width: 1400px;} |
| |
| /* Sidebar */ |
| section[data-testid="stSidebar"] { |
| background-color: #0E1218; |
| border-right: 1px solid var(--border); |
| } |
| section[data-testid="stSidebar"] * {color: var(--text) !important;} |
| section[data-testid="stSidebar"] a {color: var(--text-dim) !important; |
| border-radius: 6px; padding: 6px 10px !important; |
| transition: background 0.15s ease, color 0.15s ease;} |
| section[data-testid="stSidebar"] a:hover { |
| background: rgba(91, 163, 218, 0.12); |
| color: var(--text) !important; |
| } |
| section[data-testid="stSidebar"] a[aria-current="page"], |
| section[data-testid="stSidebar"] li[aria-current="page"] a { |
| background: rgba(91, 163, 218, 0.16) !important; |
| color: var(--text) !important; |
| font-weight: 600; |
| } |
| section[data-testid="stSidebar"] [data-testid="stSidebarNavSeparator"] { |
| border-color: var(--border) !important; |
| } |
| section[data-testid="stSidebar"] h1, |
| section[data-testid="stSidebar"] h2, |
| section[data-testid="stSidebar"] h3 { |
| color: var(--text) !important; |
| letter-spacing: 0.04em; |
| text-transform: uppercase; |
| font-size: 0.78rem !important; |
| } |
| |
| /* Type */ |
| h1 {color: var(--text); font-weight: 700; letter-spacing: -0.01em;} |
| h2 {color: var(--text); border-bottom: 1px solid var(--border); |
| padding-bottom: 0.5rem; margin-top: 1.6rem; font-weight: 600;} |
| h3 {color: var(--secondary); font-weight: 600;} |
| h4, h5 {color: var(--text);} |
| p, li, span, label, div {color: var(--text);} |
| a {color: var(--secondary);} |
| |
| /* Streamlit primitives */ |
| .stMetric label {font-size: 0.72rem !important; color: var(--text-dim) !important; |
| text-transform: uppercase; letter-spacing: 0.06em; |
| font-weight: 600 !important;} |
| [data-testid="stMetricValue"] {font-size: 1.55rem !important; |
| color: var(--text) !important; font-weight: 600;} |
| [data-testid="stMetricDelta"] {color: var(--text-dim) !important; |
| font-size: 0.78rem !important;} |
| |
| /* Inputs */ |
| .stSelectbox label, .stMultiSelect label, .stSlider label, |
| .stCheckbox label, .stRadio label { |
| color: var(--text-dim) !important; |
| font-size: 0.82rem !important; |
| font-weight: 500 !important; |
| } |
| [data-baseweb="select"] > div, [data-baseweb="input"] > div { |
| background-color: var(--surface) !important; |
| border-color: var(--border) !important; |
| } |
| |
| /* Buttons + tabs */ |
| .stTabs [data-baseweb="tab-list"] { |
| gap: 0; border-bottom: 1px solid var(--border); |
| } |
| .stTabs [data-baseweb="tab"] { |
| color: var(--text-dim); padding: 8px 18px; |
| background: transparent; font-weight: 500; |
| } |
| .stTabs [aria-selected="true"] { |
| color: var(--text) !important; |
| border-bottom: 2px solid var(--secondary) !important; |
| } |
| |
| /* Cards */ |
| .rhtp-card {background: var(--surface); padding: 1.0rem 1.2rem; |
| border-radius: 8px; border: 1px solid var(--border); |
| margin-bottom: 0.8rem; |
| box-shadow: 0 1px 2px rgba(0,0,0,0.30); |
| transition: border-color 0.15s ease, transform 0.15s ease;} |
| .rhtp-card:hover {border-color: var(--secondary);} |
| .rhtp-card h4 {margin-top: 0; color: var(--text);} |
| |
| /* Pill tags */ |
| .rhtp-pill {display: inline-block; padding: 3px 10px; border-radius: 12px; |
| font-size: 0.74rem; font-weight: 600; margin-right: 6px; |
| letter-spacing: 0.02em;} |
| .rhtp-pill.treated {background: rgba(91, 163, 218, 0.18); |
| color: var(--secondary); border: 1px solid rgba(91, 163, 218, 0.30);} |
| .rhtp-pill.control {background: rgba(225, 90, 99, 0.16); |
| color: var(--primary); border: 1px solid rgba(225, 90, 99, 0.30);} |
| .rhtp-pill.tribal {background: rgba(224, 180, 88, 0.14); |
| color: var(--accent); border: 1px solid rgba(224, 180, 88, 0.30);} |
| .rhtp-pill.urban {background: rgba(63, 174, 123, 0.14); |
| color: var(--good); border: 1px solid rgba(63, 174, 123, 0.30);} |
| .rhtp-pill.rural {background: rgba(164, 135, 224, 0.14); |
| color: #B7A3E8; border: 1px solid rgba(164, 135, 224, 0.30);} |
| |
| /* Verdict chips */ |
| .rhtp-verdict {display: inline-block; padding: 3px 10px; border-radius: 4px; |
| font-size: 0.78rem; font-weight: 600; letter-spacing: 0.02em;} |
| .rhtp-verdict.pass {background: rgba(63, 174, 123, 0.18); |
| color: var(--good); border: 1px solid rgba(63, 174, 123, 0.32);} |
| .rhtp-verdict.partial {background: rgba(91, 163, 218, 0.16); |
| color: var(--secondary); border: 1px solid rgba(91, 163, 218, 0.30);} |
| .rhtp-verdict.weak {background: rgba(224, 180, 88, 0.14); |
| color: var(--accent); border: 1px solid rgba(224, 180, 88, 0.30);} |
| .rhtp-verdict.fail {background: rgba(217, 83, 79, 0.16); |
| color: var(--bad); border: 1px solid rgba(217, 83, 79, 0.32);} |
| .rhtp-verdict.context {background: rgba(156, 163, 175, 0.16); |
| color: var(--text-dim); border: 1px solid rgba(156, 163, 175, 0.30);} |
| |
| /* Callouts */ |
| .rhtp-callout {padding: 0.95rem 1.15rem; border-radius: 6px; |
| background: var(--surface); border: 1px solid var(--border); |
| border-left: 3px solid var(--secondary); |
| margin: 0.8rem 0; color: var(--text);} |
| .rhtp-callout strong {color: var(--text);} |
| .rhtp-callout.success {border-left-color: var(--good); |
| background: rgba(63, 174, 123, 0.05);} |
| .rhtp-callout.warn {border-left-color: var(--accent); |
| background: rgba(224, 180, 88, 0.05);} |
| .rhtp-callout.bad {border-left-color: var(--bad); |
| background: rgba(217, 83, 79, 0.05);} |
| .rhtp-callout.info {border-left-color: var(--secondary); |
| background: rgba(91, 163, 218, 0.04);} |
| |
| /* Tables */ |
| [data-testid="stDataFrame"] {background: var(--surface); border-radius: 6px;} |
| |
| /* Misc */ |
| hr {border-top: 1px solid var(--border);} |
| code {color: var(--accent); background: rgba(224, 180, 88, 0.10); |
| padding: 1px 6px; border-radius: 3px; font-size: 0.92em;} |
| .formula {background: var(--surface); padding: 0.85rem 1.1rem; |
| border-radius: 6px; border: 1px solid var(--border); font-size: 1.05rem; |
| color: var(--text);} |
| |
| /* Section divider used between Data Explorer / Models / Verdict */ |
| .rhtp-divider { |
| height: 1px; border: none; |
| background: linear-gradient(90deg, |
| rgba(255,255,255,0) 0%, |
| var(--border) 18%, var(--border) 82%, |
| rgba(255,255,255,0) 100%); |
| margin: 2.6rem 0 1.2rem 0; |
| } |
| .rhtp-divider-label { |
| text-align: center; |
| color: var(--text-dim); font-size: 0.74rem; |
| letter-spacing: 0.18em; text-transform: uppercase; |
| margin: -1.6rem 0 0.8rem 0; |
| } |
| |
| /* Scope tables: KPOs and intervention inputs */ |
| .rhtp-scope { |
| background: var(--surface); border: 1px solid var(--border); |
| border-radius: 8px; padding: 0; margin-bottom: 0.8rem; |
| overflow: hidden; |
| } |
| .rhtp-scope-head { |
| background: rgba(91, 163, 218, 0.06); |
| border-bottom: 1px solid var(--border); |
| padding: 0.65rem 1.0rem; |
| color: var(--text); font-weight: 600; |
| font-size: 0.92rem; letter-spacing: 0.02em; |
| } |
| .rhtp-scope-head .kicker { |
| display: block; |
| font-size: 0.68rem; color: var(--text-dim); |
| text-transform: uppercase; letter-spacing: 0.10em; |
| margin-bottom: 2px; font-weight: 600; |
| } |
| .rhtp-scope-table {width: 100%; border-collapse: collapse;} |
| .rhtp-scope-table th, .rhtp-scope-table td { |
| padding: 9px 14px; vertical-align: top; |
| font-size: 0.86rem; color: var(--text); |
| border-bottom: 1px solid var(--border); |
| } |
| .rhtp-scope-table tr:last-child td {border-bottom: none;} |
| .rhtp-scope-table th { |
| color: var(--text-dim); text-align: left; |
| font-weight: 600; font-size: 0.72rem; |
| letter-spacing: 0.06em; text-transform: uppercase; |
| background: rgba(0,0,0,0.15); |
| } |
| .rhtp-scope-table td.level {color: var(--text-dim); white-space: nowrap; |
| font-size: 0.78rem;} |
| .rhtp-scope-table td.target {color: var(--good); white-space: nowrap; |
| font-size: 0.82rem;} |
| .rhtp-scope-table td.baseline {color: var(--text-dim); white-space: nowrap; |
| font-size: 0.82rem;} |
| |
| /* Role box (model role legend) */ |
| .rhtp-roles { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-left: 3px solid var(--accent); |
| border-radius: 6px; |
| padding: 0.85rem 1.1rem; |
| margin: 0.8rem 0 1.2rem 0; |
| display: grid; gap: 6px; |
| } |
| .rhtp-roles .row { |
| display: grid; grid-template-columns: 180px 1fr; gap: 14px; |
| font-size: 0.88rem; |
| } |
| .rhtp-roles .label { |
| color: var(--accent); font-weight: 600; |
| font-family: ui-monospace, "SF Mono", Menlo, monospace; |
| font-size: 0.82rem; |
| } |
| .rhtp-roles .desc {color: var(--text);} |
| |
| /* Equation legend (symbol -> definition) */ |
| .rhtp-legend { |
| background: rgba(91, 163, 218, 0.04); |
| border: 1px solid var(--border); |
| border-radius: 6px; |
| padding: 0.7rem 1.0rem; |
| margin: 0.4rem 0 1.0rem 0; |
| } |
| .rhtp-legend-row { |
| display: grid; grid-template-columns: 110px 1fr; gap: 14px; |
| padding: 4px 0; font-size: 0.86rem; |
| } |
| .rhtp-legend-row + .rhtp-legend-row { |
| border-top: 1px dashed var(--border); |
| } |
| .rhtp-legend .sym { |
| color: var(--accent); font-style: italic; |
| font-family: "Latin Modern Math", "STIX Two Math", "Cambria Math", serif; |
| font-size: 0.95rem; |
| } |
| .rhtp-legend .def {color: var(--text);} |
| |
| /* Glossary card */ |
| .rhtp-glossary { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: 8px; |
| padding: 0; margin-bottom: 1.0rem; |
| overflow: hidden; |
| } |
| .rhtp-glossary-head { |
| padding: 0.6rem 1.0rem; |
| background: rgba(224, 180, 88, 0.05); |
| border-bottom: 1px solid var(--border); |
| color: var(--text); font-weight: 600; font-size: 0.9rem; |
| } |
| .rhtp-glossary-row { |
| display: grid; grid-template-columns: 180px 1fr; gap: 14px; |
| padding: 8px 14px; border-bottom: 1px solid var(--border); |
| font-size: 0.86rem; |
| } |
| .rhtp-glossary-row:last-child {border-bottom: none;} |
| .rhtp-glossary-row .term { |
| color: var(--accent); font-weight: 600; |
| font-family: ui-monospace, "SF Mono", Menlo, monospace; |
| font-size: 0.82rem; |
| } |
| .rhtp-glossary-row .def {color: var(--text);} |
| |
| /* Plotly toolbar contrast */ |
| .modebar-btn path {fill: var(--text-dim) !important;} |
| .modebar {background: transparent !important;} |
| |
| /* Brand strip */ |
| .rhtp-brandbar { |
| display: flex; align-items: center; gap: 14px; |
| padding: 10px 16px; margin-bottom: 12px; |
| background: linear-gradient(90deg, rgba(225,90,99,0.08), rgba(91,163,218,0.04)); |
| border: 1px solid var(--border); border-radius: 8px; |
| } |
| .rhtp-brandbar .kicker { |
| font-size: 0.74rem; color: var(--text-dim); |
| text-transform: uppercase; letter-spacing: 0.10em; |
| } |
| .rhtp-brandbar h1 {margin: 0; font-size: 1.55rem;} |
| .rhtp-brandbar .subtitle {color: var(--text-dim); font-size: 0.96rem; |
| margin-top: 1px;} |
| </style> |
| """ |
|
|
|
|
| def header(title: str, subtitle: str | None = None, |
| pill: tuple[str, str] | None = None) -> None: |
| pill_html = "" |
| if pill: |
| text, kind = pill |
| pill_html = f' <span class="rhtp-pill {kind}">{text}</span>' |
| st.markdown(f"# {title}{pill_html}", unsafe_allow_html=True) |
| if subtitle: |
| st.markdown( |
| f"<div style='color:#9CA3AF; margin-top:-0.6rem; " |
| f"margin-bottom:1.0rem; font-size:1.02rem;'>{subtitle}</div>", |
| unsafe_allow_html=True, |
| ) |
| st.markdown("<hr/>", unsafe_allow_html=True) |
|
|
|
|
| def section(title: str, kicker: str | None = None) -> None: |
| st.markdown(f"## {title}") |
| if kicker: |
| st.markdown( |
| f"<div style='color:#9CA3AF; margin-top:-0.6rem; font-size:0.95rem;'>" |
| f"{kicker}</div>", |
| unsafe_allow_html=True, |
| ) |
|
|
|
|
| def callout(body: str, kind: str = "info", title: str | None = None) -> None: |
| cls = {"info": " info", "success": " success", |
| "warn": " warn", "bad": " bad"}.get(kind, "") |
| head = f"<strong>{title}</strong><br/>" if title else "" |
| st.markdown( |
| f"<div class='rhtp-callout{cls}'>{head}{body}</div>", |
| unsafe_allow_html=True, |
| ) |
|
|
|
|
| def formula(latex: str, caption: str | None = None) -> None: |
| st.latex(latex) |
| if caption: |
| st.caption(caption) |
|
|
|
|
| |
| |
| VERDICT_LABELS = { |
| "pass": "Effective", |
| "partial": "Right direction", |
| "weak": "Imprecise", |
| "fail": "Wrong direction", |
| "context": "Contextual", |
| } |
|
|
|
|
| def verdict_chip(kind: str, text: str | None = None) -> str: |
| """Return an HTML chip with the right CSS class. Used inside markdown.""" |
| cls = kind if kind in {"pass", "partial", "weak", "fail", "context"} else "context" |
| label = text or VERDICT_LABELS.get(kind, kind) |
| return f"<span class='rhtp-verdict {cls}'>{label}</span>" |
|
|
|
|
| def section_divider(label: str | None = None) -> None: |
| """Visible divider used between major page sections.""" |
| st.markdown("<hr class='rhtp-divider'/>", unsafe_allow_html=True) |
| if label: |
| st.markdown( |
| f"<div class='rhtp-divider-label'>{label}</div>", |
| unsafe_allow_html=True, |
| ) |
|
|
|
|
| def initiative_scope( |
| *, |
| initiative_no: int, |
| title: str, |
| kpos: list[dict], |
| inputs: list[dict], |
| ) -> None: |
| """Render the KPO table and the treatment-input table side-by-side. |
| |
| Each kpo dict: name, level, baseline, target. |
| Each input dict: name, level, notes. |
| """ |
| st.markdown(f"## Initiative scope") |
| st.markdown( |
| f"<div style='color:#9CA3AF; margin-top:-0.4rem; " |
| f"font-size:0.95rem; margin-bottom:1.0rem;'>" |
| f"What this initiative is trying to move and the levers it pulls. " |
| f"All baselines and FY2031 targets are derived from the synthetic " |
| f"data in <code>/data/</code> and the RHTP plan structure.</div>", |
| unsafe_allow_html=True, |
| ) |
|
|
| def _kpo_row(k: dict) -> str: |
| return ( |
| "<tr>" |
| f"<td><strong>{k['name']}</strong></td>" |
| f"<td class='level'>{k['level']}</td>" |
| f"<td class='baseline'>{k['baseline']}</td>" |
| f"<td class='target'>{k['target']}</td>" |
| "</tr>" |
| ) |
|
|
| def _input_row(i: dict) -> str: |
| notes = i.get("notes", "") |
| return ( |
| "<tr>" |
| f"<td><strong>{i['name']}</strong></td>" |
| f"<td class='level'>{i['level']}</td>" |
| f"<td>{notes}</td>" |
| "</tr>" |
| ) |
|
|
| |
| kpo_body = ( |
| "<div class='rhtp-scope'>" |
| "<div class='rhtp-scope-head'>" |
| "<span class='kicker'>Initiative " + f"{initiative_no:02d} · KPOs" |
| + "</span>" |
| "Key Program Outcomes monitored" |
| "</div>" |
| "<table class='rhtp-scope-table'>" |
| "<thead><tr><th>Outcome</th><th>Level</th>" |
| "<th>Baseline</th><th>FY2031 target</th></tr></thead>" |
| "<tbody>" |
| + "".join(_kpo_row(k) for k in kpos) |
| + "</tbody></table></div>" |
| ) |
| st.markdown(kpo_body, unsafe_allow_html=True) |
|
|
| input_body = ( |
| "<div class='rhtp-scope'>" |
| "<div class='rhtp-scope-head'>" |
| "<span class='kicker'>Initiative " + f"{initiative_no:02d} · inputs" |
| + "</span>" |
| "Treatment / intervention levers" |
| "</div>" |
| "<table class='rhtp-scope-table'>" |
| "<thead><tr><th>Input</th><th>Level</th><th>Notes</th></tr></thead>" |
| "<tbody>" |
| + "".join(_input_row(i) for i in inputs) |
| + "</tbody></table></div>" |
| ) |
| st.markdown(input_body, unsafe_allow_html=True) |
|
|
|
|
| def equation_legend(rows: list[tuple[str, str]]) -> None: |
| """Render an equation symbol -> definition legend.""" |
| body = "<div class='rhtp-legend'>" |
| for sym, defn in rows: |
| body += ( |
| "<div class='rhtp-legend-row'>" |
| f"<div class='sym'>{sym}</div>" |
| f"<div class='def'>{defn}</div>" |
| "</div>" |
| ) |
| body += "</div>" |
| st.markdown(body, unsafe_allow_html=True) |
|
|
|
|
| def role_box(rows: list[tuple[str, str]], title: str | None = None) -> None: |
| """Small box labeling the role each model component plays.""" |
| body = "<div class='rhtp-roles'>" |
| if title: |
| body += ( |
| f"<div style='color:#E6E8EB; font-weight:600; " |
| f"margin-bottom:4px; font-size:0.94rem;'>{title}</div>" |
| ) |
| for label, desc in rows: |
| body += ( |
| "<div class='row'>" |
| f"<div class='label'>{label}</div>" |
| f"<div class='desc'>{desc}</div>" |
| "</div>" |
| ) |
| body += "</div>" |
| st.markdown(body, unsafe_allow_html=True) |
|
|
|
|
| def glossary(title: str, rows: list[tuple[str, str]]) -> None: |
| """Render a term/definition glossary card.""" |
| body = ( |
| "<div class='rhtp-glossary'>" |
| f"<div class='rhtp-glossary-head'>{title}</div>" |
| ) |
| for term, defn in rows: |
| body += ( |
| "<div class='rhtp-glossary-row'>" |
| f"<div class='term'>{term}</div>" |
| f"<div class='def'>{defn}</div>" |
| "</div>" |
| ) |
| body += "</div>" |
| st.markdown(body, unsafe_allow_html=True) |
|
|