IQA-Interpretation / pages /visualizations.py
dvarfe's picture
fix pages bug
2cecbb7
Raw
History Blame Contribute Delete
44.9 kB
from __future__ import annotations
from pathlib import Path
import dash
import numpy as np
from dash import Input, Output, State, callback, clientside_callback, ctx, dcc, html, no_update
from analysis.viz.umap_plot import apply_feature_umap_highlight
from analysis.viz.vis_heatmaps_plotly import (
TOGGLE_HEATMAP_OVERLAY_CLIENTSIDE,
empty_heatmap_figure,
heatmap_overlay_checklist_props,
set_heatmap_overlay_visible,
)
from dashboard.image_utils import get_top_feature_overlays, overlay_show_heatmap_enabled
from dashboard.layout import page_back_nav
from dashboard.model_catalog import (
dashboard_dataset_label,
get_model_record,
summarize_model_record,
summarize_selector_cache,
)
from dashboard.umap_service import (
AGGREGATION_MODE_LABELS,
AGGREGATION_MODES,
DEFAULT_AGGREGATION_MODE,
DEFAULT_COLOR_MODE,
FEATURE_COLOR_MODE,
FEATURE_UMAP_FILTERED_HELP,
FEATURE_UMAP_GARBAGE_HELP,
IMAGE_COLOR_MODE_LABELS,
IMAGE_COLOR_MODES,
THUMB_SIZE,
UMAP_DATASET,
build_features_umap,
build_images_umap,
empty_umap_figure,
format_feature_umap_status,
format_umap_status,
)
from dashboard.viz_helpers import (
UMAP_TOOLTIP_CLASSNAME,
UMAP_TOOLTIP_ZINDEX,
build_feature_detail_content,
build_feature_umap_hover_tooltip,
cached_hover_meta_line,
umap_tooltip_clientside,
cached_hover_thumb,
feature_empty_state,
feature_ids,
feature_rows_for_selector,
feature_unavailable_message,
hover_image_idx,
select_feature_id,
selector_option_label,
visualization_params,
)
ROOT = Path(__file__).resolve().parents[1]
FEATURE_TOP_IMAGE_SLOTS = 1
FEATURE_TOP_IMAGE_TOP_N = 5
HEATMAP_GRAPH_CONFIG = {
"displayModeBar": False,
"responsive": True,
"scrollZoom": False,
"doubleClick": "reset+autosize",
}
dash.register_page(__name__, path="/visualizations", name="Visualizations", title="SAE Explorer")
def _umap_images_cache_key(
metric: str,
model_key: str,
aggregation_mode: str,
color_mode: str,
selected_category: str | None,
) -> str:
category = "" if selected_category in {None, ""} else str(selected_category)
return f"{metric}:{model_key}:{aggregation_mode}:{color_mode}:{category}"
_HYPERPARAM_HELP_ROWS: tuple[tuple[str, str], ...] = (
("layer_num", "IQA layer used as SAE input"),
("swin_num", "Swin stage (MANIQA only)"),
# ("liqe_variant", "LIQE backbone variant"),
("lambda_param", "sparsity penalty in the SAE loss"),
("scaling_factor", "activation scaling before the SAE"),
("n_epochs", "number of training epochs"),
("sae_type", "sparse autoencoder architecture(sae - standard ReLU SAE, mp_sae - Matching Pursuit SAE)"),
("l0_loss", "mean active SAE features per validation image"),
)
def _label_desc_line(label: str, description: str, *, tag: str = "div") -> html.Div | html.Li:
line = [
html.Strong(label),
html.Span(f" — {description}", className="help-desc"),
]
if tag == "li":
return html.Li(line)
return html.Div(line, className="help-desc-line")
def _sae_hyperparams_help() -> html.Div:
return html.Div(
[
html.Span("?", className="upload-help viz-sae-help-trigger", **{"aria-label": "SAE hyperparameters help"}),
html.Div(
[
html.Div("Hyperparameters (from training logs):", className="viz-help-popover-title"),
*[
_label_desc_line(label, desc)
for label, desc in _HYPERPARAM_HELP_ROWS
],
],
className="viz-sae-help-popover",
),
],
className="viz-sae-help-wrap",
)
def _umap_help() -> html.Div:
return html.Div(
[
html.Span("?", className="upload-help viz-sae-help-trigger", **{"aria-label": "UMAP help"}),
html.Div(
[
html.Div("UMAP views", className="viz-help-popover-title"),
html.Div("Image UMAP", className="viz-help-popover-subtitle"),
html.Div(
f"Each point is one image from {UMAP_DATASET}: aggregated SAE activations "
"projected to 2D (UMAP).",
className="viz-help-popover-lead",
),
_label_desc_line(
"Mean activation",
"aggregate patch activations per image with the mean over non-zero values.",
),
_label_desc_line("Maximum", "aggregate with the per-image maximum activation."),
_label_desc_line("Sum", "aggregate with the per-image sum of activations."),
_label_desc_line(
"Distortion group",
"color points by distortion group.",
),
_label_desc_line(
"Distortion type",
"color points by distortion type.",
),
html.Div("Feature UMAP", className="viz-help-popover-subtitle"),
html.Div(
"Each point is one sparse SAE feature (latent direction) after filtering, "
"projected to 2D (UMAP).",
className="viz-help-popover-lead",
),
_label_desc_line(
"Distortion group",
"color each feature by the distortion group with the strongest patch-level |correlation|.",
),
_label_desc_line("filtered", FEATURE_UMAP_FILTERED_HELP),
_label_desc_line("garbage", FEATURE_UMAP_GARBAGE_HELP),
html.Div(
"filtered and garbage points are hidden by default; click their legend entries to show them.",
className="viz-help-popover-lead",
),
],
className="viz-sae-help-popover viz-umap-help-popover",
),
],
className="viz-sae-help-wrap viz-umap-help-wrap",
)
def _feature_workbench_help() -> html.Div:
return html.Div(
[
html.Span(
"?",
className="upload-help viz-sae-help-trigger",
**{"aria-label": "Feature workbench help"},
),
html.Div(
[
html.Div("Feature workbench", className="viz-help-popover-title"),
html.Div("Feature sort selector", className="viz-help-popover-subtitle"),
html.Div(
"Picks which precomputed ranking defines the ordered feature list "
"for this model and dataset (correlation, ROC-AUC, precision,"
"recall and similar metrics).",
className="viz-help-popover-lead",
),
html.Div("Feature navigation", className="viz-help-popover-subtitle"),
_label_desc_line(
"Prev / Next",
"step through features in the active selector ranking.",
),
_label_desc_line(
"Jump to feature id",
"go directly to a global SAE feature id (even if it is not top-ranked).",
),
_label_desc_line(
"Feature UMAP click",
"in Features UMAP mode, clicking a point selects that feature id.",
),
html.Div("Image ranking", className="viz-help-popover-subtitle"),
_label_desc_line(
"IoU",
"top images with the largest overlap between the feature heatmap and "
"dataset distortion masks.",
),
_label_desc_line(
"Maximum activation",
"top images with the highest peak SAE activation for this feature.",
),
],
className="viz-sae-help-popover viz-feature-workbench-help-popover",
),
],
className="viz-sae-help-wrap viz-feature-workbench-help-wrap",
)
def _feature_filter_intro_block() -> html.Div:
return html.Div(
[
html.P(
"Cached activations are filtered before further analysis.",
className="viz-filter-intro-lead",
),
html.P("Available filters:", className="viz-filter-intro-lead"),
html.Ul(
[
_label_desc_line(
"nonzero_max",
"removes features whose maximum activation is 0 (they never fire on any image).",
tag="li",
),
_label_desc_line(
"kruskal_wallis",
"keeps features with statistically significant differences across "
"distortion groups (Kruskal–Wallis test on activations).",
tag="li",
),
],
className="viz-filter-intro-list",
),
html.P(
"Each line below is one applied filter step and reports before / after / removed feature counts.",
className="viz-filter-intro-lead",
),
],
className="viz-filter-intro",
)
def _format_filter_result_line(line: str) -> html.Div:
if ":" not in line:
return html.Div(line, className="viz-filter-line")
name, rest = line.split(":", 1)
return html.Div(
[
html.Strong(f"{name.strip()}:"),
html.Span(rest, className="help-desc"),
],
className="viz-filter-line",
)
def _skeleton_lines(n: int = 3, short_last: bool = True) -> list[html.Div]:
lines = []
for index in range(n):
class_name = "skeleton-line"
if short_last and index == n - 1:
class_name += " skeleton-line-short"
lines.append(html.Div(className=class_name))
return lines
def _feature_image_slot(slot_idx: int) -> html.Div:
return html.Div(
[
dcc.Graph(
id=f"feature-image-{slot_idx}",
figure=empty_heatmap_figure("Loading..."),
config=HEATMAP_GRAPH_CONFIG,
className="heatmap-graph",
),
html.Div(
"Top images overlay is not available yet.",
id=f"feature-image-caption-{slot_idx}",
className="feature-image-caption",
),
],
id=f"feature-image-card-{slot_idx}",
className="feature-image-card",
)
def _empty_feature_image_cache(message: str) -> tuple[object, str, str]:
return None, message, "Top images overlay is not available yet."
layout = html.Div(
[
page_back_nav(),
html.Div(
[html.H1("SAE Explorer", className="app-title")],
className="hero-card",
),
html.Div(
[
html.Div(
[
html.Div("Selected SAE", className="panel-label"),
_sae_hyperparams_help(),
dcc.Loading(
id="viz-model-info-loading",
type="default",
color="#2563eb",
className="loading-block viz-model-info-loading",
children=html.Div(
id="viz-model-info",
className="mock-summary-lines",
children=_skeleton_lines(4),
),
),
],
className="mock-card viz-sae-card",
),
html.Div(
[
html.Div("UMAP SAE vector", className="panel-label"),
_umap_help(),
html.Div(
[
html.Div(
[
html.Div(
[
html.Div("UMAP mode", className="umap-mode-label"),
dcc.RadioItems(
id="umap-mode-dropdown",
options=[
{"label": "Images", "value": "images"},
{"label": "Features", "value": "features"},
],
value="images",
className="umap-mode-switch",
labelClassName="umap-mode-pill",
inputClassName="umap-mode-input",
),
],
className="umap-mode-primary",
),
html.Div(
[
html.Div(
[
html.Div("Aggregation mode", className="section-label"),
dcc.Dropdown(
id="aggregation-dropdown",
options=[
{
"label": AGGREGATION_MODE_LABELS[mode],
"value": mode,
}
for mode in AGGREGATION_MODES
],
value=DEFAULT_AGGREGATION_MODE,
clearable=False,
className="dash-dropdown model-dropdown",
),
],
id="aggregation-control-block",
className="umap-control-block",
),
html.Div(
[
html.Div("Color mode", className="section-label"),
dcc.Dropdown(
id="color-dropdown",
options=[
{
"label": IMAGE_COLOR_MODE_LABELS[mode],
"value": mode,
}
for mode in IMAGE_COLOR_MODES
],
value=DEFAULT_COLOR_MODE,
clearable=False,
className="dash-dropdown model-dropdown",
),
],
id="color-control-block",
className="umap-control-block",
),
html.Div(
[],
id="feature-color-source-control-block",
className="umap-control-block",
style={"display": "none"},
),
],
className="umap-controls-row umap-controls-secondary",
),
],
className="umap-controls",
),
html.Div(
[
html.Div(id="umap-status", className="umap-status"),
dcc.Loading(
id="umap-graph-loading",
type="circle",
color="#2563eb",
className="loading-block umap-graph-loading",
children=dcc.Graph(
id="umap-graph",
figure=empty_umap_figure("Loading UMAP..."),
clear_on_unhover=True,
config={"displayModeBar": False, "responsive": True},
className="umap-graph",
style={"height": "min(58vh, 640px)", "minHeight": "430px"},
),
),
dcc.Tooltip(
id="umap-tooltip",
className=UMAP_TOOLTIP_CLASSNAME,
zindex=UMAP_TOOLTIP_ZINDEX,
direction="right",
),
],
className="umap-hover-shell",
),
dcc.Store(id="category-filter-store", data=None),
dcc.Store(id="umap-figures-cache", data=None),
],
className="mock-placeholder",
),
],
className="placeholder-card viz-umap-card",
),
],
className="mock-grid",
),
html.Div(
[
html.Div(
[
html.Div("Feature workbench", className="panel-label"),
_feature_workbench_help(),
],
className="feature-workbench-title-row",
),
html.Div(
[
html.Div(
[
html.Div(
[
html.Div("Feature sort selector", className="section-label"),
dcc.Dropdown(
id="feature-sort-selector",
options=[],
value=None,
clearable=False,
disabled=True,
className="dash-dropdown model-dropdown",
),
],
className="feature-workbench-controls feature-workbench-controls-sort",
),
html.Div(
[
html.Div("Feature navigation", className="section-label"),
html.Div(
[
html.Button(
"Prev", id="feature-prev", n_clicks=0, className="nav-button"
),
dcc.Input(
id="feature-jump-input",
type="text",
inputMode="numeric",
pattern="[0-9]*",
debounce=True,
className="feature-jump-input",
value="",
),
html.Button(
"Next", id="feature-next", n_clicks=0, className="nav-button"
),
],
className="feature-nav-row",
),
],
className="feature-workbench-controls feature-workbench-controls-nav",
),
html.Div(
[
html.Div("Image ranking", className="section-label"),
dcc.Dropdown(
id="feature-image-ranking",
options=[
{"label": "IoU", "value": "iou"},
{"label": "Maximum activation", "value": "activation"},
],
value="iou",
clearable=False,
className="dash-dropdown feature-image-ranking-dropdown",
),
],
className="feature-workbench-controls feature-workbench-controls-ranking",
),
],
className="feature-workbench-toolbar",
),
html.Div(id="feature-status", className="feature-status"),
],
className="feature-workbench-header",
),
html.Div(
id="feature-detail-card",
className="feature-detail-card",
children=[
dcc.Loading(
id="feature-detail-body-loading",
type="default",
color="#2563eb",
className="loading-block feature-detail-body-loading",
children=html.Div(
id="feature-detail-body",
children=[
html.Div("Loading feature details…", className="feature-empty"),
],
),
),
html.Div(
[
html.Div(
[
html.Div("Top images", className="feature-placeholder-title"),
dcc.Checklist(
id="feature-show-heatmap-overlay",
**heatmap_overlay_checklist_props(),
),
],
className="feature-top-images-header",
),
dcc.Store(id="feature-top-images-cache", data=None),
dcc.Loading(
id="feature-images-loading",
type="dot",
color="#2563eb",
className="loading-block feature-images-loading",
children=[
html.Div(
"No feature selected.",
id="feature-top-images-status",
className="feature-empty",
),
html.Div(
id="feature-top-images",
className="feature-top-images-grid",
children=[_feature_image_slot(idx) for idx in range(FEATURE_TOP_IMAGE_SLOTS)],
),
],
),
],
className="feature-placeholder",
),
],
),
dcc.Store(id="selected-feature-id", data=None),
],
className="feature-workbench",
),
],
className="app-shell",
)
@callback(
Output("viz-model-info", "children"),
Input("_pages_location", "search"),
)
def render_model_info(search: str | None):
params = visualization_params(search)
if params is None:
return html.Div("Missing visualization parameters.", className="feature-empty")
metric, selection_dataset, model_key = params
record = get_model_record(metric, model_key)
children = [
html.Div([html.Span("Metric: "), html.Strong(metric)]),
html.Div([html.Span("Dataset: "), html.Strong(dashboard_dataset_label(selection_dataset))]),
html.Div([html.Span("Image UMAP embeddings: "), html.Strong(UMAP_DATASET)]),
]
if record is None:
children.append(html.Div("SAE checkpoint not found.", className="mock-summary-meta"))
return children
children.append(
html.Div(summarize_model_record(record), className="mock-summary-hyperparams")
)
try:
from dashboard.model_catalog import summarize_feature_filter_cache
filter_lines = summarize_feature_filter_cache(record.checkpoint_path, selection_dataset)
except Exception:
filter_lines = ["Feature filter summary unavailable"]
children.append(html.Hr(className="viz-summary-divider"))
children.append(
html.Div(
[
_feature_filter_intro_block(),
*[_format_filter_result_line(line) for line in filter_lines],
],
className="viz-filter-summary",
)
)
return children
@callback(
Output("feature-sort-selector", "options"),
Output("feature-sort-selector", "value"),
Output("feature-sort-selector", "disabled"),
Input("_pages_location", "search"),
)
def init_feature_selector(search: str | None):
params = visualization_params(search)
if params is None:
return [], None, True
metric, selection_dataset, model_key = params
record = get_model_record(metric, model_key)
if record is None:
return [], None, True
summaries = summarize_selector_cache(record.checkpoint_path, selection_dataset)
if not summaries:
return [], None, True
options = [
{"label": selector_option_label(summary.selector_name), "value": summary.selector_name}
for summary in summaries
]
return options, summaries[0].selector_name, False
@callback(
Output("aggregation-control-block", "style"),
Output("color-control-block", "style"),
Input("umap-mode-dropdown", "value"),
)
def toggle_umap_secondary_controls(umap_mode: str | None):
if str(umap_mode or "images") == "features":
return (
{"display": "none"},
{"display": "none"},
)
return ({}, {})
@callback(
Output("category-filter-store", "data"),
Input("umap-graph", "clickData"),
State("category-filter-store", "data"),
State("umap-mode-dropdown", "value"),
prevent_initial_call=True,
)
def update_category_filter(click_data, current_category, umap_mode):
if str(umap_mode or "images") != "images" or not click_data:
return no_update
point = click_data["points"][0]
customdata = point.get("customdata", None)
clicked_category = None
if isinstance(customdata, (list, tuple, np.ndarray)) and len(customdata) >= 2:
clicked_category = str(customdata[1])
if not clicked_category:
return no_update
if current_category is not None and str(current_category) == clicked_category:
return None
return clicked_category
@callback(
Output("umap-figures-cache", "data"),
Input("_pages_location", "search"),
Input("aggregation-dropdown", "value"),
Input("color-dropdown", "value"),
Input("category-filter-store", "data"),
State("umap-figures-cache", "data"),
running=[
(Output("umap-status", "className"), "umap-status is-loading", "umap-status"),
(Output("umap-status", "children"), "Loading UMAP embeddings…", ""),
],
)
def preload_umap_figures(search, aggregation_mode, color_mode, selected_category, prev_cache):
params = visualization_params(search)
if params is None:
return None
metric, _selection_dataset, model_key = params
if not model_key or aggregation_mode is None:
return no_update
aggregation_mode = str(aggregation_mode)
images_color_mode = str(color_mode or DEFAULT_COLOR_MODE)
if images_color_mode not in IMAGE_COLOR_MODES:
images_color_mode = DEFAULT_COLOR_MODE
prev_cache = prev_cache or {}
cache_key = _umap_images_cache_key(
metric, model_key, aggregation_mode, images_color_mode, selected_category
)
prev_images = prev_cache.get("images") if isinstance(prev_cache.get("images"), dict) else None
if prev_images and prev_images.get("cache_key") == cache_key:
return no_update
try:
images_fig, images_info = build_images_umap(
metric,
model_key,
UMAP_DATASET,
aggregation_mode,
color_mode=images_color_mode,
selected_category=selected_category,
)
images_payload = {
"figure": images_fig.to_plotly_json(),
"status": format_umap_status(images_info, aggregation_mode, images_color_mode),
"cache_key": cache_key,
}
return {
"images": images_payload,
"features": prev_cache.get("features") if ctx.triggered_id == "category-filter-store" else None,
}
except Exception as exc:
error_fig = empty_umap_figure(f"UMAP unavailable: {exc}").to_plotly_json()
message = f"UMAP unavailable: {exc}"
error_payload = {"figure": error_fig, "status": message, "cache_key": cache_key}
return {"images": error_payload, "features": prev_cache.get("features")}
@callback(
Output("umap-figures-cache", "data", allow_duplicate=True),
Input("umap-figures-cache", "data"),
Input("umap-mode-dropdown", "value"),
State("_pages_location", "search"),
State("aggregation-dropdown", "value"),
prevent_initial_call=True,
running=[
(Output("umap-status", "className"), "umap-status is-loading", "umap-status"),
(Output("umap-status", "children"), "Preloading feature UMAP embeddings…", ""),
],
)
def preload_feature_umap(cache, umap_mode, search, aggregation_mode):
# Eager preload: runs when images cache is ready (first page load), not only on mode switch.
params = visualization_params(search)
if params is None:
return no_update
metric, _selection_dataset, model_key = params
if not model_key or aggregation_mode is None:
return no_update
if not cache or not cache.get("images"):
return no_update
if ctx.triggered_id == "umap-mode-dropdown" and str(umap_mode or "images") != "features":
return no_update
aggregation_mode = str(aggregation_mode)
feature_cache = cache.get("features") if isinstance(cache.get("features"), dict) else {}
if feature_cache.get("figure"):
return no_update
try:
features_fig, features_info = build_features_umap(
metric,
model_key,
UMAP_DATASET,
aggregation_mode,
hide_filtered_garbage=True,
)
features_payload = {
"figure": features_fig.to_plotly_json(),
"status": format_feature_umap_status(features_info, "group"),
}
next_cache = dict(cache)
feature_cache = dict(feature_cache)
feature_cache.update(features_payload)
next_cache["features"] = feature_cache
return next_cache
except Exception as exc:
error_fig = empty_umap_figure(f"UMAP unavailable: {exc}").to_plotly_json()
message = f"UMAP unavailable: {exc}"
next_cache = dict(cache)
feature_cache = dict(feature_cache)
feature_cache.update({"figure": error_fig, "status": message})
return {
"images": cache["images"],
"features": feature_cache,
}
def _parse_selected_feature_id(value) -> int | None:
if value is None:
return None
try:
return int(value)
except (TypeError, ValueError):
return None
@callback(
Output("umap-graph", "figure"),
Output("umap-status", "children"),
Output("umap-status", "className"),
Input("umap-mode-dropdown", "value"),
Input("umap-figures-cache", "data"),
Input("selected-feature-id", "data"),
)
def display_umap_from_cache(umap_mode, cache, selected_feature_id):
if not cache:
return empty_umap_figure("Loading UMAP..."), "Preloading UMAP embeddings...", "umap-status is-loading"
mode = str(umap_mode or "images")
if mode == "features":
payload = cache.get("features")
if payload is None:
return (
empty_umap_figure("Loading feature UMAP..."),
"Preloading feature UMAP embeddings...",
"umap-status is-loading",
)
if not payload:
return empty_umap_figure("UMAP unavailable."), "UMAP unavailable.", "umap-status"
figure = payload.get("figure")
highlighted = apply_feature_umap_highlight(figure, _parse_selected_feature_id(selected_feature_id))
return highlighted, payload.get("status", ""), "umap-status"
payload = cache.get(mode)
if payload is None and mode == "images":
return (
empty_umap_figure("Loading UMAP..."),
"Preloading UMAP embeddings...",
"umap-status is-loading",
)
if payload is None:
return empty_umap_figure("UMAP unavailable."), "UMAP unavailable.", "umap-status"
if not payload:
return empty_umap_figure("UMAP unavailable."), "UMAP unavailable.", "umap-status"
return payload["figure"], payload.get("status", ""), "umap-status"
clientside_callback(
umap_tooltip_clientside("umap-graph"),
Output("umap-tooltip", "show"),
Output("umap-tooltip", "bbox"),
Output("umap-tooltip", "direction"),
Input("umap-graph", "hoverData"),
)
@callback(
Output("umap-tooltip", "children"),
Input("umap-graph", "hoverData"),
State("_pages_location", "search"),
State("umap-mode-dropdown", "value"),
)
def display_umap_hover_content(hover_data, search: str | None, umap_mode: str | None):
if hover_data is None:
return no_update
umap_mode = str(umap_mode or "images")
pt = hover_data["points"][0]
customdata = pt.get("customdata", None)
if umap_mode == "features":
if not isinstance(customdata, (list, tuple, np.ndarray)) or len(customdata) < 4:
return no_update
try:
fid = int(customdata[0])
mean_val = float(customdata[1])
std_val = float(customdata[2])
nonzero = float(customdata[3])
except (TypeError, ValueError):
return no_update
corr_label = str(pt.get("fullData", {}).get("name", ""))
return build_feature_umap_hover_tooltip(
fid,
mean_val,
std_val,
nonzero,
corr_label,
corr_source_label="corr(group_patch)",
)
params = visualization_params(search)
if params is None:
return no_update
metric, _selection_dataset, model_key = params
if not model_key:
return no_update
img_idx = hover_image_idx(hover_data)
if img_idx is None or img_idx < 0:
return no_update
meta_line = cached_hover_meta_line(metric, model_key, img_idx)
if meta_line is None:
return no_update
b64 = cached_hover_thumb(metric, model_key, img_idx)
if b64 is None:
body = html.Div("Preview unavailable", style={"fontSize": "12px"})
else:
body = html.Img(
src=f"data:image/jpeg;base64,{b64}",
style={
"width": f"{THUMB_SIZE}px",
"height": f"{THUMB_SIZE}px",
"objectFit": "cover",
"border": "1px solid #CBD5E1",
},
)
return html.Div(
[
html.Div(meta_line, style={"fontSize": "12px", "marginBottom": "8px"}),
body,
],
style={
"width": f"{max(250, THUMB_SIZE + 20)}px",
"whiteSpace": "normal",
"padding": "10px",
"backgroundColor": "#F8FAFC",
"border": "1px solid #CBD5E1",
"borderRadius": "8px",
},
)
@callback(
Output("selected-feature-id", "data"),
Output("feature-status", "children"),
Output("feature-jump-input", "value"),
Input("feature-sort-selector", "value"),
Input("feature-prev", "n_clicks"),
Input("feature-next", "n_clicks"),
Input("feature-jump-input", "value"),
Input("umap-graph", "clickData"),
State("selected-feature-id", "data"),
State("_pages_location", "search"),
State("umap-mode-dropdown", "value"),
)
def update_selected_feature(
selector_name: str | None,
_prev_clicks: int,
_next_clicks: int,
jump_value,
click_data,
current_feature_id,
search: str | None,
umap_mode: str | None,
):
params = visualization_params(search)
if params is None:
return None, "", ""
metric, selection_dataset, model_key = params
rows = feature_rows_for_selector(metric, selection_dataset, model_key, selector_name)
trigger_id = ctx.triggered_id
requested_feature_id: int | float | str | None = jump_value
if trigger_id == "umap-graph":
if str(umap_mode or "images") != "features" or not click_data:
return no_update, no_update, no_update
customdata = click_data["points"][0].get("customdata", None)
if not isinstance(customdata, (list, tuple, np.ndarray)) or len(customdata) < 1:
return no_update, no_update, no_update
try:
requested_feature_id = int(customdata[0])
except (TypeError, ValueError):
return no_update, no_update, no_update
selected_feature_id, status = select_feature_id(
feature_rows=rows,
current_feature_id=current_feature_id,
trigger_id=trigger_id,
requested_feature_id=requested_feature_id,
)
if selected_feature_id is not None:
unavailable = feature_unavailable_message(
metric,
model_key,
int(selected_feature_id),
activation_dataset=UMAP_DATASET,
)
if unavailable is not None:
status = unavailable
jump_display = str(selected_feature_id) if selected_feature_id is not None else ""
return selected_feature_id, status, jump_display
@callback(
Output("feature-detail-body", "children"),
Input("selected-feature-id", "data"),
State("_pages_location", "search"),
State("feature-sort-selector", "value"),
)
def render_feature_detail(
selected_feature_id,
search: str | None,
selector_name: str | None,
):
params = visualization_params(search)
if params is None:
return html.Div("No selector cache files were found for this model.", className="feature-empty")
metric, selection_dataset, model_key = params
return build_feature_detail_content(
metric,
selection_dataset,
model_key,
selected_feature_id,
selector_name,
)
@callback(
Output("feature-top-images-cache", "data"),
Output("feature-top-images-status", "children"),
Output("feature-image-caption-0", "children"),
Input("selected-feature-id", "data"),
Input("feature-image-ranking", "value"),
State("_pages_location", "search"),
)
def load_feature_top_images_cache(
selected_feature_id,
ranking_mode: str | None,
search: str | None,
):
params = visualization_params(search)
if params is None:
return _empty_feature_image_cache("No images available.")
metric, selection_dataset, model_key = params
try:
feature_id = int(selected_feature_id)
except Exception:
return _empty_feature_image_cache("No feature selected.")
unavailable = feature_unavailable_message(
metric,
model_key,
feature_id,
activation_dataset=selection_dataset,
)
if unavailable is not None:
return _empty_feature_image_cache(unavailable)
ranking_mode = str(ranking_mode or "iou")
try:
overlays = get_top_feature_overlays(
metric,
selection_dataset,
model_key,
feature_id,
top_n=FEATURE_TOP_IMAGE_TOP_N,
ranking_mode=ranking_mode,
)
except Exception as exc:
return _empty_feature_image_cache(f"Failed to build top images: {exc}")
if not overlays:
return _empty_feature_image_cache("No top images found for this feature.")
figure = overlays[0].get("figure")
if not isinstance(figure, dict):
return _empty_feature_image_cache("No top images found for this feature.")
caption = str(overlays[0].get("caption") or "Top image 1")
return figure, "", caption
@callback(
Output("feature-image-0", "figure"),
Input("feature-top-images-cache", "data"),
State("feature-show-heatmap-overlay", "value"),
)
def load_feature_top_images_figure(cache, show_heatmap_value):
show_overlay = overlay_show_heatmap_enabled(show_heatmap_value)
if not isinstance(cache, dict):
return empty_heatmap_figure("Top images overlay is not available yet.").to_plotly_json()
return set_heatmap_overlay_visible(cache, show_overlay)
clientside_callback(
TOGGLE_HEATMAP_OVERLAY_CLIENTSIDE,
Output("feature-image-0", "figure", allow_duplicate=True),
Input("feature-show-heatmap-overlay", "value"),
State("feature-image-0", "figure"),
prevent_initial_call=True,
)