Spaces:
Running
Running
added hover on doughnut and tooltip on experiment image
Browse files- streamlit_hf/home.py +40 -1
- streamlit_hf/lib/plots.py +46 -13
- streamlit_hf/static/experiment.svg +224 -203
streamlit_hf/home.py
CHANGED
|
@@ -76,6 +76,43 @@ def _downsample_latent_df(df, max_points: int = 6500, seed: int = 42):
|
|
| 76 |
return df.sample(n=max_points, random_state=seed)
|
| 77 |
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
ui.inject_app_styles()
|
| 80 |
ui.inject_home_landing_styles()
|
| 81 |
|
|
@@ -94,7 +131,7 @@ with st.container(border=True):
|
|
| 94 |
fig_col, text_col = st.columns([0.42, 0.58], gap="large")
|
| 95 |
with fig_col:
|
| 96 |
if _EXPERIMENT_SVG.is_file():
|
| 97 |
-
|
| 98 |
else:
|
| 99 |
st.caption("Experimental schematic (`static/experiment.svg`) is missing.")
|
| 100 |
with text_col:
|
|
@@ -219,6 +256,7 @@ if bundle is not None and df_features is not None:
|
|
| 219 |
top_n_pie=_HOME_PIE_TOP_N,
|
| 220 |
chart_outline=False,
|
| 221 |
modality_mix_hole=0.66,
|
|
|
|
| 222 |
)
|
| 223 |
fig_g.update_layout(title_text="", margin=dict(l=36, r=36, t=48, b=100))
|
| 224 |
fig_g.update_annotations(font_size=12)
|
|
@@ -269,6 +307,7 @@ elif df_features is not None:
|
|
| 269 |
top_n_pie=_HOME_PIE_TOP_N,
|
| 270 |
chart_outline=False,
|
| 271 |
modality_mix_hole=0.66,
|
|
|
|
| 272 |
)
|
| 273 |
fig_g.update_layout(title_text="", margin=dict(l=36, r=36, t=48, b=100))
|
| 274 |
st.plotly_chart(fig_g, width="stretch", config={"displayModeBar": False, "displaylogo": False})
|
|
|
|
| 76 |
return df.sample(n=max_points, random_state=seed)
|
| 77 |
|
| 78 |
|
| 79 |
+
def _render_experiment_schematic(width_px: int) -> None:
|
| 80 |
+
"""Show the schematic as inline SVG so each group can use CSS hover and native tooltips."""
|
| 81 |
+
raw = _EXPERIMENT_SVG.read_text(encoding="utf-8")
|
| 82 |
+
if raw.lstrip().startswith("<?xml"):
|
| 83 |
+
raw = raw.split("?>", 1)[1].lstrip()
|
| 84 |
+
html_doc = f"""<!DOCTYPE html>
|
| 85 |
+
<html><head><meta charset="utf-8"/><style>
|
| 86 |
+
html, body {{ margin: 0; padding: 0; overflow: hidden; background: transparent; }}
|
| 87 |
+
.ff-experiment-svg-wrap {{ width: {width_px}px; max-width: 100%; }}
|
| 88 |
+
.ff-experiment-svg-wrap svg {{ width: 100%; height: auto; display: block; }}
|
| 89 |
+
.ff-experiment-svg-wrap svg g[id] {{
|
| 90 |
+
cursor: help;
|
| 91 |
+
transition: filter 0.15s ease;
|
| 92 |
+
}}
|
| 93 |
+
.ff-experiment-svg-wrap svg g[id]:hover {{
|
| 94 |
+
filter: brightness(1.12) drop-shadow(0 0 1.5px rgba(79, 70, 229, 0.55));
|
| 95 |
+
}}
|
| 96 |
+
/* Microscope: decorative only (no tooltip); ignore pointer so it does not steal hovers. */
|
| 97 |
+
.ff-experiment-svg-wrap svg #microscope,
|
| 98 |
+
.ff-experiment-svg-wrap svg #microscope * {{
|
| 99 |
+
pointer-events: none;
|
| 100 |
+
}}
|
| 101 |
+
.ff-experiment-svg-wrap svg text {{
|
| 102 |
+
cursor: help;
|
| 103 |
+
transition: filter 0.15s ease;
|
| 104 |
+
}}
|
| 105 |
+
.ff-experiment-svg-wrap svg text:hover {{
|
| 106 |
+
filter: brightness(1.08);
|
| 107 |
+
}}
|
| 108 |
+
</style></head><body>
|
| 109 |
+
<div class="ff-experiment-svg-wrap">
|
| 110 |
+
{raw}
|
| 111 |
+
</div>
|
| 112 |
+
</body></html>"""
|
| 113 |
+
st.iframe(html_doc, width=width_px, height="content")
|
| 114 |
+
|
| 115 |
+
|
| 116 |
ui.inject_app_styles()
|
| 117 |
ui.inject_home_landing_styles()
|
| 118 |
|
|
|
|
| 131 |
fig_col, text_col = st.columns([0.42, 0.58], gap="large")
|
| 132 |
with fig_col:
|
| 133 |
if _EXPERIMENT_SVG.is_file():
|
| 134 |
+
_render_experiment_schematic(_EXPERIMENT_FIGURE_WIDTH_PX)
|
| 135 |
else:
|
| 136 |
st.caption("Experimental schematic (`static/experiment.svg`) is missing.")
|
| 137 |
with text_col:
|
|
|
|
| 256 |
top_n_pie=_HOME_PIE_TOP_N,
|
| 257 |
chart_outline=False,
|
| 258 |
modality_mix_hole=0.66,
|
| 259 |
+
modality_mix_hover_feature_list=True,
|
| 260 |
)
|
| 261 |
fig_g.update_layout(title_text="", margin=dict(l=36, r=36, t=48, b=100))
|
| 262 |
fig_g.update_annotations(font_size=12)
|
|
|
|
| 307 |
top_n_pie=_HOME_PIE_TOP_N,
|
| 308 |
chart_outline=False,
|
| 309 |
modality_mix_hole=0.66,
|
| 310 |
+
modality_mix_hover_feature_list=True,
|
| 311 |
)
|
| 312 |
fig_g.update_layout(title_text="", margin=dict(l=36, r=36, t=48, b=100))
|
| 313 |
st.plotly_chart(fig_g, width="stretch", config={"displayModeBar": False, "displaylogo": False})
|
streamlit_hf/lib/plots.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
from typing import Any
|
| 6 |
|
| 7 |
import numpy as np
|
|
@@ -557,6 +558,18 @@ def attention_cohort_view(
|
|
| 557 |
return fig
|
| 558 |
|
| 559 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
def global_rank_triple_panel(
|
| 561 |
df_features,
|
| 562 |
top_n: int = 20,
|
|
@@ -564,6 +577,7 @@ def global_rank_triple_panel(
|
|
| 564 |
*,
|
| 565 |
chart_outline: bool = True,
|
| 566 |
modality_mix_hole: float = 0.0,
|
|
|
|
| 567 |
):
|
| 568 |
"""
|
| 569 |
Global top-N by latent-shift and by attention (min-max scaled), plus pie or donut of modality mix
|
|
@@ -571,6 +585,8 @@ def global_rank_triple_panel(
|
|
| 571 |
|
| 572 |
Set ``chart_outline=False`` for a flatter look (e.g. home page); Feature Insights keeps outlines by default.
|
| 573 |
Set ``modality_mix_hole`` in (0, 1), e.g. ``0.66``, for a donut instead of a full pie (e.g. home page).
|
|
|
|
|
|
|
| 574 |
"""
|
| 575 |
d = df_features.copy()
|
| 576 |
for col in ("importance_shift", "importance_att"):
|
|
@@ -636,20 +652,37 @@ def global_rank_triple_panel(
|
|
| 636 |
_hole = float(modality_mix_hole) if modality_mix_hole and modality_mix_hole > 0 else 0.0
|
| 637 |
# Narrow third subplot: "auto" avoids clipped outside labels on donuts.
|
| 638 |
_pie_textpos = "auto"
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
line=pie_line,
|
| 646 |
-
),
|
| 647 |
-
textinfo="label+percent",
|
| 648 |
-
textfont_size=12,
|
| 649 |
-
textposition=_pie_textpos,
|
| 650 |
-
hole=_hole,
|
| 651 |
-
showlegend=False,
|
| 652 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
row=1,
|
| 654 |
col=3,
|
| 655 |
)
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import html
|
| 6 |
from typing import Any
|
| 7 |
|
| 8 |
import numpy as np
|
|
|
|
| 558 |
return fig
|
| 559 |
|
| 560 |
|
| 561 |
+
def _pie_hover_feature_lines(names: list[str], *, names_per_line: int = 5) -> str:
|
| 562 |
+
"""Join feature names with commas; start a new hover row every ``names_per_line`` items (HTML ``<br>``)."""
|
| 563 |
+
if not names:
|
| 564 |
+
return "—"
|
| 565 |
+
safe = [html.escape(str(n), quote=False) for n in names]
|
| 566 |
+
step = max(1, int(names_per_line))
|
| 567 |
+
lines: list[str] = []
|
| 568 |
+
for i in range(0, len(safe), step):
|
| 569 |
+
lines.append(", ".join(safe[i : i + step]))
|
| 570 |
+
return "<br>".join(lines)
|
| 571 |
+
|
| 572 |
+
|
| 573 |
def global_rank_triple_panel(
|
| 574 |
df_features,
|
| 575 |
top_n: int = 20,
|
|
|
|
| 577 |
*,
|
| 578 |
chart_outline: bool = True,
|
| 579 |
modality_mix_hole: float = 0.0,
|
| 580 |
+
modality_mix_hover_feature_list: bool = False,
|
| 581 |
):
|
| 582 |
"""
|
| 583 |
Global top-N by latent-shift and by attention (min-max scaled), plus pie or donut of modality mix
|
|
|
|
| 585 |
|
| 586 |
Set ``chart_outline=False`` for a flatter look (e.g. home page); Feature Insights keeps outlines by default.
|
| 587 |
Set ``modality_mix_hole`` in (0, 1), e.g. ``0.66``, for a donut instead of a full pie (e.g. home page).
|
| 588 |
+
Set ``modality_mix_hover_feature_list=True`` to show comma-separated feature names per donut slice on hover
|
| 589 |
+
(same pool as the pie: strongest by mean rank within each modality), wrapped every few names for readability.
|
| 590 |
"""
|
| 591 |
d = df_features.copy()
|
| 592 |
for col in ("importance_shift", "importance_att"):
|
|
|
|
| 652 |
_hole = float(modality_mix_hole) if modality_mix_hole and modality_mix_hole > 0 else 0.0
|
| 653 |
# Narrow third subplot: "auto" avoids clipped outside labels on donuts.
|
| 654 |
_pie_textpos = "auto"
|
| 655 |
+
_pie_kwargs: dict = dict(
|
| 656 |
+
labels=pie_labels,
|
| 657 |
+
values=pie_vals,
|
| 658 |
+
marker=dict(
|
| 659 |
+
colors=[MODALITY_PIE_COLOR.get(l, "#64748b") for l in pie_labels],
|
| 660 |
+
line=pie_line,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 661 |
),
|
| 662 |
+
textinfo="label+percent",
|
| 663 |
+
textfont_size=12,
|
| 664 |
+
textposition=_pie_textpos,
|
| 665 |
+
hole=_hole,
|
| 666 |
+
showlegend=False,
|
| 667 |
+
)
|
| 668 |
+
if modality_mix_hover_feature_list:
|
| 669 |
+
_hover_texts: list[str] = []
|
| 670 |
+
for lab in pie_labels:
|
| 671 |
+
sub = pie_pool[pie_pool["modality"] == lab]
|
| 672 |
+
if sub.empty:
|
| 673 |
+
_hover_texts.append("—")
|
| 674 |
+
else:
|
| 675 |
+
sub = sub.sort_values("mean_rank", ascending=True, kind="mergesort")
|
| 676 |
+
_per_line = 1 if lab == "Flux" else 5
|
| 677 |
+
_hover_texts.append(
|
| 678 |
+
_pie_hover_feature_lines(sub["feature"].astype(str).tolist(), names_per_line=_per_line)
|
| 679 |
+
)
|
| 680 |
+
_pie_kwargs["hovertext"] = _hover_texts
|
| 681 |
+
_pie_kwargs["hovertemplate"] = (
|
| 682 |
+
"<b>%{label}</b> · %{value} features (%{percent:.1%})<br><br>%{hovertext}<extra></extra>"
|
| 683 |
+
)
|
| 684 |
+
fig.add_trace(
|
| 685 |
+
go.Pie(**_pie_kwargs),
|
| 686 |
row=1,
|
| 687 |
col=3,
|
| 688 |
)
|
streamlit_hf/static/experiment.svg
CHANGED
|
|
|
|