"""Final tab / Chapter 6: Your Report — field report generation and export."""
import gradio as gr
from src.plot_config import COLORS
# =====================================================================
# Module-level HTML builders (callable from callbacks)
# =====================================================================
def build_report_html(state, data: dict) -> str:
"""Build a presentation-ready styled HTML field report.
Parameters
----------
state : AppState
Current application state with selected_line, backpack, achievements.
data : dict
Loaded data dictionary with line_stats, embedding, similarity, etc.
Returns
-------
str
Self-contained HTML string with inline CSS for portability.
"""
if state is None or not state.selected_line:
return (
'
'
'
No line selected yet.
'
'
Go back to Chapter 0 to choose a line first.
'
"
"
)
line_id = state.selected_line
line_stats = data["line_stats"]
embedding = data["embedding"]
similarity = data["similarity"]
gene_freq = data["gene_freq"]
pav = data.get("pav")
# -- Gather stats --
ls_row = line_stats[line_stats["line_id"] == line_id]
country = ls_row.iloc[0]["country"] if len(ls_row) > 0 else "Unknown"
genes_present = int(ls_row.iloc[0]["genes_present_count"]) if len(ls_row) > 0 else 0
unique_genes = int(ls_row.iloc[0]["unique_genes_count"]) if len(ls_row) > 0 else 0
emb_row = embedding[embedding["line_id"] == line_id]
cluster_id = int(emb_row.iloc[0]["cluster_id"]) if len(emb_row) > 0 else -1
# Nearest neighbors
sim_rows = similarity[similarity["line_id"] == line_id].nlargest(3, "jaccard_score")
neighbors = [
(r["neighbor_line_id"], f"{r['jaccard_score']:.3f}")
for _, r in sim_rows.iterrows()
]
# Core/shell/cloud breakdown
core_count = shell_count = cloud_count = 0
if pav is not None and line_id in pav.columns:
my_genes = set(pav.index[pav[line_id] == 1])
my_freq = gene_freq[gene_freq["gene_id"].isin(my_genes)]
core_count = int((my_freq["core_class"] == "core").sum())
shell_count = int((my_freq["core_class"] == "shell").sum())
cloud_count = int((my_freq["core_class"] == "cloud").sum())
# Rare genes
rare_rows = []
if pav is not None and line_id in pav.columns:
my_genes_list = pav.index[pav[line_id] == 1].tolist()
rare = gene_freq[
(gene_freq["gene_id"].isin(my_genes_list))
& (gene_freq["freq_count"] <= 5)
].nsmallest(5, "freq_count")
for _, r in rare.iterrows():
rare_rows.append((r["gene_id"], int(r["freq_count"]), r["core_class"]))
# Backpack
backpack_rows = []
for g in (state.backpack_genes or []):
gf = gene_freq[gene_freq["gene_id"] == g]
if len(gf) > 0:
backpack_rows.append((g, gf.iloc[0]["core_class"], int(gf.iloc[0]["freq_count"])))
else:
backpack_rows.append((g, "unknown", 0))
# -- Build HTML --
# Metric cards row
metrics = [
(f"{genes_present:,}", "Genes Present", COLORS["core"]),
(str(unique_genes), "Unique Genes", COLORS["accent"]),
(str(cluster_id), "Cluster", COLORS["selected"]),
(str(len(state.backpack_genes)), "Backpack Genes", COLORS["shell"]),
]
metric_cards = ""
for val, label, color in metrics:
metric_cards += (
f'"
)
# Neighbor rows
neighbor_html = ""
for name, score in neighbors:
neighbor_html += (
f''
f'{name}'
f'{score}'
f"
"
)
# Composition bar
total_csc = core_count + shell_count + cloud_count
if total_csc > 0:
core_pct = core_count / total_csc * 100
shell_pct = shell_count / total_csc * 100
cloud_pct = cloud_count / total_csc * 100
else:
core_pct = shell_pct = cloud_pct = 0
composition_bar = (
f'"
f''
f'Core: {core_count:,}'
f'Shell: {shell_count:,}'
f'Cloud: {cloud_count:,}'
f"
"
)
# Rare genes table
rare_html = ""
if rare_rows:
rare_html += (
''
''
'| Gene | '
'Lines | '
'Class | '
"
"
)
for gene, count, cls in rare_rows:
badge_color = {"core": COLORS["core"], "shell": COLORS["shell"], "cloud": COLORS["cloud"]}.get(cls, "#9E9E9E")
badge_text_color = "#FFFFFF" if cls != "shell" else "#333333"
rare_html += (
f''
f'| {gene} | '
f'{count} | '
f''
f'{cls} |
'
)
rare_html += "
"
else:
rare_html = 'No rare genes (≤5 lines) found.
'
# Backpack table
backpack_html = ""
if backpack_rows:
backpack_html += (
''
''
'| Gene | '
'Class | '
'Lines | '
"
"
)
for gene, cls, count in backpack_rows:
badge_color = {"core": COLORS["core"], "shell": COLORS["shell"], "cloud": COLORS["cloud"]}.get(cls, "#9E9E9E")
badge_text_color = "#FFFFFF" if cls != "shell" else "#333333"
backpack_html += (
f''
f'| {gene} | '
f''
f'{cls} | '
f'{count} | '
f"
"
)
backpack_html += "
"
else:
backpack_html = 'No genes pinned to backpack.
'
# Achievement pills
achievements_pills = ""
for a in sorted(state.achievements):
achievements_pills += (
f''
f"{a}"
)
if not achievements_pills:
achievements_pills = 'No achievements yet'
# -- Assemble full report --
html = f'''
Field Report
{line_id}
Origin: {country} · Cluster {cluster_id}
{metric_cards}
Nearest Neighbors
{neighbor_html}
Gene Composition
{composition_bar}
Top 5 Rare Genes
{rare_html}
Backpack Collection
{backpack_html}
Achievements Earned
{achievements_pills}
Generated by Pigeon Pea Pangenome Atlas
'''
return html
def build_achievements_html(state) -> str:
"""Build styled HTML for achievement badges.
Parameters
----------
state : AppState
Current application state.
Returns
-------
str
HTML string with styled badge pills.
"""
if state is None or not state.achievements:
return (
''
"Complete quests to earn badges!
"
)
pills = []
for a in sorted(state.achievements):
pills.append(
f'{a}'
)
return '' + "".join(pills) + "
"
# =====================================================================
# Gradio UI builder
# =====================================================================
def build_final_tab():
"""Build Final Report tab components. Returns dict of components.
Returned keys (prefixed with ``final_`` by layout.py):
tab, generate_btn, report_md, download_json, download_csv, achievements_html
"""
with gr.Tab("Your Report", id="final") as tab:
# -- Header --
gr.HTML(
''
'
Chapter 6: Your Report
'
'
'
"Generate a presentation-ready summary of your pangenome exploration, "
"including your selected line, findings, and backpack collection."
"
"
)
generate_btn = gr.Button(
"Generate Report",
variant="primary",
size="lg",
)
report_md = gr.Markdown(
value="*Click 'Generate Report' to create your field report.*"
)
with gr.Row():
download_json = gr.File(label="Download JSON", visible=False)
download_csv = gr.File(label="Download CSV", visible=False)
gr.HTML(
''
'
Achievements Earned
'
""
)
achievements_html = gr.HTML(
value=(
''
"Complete quests to earn badges!
"
),
)
return {
"tab": tab,
"generate_btn": generate_btn,
"report_md": report_md,
"download_json": download_json,
"download_csv": download_csv,
"achievements_html": achievements_html,
}