Fix: Scatter plot zoom and 'Show all labels' not working

#29
Files changed (6) hide show
  1. app.py +2 -147
  2. content.py +5 -8
  3. leaderboard_transformer.py +173 -181
  4. simple_data_loader.py +1 -2
  5. ui_components.py +11 -25
  6. visualizations.py +1 -11
app.py CHANGED
@@ -2,7 +2,6 @@
2
  import logging
3
  import sys
4
  import os
5
- import json
6
 
7
  from constants import FONT_FAMILY_SHORT
8
 
@@ -77,29 +76,6 @@ redirect_script = """
77
  if (window.location.pathname === '/') { window.location.replace('/home'); }
78
  </script>
79
  """
80
- hf_space_fetch_credentials_script = """
81
- <script>
82
- (function() {
83
- const hfSpaceApiPrefix = "https://openhands-openhands-index.hf.space/gradio_api/";
84
- const originalFetch = window.fetch.bind(window);
85
-
86
- window.fetch = function(input, init) {
87
- const url = typeof input === "string" ? input : input?.url;
88
-
89
- if (url && url.startsWith(hfSpaceApiPrefix)) {
90
- if (input instanceof Request) {
91
- input = new Request(input, { ...(init || {}), credentials: "omit" });
92
- init = undefined;
93
- } else {
94
- init = { ...(init || {}), credentials: "omit" };
95
- }
96
- }
97
-
98
- return originalFetch(input, init);
99
- };
100
- })();
101
- </script>
102
- """
103
 
104
  # JavaScript to fix navigation links to use relative paths (avoids domain mismatch when behind proxy)
105
  fix_nav_links_script = """
@@ -376,15 +352,7 @@ logger.info("Creating Gradio application")
376
  demo = gr.Blocks(
377
  theme=theme,
378
  css=final_css,
379
- head=(
380
- hf_space_fetch_credentials_script
381
- + posthog_script
382
- + scroll_script
383
- + redirect_script
384
- + fix_nav_links_script
385
- + tooltip_script
386
- + dark_mode_script
387
- ),
388
  title="OpenHands Index",
389
  )
390
 
@@ -436,119 +404,6 @@ class RootRedirectMiddleware(BaseHTTPMiddleware):
436
  return await call_next(request)
437
 
438
 
439
- class StringifiedGradioJSONMiddleware:
440
- """Normalize custom-domain Gradio requests before they reach Gradio.
441
-
442
- Requests sent through index.openhands.dev can arrive at Gradio as a JSON
443
- string containing the real request object, which makes FastAPI validation
444
- reject interactive callbacks with 422. Direct HF Space traffic already sends
445
- proper JSON objects, so this only rewrites bodies that decode to strings.
446
-
447
- The custom domain also loads the Gradio frontend from Vercel while
448
- window.gradio_config.root points to the hf.space runtime. Chrome therefore
449
- requires successful credentialed CORS preflights for queue and heartbeat
450
- endpoints.
451
- """
452
-
453
- CUSTOM_DOMAIN_ORIGIN = "https://index.openhands.dev"
454
-
455
- def __init__(self, app):
456
- self.app = app
457
-
458
- async def __call__(self, scope, receive, send):
459
- origin = None
460
- if scope["type"] == "http":
461
- headers = {
462
- key.decode("latin-1").lower(): value.decode("latin-1")
463
- for key, value in scope.get("headers", [])
464
- }
465
- origin = headers.get("origin")
466
-
467
- should_apply_cors = (
468
- scope["type"] == "http"
469
- and scope.get("path", "").startswith("/gradio_api/")
470
- and origin == self.CUSTOM_DOMAIN_ORIGIN
471
- )
472
-
473
- if should_apply_cors:
474
- cors_headers = [
475
- (b"access-control-allow-origin", self.CUSTOM_DOMAIN_ORIGIN.encode("latin-1")),
476
- (b"access-control-allow-credentials", b"true"),
477
- (b"access-control-allow-methods", b"DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT"),
478
- (b"access-control-allow-headers", b"*"),
479
- (b"access-control-expose-headers", b"*"),
480
- (b"vary", b"Origin"),
481
- ]
482
-
483
- if scope.get("method") == "OPTIONS":
484
- await send({
485
- "type": "http.response.start",
486
- "status": 200,
487
- "headers": cors_headers,
488
- })
489
- await send({"type": "http.response.body", "body": b""})
490
- return
491
-
492
- async def cors_send(message):
493
- if message["type"] == "http.response.start":
494
- message["headers"] = [
495
- (key, value)
496
- for key, value in message.get("headers", [])
497
- if not key.lower().startswith(b"access-control-")
498
- and key.lower() != b"vary"
499
- ] + cors_headers
500
- await send(message)
501
- else:
502
- cors_send = send
503
-
504
- if (
505
- scope["type"] == "http"
506
- and scope.get("method") == "POST"
507
- and scope.get("path", "").startswith("/gradio_api/")
508
- ):
509
- content_type = headers.get("content-type", "")
510
- if "application/json" not in content_type:
511
- return await self.app(scope, receive, cors_send)
512
-
513
- body_parts = []
514
- while True:
515
- message = await receive()
516
- if message["type"] != "http.request":
517
- break
518
- body_parts.append(message.get("body", b""))
519
- if not message.get("more_body", False):
520
- break
521
-
522
- body = b"".join(body_parts)
523
- replacement_body = body
524
- try:
525
- decoded = json.loads(body)
526
- except json.JSONDecodeError:
527
- decoded = None
528
-
529
- if isinstance(decoded, str):
530
- stripped = decoded.strip()
531
- if stripped.startswith(("{", "[")):
532
- replacement_body = stripped.encode("utf-8")
533
-
534
- sent = False
535
-
536
- async def replay_receive():
537
- nonlocal sent
538
- if sent:
539
- return {"type": "http.request", "body": b"", "more_body": False}
540
- sent = True
541
- return {
542
- "type": "http.request",
543
- "body": replacement_body,
544
- "more_body": False,
545
- }
546
-
547
- return await self.app(scope, replay_receive, cors_send)
548
-
549
- return await self.app(scope, receive, cors_send)
550
-
551
-
552
  # Create a parent FastAPI app with redirect_slashes=False to prevent
553
  # automatic trailing slash redirects that cause issues with Gradio
554
  root_app = FastAPI(redirect_slashes=False)
@@ -560,7 +415,6 @@ root_app.mount("/api", api_app)
560
 
561
  # Mount Gradio app at root path
562
  app = gr.mount_gradio_app(root_app, demo, path="/")
563
- app = StringifiedGradioJSONMiddleware(app)
564
  logger.info("REST API mounted at /api, Gradio app mounted at /")
565
 
566
 
@@ -573,3 +427,4 @@ if __name__ == "__main__":
573
  logger.info(f"Launching app on {host}:{port}")
574
  uvicorn.run(app, host=host, port=port)
575
  logger.info("App launched successfully")
 
 
2
  import logging
3
  import sys
4
  import os
 
5
 
6
  from constants import FONT_FAMILY_SHORT
7
 
 
76
  if (window.location.pathname === '/') { window.location.replace('/home'); }
77
  </script>
78
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
  # JavaScript to fix navigation links to use relative paths (avoids domain mismatch when behind proxy)
81
  fix_nav_links_script = """
 
352
  demo = gr.Blocks(
353
  theme=theme,
354
  css=final_css,
355
+ head=posthog_script + scroll_script + redirect_script + fix_nav_links_script + tooltip_script + dark_mode_script,
 
 
 
 
 
 
 
 
356
  title="OpenHands Index",
357
  )
358
 
 
404
  return await call_next(request)
405
 
406
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  # Create a parent FastAPI app with redirect_slashes=False to prevent
408
  # automatic trailing slash redirects that cause issues with Gradio
409
  root_app = FastAPI(redirect_slashes=False)
 
415
 
416
  # Mount Gradio app at root path
417
  app = gr.mount_gradio_app(root_app, demo, path="/")
 
418
  logger.info("REST API mounted at /api, Gradio app mounted at /")
419
 
420
 
 
427
  logger.info(f"Launching app on {host}:{port}")
428
  uvicorn.run(app, host=host, port=port)
429
  logger.info("App launched successfully")
430
+
content.py CHANGED
@@ -547,20 +547,19 @@ span.wrap[tabindex="0"][role="button"][data-editable="false"] {
547
  width: 100% !important;
548
  align-items: center;
549
  }
550
- .nav-holder nav a[href*="alternative-agents"] {
551
- grid-row: 1 !important;
552
- grid-column: 7 !important;
553
- white-space: nowrap !important;
554
- }
555
  .nav-holder nav a[href*="about"] {
556
  grid-row: 1 !important;
557
- grid-column: 8 !important;
558
  }
559
  .nav-holder nav a[href*="submit"] {
560
  grid-row: 1 !important;
561
  grid-column: 8 !important;
562
  white-space: nowrap !important;
563
  }
 
 
 
 
564
 
565
  /* Divider line between header and category nav */
566
  .nav-holder nav::after {
@@ -599,7 +598,6 @@ span.wrap[tabindex="0"][role="button"][data-editable="false"] {
599
  .nav-holder nav a[href*="discovery"] { grid-column: 4 !important; }
600
 
601
  /* Navigation hover styles */
602
- .nav-holder nav a[href*="alternative-agents"]:hover,
603
  .nav-holder nav a[href*="about"]:hover,
604
  .nav-holder nav a[href*="submit"]:hover,
605
  .nav-holder nav a[href*="literature-understanding"]:hover,
@@ -609,7 +607,6 @@ span.wrap[tabindex="0"][role="button"][data-editable="false"] {
609
  background-color: #FDF9F4;
610
  }
611
 
612
- .dark .nav-holder nav a[href*="alternative-agents"]:hover,
613
  .dark .nav-holder nav a[href*="about"]:hover,
614
  .dark .nav-holder nav a[href*="submit"]:hover,
615
  .dark .nav-holder nav a[href*="literature-understanding"]:hover,
 
547
  width: 100% !important;
548
  align-items: center;
549
  }
 
 
 
 
 
550
  .nav-holder nav a[href*="about"] {
551
  grid-row: 1 !important;
552
+ grid-column: 7 !important;
553
  }
554
  .nav-holder nav a[href*="submit"] {
555
  grid-row: 1 !important;
556
  grid-column: 8 !important;
557
  white-space: nowrap !important;
558
  }
559
+ /* Hide the Alternative Agents page from the top-level nav for now. */
560
+ .nav-holder nav a[href*="alternative-agents"] {
561
+ display: none !important;
562
+ }
563
 
564
  /* Divider line between header and category nav */
565
  .nav-holder nav::after {
 
598
  .nav-holder nav a[href*="discovery"] { grid-column: 4 !important; }
599
 
600
  /* Navigation hover styles */
 
601
  .nav-holder nav a[href*="about"]:hover,
602
  .nav-holder nav a[href*="submit"]:hover,
603
  .nav-holder nav a[href*="literature-understanding"]:hover,
 
607
  background-color: #FDF9F4;
608
  }
609
 
 
610
  .dark .nav-holder nav a[href*="about"]:hover,
611
  .dark .nav-holder nav a[href*="submit"]:hover,
612
  .dark .nav-holder nav a[href*="literature-understanding"]:hover,
leaderboard_transformer.py CHANGED
@@ -12,38 +12,6 @@ from constants import FONT_FAMILY, FONT_FAMILY_SHORT
12
 
13
  logger = logging.getLogger(__name__)
14
 
15
- _DATA_URI_CACHE: dict[str, str] = {}
16
-
17
-
18
- def get_asset_data_uri(path: str) -> Optional[str]:
19
- """Return a cached data URI for a local image asset."""
20
- if path in _DATA_URI_CACHE:
21
- return _DATA_URI_CACHE[path]
22
-
23
- if not os.path.exists(path):
24
- _DATA_URI_CACHE[path] = ""
25
- return None
26
-
27
- try:
28
- with open(path, "rb") as f:
29
- encoded = base64.b64encode(f.read()).decode("utf-8")
30
- except Exception as e:
31
- logger.warning(f"Could not load image asset {path}: {e}")
32
- _DATA_URI_CACHE[path] = ""
33
- return None
34
-
35
- ext = os.path.splitext(path)[1].lower()
36
- if ext == ".svg":
37
- mime = "image/svg+xml"
38
- elif ext == ".png":
39
- mime = "image/png"
40
- else:
41
- mime = "application/octet-stream"
42
-
43
- uri = f"data:{mime};base64,{encoded}"
44
- _DATA_URI_CACHE[path] = uri
45
- return uri
46
-
47
  # Company logo mapping for graphs - maps model name patterns to company logo files
48
  COMPANY_LOGO_MAP = {
49
  "anthropic": {"path": "assets/logo-anthropic.svg", "name": "Anthropic"},
@@ -141,34 +109,42 @@ def get_openhands_logo_images():
141
  images = []
142
 
143
  # Light mode logo (visible in light mode, hidden in dark mode)
144
- light_logo_uri = get_asset_data_uri(OPENHANDS_LOGO_PATH_LIGHT)
145
- if light_logo_uri:
146
- images.append(dict(
147
- source=light_logo_uri.replace("data:image/png;base64,", "data:image/png;openhands=lightlogo;base64,"),
148
- xref="paper",
149
- yref="paper",
150
- x=0,
151
- y=-0.15,
152
- sizex=0.15,
153
- sizey=0.15,
154
- xanchor="left",
155
- yanchor="bottom",
156
- ))
 
 
 
 
157
 
158
  # Dark mode logo (hidden in light mode, visible in dark mode)
159
- dark_logo_uri = get_asset_data_uri(OPENHANDS_LOGO_PATH_DARK)
160
- if dark_logo_uri:
161
- images.append(dict(
162
- source=dark_logo_uri.replace("data:image/png;base64,", "data:image/png;openhands=darklogo;base64,"),
163
- xref="paper",
164
- yref="paper",
165
- x=0,
166
- y=-0.15,
167
- sizex=0.15,
168
- sizey=0.15,
169
- xanchor="left",
170
- yanchor="bottom",
171
- ))
 
 
 
 
172
 
173
  return images
174
 
@@ -535,50 +511,54 @@ def create_scatter_chart(
535
  marker_info = get_marker_icon(model_name, openness, mark_by)
536
  logo_path = marker_info['path']
537
 
538
- logo_uri = get_asset_data_uri(logo_path)
539
- if not logo_uri:
540
- continue
541
-
542
- if x_type == "date":
543
- # For date axes, use data coordinates directly
544
- layout_images.append(dict(
545
- source=logo_uri,
546
- xref="x",
547
- yref="y",
548
- x=x_val,
549
- y=y_val,
550
- sizex=15 * 24 * 60 * 60 * 1000, # ~15 days in milliseconds
551
- sizey=3, # score units
552
- xanchor="center",
553
- yanchor="middle",
554
- layer="above"
555
- ))
556
- else:
557
- # For log axes, use domain coordinates (0-1 range)
558
- if x_type == "log" and x_val > 0:
559
- log_x = np.log10(x_val)
560
- domain_x = (log_x - x_range_log[0]) / (x_range_log[1] - x_range_log[0])
561
- else:
562
- domain_x = 0.5
563
-
564
- domain_y = (y_val - y_range[0]) / (y_range[1] - y_range[0]) if (y_range[1] - y_range[0]) > 0 else 0.5
565
-
566
- # Clamp to valid range
567
- domain_x = max(0, min(1, domain_x))
568
- domain_y = max(0, min(1, domain_y))
569
-
570
- layout_images.append(dict(
571
- source=logo_uri,
572
- xref="x domain",
573
- yref="y domain",
574
- x=domain_x,
575
- y=domain_y,
576
- sizex=0.04,
577
- sizey=0.06,
578
- xanchor="center",
579
- yanchor="middle",
580
- layer="above"
581
- ))
 
 
 
 
582
 
583
  # Add labels for frontier points only
584
  for row in frontier_rows:
@@ -991,7 +971,7 @@ def _plot_scatter_plotly(
991
  name: Optional[str] = None,
992
  plot_type: str = 'cost', # 'cost' or 'runtime'
993
  mark_by: Optional[str] = None, # 'Company', 'Openness', or 'Country'
994
- show_all_labels: bool = False # Show labels for all points vs only Pareto frontier
995
  ) -> go.Figure:
996
  from constants import MARK_BY_DEFAULT
997
  if mark_by is None:
@@ -1213,8 +1193,25 @@ def _plot_scatter_plotly(
1213
  y_min = min_score - 5 if min_score > 5 else 0
1214
  y_max = max_score + 5
1215
 
 
 
 
 
1216
  def _encode_logo(path: str) -> Optional[str]:
1217
- return get_asset_data_uri(path)
 
 
 
 
 
 
 
 
 
 
 
 
 
1218
 
1219
  # Composite markers: on the Alternative Agents page the dataframe carries
1220
  # an "Agent" column (Claude Code / Codex / Gemini CLI / OpenHands Sub-agents),
@@ -1271,107 +1268,93 @@ def _plot_scatter_plotly(
1271
  domain_x = max(0, min(1, domain_x))
1272
  domain_y = max(0, min(1, domain_y))
1273
 
1274
- # Convert to data coordinates
1275
- # For log scale x: use log10(x) to match the axis type
1276
- x_log = np.log10(x_val) if x_val > 0 else x_min_log
1277
-
1278
  if harness_uri is not None:
1279
- # Composite: stack model on top, harness on bottom
1280
- # Use data coordinates (x, y) so logos zoom/pan together with labels
1281
- y_offset = 0.8 # Offset above the data point (in score units)
 
 
1282
  layout_images.append(dict(
1283
  source=model_logo_uri,
1284
- xref="x", yref="y",
1285
- x=x_log, y=y_val + y_offset,
1286
- sizex=STACKED_SIZE_X * (x_max_log - x_min_log),
1287
- sizey=STACKED_SIZE_Y * (y_max - y_min),
1288
  xanchor="center", yanchor="middle",
1289
  layer="above",
1290
  ))
1291
  layout_images.append(dict(
1292
  source=harness_uri,
1293
- xref="x", yref="y",
1294
- x=x_log, y=y_val - y_offset,
1295
- sizex=STACKED_SIZE_X * (x_max_log - x_min_log),
1296
- sizey=STACKED_SIZE_Y * (y_max - y_min),
1297
  xanchor="center", yanchor="middle",
1298
  layer="above",
1299
  ))
1300
  else:
1301
- # Single marker - use data coordinates so logo zooms/pans with labels
 
 
 
1302
  layout_images.append(dict(
1303
  source=model_logo_uri,
1304
- xref="x", yref="y",
1305
- x=x_log, y=y_val,
1306
- sizex=SINGLE_SIZE_X * (x_max_log - x_min_log),
1307
- sizey=SINGLE_SIZE_Y * (y_max - y_min),
1308
  xanchor="center", yanchor="middle",
1309
  layer="above",
1310
  ))
1311
 
1312
- # --- Section 7: Add Model Name Labels ---
1313
- # Show labels for all points if show_all_labels is True, otherwise just Pareto frontier
1314
- if show_all_labels:
1315
- # Label all data points
1316
- labels_data = []
1317
- for _, row in data_plot.iterrows():
1318
- x_val = row[x_col_to_use]
1319
- y_val = row[y_col_to_use]
1320
-
1321
- model_name = row.get('Language Model', '')
1322
- if isinstance(model_name, list):
1323
- model_name = model_name[0] if model_name else ''
1324
- model_name = str(model_name).split('/')[-1]
1325
- if len(model_name) > 25:
1326
- model_name = model_name[:22] + '...'
1327
-
1328
- labels_data.append({'x': x_val, 'y': y_val, 'label': model_name})
1329
- elif frontier_rows:
1330
- # Label only Pareto frontier points
1331
- labels_data = []
1332
 
1333
  for row in frontier_rows:
1334
  x_val = row[x_col_to_use]
1335
  y_val = row[y_col_to_use]
1336
 
 
1337
  model_name = row.get('Language Model', '')
1338
  if isinstance(model_name, list):
1339
  model_name = model_name[0] if model_name else ''
 
1340
  model_name = str(model_name).split('/')[-1]
 
1341
  if len(model_name) > 25:
1342
  model_name = model_name[:22] + '...'
1343
 
1344
- labels_data.append({'x': x_val, 'y': y_val, 'label': model_name})
1345
- else:
1346
- labels_data = []
1347
-
1348
- # Add annotations for each label
1349
- # For log scale x-axis, annotations need log10(x) coordinates (Plotly issue #2580)
1350
- for item in labels_data:
1351
- x_val = item['x']
1352
- y_val = item['y']
1353
- label = item['label']
1354
-
1355
- # Transform x to log10 for annotation positioning on log scale
1356
- if x_val > 0:
1357
- x_log = np.log10(x_val)
1358
- else:
1359
- x_log = x_min_log
1360
 
1361
- fig.add_annotation(
1362
- x=x_log,
1363
- y=y_val,
1364
- text=label,
1365
- showarrow=False,
1366
- yshift=25, # Move label higher above the icon
1367
- font=dict(
1368
- size=10,
1369
- color='#0D0D0F', # neutral-950
1370
- family=FONT_FAMILY_SHORT
1371
- ),
1372
- xanchor='center',
1373
- yanchor='bottom'
1374
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
1375
 
1376
  # --- Section 8: Configure Layout ---
1377
  # Use the same axis ranges as calculated for domain coordinates
@@ -1490,38 +1473,47 @@ def format_score_column(df: pd.DataFrame, score_col_name: str) -> pd.DataFrame:
1490
  return df.assign(**{score_col_name: df[score_col_name].apply(apply_formatting)})
1491
 
1492
 
 
 
 
 
 
 
 
 
 
1493
  def format_runtime_column(df: pd.DataFrame, runtime_col_name: str) -> pd.DataFrame:
1494
  """
1495
  Applies custom formatting to a runtime column based on its corresponding score column.
1496
  - If runtime is not null, formats as time with 's' suffix.
1497
  - If runtime is null but score is not, it becomes "Missing".
1498
  - If both runtime and score are null, it becomes "Not Submitted".
 
1499
  Args:
1500
  df: The DataFrame to modify.
1501
  runtime_col_name: The name of the runtime column to format (e.g., "Average Runtime").
1502
  Returns:
1503
  The DataFrame with the formatted runtime column.
1504
  """
1505
- # Find the corresponding score column by replacing "Runtime" with "Score"
1506
  score_col_name = runtime_col_name.replace("Runtime", "Score")
1507
 
1508
- # Ensure the score column actually exists to avoid errors
1509
  if score_col_name not in df.columns:
1510
- return df # Return the DataFrame unmodified if there's no matching score
1511
 
1512
  def apply_formatting_logic(row):
1513
  runtime_value = row[runtime_col_name]
1514
  score_value = row[score_col_name]
1515
  status_color = "#ec4899"
 
 
1516
 
1517
  if pd.notna(runtime_value) and isinstance(runtime_value, (int, float)):
1518
- return f"{runtime_value:.0f}s"
1519
  elif pd.notna(score_value):
1520
- return f'<span style="color: {status_color};">Missing</span>' # Score exists, but runtime is missing
1521
  else:
1522
- return f'<span style="color: {status_color};">Not Submitted</span>' # Neither score nor runtime exists
1523
 
1524
- # Apply the logic to the specified runtime column and update the DataFrame
1525
  df[runtime_col_name] = df.apply(apply_formatting_logic, axis=1)
1526
 
1527
  return df
 
12
 
13
  logger = logging.getLogger(__name__)
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  # Company logo mapping for graphs - maps model name patterns to company logo files
16
  COMPANY_LOGO_MAP = {
17
  "anthropic": {"path": "assets/logo-anthropic.svg", "name": "Anthropic"},
 
109
  images = []
110
 
111
  # Light mode logo (visible in light mode, hidden in dark mode)
112
+ if os.path.exists(OPENHANDS_LOGO_PATH_LIGHT):
113
+ try:
114
+ with open(OPENHANDS_LOGO_PATH_LIGHT, "rb") as f:
115
+ logo_data = base64.b64encode(f.read()).decode('utf-8')
116
+ images.append(dict(
117
+ source=f"data:image/png;openhands=lightlogo;base64,{logo_data}",
118
+ xref="paper",
119
+ yref="paper",
120
+ x=0,
121
+ y=-0.15,
122
+ sizex=0.15,
123
+ sizey=0.15,
124
+ xanchor="left",
125
+ yanchor="bottom",
126
+ ))
127
+ except Exception:
128
+ pass
129
 
130
  # Dark mode logo (hidden in light mode, visible in dark mode)
131
+ if os.path.exists(OPENHANDS_LOGO_PATH_DARK):
132
+ try:
133
+ with open(OPENHANDS_LOGO_PATH_DARK, "rb") as f:
134
+ logo_data = base64.b64encode(f.read()).decode('utf-8')
135
+ images.append(dict(
136
+ source=f"data:image/png;openhands=darklogo;base64,{logo_data}",
137
+ xref="paper",
138
+ yref="paper",
139
+ x=0,
140
+ y=-0.15,
141
+ sizex=0.15,
142
+ sizey=0.15,
143
+ xanchor="left",
144
+ yanchor="bottom",
145
+ ))
146
+ except Exception:
147
+ pass
148
 
149
  return images
150
 
 
511
  marker_info = get_marker_icon(model_name, openness, mark_by)
512
  logo_path = marker_info['path']
513
 
514
+ if os.path.exists(logo_path):
515
+ try:
516
+ with open(logo_path, 'rb') as f:
517
+ encoded_logo = base64.b64encode(f.read()).decode('utf-8')
518
+ logo_uri = f"data:image/svg+xml;base64,{encoded_logo}"
519
+
520
+ if x_type == "date":
521
+ # For date axes, use data coordinates directly
522
+ layout_images.append(dict(
523
+ source=logo_uri,
524
+ xref="x",
525
+ yref="y",
526
+ x=x_val,
527
+ y=y_val,
528
+ sizex=15 * 24 * 60 * 60 * 1000, # ~15 days in milliseconds
529
+ sizey=3, # score units
530
+ xanchor="center",
531
+ yanchor="middle",
532
+ layer="above"
533
+ ))
534
+ else:
535
+ # For log axes, use domain coordinates (0-1 range)
536
+ if x_type == "log" and x_val > 0:
537
+ log_x = np.log10(x_val)
538
+ domain_x = (log_x - x_range_log[0]) / (x_range_log[1] - x_range_log[0])
539
+ else:
540
+ domain_x = 0.5
541
+
542
+ domain_y = (y_val - y_range[0]) / (y_range[1] - y_range[0]) if (y_range[1] - y_range[0]) > 0 else 0.5
543
+
544
+ # Clamp to valid range
545
+ domain_x = max(0, min(1, domain_x))
546
+ domain_y = max(0, min(1, domain_y))
547
+
548
+ layout_images.append(dict(
549
+ source=logo_uri,
550
+ xref="x domain",
551
+ yref="y domain",
552
+ x=domain_x,
553
+ y=domain_y,
554
+ sizex=0.04,
555
+ sizey=0.06,
556
+ xanchor="center",
557
+ yanchor="middle",
558
+ layer="above"
559
+ ))
560
+ except Exception:
561
+ pass
562
 
563
  # Add labels for frontier points only
564
  for row in frontier_rows:
 
971
  name: Optional[str] = None,
972
  plot_type: str = 'cost', # 'cost' or 'runtime'
973
  mark_by: Optional[str] = None, # 'Company', 'Openness', or 'Country'
974
+ show_all_labels: bool = False
975
  ) -> go.Figure:
976
  from constants import MARK_BY_DEFAULT
977
  if mark_by is None:
 
1193
  y_min = min_score - 5 if min_score > 5 else 0
1194
  y_max = max_score + 5
1195
 
1196
+ # Cache base64-encoded logos across rows — every Claude model on the
1197
+ # Alternative Agents page points at the same assets/harness-claude-code.svg,
1198
+ # so decoding once per path is ~N× cheaper than once per point.
1199
+ _logo_cache: dict[str, str] = {}
1200
  def _encode_logo(path: str) -> Optional[str]:
1201
+ if path in _logo_cache:
1202
+ return _logo_cache[path]
1203
+ if not os.path.exists(path):
1204
+ return None
1205
+ try:
1206
+ with open(path, "rb") as f:
1207
+ encoded = base64.b64encode(f.read()).decode("utf-8")
1208
+ except Exception as e:
1209
+ logger.warning(f"Could not load logo {path}: {e}")
1210
+ return None
1211
+ mime = "svg+xml" if path.lower().endswith(".svg") else "png"
1212
+ uri = f"data:image/{mime};base64,{encoded}"
1213
+ _logo_cache[path] = uri
1214
+ return uri
1215
 
1216
  # Composite markers: on the Alternative Agents page the dataframe carries
1217
  # an "Agent" column (Claude Code / Codex / Gemini CLI / OpenHands Sub-agents),
 
1268
  domain_x = max(0, min(1, domain_x))
1269
  domain_y = max(0, min(1, domain_y))
1270
 
 
 
 
 
1271
  if harness_uri is not None:
1272
+ # Composite: stack model on top, harness on bottom, clamping
1273
+ # each half to the plot area so markers near the edges don't
1274
+ # drift off-canvas.
1275
+ model_y = min(1, domain_y + STACKED_Y_OFFSET)
1276
+ harness_y = max(0, domain_y - STACKED_Y_OFFSET)
1277
  layout_images.append(dict(
1278
  source=model_logo_uri,
1279
+ xref="x domain", yref="y domain",
1280
+ x=domain_x, y=model_y,
1281
+ sizex=STACKED_SIZE_X, sizey=STACKED_SIZE_Y,
 
1282
  xanchor="center", yanchor="middle",
1283
  layer="above",
1284
  ))
1285
  layout_images.append(dict(
1286
  source=harness_uri,
1287
+ xref="x domain", yref="y domain",
1288
+ x=domain_x, y=harness_y,
1289
+ sizex=STACKED_SIZE_X, sizey=STACKED_SIZE_Y,
 
1290
  xanchor="center", yanchor="middle",
1291
  layer="above",
1292
  ))
1293
  else:
1294
+ # Single marker (canonical OpenHands pages, or Alternative Agents
1295
+ # rows with an unknown harness name — the latter shouldn't happen
1296
+ # in practice since HARNESS_LOGO_PATHS covers every agent_name the
1297
+ # push-to-index script emits).
1298
  layout_images.append(dict(
1299
  source=model_logo_uri,
1300
+ xref="x domain", yref="y domain",
1301
+ x=domain_x, y=domain_y,
1302
+ sizex=SINGLE_SIZE_X, sizey=SINGLE_SIZE_Y,
 
1303
  xanchor="center", yanchor="middle",
1304
  layer="above",
1305
  ))
1306
 
1307
+ # --- Section 7: Add Model Name Labels to Frontier Points ---
1308
+ if frontier_rows:
1309
+ frontier_labels_data = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1310
 
1311
  for row in frontier_rows:
1312
  x_val = row[x_col_to_use]
1313
  y_val = row[y_col_to_use]
1314
 
1315
+ # Get the model name for the label
1316
  model_name = row.get('Language Model', '')
1317
  if isinstance(model_name, list):
1318
  model_name = model_name[0] if model_name else ''
1319
+ # Clean the model name (remove path prefixes)
1320
  model_name = str(model_name).split('/')[-1]
1321
+ # Truncate long names
1322
  if len(model_name) > 25:
1323
  model_name = model_name[:22] + '...'
1324
 
1325
+ frontier_labels_data.append({
1326
+ 'x': x_val,
1327
+ 'y': y_val,
1328
+ 'label': model_name
1329
+ })
 
 
 
 
 
 
 
 
 
 
 
1330
 
1331
+ # Add annotations for each frontier label
1332
+ # For log scale x-axis, annotations need log10(x) coordinates (Plotly issue #2580)
1333
+ for item in frontier_labels_data:
1334
+ x_val = item['x']
1335
+ y_val = item['y']
1336
+ label = item['label']
1337
+
1338
+ # Transform x to log10 for annotation positioning on log scale
1339
+ if x_val > 0:
1340
+ x_log = np.log10(x_val)
1341
+ else:
1342
+ x_log = x_min_log
1343
+
1344
+ fig.add_annotation(
1345
+ x=x_log,
1346
+ y=y_val,
1347
+ text=label,
1348
+ showarrow=False,
1349
+ yshift=25, # Move label higher above the icon
1350
+ font=dict(
1351
+ size=10,
1352
+ color='#0D0D0F', # neutral-950
1353
+ family=FONT_FAMILY_SHORT
1354
+ ),
1355
+ xanchor='center',
1356
+ yanchor='bottom'
1357
+ )
1358
 
1359
  # --- Section 8: Configure Layout ---
1360
  # Use the same axis ranges as calculated for domain coordinates
 
1473
  return df.assign(**{score_col_name: df[score_col_name].apply(apply_formatting)})
1474
 
1475
 
1476
+ def _hidden_runtime_sort_key(runtime_value: float | int | None, score_value: float | int | None) -> str:
1477
+ """Build a hidden prefix so Gradio's string-based runtime sorting behaves numerically."""
1478
+ if pd.notna(runtime_value) and isinstance(runtime_value, (int, float)):
1479
+ return f"{float(runtime_value):020.6f}"
1480
+ if pd.notna(score_value):
1481
+ return "99999999999999999998"
1482
+ return "99999999999999999999"
1483
+
1484
+
1485
  def format_runtime_column(df: pd.DataFrame, runtime_col_name: str) -> pd.DataFrame:
1486
  """
1487
  Applies custom formatting to a runtime column based on its corresponding score column.
1488
  - If runtime is not null, formats as time with 's' suffix.
1489
  - If runtime is null but score is not, it becomes "Missing".
1490
  - If both runtime and score are null, it becomes "Not Submitted".
1491
+ - Adds a hidden, zero-padded numeric prefix so Gradio sorts the column numerically.
1492
  Args:
1493
  df: The DataFrame to modify.
1494
  runtime_col_name: The name of the runtime column to format (e.g., "Average Runtime").
1495
  Returns:
1496
  The DataFrame with the formatted runtime column.
1497
  """
 
1498
  score_col_name = runtime_col_name.replace("Runtime", "Score")
1499
 
 
1500
  if score_col_name not in df.columns:
1501
+ return df
1502
 
1503
  def apply_formatting_logic(row):
1504
  runtime_value = row[runtime_col_name]
1505
  score_value = row[score_col_name]
1506
  status_color = "#ec4899"
1507
+ sort_key = _hidden_runtime_sort_key(runtime_value, score_value)
1508
+ hidden_sort_prefix = f'<span style="display:none">{sort_key}</span>'
1509
 
1510
  if pd.notna(runtime_value) and isinstance(runtime_value, (int, float)):
1511
+ return f"{hidden_sort_prefix}{runtime_value:.0f}s"
1512
  elif pd.notna(score_value):
1513
+ return f'{hidden_sort_prefix}<span style="color: {status_color};">Missing</span>'
1514
  else:
1515
+ return f'{hidden_sort_prefix}<span style="color: {status_color};">Not Submitted</span>'
1516
 
 
1517
  df[runtime_col_name] = df.apply(apply_formatting_logic, axis=1)
1518
 
1519
  return df
simple_data_loader.py CHANGED
@@ -245,6 +245,7 @@ class SimpleLeaderboardViewer:
245
  'acp-claude': 'Claude Code',
246
  'acp-codex': 'Codex',
247
  'acp-gemini': 'Gemini CLI',
 
248
  }
249
  alt_dir = self.config_path / "alternative_agents"
250
  if alt_dir.exists():
@@ -252,8 +253,6 @@ class SimpleLeaderboardViewer:
252
  if not type_dir.is_dir():
253
  continue
254
  default_name = agent_type_default_name.get(type_dir.name)
255
- if default_name is None:
256
- continue # skip unlisted agent types (e.g. openhands_subagents)
257
  for agent_dir in type_dir.iterdir():
258
  if not agent_dir.is_dir():
259
  continue
 
245
  'acp-claude': 'Claude Code',
246
  'acp-codex': 'Codex',
247
  'acp-gemini': 'Gemini CLI',
248
+ 'openhands_subagents': 'OpenHands Sub-agents',
249
  }
250
  alt_dir = self.config_path / "alternative_agents"
251
  if alt_dir.exists():
 
253
  if not type_dir.is_dir():
254
  continue
255
  default_name = agent_type_default_name.get(type_dir.name)
 
 
256
  for agent_dir in type_dir.iterdir():
257
  if not agent_dir.is_dir():
258
  continue
ui_components.py CHANGED
@@ -43,8 +43,6 @@ from content import (
43
  api = HfApi()
44
  os.makedirs(EXTRACTED_DATA_DIR, exist_ok=True)
45
 
46
- _SVG_DATA_URI_CACHE: dict[str, str] = {}
47
-
48
 
49
  def get_company_logo_html(model_name: str) -> str:
50
  """
@@ -83,18 +81,12 @@ OPENNESS_SVG_MAP = {
83
 
84
  def get_svg_as_data_uri(path: str) -> str:
85
  """Reads an SVG file and returns it as a base64-encoded data URI."""
86
- if path in _SVG_DATA_URI_CACHE:
87
- return _SVG_DATA_URI_CACHE[path]
88
-
89
  try:
90
  with open(path, "rb") as svg_file:
91
  encoded_svg = base64.b64encode(svg_file.read()).decode("utf-8")
92
- uri = f"data:image/svg+xml;base64,{encoded_svg}"
93
- _SVG_DATA_URI_CACHE[path] = uri
94
- return uri
95
  except FileNotFoundError:
96
  print(f"Warning: SVG file not found at {path}")
97
- _SVG_DATA_URI_CACHE[path] = ""
98
  return ""
99
 
100
 
@@ -962,7 +954,7 @@ def create_leaderboard_display(
962
  if not new_df.empty:
963
  new_transformer = DataTransformer(new_df, new_tag_map)
964
  new_df_view_full, _ = new_transformer.view(tag=category_name, use_plotly=True)
965
-
966
  # Prepare both complete and all entries versions
967
  if 'Categories Attempted' in new_df_view_full.columns:
968
  new_df_view_complete = new_df_view_full[new_df_view_full['Categories Attempted'] == '5/5'].copy()
@@ -1022,22 +1014,16 @@ def create_leaderboard_display(
1022
 
1023
  # Connect the timer to the refresh function
1024
  if show_incomplete_checkbox is not None:
 
1025
  if show_open_only_checkbox is not None:
1026
- refresh_timer.tick(
1027
- fn=check_and_refresh_data,
1028
- inputs=[show_incomplete_checkbox, show_open_only_checkbox, mark_by_dropdown, show_all_labels_checkbox],
1029
- outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
1030
- )
1031
- else:
1032
- # No open/closed split in this dataset — gr.State can't fill the gap in a
1033
- # timer tick (no session context), so use a wrapper that fixes show_open_only=False.
1034
- def _timer_refresh_no_open(show_incomplete, mark_by, show_all_labels):
1035
- return check_and_refresh_data(show_incomplete, False, mark_by, show_all_labels)
1036
- refresh_timer.tick(
1037
- fn=_timer_refresh_no_open,
1038
- inputs=[show_incomplete_checkbox, mark_by_dropdown, show_all_labels_checkbox],
1039
- outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
1040
- )
1041
  else:
1042
  # If no incomplete checkbox, always show all data (but still filter by open if needed)
1043
  def check_and_refresh_all(show_open_only=False, mark_by=MARK_BY_DEFAULT, show_all_labels=False):
 
43
  api = HfApi()
44
  os.makedirs(EXTRACTED_DATA_DIR, exist_ok=True)
45
 
 
 
46
 
47
  def get_company_logo_html(model_name: str) -> str:
48
  """
 
81
 
82
  def get_svg_as_data_uri(path: str) -> str:
83
  """Reads an SVG file and returns it as a base64-encoded data URI."""
 
 
 
84
  try:
85
  with open(path, "rb") as svg_file:
86
  encoded_svg = base64.b64encode(svg_file.read()).decode("utf-8")
87
+ return f"data:image/svg+xml;base64,{encoded_svg}"
 
 
88
  except FileNotFoundError:
89
  print(f"Warning: SVG file not found at {path}")
 
90
  return ""
91
 
92
 
 
954
  if not new_df.empty:
955
  new_transformer = DataTransformer(new_df, new_tag_map)
956
  new_df_view_full, _ = new_transformer.view(tag=category_name, use_plotly=True)
957
+
958
  # Prepare both complete and all entries versions
959
  if 'Categories Attempted' in new_df_view_full.columns:
960
  new_df_view_complete = new_df_view_full[new_df_view_full['Categories Attempted'] == '5/5'].copy()
 
1014
 
1015
  # Connect the timer to the refresh function
1016
  if show_incomplete_checkbox is not None:
1017
+ timer_inputs = [show_incomplete_checkbox]
1018
  if show_open_only_checkbox is not None:
1019
+ timer_inputs.append(show_open_only_checkbox)
1020
+ timer_inputs.append(mark_by_dropdown) # Always include mark_by
1021
+ timer_inputs.append(show_all_labels_checkbox)
1022
+ refresh_timer.tick(
1023
+ fn=check_and_refresh_data,
1024
+ inputs=timer_inputs,
1025
+ outputs=[dataframe_component, cost_plot_component, runtime_plot_component]
1026
+ )
 
 
 
 
 
 
 
1027
  else:
1028
  # If no incomplete checkbox, always show all data (but still filter by open if needed)
1029
  def check_and_refresh_all(show_open_only=False, mark_by=MARK_BY_DEFAULT, show_all_labels=False):
visualizations.py CHANGED
@@ -108,17 +108,7 @@ def create_accuracy_by_size_chart(df: pd.DataFrame, mark_by: str = None) -> go.F
108
  open_aliases = [aliases.CANONICAL_OPENNESS_OPEN] + list(
109
  aliases.OPENNESS_ALIASES.get(aliases.CANONICAL_OPENNESS_OPEN, [])
110
  )
111
- openness_col = _find_column(df, ['Openness', 'openness'])
112
- if openness_col is None:
113
- fig = go.Figure()
114
- fig.add_annotation(
115
- text="No openness data available",
116
- xref="paper", yref="paper",
117
- x=0.5, y=0.5, showarrow=False,
118
- font=STANDARD_FONT
119
- )
120
- fig.update_layout(**STANDARD_LAYOUT, title="Open Model Accuracy by Size")
121
- return fig
122
 
123
  plot_df = df[
124
  (df[param_col].notna()) &
 
108
  open_aliases = [aliases.CANONICAL_OPENNESS_OPEN] + list(
109
  aliases.OPENNESS_ALIASES.get(aliases.CANONICAL_OPENNESS_OPEN, [])
110
  )
111
+ openness_col = 'Openness' if 'Openness' in df.columns else 'openness'
 
 
 
 
 
 
 
 
 
 
112
 
113
  plot_df = df[
114
  (df[param_col].notna()) &