feat(dashboard): aquarium click handler with info panel + ancestor lineage

Rimuove sidebar acquario (slider max-pesci, toggle label): la dimensione
popolazione è già definita dal GA, le label sono ridondanti col pannello
di ispezione. Mostra tutti i pesci della generazione selezionata.

Aggiunge `build_lineage_index` (mappa ogni genome_id della run ai suoi
attributi) e `trace_ancestors` (BFS sui parent_ids fino a max_levels,
guardia su cicli). `build_fish_dataset` accetta ora il lineage_index e
allega il campo `ancestors` ad ogni pesce; conserva la firma legacy per
compat con i fixture di test esistenti.

`build_aquarium_html` perde `show_labels`. Embedda click handler con
hit-test in canvas pixel space (account per CSS scaling) + pannello
info top-right con stile, fitness/DSR/Sharpe/maxDD/trades, prompt e
albero discendenza colorato per cognitive_style.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 09:47:10 +02:00
parent 4dad8be36b
commit 3688611a40
3 changed files with 688 additions and 153 deletions
+42 -39
View File
@@ -7,10 +7,10 @@ from multi_swarm.dashboard.aquarium import (
STYLE_COLORS,
build_aquarium_html,
build_fish_dataset,
build_lineage_index,
)
from multi_swarm.dashboard.data import (
evaluations_df,
generations_df,
genomes_df,
get_repo,
list_runs_df,
@@ -18,8 +18,8 @@ from multi_swarm.dashboard.data import (
st.title("Aquarium 2D")
st.caption(
"Ogni genoma è un pesce: dimensione proporzionale alla fitness, "
"colore per cognitive_style."
"Pesci colorati per stile cognitivo, dimensione proporzionale a fitness. "
"Click su un pesce per dettaglio + discendenza."
)
db_path = st.session_state.get("db_path", "./runs.db")
@@ -27,52 +27,55 @@ repo = get_repo(db_path)
runs = list_runs_df(repo)
if runs.empty:
st.info("Nessuna run.")
st.info("Nessuna run nel database.")
st.stop()
selected = st.selectbox("Run", runs["id"].tolist())
selected_run = st.selectbox("Run", runs["id"].tolist())
gens = generations_df(repo, selected)
gen_options: list[int | None] = [None]
if not gens.empty:
gen_options.extend(sorted(gens["generation_idx"].unique().tolist()))
# Fetch ALL genomes of the run (no gen filter): needed to build the lineage
# index across generations. The active set is filtered afterwards.
all_genomes = genomes_df(repo, selected_run)
all_evals = evaluations_df(repo, selected_run)
def _fmt_gen(v: int | None) -> str:
return "tutte le generazioni" if v is None else f"gen {v}"
# Default to latest gen if available.
default_idx = len(gen_options) - 1 if len(gen_options) > 1 else 0
gen_choice = st.selectbox(
"Generazione", gen_options, index=default_idx, format_func=_fmt_gen
)
with st.sidebar:
st.subheader("Aquarium controls")
max_fish = st.slider("Max pesci", min_value=5, max_value=100, value=30, step=1)
show_labels = st.toggle("Mostra ID genoma", value=False)
if st.button("Refresh"):
st.rerun()
evals = evaluations_df(repo, selected)
genomes = genomes_df(repo, selected, generation_idx=gen_choice)
if evals.empty or genomes.empty:
st.warning("Nessun dato disponibile per questa run/generazione.")
if all_genomes.empty:
st.warning("Nessun genoma per questa run.")
st.stop()
merged = evals.merge(
genomes, left_on="genome_id", right_on="id", how="inner", suffixes=("", "_g")
available_gens = sorted(all_genomes["generation_idx"].unique().tolist())
selected_gen = st.selectbox(
"Generazione",
available_gens,
index=len(available_gens) - 1, # default ultima
)
if merged.empty:
st.warning("Nessuna evaluation associata ai genomi della generazione scelta.")
active_genomes = all_genomes[all_genomes["generation_idx"] == selected_gen]
active_evals = (
all_evals[all_evals["genome_id"].isin(active_genomes["id"])]
if not all_evals.empty
else all_evals
)
if not active_evals.empty:
active_merged = active_genomes.merge(
active_evals,
left_on="id",
right_on="genome_id",
how="left",
suffixes=("", "_eval"),
)
else:
active_merged = active_genomes.copy()
active_merged["genome_id"] = active_merged["id"]
lineage = build_lineage_index(all_genomes, all_evals)
fish = build_fish_dataset(active_merged, lineage, max_lineage_levels=5)
if not fish:
st.warning("Nessun agente attivo in questa generazione.")
st.stop()
fish = build_fish_dataset(merged, max_fish=max_fish)
st.write(f"Visualizzati {len(fish)} pesci (top per fitness).")
st.caption(f"{len(fish)} agenti in generazione {selected_gen}")
html_str = build_aquarium_html(
fish, canvas_w=1000, canvas_h=600, show_labels=show_labels
)
html_str = build_aquarium_html(fish, canvas_w=1000, canvas_h=600)
components.html(html_str, height=620, scrolling=False)
with st.expander("Legenda colori"):