biointel / app.py
Umesh1608's picture
Link each pivotal trial to its dossier
5041d67 verified
Raw History Blame Contribute Delete
27.2 kB
"""BioIntel β€” biotech catalyst and wet-lab demand intelligence.
Two public sources, joined into one view:
* SEC EDGAR 8-K full-text search β€” catalysts that have already happened
* ClinicalTrials.gov v2 β€” dated catalysts that have not happened yet
Data is produced by the ingest pipeline and published as parquet to a Hugging
Face Dataset repo; this app downloads it once at startup and queries it in
memory. Not investment advice.
"""
from __future__ import annotations
import json
import os
from datetime import datetime, timezone
from pathlib import Path
import gradio as gr
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
DATASET_REPO = os.environ.get("BIOINTEL_DATASET", "Umesh1608/biointel-data")
# Dossiers live in a separate *static* Space: it renders HTML natively, never
# sleeps, and costs nothing against the PRO allowance.
REPORTS_BASE = os.environ.get(
"BIOINTEL_REPORTS", "https://umesh1608-biointel-reports.static.hf.space")
LOCAL_DATA = Path(os.environ.get("BIOINTEL_DATA",
Path.home() / "biointel" / "data"))
# A restrained palette: one accent, one alarm colour, everything else neutral.
INK, ACCENT, ALARM, MUTED = "#0F172A", "#0D9488", "#DC2626", "#64748B"
SERIES = ["#0D9488", "#0EA5E9", "#8B5CF6", "#F59E0B", "#DC2626", "#10B981",
"#6366F1", "#EC4899", "#14B8A6", "#F97316", "#64748B", "#84CC16"]
NEGATIVE_SIGNALS = {"endpoint_missed", "crl", "clinical_hold", "discontinue"}
# --------------------------------------------------------------------------
# data
# --------------------------------------------------------------------------
def _load(name: str, parquet: bool = True):
local = LOCAL_DATA / (f"{name}.parquet" if parquet else name)
if local.exists():
return pd.read_parquet(local) if parquet else json.loads(local.read_text())
try:
from huggingface_hub import hf_hub_download
p = hf_hub_download(DATASET_REPO,
f"data/{name}.parquet" if parquet else f"data/{name}",
repo_type="dataset")
return pd.read_parquet(p) if parquet else json.loads(Path(p).read_text())
except Exception as e: # noqa: BLE001 β€” render the shell even with no data
print(f"[warn] could not load {name}: {e}")
return pd.DataFrame() if parquet else {}
CATALYSTS = _load("catalysts")
READOUTS = _load("readouts")
MANIFEST = _load("manifest.json", parquet=False)
PROC_DATE = (MANIFEST.get("generated_at") or "unknown")[:10]
for _df, _col in ((CATALYSTS, "filed_date"), (READOUTS, "primary_completion")):
if not _df.empty:
_df["_dt"] = pd.to_datetime(_df[_col], errors="coerce")
def _style(fig, height=380):
"""One chart style everywhere: no chartjunk, muted grid, honest axes."""
fig.update_layout(
height=height, margin=dict(l=8, r=8, t=48, b=8),
font=dict(family="Inter, ui-sans-serif, system-ui", size=12.5),
title=dict(font=dict(size=15), x=0, xanchor="left"),
plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)",
legend=dict(title_text="", orientation="h", y=-0.18, font=dict(size=11)),
colorway=SERIES, hoverlabel=dict(font_size=12),
)
fig.update_xaxes(showgrid=False, linecolor="rgba(100,116,139,.25)", title=None)
fig.update_yaxes(gridcolor="rgba(100,116,139,.15)", zeroline=False,
linecolor="rgba(0,0,0,0)", title=None)
return fig
def _empty(msg="No data loaded"):
f = go.Figure()
f.add_annotation(text=msg, showarrow=False, font=dict(size=14, color=MUTED))
f.update_layout(height=320, plot_bgcolor="rgba(0,0,0,0)",
paper_bgcolor="rgba(0,0,0,0)",
xaxis=dict(visible=False), yaxis=dict(visible=False))
return f
# --------------------------------------------------------------------------
# header
# --------------------------------------------------------------------------
def kpis() -> str:
if CATALYSTS.empty and READOUTS.empty:
return "<div class='bi-kpis'><div class='bi-card'>No data</div></div>"
n_cat = len(CATALYSTS)
n_neg = int(CATALYSTS.negative.sum()) if not CATALYSTS.empty else 0
n_co = CATALYSTS.company.nunique() if not CATALYSTS.empty else 0
n_ro = len(READOUTS)
n_ph3 = int(READOUTS.phase.str.contains("Ph3", na=False).sum()) if not READOUTS.empty else 0
cards = [
("Catalyst events", f"{n_cat:,}", "8-K filings Β· last 90 days"),
("Companies", f"{n_co:,}", "distinct issuers"),
("Negative signals", f"{n_neg:,}", f"{n_neg / max(n_cat, 1):.0%} of events"),
("Upcoming readouts", f"{n_ro:,}", "industry trials Β· next 12 mo"),
("Phase 3 ahead", f"{n_ph3:,}", "highest-stakes subset"),
]
html = "".join(
f"<div class='bi-card'><div class='bi-label'>{lbl}</div>"
f"<div class='bi-value'>{val}</div><div class='bi-sub'>{sub}</div></div>"
for lbl, val, sub in cards)
return f"<div class='bi-kpis'>{html}</div>"
def freshness() -> str:
ts = MANIFEST.get("generated_at")
if not ts:
return "<div class='bi-stale'>⚠ No manifest found β€” data may be missing.</div>"
try:
gen = datetime.fromisoformat(ts)
age = (datetime.now(timezone.utc) - gen).total_seconds() / 3600
except ValueError:
return f"<div class='bi-fresh'>Data generated {ts}</div>"
cls = "bi-stale" if age > 48 else "bi-fresh"
note = " β€” stale, the refresh job may have failed" if age > 48 else ""
return (f"<div class='{cls}'>● Data generated "
f"<b>{gen:%d %b %Y, %H:%M} UTC</b> Β· {age:.0f}h ago{note}</div>")
# --------------------------------------------------------------------------
# tab 1 β€” catalyst feed
# --------------------------------------------------------------------------
CAT_COLS = ["filed_date", "ticker", "company", "signal", "negative", "url"]
def filter_catalysts(signals, neg_only, search, limit):
df = CATALYSTS
if df.empty:
return pd.DataFrame({"note": ["No catalyst data loaded."]})
if signals:
df = df[df.signal.isin(signals)]
if neg_only:
df = df[df.negative]
if search:
s = search.strip().lower()
df = df[df.company.str.lower().str.contains(s, na=False)
| df.ticker.fillna("").str.lower().str.contains(s)]
out = df.sort_values("filed_date", ascending=False)[CAT_COLS].head(int(limit)).copy()
out["negative"] = out.negative.map({True: "β–Ό negative", False: ""})
return out.rename(columns={"filed_date": "Filed", "ticker": "Ticker",
"company": "Company", "signal": "Signal",
"negative": "Tone", "url": "Filing"})
def catalyst_trend(signals):
df = CATALYSTS
if df.empty:
return _empty()
if signals:
df = df[df.signal.isin(signals)]
if df.empty:
return _empty("No events match these filters")
d = df.copy()
d["week"] = d._dt.dt.to_period("W").dt.start_time
g = d.groupby(["week", "signal"]).size().reset_index(name="filings")
return _style(px.bar(g, x="week", y="filings", color="signal",
title="Catalyst filings per week"))
def catalyst_mix(signals):
df = CATALYSTS
if df.empty:
return _empty()
if signals:
df = df[df.signal.isin(signals)]
if df.empty:
return _empty("No events match these filters")
g = df.signal.value_counts().reset_index()
g.columns = ["signal", "events"]
g["tone"] = g.signal.isin(NEGATIVE_SIGNALS).map(
{True: "negative", False: "neutral / positive"})
fig = px.bar(g, x="events", y="signal", orientation="h", color="tone",
color_discrete_map={"negative": ALARM,
"neutral / positive": ACCENT},
title="Which signals are firing")
fig.update_layout(yaxis=dict(autorange="reversed"))
return _style(fig)
# --------------------------------------------------------------------------
# tab 2 β€” readout calendar
# --------------------------------------------------------------------------
RO_COLS = ["primary_completion", "date_precision", "phase", "sponsor",
"modality", "conditions", "enrollment", "url"]
def filter_readouts(phases, modalities, sponsor, months, exact_only, limit):
df = READOUTS
if df.empty:
return pd.DataFrame({"note": ["No readout data loaded."]})
if phases:
df = df[df.phase.isin(phases)]
if modalities:
df = df[df.modality.isin(modalities)]
if sponsor:
df = df[df.sponsor.str.lower().str.contains(sponsor.strip().lower(), na=False)]
if exact_only and "date_precision" in df.columns:
df = df[df.date_precision == "day"]
if months:
cutoff = pd.Timestamp.today() + pd.DateOffset(months=int(months))
df = df[df._dt <= cutoff]
cols = [c for c in RO_COLS if c in df.columns]
out = df.sort_values("primary_completion")[cols].head(int(limit)).copy()
if "date_precision" in out.columns:
out["date_precision"] = out.date_precision.map(
{"day": "exact", "month": "month only",
"year": "year only"}).fillna("β€”")
return out.rename(columns={"primary_completion": "Readout due",
"date_precision": "Date precision",
"phase": "Phase", "sponsor": "Sponsor",
"modality": "Modality", "conditions": "Condition",
"enrollment": "N", "url": "Registry"})
def readout_chart(phases, modalities):
df = READOUTS
if df.empty:
return _empty()
if phases:
df = df[df.phase.isin(phases)]
if modalities:
df = df[df.modality.isin(modalities)]
if df.empty:
return _empty("No trials match these filters")
d = df.copy()
d["month"] = d._dt.dt.to_period("M").dt.start_time
d["precision"] = (d.date_precision.map({"day": "exact date",
"month": "month only"})
.fillna("month only")
if "date_precision" in d.columns else "unknown")
g = d.groupby(["month", "precision"]).size().reset_index(name="trials")
fig = px.bar(g, x="month", y="trials", color="precision",
color_discrete_map={"exact date": ACCENT,
"month only": "#CBD5E1"},
title="Readouts by month β€” split by how precise the date is")
return _style(fig)
def modality_chart(phases):
df = READOUTS
if df.empty:
return _empty()
if phases:
df = df[df.phase.isin(phases)]
if df.empty:
return _empty("No trials match these filters")
g = df.modality.value_counts().reset_index()
g.columns = ["modality", "trials"]
fig = px.bar(g.head(12), x="trials", y="modality", orientation="h",
title="Modality mix of the forward pipeline")
fig.update_traces(marker_color=ACCENT)
fig.update_layout(yaxis=dict(autorange="reversed"))
return _style(fig)
def phase_chart():
if READOUTS.empty:
return _empty()
g = READOUTS.phase.value_counts().reset_index()
g.columns = ["phase", "trials"]
fig = px.pie(g.head(8), names="phase", values="trials", hole=0.62,
title="Phase mix")
fig.update_traces(textposition="outside", textinfo="label+percent")
return _style(fig)
def _choices(df, col):
return sorted(df[col].dropna().unique().tolist()) if not df.empty else []
# --------------------------------------------------------------------------
# tab 3 β€” pivotal (Phase 3) readouts
# --------------------------------------------------------------------------
# "Pivotal" = anything carrying a Phase 3 label, so Ph2/Ph3 seamless designs
# are included: they are registrational too, and excluding them would hide
# real risk.
PIVOTAL = (READOUTS[READOUTS.phase.str.contains("Ph3", na=False)].copy()
if not READOUTS.empty else pd.DataFrame())
PIV_COLS = ["primary_completion", "date_precision", "phase", "sponsor",
"enrollment", "modality", "conditions", "url"]
def pivotal_kpis() -> str:
if PIVOTAL.empty:
return "<div class='bi-kpis'><div class='bi-card'>No pivotal trials</div></div>"
soon = int((PIVOTAL._dt <= pd.Timestamp.today() + pd.DateOffset(days=90)).sum())
exact = int((PIVOTAL.date_precision == "day").sum())
med = PIVOTAL.enrollment.median()
cards = [
("Pivotal readouts", f"{len(PIVOTAL):,}", "Phase 3 or Ph2/Ph3"),
("Due in 90 days", f"{soon:,}", "nearest-term risk"),
("Exact-dated", f"{exact / len(PIVOTAL):.0%}", f"{exact:,} of {len(PIVOTAL):,}"),
("Sponsors", f"{PIVOTAL.sponsor.nunique():,}", "distinct companies"),
("Median enrolment", f"{med:,.0f}" if pd.notna(med) else "β€”", "patients per trial"),
]
html = "".join(
f"<div class='bi-card'><div class='bi-label'>{lbl}</div>"
f"<div class='bi-value'>{val}</div><div class='bi-sub'>{sub}</div></div>"
for lbl, val, sub in cards)
return f"<div class='bi-kpis'>{html}</div>"
def filter_pivotal(days, exact_only, sponsor, modalities, min_n, limit):
df = PIVOTAL
if df.empty:
return pd.DataFrame({"note": ["No pivotal trial data loaded."]})
if days:
df = df[df._dt <= pd.Timestamp.today() + pd.DateOffset(days=int(days))]
if exact_only:
df = df[df.date_precision == "day"]
if sponsor:
df = df[df.sponsor.str.lower().str.contains(sponsor.strip().lower(), na=False)]
if modalities:
df = df[df.modality.isin(modalities)]
if min_n:
df = df[df.enrollment.fillna(0) >= int(min_n)]
out = df.sort_values("primary_completion")[PIV_COLS + ["nct_id"]].head(int(limit)).copy()
out["date_precision"] = out.date_precision.map(
{"day": "exact", "month": "month only", "year": "year only"}).fillna("β€”")
# Markdown link so the dossier opens in its own window.
out["dossier"] = out.nct_id.map(
lambda n: f"[open report]({REPORTS_BASE}/{n}.html)")
out = out.drop(columns=["nct_id"])
return out.rename(columns={"dossier": "Dossier",
"primary_completion": "Readout due",
"date_precision": "Date precision",
"phase": "Phase", "sponsor": "Sponsor",
"enrollment": "Patients", "modality": "Modality",
"conditions": "Condition", "url": "Registry"})
def pivotal_month_chart(exact_only):
df = PIVOTAL
if df.empty:
return _empty()
if exact_only:
df = df[df.date_precision == "day"]
if df.empty:
return _empty("No pivotal trials match")
d = df.copy()
d["month"] = d._dt.dt.to_period("M").dt.start_time
d["precision"] = d.date_precision.map(
{"day": "exact date", "month": "month only"}).fillna("month only")
g = d.groupby(["month", "precision"]).size().reset_index(name="trials")
fig = px.bar(g, x="month", y="trials", color="precision",
color_discrete_map={"exact date": ACCENT, "month only": "#CBD5E1"},
title="Pivotal readouts by month")
return _style(fig)
def pivotal_sponsor_chart(days):
df = PIVOTAL
if df.empty:
return _empty()
if days:
df = df[df._dt <= pd.Timestamp.today() + pd.DateOffset(days=int(days))]
if df.empty:
return _empty("No pivotal trials in this window")
g = df.sponsor.value_counts().head(12).reset_index()
g.columns = ["sponsor", "trials"]
g["sponsor"] = g.sponsor.str.slice(0, 34)
fig = px.bar(g, x="trials", y="sponsor", orientation="h",
title="Most pivotal readouts ahead, by sponsor")
fig.update_traces(marker_color=SERIES[1])
fig.update_layout(yaxis=dict(autorange="reversed"))
return _style(fig)
# --------------------------------------------------------------------------
# styling
# --------------------------------------------------------------------------
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
.gradio-container { font-family:'Inter',ui-sans-serif,system-ui !important;
max-width:1460px !important; }
#bi-hero { padding:6px 0 0; }
#bi-hero h1 { font-size:2.1rem; font-weight:700; letter-spacing:-.028em;
margin:0; line-height:1.1; }
#bi-hero p { margin:.35rem 0 0; color:#64748B; font-size:1.02rem; }
.bi-kpis { display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr));
gap:12px; margin:14px 0 8px; }
.bi-card { background:var(--block-background-fill); border:1px solid var(--border-color-primary);
border-radius:14px; padding:14px 16px; }
.bi-label { font-size:.7rem; font-weight:600; letter-spacing:.08em;
text-transform:uppercase; color:#64748B; }
.bi-value { font-size:1.9rem; font-weight:700; line-height:1.15; margin:.2rem 0;
font-variant-numeric:tabular-nums; letter-spacing:-.02em; }
.bi-sub { font-size:.75rem; color:#94A3B8; }
.bi-fresh, .bi-stale { display:inline-block; font-size:.8rem; padding:5px 12px;
border-radius:999px; margin:6px 0 2px; font-weight:500; }
.bi-fresh { background:#ECFDF5; color:#047857; border:1px solid #A7F3D0; }
.bi-stale { background:#FEF2F2; color:#B91C1C; border:1px solid #FECACA; }
.dark .bi-fresh { background:#06281F; border-color:#115E4A; color:#5EEAD4; }
.dark .bi-stale { background:#2A1113; border-color:#7F1D1D; color:#FCA5A5; }
.bi-note { font-size:.84rem; color:#64748B; border-left:3px solid #0D9488;
padding:3px 0 3px 12px; margin:6px 0 14px; line-height:1.5; }
footer, .built-with { display:none !important; }
.tab-nav button { font-weight:600 !important; font-size:.95rem !important; }
"""
ABOUT = f"""
## What this shows
Two public sources, joined into one view of biotech catalysts.
**SEC EDGAR 8-K full-text search** β€” *what already happened.* An 8-K is how a
US-listed company must announce a material event, so it is the closest public
thing to a ground-truth catalyst feed. Twelve signal types are matched on
phrase groups chosen by **measurement, not guesswork**: over a sample window
*"met its primary endpoint"* appeared in 54 filings where *"met the primary
endpoint"* appeared in 19, so both are searched. EDGAR accepts uppercase `OR`
between quoted phrases β€” a `|` silently returns zero results.
**ClinicalTrials.gov API v2** β€” *what is coming.* A trial whose primary
completion date falls next quarter is a dated, public, not-yet-happened
catalyst. Only industry-sponsored trials are kept.
## Read these caveats before trusting a number
* **40% of readout dates are month-only.** Sponsors often register just
`2026-12`. Converting that to a day manufactures precision: in one sample
46% of trials landed on day 1 purely as an artifact of that conversion, and
a further 28% sat on month-end placeholders. A naive chart shows a December
"catalyst wall" of 913 trials that is mostly rounding. The calendar
therefore **labels every date's precision** and can filter to exact dates.
* **Signals co-occur, and that is real.** One 8-K can match both *endpoint
met* and *endpoint missed* β€” a release reporting a hit on one endpoint and a
miss on another. Treat the columns as flags, not a verdict.
* **Trial dates slip.** A primary completion date is a plan, not a commitment.
* **Modality is inferred**, not a registry field. It reports `unknown` rather
than guessing, because a wrong label is worse than a missing one.
* **Not every catalyst is an 8-K.** Conference presentations and releases not
deemed material never appear.
* **Both APIs truncate silently.** One EDGAR call returns at most 100 hits
regardless of the reported total, and filtering trials client-side after a
truncated fetch drops most of the result set. Both are paged and filtered
server-side here.
## What is *not* here, and why
The thesis this was modelled on promises CRO lab occupancy, lease renewal
spreads and options flow. **Those are not obtainable from public data.** There
is no free, redistributable source of options or intraday price data β€” OPRA
licensing runs \\$650–1,500/month β€” so that signal is **not replicated** rather
than approximated from a prohibited source. Public proxies for lab demand do
exist and are the natural next addition. Facility-level utilisation is not
among them: Charles River's own 10-K says it tracks room utilisation and
prints no figure.
## Attribution and licensing
**ClinicalTrials.gov.** Trial data courtesy of the U.S. National Library of
Medicine, retrieved from the ClinicalTrials.gov API v2. **Processing date:
{PROC_DATE}.** The data has been **modified** from source: filtered to
industry-sponsored studies with a future primary completion date, dates
normalised for sorting with their original precision retained in a separate
column, and a `modality` label **inferred** by this project from intervention
names and titles. NLM does not endorse this reuse.
**SEC EDGAR.** Filings are public information and may be copied or further
distributed without the SEC's permission. Accessed within the published
10 requests/second limit with a descriptive User-Agent.
Sources whose terms forbid redistribution or automated retrieval are
deliberately **excluded** β€” PMC full text, EPO OPS, and every commercial
market-data free tier. The PatentsView API was retired in March 2026.
**An information tool built on public filings. Not investment advice.**
"""
with gr.Blocks(title="BioIntel", css=CSS, fill_width=True) as demo:
gr.HTML("<div id='bi-hero'><h1>BioIntel</h1><p>Biotech catalyst "
"intelligence from public filings and trial registries</p></div>")
gr.HTML(freshness())
gr.HTML(kpis())
with gr.Tab("Catalyst feed"):
gr.HTML("<div class='bi-note'>8-K filings matching clinical and "
"regulatory catalyst language. <b>Tone</b> marks signals that are "
"usually bad news β€” though a single filing can carry both.</div>")
with gr.Row():
f_sig = gr.Dropdown(_choices(CATALYSTS, "signal"), multiselect=True,
label="Signal", scale=3)
f_q = gr.Textbox(label="Company or ticker",
placeholder="e.g. BridgeBio, EYPT", scale=2)
f_neg = gr.Checkbox(label="Negative only", scale=1)
f_lim = gr.Slider(25, 1000, value=150, step=25, label="Rows", scale=2)
t_cat = gr.Dataframe(value=filter_catalysts([], False, "", 150),
wrap=True, max_height=430)
with gr.Row():
p_trend = gr.Plot(catalyst_trend([]))
p_mix = gr.Plot(catalyst_mix([]))
cins = [f_sig, f_neg, f_q, f_lim]
for c in cins:
c.change(filter_catalysts, cins, t_cat)
f_sig.change(catalyst_trend, f_sig, p_trend)
f_sig.change(catalyst_mix, f_sig, p_mix)
with gr.Tab("Readout calendar"):
gr.HTML("<div class='bi-note'>Industry trials with a primary completion "
"date ahead. <b>40% of registry dates are month-only</b> β€” the "
"precision column says which, and the chart separates them so a "
"rounding artifact cannot read as a catalyst cluster.</div>")
with gr.Row():
r_ph = gr.Dropdown(_choices(READOUTS, "phase"), multiselect=True,
label="Phase", scale=2)
r_mod = gr.Dropdown(_choices(READOUTS, "modality"), multiselect=True,
label="Modality", scale=2)
r_sp = gr.Textbox(label="Sponsor contains",
placeholder="e.g. Novartis", scale=2)
r_mo = gr.Slider(1, 12, value=6, step=1, label="Within months", scale=2)
r_ex = gr.Checkbox(label="Exact dates only", scale=1)
r_lim = gr.Slider(25, 1000, value=150, step=25, label="Rows", scale=1)
t_ro = gr.Dataframe(value=filter_readouts([], [], "", 6, False, 150),
wrap=True, max_height=430)
with gr.Row():
p_ro = gr.Plot(readout_chart([], []))
p_mod = gr.Plot(modality_chart([]))
rins = [r_ph, r_mod, r_sp, r_mo, r_ex, r_lim]
for c in rins:
c.change(filter_readouts, rins, t_ro)
for c in (r_ph, r_mod):
c.change(readout_chart, [r_ph, r_mod], p_ro)
r_ph.change(modality_chart, r_ph, p_mod)
with gr.Tab("Pivotal readouts"):
gr.HTML("<div class='bi-note'>Phase 3 and Ph2/Ph3 trials only β€” the "
"readouts that reprice a company. <b>Exact dates only</b> is on "
"by default here: a pivotal date that is really just a month is "
"not something you can plan around. Each row links to a "
"<b>dossier</b> β€” design, endpoint, scale and sponsor context "
"β€” which opens in its own window.</div>")
gr.HTML(pivotal_kpis())
with gr.Row():
v_days = gr.Slider(30, 365, value=180, step=30, label="Within days", scale=2)
v_sp = gr.Textbox(label="Sponsor contains",
placeholder="e.g. AstraZeneca", scale=2)
v_mod = gr.Dropdown(_choices(PIVOTAL, "modality"), multiselect=True,
label="Modality", scale=2)
v_n = gr.Slider(0, 2000, value=0, step=50, label="Min patients", scale=2)
v_ex = gr.Checkbox(label="Exact dates only", value=True, scale=1)
v_lim = gr.Slider(25, 1000, value=200, step=25, label="Rows", scale=1)
t_piv = gr.Dataframe(
value=filter_pivotal(180, True, "", [], 0, 200),
wrap=True, max_height=430,
datatype=["str", "str", "str", "str", "number", "str", "str",
"markdown", "markdown"])
with gr.Row():
p_pm = gr.Plot(pivotal_month_chart(True))
p_ps = gr.Plot(pivotal_sponsor_chart(180))
vins = [v_days, v_ex, v_sp, v_mod, v_n, v_lim]
for c in vins:
c.change(filter_pivotal, vins, t_piv)
v_ex.change(pivotal_month_chart, v_ex, p_pm)
v_days.change(pivotal_sponsor_chart, v_days, p_ps)
with gr.Tab("Pipeline mix"):
gr.HTML("<div class='bi-note'>Modality and phase mix of the forward "
"pipeline β€” the closest public read on what kind of wet-lab and "
"manufacturing capacity will be needed.</div>")
with gr.Row():
gr.Plot(modality_chart([]))
gr.Plot(phase_chart())
with gr.Tab("About & caveats"):
gr.Markdown(ABOUT)
gr.HTML("<div style='margin-top:16px;font-size:.78rem;color:#94A3B8'>"
"Trial data courtesy of the U.S. National Library of Medicine "
"(ClinicalTrials.gov), modified β€” see About. SEC filings are public "
"domain. Not investment advice.</div>")
if __name__ == "__main__":
demo.launch(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="slate"),
server_name="0.0.0.0")