Download src/utils/styling.py from rmbielski/RHTP: direct link, hf CLI and curl.
- Browser
- Download file 18.4 kB
-
https://huggingface.co/spaces/rmbielski/RHTP/resolve/main/src/utils/styling.py
- Command line
-
hf download hf://spaces/rmbielski/RHTP/src/utils/styling.py
-
curl -L -o styling.py https://huggingface.co/spaces/rmbielski/RHTP/resolve/main/src/utils/styling.py
18.4 kB
| """Shared visual style: page config, dark-mode CSS, palette.""" | |
| from __future__ import annotations | |
| import streamlit as st | |
| # Polished dark palette inspired by analyst-tool surfaces (Looker, Hex, Mode). | |
| PALETTE = { | |
| "primary": "#E15A63", # accent crimson | |
| "secondary": "#5BA3DA", # accent steel-blue | |
| "accent": "#E0B458", # mountain gold | |
| "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", # workforce (gold) | |
| 2: "#5BA3DA", # facility (blue) | |
| 3: "#3FAE7B", # innovative (green) | |
| 4: "#D679A4", # prevention (rose) | |
| 5: "#A487E0", # technology (violet) | |
| } | |
| 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) | |
| # Plain-text verdict labels (no emojis). Callers can pass these to a styler | |
| # inside dataframes, or embed them in markdown via verdict_chip(). | |
| 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>" | |
| ) | |
| # Stacked vertically: KPOs first (full width), then inputs. | |
| 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) | |