import base64
from pathlib import Path
from typing import Dict
import gradio as gr
import numpy as np
import plotly.graph_objects as go
from scipy.interpolate import UnivariateSpline
from scipy.optimize import curve_fit
def get_color_for_accuracy(accuracy: float) -> str:
"""Get background color based on accuracy (0-1). Closer to 1.0 = greener."""
acc_pct = accuracy * 100
if acc_pct >= 90:
green = 200 + int((acc_pct - 90) * 5.5)
return f"rgb({255 - green}, {min(255, green)}, {100})"
elif acc_pct >= 70:
green = 150 + int((acc_pct - 70) * 2.5)
return f"rgb({255 - green}, {min(255, green)}, {50})"
elif acc_pct >= 50:
yellow = 200 + int((acc_pct - 50) * 2.75)
return f"rgb({255}, {min(255, yellow)}, {100})"
elif acc_pct >= 30:
red = 255 - int((acc_pct - 30) * 2.75)
return f"rgb({red}, {150 + int((acc_pct - 30) * 2.75)}, {50})"
else:
red = 255
green = 100 + int(acc_pct * 1.67)
return f"rgb({red}, {green}, {100})"
def _short_label(name: str, max_len: int = 35) -> str:
return name if len(name) <= max_len else name[: max_len - 1] + "\u2026"
def escape_html(s: str) -> str:
return str(s).replace("&", "&").replace("<", "<").replace(">", ">")
# ── Weight / Overall-Score helpers ──────────────────────────────────────
DEFAULT_WEIGHTS = {
"Detection Rate (%)": 1.0,
"FPR (%)": 1.0,
"Mean Latency (ms)": 1.0,
"Total Cost": 1.0,
}
_RANK_CONFIG = {
"Detection Rate (%)": ("_accuracy", False),
"FPR (%)": ("_fpr", True),
"Mean Latency (ms)": ("_mean_latency", True),
"Total Cost": ("_total_cost", True),
}
_WEIGHT_COLORS = {
"Detection Rate (%)": "#22c55e",
"FPR (%)": "#ef4444",
"Mean Latency (ms)": "#3b82f6",
"Total Cost": "#f59e0b",
}
_WEIGHT_SHORT_LABELS = {
"Detection Rate (%)": "Detection",
"FPR (%)": "FPR",
"Mean Latency (ms)": "Latency",
"Total Cost": "Cost",
}
def sort_by_overall_score(df, weights: Dict[str, float] | None = None):
"""Sort dataframe by weighted average rank across metrics."""
if weights is None:
weights = DEFAULT_WEIGHTS
rank_cols = []
for metric, (col, ascending) in _RANK_CONFIG.items():
w = weights.get(metric, 0.0)
if w == 0.0:
continue
rank_col = f"_rank_{col}"
df[rank_col] = df[col].rank(ascending=ascending, method="min")
rank_cols.append((rank_col, w))
if not rank_cols:
return df
df["_overall_score"] = sum(df[rc] * w for rc, w in rank_cols) / sum(w for _, w in rank_cols)
df = df.sort_values(by="_overall_score", ascending=True)
return df
def create_weight_bar_html(weights: Dict[str, float] | None = None) -> str:
"""Create an HTML horizontal stacked bar showing the weight distribution."""
if weights is None:
weights = DEFAULT_WEIGHTS
total = sum(max(w, 0) for w in weights.values())
if total == 0:
return '
'
segments = ""
for metric, w in weights.items():
if w <= 0:
continue
pct = w / total * 100
color = _WEIGHT_COLORS.get(metric, "#6b7280")
label = _WEIGHT_SHORT_LABELS.get(metric, metric)
text = f"{label} {pct:.0f}%" if pct >= 12 else f"{pct:.0f}%" if pct >= 5 else ""
segments += (
f''
f"{text}
"
)
return (
f'{segments}
'
)
def build_weights(w_det, w_fpr, w_lat, w_cost):
return {
"Detection Rate (%)": w_det if w_det is not None else 0.0,
"FPR (%)": w_fpr if w_fpr is not None else 0.0,
"Mean Latency (ms)": w_lat if w_lat is not None else 0.0,
"Total Cost": w_cost if w_cost is not None else 0.0,
}
# ── Pareto helpers ──────────────────────────────────────────────────────
def create_empty_pareto_figure(message: str = "Please select different metrics for X and Y axes"):
fig = go.Figure()
fig.add_annotation(
text=message,
x=0.5,
y=0.5,
showarrow=False,
font={"size": 16},
xref="paper",
yref="paper",
)
fig.update_layout(
xaxis={"visible": False},
yaxis={"visible": False},
template="plotly_white",
)
return fig
def build_pareto_figure(df, x_metric, y_metric, metric_map):
"""Build a Pareto frontier plot from a prepared leaderboard dataframe.
Args:
df: DataFrame with columns including Model Snapshot and internal sort columns.
x_metric: Display name of x metric (key in metric_map).
y_metric: Display name of y metric (key in metric_map).
metric_map: Dict mapping display metric name to (column_name, minimize_bool).
"""
if df.empty:
return create_empty_pareto_figure("No data available for the selected dataset")
x_col, x_minimize = metric_map[x_metric]
y_col, y_minimize = metric_map[y_metric]
x = df[x_col]
y = df[y_col]
# Pareto detection
is_pareto = []
for i in range(len(df)):
dominated = False
for j in range(len(df)):
if i == j:
continue
better_x = (x.iloc[j] <= x.iloc[i]) if x_minimize else (x.iloc[j] >= x.iloc[i])
better_y = (y.iloc[j] <= y.iloc[i]) if y_minimize else (y.iloc[j] >= y.iloc[i])
strictly = (x.iloc[j] != x.iloc[i]) or (y.iloc[j] != y.iloc[i])
if better_x and better_y and strictly:
dominated = True
break
is_pareto.append(not dominated)
df["pareto"] = is_pareto
fig = go.Figure()
# Dominated points
fig.add_trace(
go.Scatter(
x=df.loc[~df.pareto, x_col],
y=df.loc[~df.pareto, y_col],
mode="markers",
name="Dominated",
marker=dict(size=10, color="gray"),
customdata=[
[model, xv, yv]
for model, xv, yv in zip(
df.loc[~df.pareto, "Model Snapshot"], df.loc[~df.pareto, x_col], df.loc[~df.pareto, y_col]
)
],
hovertemplate=(
"%{customdata[0]}
"
f"{x_metric}:"
" %{customdata[1]:.3f}
"
f"{y_metric}:"
" %{customdata[2]:.3f}"
""
),
)
)
pareto_df = df[df.pareto].reset_index(drop=True)
pareto_df_sorted = pareto_df.sort_values(by=x_col).reset_index(drop=True)
# Pareto frontier curve
if len(pareto_df_sorted) >= 2:
try:
x_pareto = np.array(pareto_df_sorted[x_col].tolist(), dtype=float)
y_pareto = np.array(pareto_df_sorted[y_col].tolist(), dtype=float)
x_is_bounded = "Detection Rate" in x_metric or "FPR" in x_metric
y_is_bounded = "Detection Rate" in y_metric or "FPR" in y_metric
# Normalize to [0, 1] to avoid overflow in power fitting
x_all = np.array(df[x_col].tolist(), dtype=float)
x_range = float(np.max(x_all) - np.min(x_all)) or 1.0
x_offset = float(np.min(x_all))
y_all = np.array(df[y_col].tolist(), dtype=float)
y_range = float(np.max(y_all) - np.min(y_all)) or 1.0
y_offset = float(np.min(y_all))
x_pareto_norm = (x_pareto - x_offset) / x_range
y_pareto_norm = (y_pareto - y_offset) / y_range
if x_minimize != y_minimize:
def pareto_func(xv, a, b):
return a * np.power(np.clip(xv, 1e-10, None), b)
else:
def pareto_func(xv, a, b):
return a * np.power(np.clip(1 - xv, 1e-10, None), b)
try:
popt, _ = curve_fit(pareto_func, x_pareto_norm, y_pareto_norm, p0=[1.0, 1.0], maxfev=5000)
# Extend curve beyond data range to cover full axis
if x_is_bounded:
x_curve_min, x_curve_max = 0.0, 1.0
else:
x_curve_min = 0.0
x_curve_max = float(np.max(x_all)) * 1.5
x_curve_norm = (np.linspace(x_curve_min, x_curve_max, 500) - x_offset) / x_range
# For regions where normalization goes negative (curve extends
# below the data minimum), linearly extrapolate from the edge
# instead of letting the power function clip to ~0.
in_range = x_curve_norm >= 0
y_curve_norm = np.empty_like(x_curve_norm)
y_curve_norm[in_range] = pareto_func(x_curve_norm[in_range], *popt)
if not np.all(in_range):
edge = 1e-3
y0 = float(pareto_func(np.array([edge]), *popt)[0])
y1 = float(pareto_func(np.array([2e-3]), *popt)[0])
slope = (y1 - y0) / 1e-3
y_curve_norm[~in_range] = y0 + slope * (x_curve_norm[~in_range] - edge)
# Denormalize back to original scale
x_curve = x_curve_norm * x_range + x_offset
y_curve = y_curve_norm * y_range + y_offset
if y_is_bounded:
valid_indices = np.where((y_curve >= 0) & (y_curve <= 1))[0]
else:
valid_indices = np.where((y_curve >= 0) & (y_curve <= float(np.max(y_all)) * 1.5))[0]
if len(valid_indices) > 0:
x_curve_valid = x_curve[valid_indices]
y_curve_valid = y_curve[valid_indices]
fig.add_trace(
go.Scatter(
x=x_curve_valid.tolist(),
y=y_curve_valid.tolist(),
mode="lines",
name="Pareto Frontier Curve",
line={"color": "red", "width": 3},
showlegend=True,
hoverinfo="skip",
)
)
except Exception:
k = min(3, len(x_pareto_norm) - 1)
spline = UnivariateSpline(x_pareto_norm, y_pareto_norm, k=k, s=0)
if x_is_bounded:
x_curve_min, x_curve_max = 0.0, 1.0
else:
x_curve_min = 0.0
x_curve_max = float(np.max(x_all)) * 1.5
x_curve_norm = (np.linspace(x_curve_min, x_curve_max, 500) - x_offset) / x_range
y_curve_norm = np.clip(spline(x_curve_norm), 0, None)
x_curve = x_curve_norm * x_range + x_offset
y_curve = y_curve_norm * y_range + y_offset
if y_is_bounded:
valid_indices = np.where((y_curve >= 0) & (y_curve <= 1))[0]
else:
valid_indices = np.where((y_curve >= 0) & (y_curve <= float(np.max(y_all)) * 1.5))[0]
if len(valid_indices) > 0:
fig.add_trace(
go.Scatter(
x=x_curve[valid_indices].tolist(),
y=y_curve[valid_indices].tolist(),
mode="lines",
name="Pareto Frontier Curve",
line={"color": "red", "width": 3},
showlegend=True,
hoverinfo="skip",
)
)
except Exception as e:
print(f"Warning: Could not create Pareto frontier curve: {e}")
# Pareto points with labels
text_positions = ["top center", "bottom center"]
text_pos = [text_positions[i % 2] for i in range(len(pareto_df))]
fig.add_trace(
go.Scatter(
x=pareto_df[x_col],
y=pareto_df[y_col],
mode="markers+text",
name="Pareto Frontier",
marker=dict(size=14, color="red"),
text=[_short_label(m) for m in pareto_df["Model Snapshot"]],
textposition=text_pos,
customdata=[
[model, xv, yv]
for model, xv, yv in zip(pareto_df["Model Snapshot"], pareto_df[x_col], pareto_df[y_col])
],
hovertemplate=(
"%{customdata[0]}
"
f"{x_metric}:"
" %{customdata[1]:.3f}
"
f"{y_metric}:"
" %{customdata[2]:.3f}"
""
),
)
)
fig.update_layout(
autosize=True,
height=800,
xaxis_title=x_metric,
yaxis_title=y_metric,
hovermode="closest",
template="plotly_white",
)
if x_minimize:
fig.update_xaxes(autorange="reversed")
if y_minimize:
fig.update_yaxes(autorange="reversed")
return fig
# ── Footer ──────────────────────────────────────────────────────────────
def build_footer():
logo_path = Path(__file__).parent / "cesia_logo.png"
if logo_path.exists():
with open(logo_path, "rb") as logo_file:
logo_data = base64.b64encode(logo_file.read()).decode()
logo_src = f"data:image/png;base64,{logo_data}"
else:
logo_src = ""
span_display = "display:inline;" if not logo_src else "display:none;"
footer_html = f"""
"""
gr.HTML(footer_html)
# ── Global CSS ──────────────────────────────────────────────────────────
GLOBAL_CSS = """
"""
# ── Shared CSS snippets for leaderboard & category tables ───────────────
LEADERBOARD_TABLE_CSS = """
"""
LEADERBOARD_DARK_MODE_CSS = """
"""
CATEGORY_TABLE_CSS = """
"""