shivapriyasom commited on
Commit
d0d0725
Β·
verified Β·
1 Parent(s): 3ec1273

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +204 -250
app.py CHANGED
@@ -106,6 +106,7 @@ DONOR_FEATURES = ["DONORF", "GRAFTYPE", "HLA_FINAL", "CONDGRPF", "CONDGRP_FINA
106
  DISEASE_FEATURES = ["NACS2YR", "EXCHTFPR", "VOC2YPR", "VOCFRQPR", "SCATXRSN"]
107
  ALL_FEATURES = PATIENT_FEATURES + DONOR_FEATURES + DISEASE_FEATURES
108
 
 
109
  ICON_OUTCOMES = ["DEAD", "GF", "AGVHD", "CGVHD", "VOCPSHI", "STROKEHI"]
110
  OUTCOME_TITLES = {
111
  "DEAD": "Death",
@@ -305,26 +306,7 @@ def lock_sex(baseline_sex):
305
 
306
 
307
  # ─────────────────────────────────────────────────────────────────────────────
308
- # CAROUSEL HELPERS
309
- # ─────────────────────────────────────────────────────────────────────────────
310
-
311
- def _nav_button_html(prefix, current_idx, total, label_list):
312
- """Generate prev/next nav bar HTML for a carousel."""
313
- current_label = label_list[current_idx] if label_list else f"{current_idx+1}/{total}"
314
- prev_disabled = "disabled" if current_idx == 0 else ""
315
- next_disabled = "disabled" if current_idx >= total - 1 else ""
316
- return (
317
- f'<div style="display:flex;align-items:center;justify-content:center;gap:12px;'
318
- f'padding:6px 0 10px;font-family:\'Segoe UI\',Arial,sans-serif;">'
319
- f'<span style="color:#888;font-size:11px;">{current_idx+1} / {total}</span>'
320
- f'<span style="font-size:13px;font-weight:600;color:#1565c0;min-width:200px;text-align:center;">'
321
- f'{current_label}</span>'
322
- f'</div>'
323
- )
324
-
325
-
326
- # ─────────────────────────────────────────────────────────────────────────────
327
- # HTML RENDERERS
328
  # ─────────────────────────────────────────────────────────────────────────────
329
 
330
  def _stick_figure_svg(color, size=16):
@@ -342,7 +324,8 @@ def _stick_figure_svg(color, size=16):
342
  )
343
 
344
 
345
- def _icon_card_html(probability, outcome, panel_label="", panel_color="#1565c0"):
 
346
  title = OUTCOME_TITLES.get(outcome, OUTCOME_DESCRIPTIONS.get(outcome, outcome))
347
  n_event = round(probability * 100)
348
  n_no_event = 100 - n_event
@@ -379,135 +362,84 @@ def _icon_card_html(probability, outcome, panel_label="", panel_color="#1565c0")
379
 
380
  return (
381
  f'<div style="background:#fff;border:1px solid #e0e0e0;border-radius:7px;'
382
- f'padding:6px 5px;text-align:center;font-family:\'Segoe UI\',Arial,sans-serif;'
383
- f'box-shadow:0 2px 4px rgba(0,0,0,0.06);box-sizing:border-box;'
384
- f'display:flex;flex-direction:column;align-items:center;">'
385
  f'{badge}'
386
- f'<div style="min-height:26px;display:flex;align-items:center;justify-content:center;'
387
- f'font-size:10px;font-weight:700;color:#222;line-height:1.3;margin-bottom:1px;">{title}</div>'
388
- f'<div style="font-size:18px;font-weight:800;color:{EVENT_COLOR};'
389
- f'line-height:1;margin-bottom:3px;">{pct_str}</div>'
390
- f'<div style="margin-bottom:3px;">{grid_html}</div>'
391
  f'<div>{legend}</div>'
392
  f'</div>'
393
  )
394
 
395
 
396
- def _build_icon_carousel_html(probs_dict, title_prefix=""):
397
- """
398
- Builds a self-contained JS carousel showing one icon array card at a time.
399
- Uses inline JS β€” no external deps.
400
- """
401
- outcomes = ICON_OUTCOMES
402
- # Serialize card HTMLs as JS array
403
- cards_js_parts = []
404
- for o in outcomes:
405
- prob = probs_dict.get(o, 0.0)
406
- card = _icon_card_html(prob, o)
407
- # Escape backticks and backslashes for JS template literal
408
- card_escaped = card.replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${")
409
- cards_js_parts.append(f"`{card_escaped}`")
410
- cards_js = "[" + ",\n".join(cards_js_parts) + "]"
411
-
412
- labels_js = "[" + ", ".join(f'"{OUTCOME_TITLES.get(o, o)}"' for o in outcomes) + "]"
413
- uid = abs(hash(title_prefix + str(list(probs_dict.values())[:2]))) % 999999
414
 
415
  footnote = (
416
- f'<div style="font-size:10px;color:#888;text-align:center;margin-top:8px;">'
417
  f'Each figure = 1 patient out of 100. '
418
  f'<span style="color:{EVENT_COLOR};font-weight:600;">&#9632; Red = Event</span> &nbsp; '
419
  f'<span style="color:{NO_EVENT_COLOR};font-weight:600;">&#9632; Green = No Event</span>'
420
  f'</div>'
421
  )
422
 
423
- return f"""
424
- <div id="icon-carousel-{uid}" style="font-family:'Segoe UI',Arial,sans-serif;padding:4px 0;">
425
- <div style="display:flex;align-items:center;justify-content:center;gap:16px;margin-bottom:10px;">
426
- <button id="ic-prev-{uid}"
427
- onclick="icNav_{uid}(-1)"
428
- style="background:#1565c0;color:#fff;border:none;border-radius:6px;
429
- padding:6px 18px;font-size:18px;cursor:pointer;font-weight:700;
430
- line-height:1;user-select:none;"
431
- aria-label="Previous outcome">&#8592;</button>
432
- <div style="text-align:center;min-width:200px;">
433
- <div id="ic-label-{uid}" style="font-size:14px;font-weight:700;color:#1565c0;"></div>
434
- <div id="ic-counter-{uid}" style="font-size:11px;color:#888;margin-top:2px;"></div>
435
- </div>
436
- <button id="ic-next-{uid}"
437
- onclick="icNav_{uid}(1)"
438
- style="background:#1565c0;color:#fff;border:none;border-radius:6px;
439
- padding:6px 18px;font-size:18px;cursor:pointer;font-weight:700;
440
- line-height:1;user-select:none;"
441
- aria-label="Next outcome">&#8594;</button>
442
- </div>
443
- <div id="ic-card-{uid}" style="max-width:320px;margin:0 auto;"></div>
444
- {footnote}
445
- </div>
446
- <script>
447
- (function() {{
448
- var cards_{uid} = {cards_js};
449
- var labels_{uid} = {labels_js};
450
- var idx_{uid} = 0;
451
- var total_{uid} = cards_{uid}.length;
452
-
453
- function render_{uid}() {{
454
- document.getElementById('ic-card-{uid}').innerHTML = cards_{uid}[idx_{uid}];
455
- document.getElementById('ic-label-{uid}').textContent = labels_{uid}[idx_{uid}];
456
- document.getElementById('ic-counter-{uid}').textContent = (idx_{uid}+1) + ' / ' + total_{uid};
457
- document.getElementById('ic-prev-{uid}').style.opacity = idx_{uid} === 0 ? '0.35' : '1';
458
- document.getElementById('ic-next-{uid}').style.opacity = idx_{uid} === total_{uid}-1 ? '0.35' : '1';
459
- }}
460
-
461
- window['icNav_{uid}'] = function(dir) {{
462
- var next = idx_{uid} + dir;
463
- if (next >= 0 && next < total_{uid}) {{
464
- idx_{uid} = next;
465
- render_{uid}();
466
- }}
467
- }};
468
-
469
- render_{uid}();
470
- }})();
471
- </script>
472
- """
473
 
474
 
475
- def _build_comparison_icon_carousel_html(all_probs_list, labels, colors):
476
- """
477
- Counterfactual version: one scenario per row, each row is its own carousel
478
- showing one outcome at a time, all rows navigate together (synced).
479
- """
480
- outcomes = ICON_OUTCOMES
481
- uid = abs(hash(str(labels) + str([list(p.values())[:2] for p in all_probs_list]))) % 999999
482
-
483
- # Build card data per scenario per outcome: cards[scenario][outcome_idx]
484
- all_cards_js_rows = []
485
- for i, (probs, label, color) in enumerate(zip(all_probs_list, labels, colors)):
486
- cards_for_scenario = []
487
- for o in outcomes:
488
- prob = probs.get(o, 0.0)
489
- card = _icon_card_html(prob, o, label, color)
490
- card_escaped = card.replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${")
491
- cards_for_scenario.append(f"`{card_escaped}`")
492
- all_cards_js_rows.append("[" + ", ".join(cards_for_scenario) + "]")
493
-
494
- all_cards_js = "[" + ", ".join(all_cards_js_rows) + "]"
495
- labels_js = "[" + ", ".join(f'"{OUTCOME_TITLES.get(o, o)}"' for o in outcomes) + "]"
496
- scenario_labels_js = "[" + ", ".join(f'"{l}"' for l in labels) + "]"
497
- scenario_colors_js = "[" + ", ".join(f'"{c}"' for c in colors) + "]"
498
-
499
- n_scenarios = len(all_probs_list)
500
-
501
- # Build the row placeholders
502
  rows_html = ""
503
- for i in range(n_scenarios):
 
 
504
  rows_html += (
505
- f'<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px;">'
506
- f'<div id="cic-label-{uid}-{i}" style="width:90px;flex-shrink:0;font-size:9px;'
507
- f'font-weight:700;text-align:right;border-radius:4px;padding:3px 6px;color:#fff;'
508
- f'background:{colors[i]};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">'
509
- f'{labels[i]}</div>'
510
- f'<div id="cic-card-{uid}-{i}" style="flex:1;"></div>'
511
  f'</div>'
512
  )
513
 
@@ -519,58 +451,21 @@ def _build_comparison_icon_carousel_html(all_probs_list, labels, colors):
519
  f'</div>'
520
  )
521
 
522
- return f"""
523
- <div id="cic-{uid}" style="font-family:'Segoe UI',Arial,sans-serif;padding:4px 0;">
524
- <div style="display:flex;align-items:center;justify-content:center;gap:16px;margin-bottom:12px;">
525
- <button onclick="cicNav_{uid}(-1)"
526
- style="background:#1565c0;color:#fff;border:none;border-radius:6px;
527
- padding:6px 18px;font-size:18px;cursor:pointer;font-weight:700;
528
- line-height:1;user-select:none;"
529
- aria-label="Previous outcome">&#8592;</button>
530
- <div style="text-align:center;min-width:200px;">
531
- <div id="cic-outlabel-{uid}" style="font-size:14px;font-weight:700;color:#1565c0;"></div>
532
- <div id="cic-counter-{uid}" style="font-size:11px;color:#888;margin-top:2px;"></div>
533
- </div>
534
- <button onclick="cicNav_{uid}(1)"
535
- style="background:#1565c0;color:#fff;border:none;border-radius:6px;
536
- padding:6px 18px;font-size:18px;cursor:pointer;font-weight:700;
537
- line-height:1;user-select:none;"
538
- aria-label="Next outcome">&#8594;</button>
539
- </div>
540
- {rows_html}
541
- {footnote}
542
- </div>
543
- <script>
544
- (function() {{
545
- var allCards_{uid} = {all_cards_js};
546
- var outLabels_{uid} = {labels_js};
547
- var nScen_{uid} = {n_scenarios};
548
- var total_{uid} = {len(outcomes)};
549
- var idx_{uid} = 0;
550
-
551
- function render_{uid}() {{
552
- document.getElementById('cic-outlabel-{uid}').textContent = outLabels_{uid}[idx_{uid}];
553
- document.getElementById('cic-counter-{uid}').textContent = (idx_{uid}+1) + ' / ' + total_{uid};
554
- for (var s = 0; s < nScen_{uid}; s++) {{
555
- var el = document.getElementById('cic-card-{uid}-' + s);
556
- if (el) el.innerHTML = allCards_{uid}[s][idx_{uid}];
557
- }}
558
- }}
559
-
560
- window['cicNav_{uid}'] = function(dir) {{
561
- var next = idx_{uid} + dir;
562
- if (next >= 0 && next < total_{uid}) {{
563
- idx_{uid} = next;
564
- render_{uid}();
565
- }}
566
- }};
567
-
568
- render_{uid}();
569
- }})();
570
- </script>
571
- """
572
 
573
 
 
 
 
 
574
  def _delta_color_html(delta, is_survival):
575
  if abs(delta) < 0.0005:
576
  color = "#888888"
@@ -685,26 +580,6 @@ def _build_violation_html(violations):
685
  # SHAP CAROUSEL HELPERS
686
  # ─────────────────────────────────────────────────────────────────────────────
687
 
688
- def _make_shap_nav_label(idx):
689
- """Return display label for SHAP plot at given index."""
690
- o = SHAP_ORDER[idx]
691
- return SHAP_LABELS.get(o, o)
692
-
693
-
694
- def _shap_prev(current_idx, plots_store):
695
- new_idx = max(0, current_idx - 1)
696
- plot = plots_store[SHAP_ORDER[new_idx]] if plots_store else None
697
- label = f"β—€ {_make_shap_nav_label(new_idx)} ({new_idx+1}/{len(SHAP_ORDER)})"
698
- return new_idx, plot, label
699
-
700
-
701
- def _shap_next(current_idx, plots_store):
702
- new_idx = min(len(SHAP_ORDER) - 1, current_idx + 1)
703
- plot = plots_store[SHAP_ORDER[new_idx]] if plots_store else None
704
- label = f"{_make_shap_nav_label(new_idx)} β–Ά ({new_idx+1}/{len(SHAP_ORDER)})"
705
- return new_idx, plot, label
706
-
707
-
708
  def _shap_counter_html(idx):
709
  labels = [SHAP_LABELS.get(o, o) for o in SHAP_ORDER]
710
  items = " Β· ".join(
@@ -742,20 +617,22 @@ def predict_gradio(*values):
742
  })
743
  df = pd.DataFrame(rows)
744
 
745
- shap_plots = create_all_shap_plots(user_vals, max_display=10)
746
- icon_html = _build_icon_carousel_html(calibrated_probs, title_prefix="predict")
747
 
748
- # Start SHAP carousel at index 0
749
- first_shap = shap_plots[SHAP_ORDER[0]]
750
- shap_crumb = _shap_counter_html(0)
 
751
 
752
  return (
753
  df,
754
- icon_html,
755
- shap_plots, # stored in State
756
- 0, # shap index State
757
- first_shap, # displayed plot
758
- shap_crumb, # breadcrumb HTML
 
 
759
  )
760
  except Exception as e:
761
  print(traceback.format_exc())
@@ -796,6 +673,13 @@ custom_css = """
796
  padding: 8px 22px !important; border-radius: 6px !important;
797
  }
798
  .shap-nav-button:hover { background: linear-gradient(to right, #263238, #455a64) !important; }
 
 
 
 
 
 
 
799
  .output-dataframe table td:first-child,
800
  .output-dataframe table th:first-child {
801
  white-space: normal !important; word-break: break-word !important; min-width: 240px !important;
@@ -880,18 +764,48 @@ with gr.Blocks(title="HCT Outcome Prediction Model", css=custom_css) as demo:
880
  wrap=True,
881
  )
882
 
883
- # ── Icon Array Carousel (Tab 1) ───────────────────────────────────
884
  gr.Markdown("---")
885
  gr.Markdown("## Outcome Probability β€” Icon Arrays")
886
  gr.Markdown("*Use the ← β†’ arrows to browse outcomes one at a time.*")
887
- icon_array_carousel = gr.HTML()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
888
 
889
  # ── SHAP Carousel (Tab 1) ─────────────────────────────────────────
890
  gr.Markdown("---")
891
  gr.Markdown("## SHAP β€” Feature Importance")
892
  gr.Markdown("*Use the ← β†’ buttons to browse SHAP plots one at a time.*")
893
 
894
- # State: store all plots dict + current index
895
  shap_plots_state = gr.State(None)
896
  shap_idx_state = gr.State(0)
897
 
@@ -902,7 +816,6 @@ with gr.Blocks(title="HCT Outcome Prediction Model", css=custom_css) as demo:
902
 
903
  shap_display = gr.Plot(label="SHAP Feature Importance")
904
 
905
- # Nav callbacks
906
  def _on_shap_prev(idx, plots):
907
  new_idx = max(0, idx - 1)
908
  plot = plots[SHAP_ORDER[new_idx]] if plots else None
@@ -929,7 +842,9 @@ with gr.Blocks(title="HCT Outcome Prediction Model", css=custom_css) as demo:
929
  inputs=inputs_list,
930
  outputs=[
931
  output_table,
932
- icon_array_carousel,
 
 
933
  shap_plots_state,
934
  shap_idx_state,
935
  shap_display,
@@ -1175,12 +1090,45 @@ with gr.Blocks(title="HCT Outcome Prediction Model", css=custom_css) as demo:
1175
  wi_table_html = gr.HTML()
1176
 
1177
  gr.Markdown("---")
 
 
1178
  with gr.Accordion("Outcome Icon Arrays β€” Baseline vs Scenarios", open=False):
1179
  gr.Markdown(
1180
- "*One row per scenario. Use the ← β†’ arrows to browse outcomes one at a time "
1181
- "across all scenarios simultaneously.*"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1182
  )
1183
- wi_icon_html = gr.HTML()
1184
 
1185
  gr.Markdown("---")
1186
 
@@ -1192,9 +1140,9 @@ with gr.Blocks(title="HCT Outcome Prediction Model", css=custom_css) as demo:
1192
  wi_shap_base_idx = gr.State(0)
1193
 
1194
  with gr.Row():
1195
- wb_shap_prev = gr.Button("←", elem_classes="shap-nav-button", size="sm", scale=0)
1196
  wb_shap_crumb = gr.HTML(value="", scale=4)
1197
- wb_shap_next = gr.Button("β†’", elem_classes="shap-nav-button", size="sm", scale=0)
1198
  wb_shap_plot = gr.Plot(label="Baseline β€” SHAP")
1199
 
1200
  def _wb_prev(idx, plots):
@@ -1242,7 +1190,6 @@ with gr.Blocks(title="HCT Outcome Prediction Model", css=custom_css) as demo:
1242
  wi_shap_scen_plots.append(s_plot)
1243
  wi_shap_scen_crumbs.append(s_crumb)
1244
 
1245
- # closure to capture correct store/idx
1246
  def _make_prev(st, ix):
1247
  def fn(idx, plots):
1248
  new_idx = max(0, idx - 1)
@@ -1293,14 +1240,15 @@ with gr.Blocks(title="HCT Outcome Prediction Model", css=custom_css) as demo:
1293
  for s in range(MAX_SCENARIOS)
1294
  ]
1295
 
1296
- # placeholders
1297
- empty_shap = None
1298
- empty_crumb = ""
1299
- empty_idx = 0
1300
-
1301
- def _empty_shap_outputs():
1302
- # store, idx, plot, crumb
1303
- return None, 0, None, ""
 
1304
 
1305
  try:
1306
  base_dict = _values_to_dict(base_vals)
@@ -1315,15 +1263,9 @@ with gr.Blocks(title="HCT Outcome Prediction Model", css=custom_css) as demo:
1315
  all_violations.extend(v)
1316
  scenario_dicts_active.append(sd)
1317
 
1318
- # Build the output list structure:
1319
- # violation, table, icon,
1320
- # base_store, base_idx, base_plot, base_crumb,
1321
- # [per scenario: store, idx, plot, crumb] Γ— MAX_SCENARIOS
1322
- n_scen_outputs = 4 * MAX_SCENARIOS # store+idx+plot+crumb each
1323
-
1324
  if all_violations:
1325
- out = [_build_violation_html(all_violations), "", ""]
1326
- out += [None, 0, None, ""] # base shap
1327
  for _ in range(MAX_SCENARIOS):
1328
  out += [None, 0, None, ""]
1329
  return tuple(out)
@@ -1353,20 +1295,19 @@ with gr.Blocks(title="HCT Outcome Prediction Model", css=custom_css) as demo:
1353
  base_probs, base_ci, scen_probs_list, scen_ci_list, scen_labels, scen_colors
1354
  )
1355
 
1356
- # Icon carousel: one row per scenario (baseline + active scenarios)
1357
- icon_html = _build_comparison_icon_carousel_html(
1358
- [base_probs] + scen_probs_list,
1359
- ["Baseline"] + scen_labels,
1360
- ["#1565c0"] + scen_colors,
1361
- )
1362
 
1363
  # SHAP β€” baseline
1364
  base_shap_plots = create_all_shap_plots(base_dict, max_display=10)
1365
  first_base_plot = base_shap_plots[SHAP_ORDER[0]]
1366
  base_crumb = _shap_counter_html(0)
1367
 
1368
- # SHAP β€” scenarios (only active ones; inactive get None)
1369
- scen_shap_data = [] # list of (store, plot0, crumb) or None
1370
  for s in range(MAX_SCENARIOS):
1371
  if s < n_scen:
1372
  sp_plots = create_all_shap_plots(scenario_dicts_active[s], max_display=10)
@@ -1374,8 +1315,19 @@ with gr.Blocks(title="HCT Outcome Prediction Model", css=custom_css) as demo:
1374
  else:
1375
  scen_shap_data.append((None, None, ""))
1376
 
1377
- out = ["", table_html, icon_html]
1378
- out += [base_shap_plots, 0, first_base_plot, base_crumb]
 
 
 
 
 
 
 
 
 
 
 
1379
  for s in range(MAX_SCENARIOS):
1380
  store, plot0, crumb0 = scen_shap_data[s]
1381
  out += [store, 0, plot0, crumb0]
@@ -1388,7 +1340,9 @@ with gr.Blocks(title="HCT Outcome Prediction Model", css=custom_css) as demo:
1388
 
1389
  # Build output list
1390
  all_run_outputs = (
1391
- [wi_violation_html, wi_table_html, wi_icon_html]
 
 
1392
  + [wi_shap_base_store, wi_shap_base_idx, wb_shap_plot, wb_shap_crumb]
1393
  + [item for s in range(MAX_SCENARIOS)
1394
  for item in (wi_shap_scen_stores[s], wi_shap_scen_idxs[s],
 
106
  DISEASE_FEATURES = ["NACS2YR", "EXCHTFPR", "VOC2YPR", "VOCFRQPR", "SCATXRSN"]
107
  ALL_FEATURES = PATIENT_FEATURES + DONOR_FEATURES + DISEASE_FEATURES
108
 
109
+ # Icon array outcomes in display order
110
  ICON_OUTCOMES = ["DEAD", "GF", "AGVHD", "CGVHD", "VOCPSHI", "STROKEHI"]
111
  OUTCOME_TITLES = {
112
  "DEAD": "Death",
 
306
 
307
 
308
  # ─────────────────────────────────────────────────────────────────────────────
309
+ # ICON ARRAY HTML RENDERERS (pure Python, no JS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  # ─────────────────────────────────────────────────────────────────────────────
311
 
312
  def _stick_figure_svg(color, size=16):
 
324
  )
325
 
326
 
327
+ def _render_single_icon_card(probability, outcome, panel_label="", panel_color="#1565c0"):
328
+ """Render a single icon array card as HTML (no JS). Used for Python-driven carousel."""
329
  title = OUTCOME_TITLES.get(outcome, OUTCOME_DESCRIPTIONS.get(outcome, outcome))
330
  n_event = round(probability * 100)
331
  n_no_event = 100 - n_event
 
362
 
363
  return (
364
  f'<div style="background:#fff;border:1px solid #e0e0e0;border-radius:7px;'
365
+ f'padding:10px 8px;text-align:center;font-family:\'Segoe UI\',Arial,sans-serif;'
366
+ f'box-shadow:0 2px 4px rgba(0,0,0,0.06);box-sizing:border-box;max-width:340px;'
367
+ f'margin:0 auto;display:flex;flex-direction:column;align-items:center;">'
368
  f'{badge}'
369
+ f'<div style="min-height:30px;display:flex;align-items:center;justify-content:center;'
370
+ f'font-size:13px;font-weight:700;color:#222;line-height:1.3;margin-bottom:4px;">{title}</div>'
371
+ f'<div style="font-size:22px;font-weight:800;color:{EVENT_COLOR};'
372
+ f'line-height:1;margin-bottom:6px;">{pct_str}</div>'
373
+ f'<div style="margin-bottom:4px;">{grid_html}</div>'
374
  f'<div>{legend}</div>'
375
  f'</div>'
376
  )
377
 
378
 
379
+ def _render_icon_carousel_page(probs_dict, idx):
380
+ """Render the full carousel HTML for a given outcome index (no JS)."""
381
+ outcome = ICON_OUTCOMES[idx]
382
+ total = len(ICON_OUTCOMES)
383
+ card_html = _render_single_icon_card(probs_dict.get(outcome, 0.0), outcome)
384
+ label = OUTCOME_TITLES.get(outcome, outcome)
385
+
386
+ # Breadcrumb dots
387
+ dots = ""
388
+ for i, o in enumerate(ICON_OUTCOMES):
389
+ active = i == idx
390
+ dots += (
391
+ f'<span style="display:inline-block;width:{"10" if active else "7"}px;'
392
+ f'height:{"10" if active else "7"}px;border-radius:50%;'
393
+ f'background:{"#1565c0" if active else "#ccc"};'
394
+ f'margin:0 3px;vertical-align:middle;"></span>'
395
+ )
 
396
 
397
  footnote = (
398
+ f'<div style="font-size:10px;color:#888;text-align:center;margin-top:10px;">'
399
  f'Each figure = 1 patient out of 100. '
400
  f'<span style="color:{EVENT_COLOR};font-weight:600;">&#9632; Red = Event</span> &nbsp; '
401
  f'<span style="color:{NO_EVENT_COLOR};font-weight:600;">&#9632; Green = No Event</span>'
402
  f'</div>'
403
  )
404
 
405
+ return (
406
+ f'<div style="font-family:\'Segoe UI\',Arial,sans-serif;padding:4px 0;text-align:center;">'
407
+ f'<div style="font-size:15px;font-weight:700;color:#1565c0;margin-bottom:2px;">{label}</div>'
408
+ f'<div style="font-size:11px;color:#888;margin-bottom:10px;">{idx+1} / {total}</div>'
409
+ f'<div style="margin-bottom:2px;">{dots}</div>'
410
+ f'<div style="margin-top:10px;">{card_html}</div>'
411
+ f'{footnote}'
412
+ f'</div>'
413
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
 
416
+ def _render_comparison_icon_page(all_probs_list, labels, colors, idx):
417
+ """Render comparison icon carousel page for a given outcome index (no JS)."""
418
+ outcome = ICON_OUTCOMES[idx]
419
+ total = len(ICON_OUTCOMES)
420
+ out_label = OUTCOME_TITLES.get(outcome, outcome)
421
+
422
+ # Dots breadcrumb
423
+ dots = ""
424
+ for i in range(total):
425
+ active = i == idx
426
+ dots += (
427
+ f'<span style="display:inline-block;width:{"10" if active else "7"}px;'
428
+ f'height:{"10" if active else "7"}px;border-radius:50%;'
429
+ f'background:{"#1565c0" if active else "#ccc"};'
430
+ f'margin:0 3px;vertical-align:middle;"></span>'
431
+ )
432
+
 
 
 
 
 
 
 
 
 
 
433
  rows_html = ""
434
+ for probs, label, color in zip(all_probs_list, labels, colors):
435
+ prob = probs.get(outcome, 0.0)
436
+ card = _render_single_icon_card(prob, outcome, label, color)
437
  rows_html += (
438
+ f'<div style="display:flex;align-items:flex-start;gap:10px;margin-bottom:12px;">'
439
+ f'<div style="width:80px;flex-shrink:0;font-size:9px;font-weight:700;'
440
+ f'text-align:center;border-radius:4px;padding:4px 6px;color:#fff;'
441
+ f'background:{color};white-space:normal;word-break:break-word;">{label}</div>'
442
+ f'<div style="flex:1;">{card}</div>'
 
443
  f'</div>'
444
  )
445
 
 
451
  f'</div>'
452
  )
453
 
454
+ return (
455
+ f'<div style="font-family:\'Segoe UI\',Arial,sans-serif;padding:4px 0;text-align:center;">'
456
+ f'<div style="font-size:15px;font-weight:700;color:#1565c0;margin-bottom:2px;">{out_label}</div>'
457
+ f'<div style="font-size:11px;color:#888;margin-bottom:6px;">{idx+1} / {total}</div>'
458
+ f'<div style="margin-bottom:10px;">{dots}</div>'
459
+ f'{rows_html}'
460
+ f'{footnote}'
461
+ f'</div>'
462
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
 
464
 
465
+ # ─────────────────────────────────────────────────────────────────────────────
466
+ # OTHER HTML RENDERERS
467
+ # ─────────────────────────────────────────────────────────────────────────────
468
+
469
  def _delta_color_html(delta, is_survival):
470
  if abs(delta) < 0.0005:
471
  color = "#888888"
 
580
  # SHAP CAROUSEL HELPERS
581
  # ─────────────────────────────────────────────────────────────────────────────
582
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
583
  def _shap_counter_html(idx):
584
  labels = [SHAP_LABELS.get(o, o) for o in SHAP_ORDER]
585
  items = " Β· ".join(
 
617
  })
618
  df = pd.DataFrame(rows)
619
 
620
+ shap_plots = create_all_shap_plots(user_vals, max_display=10)
 
621
 
622
+ # Icon carousel: start at index 0, store probs in State
623
+ icon_html = _render_icon_carousel_page(calibrated_probs, 0)
624
+ first_shap = shap_plots[SHAP_ORDER[0]]
625
+ shap_crumb = _shap_counter_html(0)
626
 
627
  return (
628
  df,
629
+ calibrated_probs, # stored in State for carousel navigation
630
+ 0, # icon carousel index State
631
+ icon_html, # displayed icon card
632
+ shap_plots, # stored in State
633
+ 0, # shap index State
634
+ first_shap, # displayed plot
635
+ shap_crumb, # breadcrumb HTML
636
  )
637
  except Exception as e:
638
  print(traceback.format_exc())
 
673
  padding: 8px 22px !important; border-radius: 6px !important;
674
  }
675
  .shap-nav-button:hover { background: linear-gradient(to right, #263238, #455a64) !important; }
676
+ .icon-nav-button {
677
+ background: linear-gradient(to right, #1565c0, #1e88e5) !important;
678
+ border: none !important; color: white !important;
679
+ font-weight: bold !important; font-size: 18px !important;
680
+ padding: 8px 22px !important; border-radius: 6px !important;
681
+ }
682
+ .icon-nav-button:hover { background: linear-gradient(to right, #0d47a1, #1565c0) !important; }
683
  .output-dataframe table td:first-child,
684
  .output-dataframe table th:first-child {
685
  white-space: normal !important; word-break: break-word !important; min-width: 240px !important;
 
764
  wrap=True,
765
  )
766
 
767
+ # ── Icon Array Carousel (Tab 1) β€” Python-driven ───────────────────
768
  gr.Markdown("---")
769
  gr.Markdown("## Outcome Probability β€” Icon Arrays")
770
  gr.Markdown("*Use the ← β†’ arrows to browse outcomes one at a time.*")
771
+
772
+ # State: store probs dict + current index
773
+ icon_probs_state = gr.State(None)
774
+ icon_idx_state = gr.State(0)
775
+
776
+ with gr.Row():
777
+ icon_prev_btn = gr.Button("←", elem_classes="icon-nav-button", size="sm", scale=0)
778
+ icon_display = gr.HTML(scale=4)
779
+ icon_next_btn = gr.Button("β†’", elem_classes="icon-nav-button", size="sm", scale=0)
780
+
781
+ def _on_icon_prev(idx, probs):
782
+ if probs is None:
783
+ return idx, ""
784
+ new_idx = max(0, idx - 1)
785
+ return new_idx, _render_icon_carousel_page(probs, new_idx)
786
+
787
+ def _on_icon_next(idx, probs):
788
+ if probs is None:
789
+ return idx, ""
790
+ new_idx = min(len(ICON_OUTCOMES) - 1, idx + 1)
791
+ return new_idx, _render_icon_carousel_page(probs, new_idx)
792
+
793
+ icon_prev_btn.click(
794
+ fn=_on_icon_prev,
795
+ inputs=[icon_idx_state, icon_probs_state],
796
+ outputs=[icon_idx_state, icon_display],
797
+ )
798
+ icon_next_btn.click(
799
+ fn=_on_icon_next,
800
+ inputs=[icon_idx_state, icon_probs_state],
801
+ outputs=[icon_idx_state, icon_display],
802
+ )
803
 
804
  # ── SHAP Carousel (Tab 1) ─────────────────────────────────────────
805
  gr.Markdown("---")
806
  gr.Markdown("## SHAP β€” Feature Importance")
807
  gr.Markdown("*Use the ← β†’ buttons to browse SHAP plots one at a time.*")
808
 
 
809
  shap_plots_state = gr.State(None)
810
  shap_idx_state = gr.State(0)
811
 
 
816
 
817
  shap_display = gr.Plot(label="SHAP Feature Importance")
818
 
 
819
  def _on_shap_prev(idx, plots):
820
  new_idx = max(0, idx - 1)
821
  plot = plots[SHAP_ORDER[new_idx]] if plots else None
 
842
  inputs=inputs_list,
843
  outputs=[
844
  output_table,
845
+ icon_probs_state, # ← store probs dict
846
+ icon_idx_state, # ← reset to 0
847
+ icon_display, # ← show first card
848
  shap_plots_state,
849
  shap_idx_state,
850
  shap_display,
 
1090
  wi_table_html = gr.HTML()
1091
 
1092
  gr.Markdown("---")
1093
+
1094
+ # ── Icon Arrays β€” comparison, Python-driven carousel ─────────────
1095
  with gr.Accordion("Outcome Icon Arrays β€” Baseline vs Scenarios", open=False):
1096
  gr.Markdown(
1097
+ "*One row per scenario. Use the ← β†’ arrows to browse outcomes one at a time.*"
1098
+ )
1099
+
1100
+ wi_cmp_probs_state = gr.State(None) # list of dicts
1101
+ wi_cmp_labels_state = gr.State(None) # list of labels
1102
+ wi_cmp_colors_state = gr.State(None) # list of colors
1103
+ wi_cmp_idx_state = gr.State(0)
1104
+
1105
+ with gr.Row():
1106
+ wi_cmp_prev_btn = gr.Button("←", elem_classes="icon-nav-button", size="sm", scale=0)
1107
+ wi_icon_html = gr.HTML(scale=4)
1108
+ wi_cmp_next_btn = gr.Button("β†’", elem_classes="icon-nav-button", size="sm", scale=0)
1109
+
1110
+ def _on_cmp_prev(idx, probs_list, labels, colors):
1111
+ if probs_list is None:
1112
+ return idx, ""
1113
+ new_idx = max(0, idx - 1)
1114
+ return new_idx, _render_comparison_icon_page(probs_list, labels, colors, new_idx)
1115
+
1116
+ def _on_cmp_next(idx, probs_list, labels, colors):
1117
+ if probs_list is None:
1118
+ return idx, ""
1119
+ new_idx = min(len(ICON_OUTCOMES) - 1, idx + 1)
1120
+ return new_idx, _render_comparison_icon_page(probs_list, labels, colors, new_idx)
1121
+
1122
+ wi_cmp_prev_btn.click(
1123
+ fn=_on_cmp_prev,
1124
+ inputs=[wi_cmp_idx_state, wi_cmp_probs_state, wi_cmp_labels_state, wi_cmp_colors_state],
1125
+ outputs=[wi_cmp_idx_state, wi_icon_html],
1126
+ )
1127
+ wi_cmp_next_btn.click(
1128
+ fn=_on_cmp_next,
1129
+ inputs=[wi_cmp_idx_state, wi_cmp_probs_state, wi_cmp_labels_state, wi_cmp_colors_state],
1130
+ outputs=[wi_cmp_idx_state, wi_icon_html],
1131
  )
 
1132
 
1133
  gr.Markdown("---")
1134
 
 
1140
  wi_shap_base_idx = gr.State(0)
1141
 
1142
  with gr.Row():
1143
+ wb_shap_prev = gr.Button("←", elem_classes="shap-nav-button", size="sm", scale=0)
1144
  wb_shap_crumb = gr.HTML(value="", scale=4)
1145
+ wb_shap_next = gr.Button("β†’", elem_classes="shap-nav-button", size="sm", scale=0)
1146
  wb_shap_plot = gr.Plot(label="Baseline β€” SHAP")
1147
 
1148
  def _wb_prev(idx, plots):
 
1190
  wi_shap_scen_plots.append(s_plot)
1191
  wi_shap_scen_crumbs.append(s_crumb)
1192
 
 
1193
  def _make_prev(st, ix):
1194
  def fn(idx, plots):
1195
  new_idx = max(0, idx - 1)
 
1240
  for s in range(MAX_SCENARIOS)
1241
  ]
1242
 
1243
+ def _empty_outputs():
1244
+ # violation, table, cmp_probs, cmp_labels, cmp_colors, cmp_idx, icon_html
1245
+ # base_store, base_idx, base_plot, base_crumb
1246
+ # per scenario: store, idx, plot, crumb
1247
+ out = ["", "", None, None, None, 0, ""]
1248
+ out += [None, 0, None, ""]
1249
+ for _ in range(MAX_SCENARIOS):
1250
+ out += [None, 0, None, ""]
1251
+ return tuple(out)
1252
 
1253
  try:
1254
  base_dict = _values_to_dict(base_vals)
 
1263
  all_violations.extend(v)
1264
  scenario_dicts_active.append(sd)
1265
 
 
 
 
 
 
 
1266
  if all_violations:
1267
+ out = [_build_violation_html(all_violations), "", None, None, None, 0, ""]
1268
+ out += [None, 0, None, ""]
1269
  for _ in range(MAX_SCENARIOS):
1270
  out += [None, 0, None, ""]
1271
  return tuple(out)
 
1295
  base_probs, base_ci, scen_probs_list, scen_ci_list, scen_labels, scen_colors
1296
  )
1297
 
1298
+ # Icon carousel: store all probs + labels + colors in State
1299
+ all_probs_list = [base_probs] + scen_probs_list
1300
+ all_labels = ["Baseline"] + scen_labels
1301
+ all_colors = ["#1565c0"] + scen_colors
1302
+ icon_html_first = _render_comparison_icon_page(all_probs_list, all_labels, all_colors, 0)
 
1303
 
1304
  # SHAP β€” baseline
1305
  base_shap_plots = create_all_shap_plots(base_dict, max_display=10)
1306
  first_base_plot = base_shap_plots[SHAP_ORDER[0]]
1307
  base_crumb = _shap_counter_html(0)
1308
 
1309
+ # SHAP β€” scenarios
1310
+ scen_shap_data = []
1311
  for s in range(MAX_SCENARIOS):
1312
  if s < n_scen:
1313
  sp_plots = create_all_shap_plots(scenario_dicts_active[s], max_display=10)
 
1315
  else:
1316
  scen_shap_data.append((None, None, ""))
1317
 
1318
+ out = [
1319
+ "", # violation html
1320
+ table_html, # table
1321
+ all_probs_list, # cmp_probs state
1322
+ all_labels, # cmp_labels state
1323
+ all_colors, # cmp_colors state
1324
+ 0, # cmp_idx state
1325
+ icon_html_first, # icon display
1326
+ base_shap_plots, # base shap store
1327
+ 0, # base shap idx
1328
+ first_base_plot, # base shap plot
1329
+ base_crumb, # base shap crumb
1330
+ ]
1331
  for s in range(MAX_SCENARIOS):
1332
  store, plot0, crumb0 = scen_shap_data[s]
1333
  out += [store, 0, plot0, crumb0]
 
1340
 
1341
  # Build output list
1342
  all_run_outputs = (
1343
+ [wi_violation_html, wi_table_html,
1344
+ wi_cmp_probs_state, wi_cmp_labels_state, wi_cmp_colors_state,
1345
+ wi_cmp_idx_state, wi_icon_html]
1346
  + [wi_shap_base_store, wi_shap_base_idx, wb_shap_plot, wb_shap_crumb]
1347
  + [item for s in range(MAX_SCENARIOS)
1348
  for item in (wi_shap_scen_stores[s], wi_shap_scen_idxs[s],