skapoor-wpi commited on
Commit
9d586f0
·
1 Parent(s): aa1f862

slider for weight and interaction. color by for scatter plot, imrpovement sto UX

Browse files
backend/app.py CHANGED
@@ -583,10 +583,12 @@ def get_umap():
583
  print(f"Using default weights")
584
 
585
  response_data, clustering_info, df, df_full = run_umap_pipeline(weights, filter_methods)
 
586
 
587
  return jsonify({
588
  'success': True,
589
  'data': response_data,
 
590
  'config': {
591
  'weights': weights,
592
  'defaultWeights': DEFAULT_WEIGHTS,
@@ -778,9 +780,11 @@ def build_cluster_stats(response_data, clustering_info, weights):
778
  for cluster_id in sorted(clusters.keys()):
779
  members = clusters[cluster_id]
780
  names = [m['name'] for m in members]
781
- # Generate a short characterization label first
 
 
782
  dominant_attrs = []
783
- for col in key_cols[:3]:
784
  short_col = SHORT_COLUMN_NAMES.get(col, col)
785
  vals_for_label = []
786
  for m in members:
@@ -958,6 +962,7 @@ Rules:
958
  3. Connect findings to the clustering: explain why methods using similar approaches end up in the same group.
959
  4. Be specific and technical. Avoid generic statements like "various methods use different approaches."
960
  5. Never reference cluster numbers. Use group names like "the sampling-based parallel-jaw group."
 
961
 
962
  Respond with ONLY the bullet points, nothing else."""
963
 
 
583
  print(f"Using default weights")
584
 
585
  response_data, clustering_info, df, df_full = run_umap_pipeline(weights, filter_methods)
586
+ _, cluster_stats = build_cluster_stats(response_data, clustering_info, weights)
587
 
588
  return jsonify({
589
  'success': True,
590
  'data': response_data,
591
+ 'clusterStats': cluster_stats,
592
  'config': {
593
  'weights': weights,
594
  'defaultWeights': DEFAULT_WEIGHTS,
 
780
  for cluster_id in sorted(clusters.keys()):
781
  members = clusters[cluster_id]
782
  names = [m['name'] for m in members]
783
+ # Generate a short characterization label from the top-weighted columns
784
+ all_label_cols = [c for c in weights.keys() if c != 'Description']
785
+ label_cols = sorted(all_label_cols, key=lambda c: weights.get(c, 0), reverse=True)[:3]
786
  dominant_attrs = []
787
+ for col in label_cols:
788
  short_col = SHORT_COLUMN_NAMES.get(col, col)
789
  vals_for_label = []
790
  for m in members:
 
962
  3. Connect findings to the clustering: explain why methods using similar approaches end up in the same group.
963
  4. Be specific and technical. Avoid generic statements like "various methods use different approaches."
964
  5. Never reference cluster numbers. Use group names like "the sampling-based parallel-jaw group."
965
+ 6. Always use the exact method names as provided in the data (e.g., "Grasp Pose Detection (GPD)" not just "GPD", "Volumetric Grasping Network (VGN)" not just "VGN"). This ensures methods are correctly linked in the interface.
966
 
967
  Respond with ONLY the bullet points, nothing else."""
968
 
backend/rag/query_engine.py CHANGED
@@ -120,8 +120,9 @@ def compute_weights_from_query(query: str, default_weights: dict, model=None) ->
120
  return weights
121
 
122
 
123
- def pick_color_by(query: str) -> str:
124
- """Deterministically pick the best color-by column from query keywords."""
 
125
  query_lower = query.lower()
126
  best_col = 'cluster'
127
  best_score = 0
@@ -132,6 +133,16 @@ def pick_color_by(query: str) -> str:
132
  best_score = score
133
  best_col = col
134
 
 
 
 
 
 
 
 
 
 
 
135
  return best_col
136
 
137
 
@@ -380,8 +391,8 @@ def deterministic_query_pipeline(query: str, df, model, default_weights: dict,
380
  # 1. Compute weights from query
381
  weights = compute_weights_from_query(query, default_weights, model)
382
 
383
- # 2. Pick color-by
384
- color_by = pick_color_by(query)
385
 
386
  # 3. Find relevant methods via embedding similarity
387
  ranked_methods = find_relevant_methods(expanded_query, df, model, top_k=15)
 
120
  return weights
121
 
122
 
123
+ def pick_color_by(query: str, weights: dict = None, default_weights: dict = None) -> str:
124
+ """Deterministically pick the best color-by column from query keywords.
125
+ Falls back to the most-boosted weight column if no keywords match."""
126
  query_lower = query.lower()
127
  best_col = 'cluster'
128
  best_score = 0
 
133
  best_score = score
134
  best_col = col
135
 
136
+ # If no keyword match but weights were boosted, color by the most-boosted column
137
+ if best_score == 0 and weights and default_weights:
138
+ max_boost = 0
139
+ for col, w in weights.items():
140
+ default = default_weights.get(col, 10)
141
+ boost = w - default
142
+ if boost > max_boost and col in COLOR_BY_KEYWORDS:
143
+ max_boost = boost
144
+ best_col = col
145
+
146
  return best_col
147
 
148
 
 
391
  # 1. Compute weights from query
392
  weights = compute_weights_from_query(query, default_weights, model)
393
 
394
+ # 2. Pick color-by (pass weights so it can fall back to most-boosted column)
395
+ color_by = pick_color_by(query, weights, default_weights)
396
 
397
  # 3. Find relevant methods via embedding similarity
398
  ranked_methods = find_relevant_methods(expanded_query, df, model, top_k=15)
chroma_db/chroma.sqlite3 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:338c12a71e04a09aab6951c671a3a396c67b550848f00f8a0dec3bb24fcf1994
3
  size 93999104
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8f526eb204a9cd00c888c66912d1fd3220c375f7135fc15b8be9291b74fe6155
3
  size 93999104
frontend/src/App.css CHANGED
@@ -30,7 +30,7 @@ body {
30
  background: var(--bg);
31
  color: var(--text);
32
  -webkit-font-smoothing: antialiased;
33
- font-size: 14px;
34
  line-height: 1.5;
35
  }
36
 
@@ -117,20 +117,21 @@ body {
117
  border-left: 3px solid var(--primary);
118
  }
119
  .insight-card-header {
120
- display: flex; align-items: center; gap: 0.4rem;
121
- padding: 0.4rem 0.7rem; background: var(--primary-wash);
122
- border-bottom: 1px solid var(--border-light);
 
123
  }
124
  .insight-icon {
125
- font-size: 0.58rem; font-weight: 700; background: var(--primary);
126
- color: white; padding: 0.1rem 0.3rem; border-radius: 2px; letter-spacing: 0.5px;
127
  }
128
- .insight-title { font-weight: 700; font-size: 0.92rem; flex: 1; color: var(--dark); }
129
  .insight-close {
130
- background: none; border: none; color: var(--text-muted);
131
  font-size: 1rem; cursor: pointer; line-height: 1;
132
  }
133
- .insight-close:hover { color: var(--text); }
134
 
135
  .insight-body { padding: 0.55rem 0.7rem; }
136
 
@@ -254,11 +255,16 @@ body {
254
  width: calc(100% - 2.5rem);
255
  }
256
  .table-panel-header {
257
- padding: 0.45rem 0.7rem;
258
  background: var(--primary-deep); color: white;
259
- font-size: 0.9rem; font-weight: 700;
260
- display: flex; justify-content: space-between; align-items: center;
261
  flex-shrink: 0;
 
 
 
 
 
262
  }
263
  .hl-indicator { font-size: 0.72rem; color: #fcd97f; font-weight: 600; }
264
 
@@ -341,16 +347,14 @@ body {
341
 
342
  /* ─── SCATTER + LEGEND ROW ─── */
343
  .scatter-section {
344
- display: flex; gap: 0;
345
  margin: 0 1.25rem 0.6rem;
346
- align-items: stretch;
347
  }
348
- .scatter-section .scatter-panel {
349
  flex: 3; min-width: 0;
350
- border-radius: 0 0 0 4px;
351
  }
352
- .scatter-section .legend-and-description {
353
- flex: 1; min-width: 200px; max-width: 260px;
354
  }
355
 
356
  .scatter-panel {
@@ -362,31 +366,136 @@ body {
362
  }
363
 
364
  /* Viz toggle */
365
- .viz-toggle {
366
- display: flex; gap: 0;
367
- border-bottom: 1px solid var(--border-light);
368
- background: var(--surface);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  }
370
  .viz-toggle-btn {
371
- flex: 1; padding: 0.35rem 0.6rem;
372
- background: none; border: none;
373
  font-family: inherit; font-size: 0.75rem; font-weight: 600;
374
- color: var(--text-muted); cursor: pointer;
375
- border-bottom: 2px solid transparent;
376
  transition: all 0.12s;
 
 
 
 
 
 
377
  }
378
- .viz-toggle-btn:hover { color: var(--text); }
379
  .viz-toggle-btn.active {
380
- color: var(--primary); border-bottom-color: var(--primary);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  background: var(--card);
 
 
 
 
 
 
 
 
382
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  /* Legend + description column */
384
  .legend-and-description {
385
  display: flex; flex-direction: column; gap: 0;
386
  flex: 1; min-width: 200px; max-width: 260px;
387
  }
388
  .legend-and-description .cluster-legend-compact {
389
- border-radius: 0 4px 0 0;
390
  border-top: none;
391
  border-left: none;
392
  }
@@ -414,10 +523,12 @@ body {
414
  border-top: none;
415
  border-radius: 0 0 4px 0;
416
  padding: 0.45rem 0.55rem;
 
 
417
  }
418
  .cluster-legend-compact .cluster-legend-title {
419
- font-size: 0.68rem; font-weight: 700; color: var(--text-muted);
420
- text-transform: uppercase; letter-spacing: 0.8px;
421
  margin-bottom: 0.35rem; padding-bottom: 0.25rem;
422
  border-bottom: 1px solid var(--border-light);
423
  }
@@ -448,8 +559,7 @@ body {
448
  }
449
  .cluster-legend-row .cluster-legend-label {
450
  flex: 1; color: var(--text); font-weight: 500;
451
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
452
- font-size: 0.7rem;
453
  }
454
  .cluster-legend-row .cluster-legend-count {
455
  color: var(--text-muted); font-size: 0.65rem; font-weight: 700;
@@ -462,16 +572,16 @@ body {
462
  border-radius: 4px; margin: 0 1.25rem 0.5rem; overflow: hidden;
463
  }
464
  .cluster-insight-header {
465
- display: flex; align-items: center; gap: 0.35rem;
466
- padding: 0.35rem 0.65rem;
467
- background: var(--primary-wash);
468
- border-bottom: 1px solid var(--border-light);
469
  }
470
  .cluster-insight-icon {
471
- font-size: 0.55rem; font-weight: 700; background: var(--primary);
472
  color: white; padding: 0.08rem 0.28rem; border-radius: 2px;
473
  }
474
- .cluster-insight-title { font-weight: 700; font-size: 0.8rem; color: var(--dark); }
475
  .cluster-insight-body { padding: 0.45rem 0.65rem; }
476
  .cluster-insight-body p { font-size: 0.8rem; line-height: 1.55; color: var(--text); margin: 0; }
477
  .cluster-insight-loading { color: var(--primary); font-style: italic; }
@@ -552,8 +662,21 @@ body {
552
 
553
  /* ─── ANALYTICS DASHBOARD ─── */
554
  .analytics-dashboard { margin: 0 1.25rem 0.5rem; }
 
 
 
 
 
 
 
 
 
 
 
 
555
  .analytics-grid {
556
  display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;
 
557
  }
558
  .analytics-card {
559
  background: var(--card); border-radius: 4px;
@@ -1199,8 +1322,9 @@ body {
1199
  /* ─── RESPONSIVE ─���─ */
1200
  @media (max-width: 960px) {
1201
  .analytics-grid { grid-template-columns: 1fr; }
1202
- .scatter-section { flex-direction: column; }
1203
- .scatter-section .cluster-legend-compact { max-width: none; }
 
1204
  .table-panel, .scatter-section, .cluster-insight-card,
1205
  .query-explanation, .analytics-dashboard, .detail-panel {
1206
  margin-left: 0.5rem; margin-right: 0.5rem;
 
30
  background: var(--bg);
31
  color: var(--text);
32
  -webkit-font-smoothing: antialiased;
33
+ font-size: 16px;
34
  line-height: 1.5;
35
  }
36
 
 
117
  border-left: 3px solid var(--primary);
118
  }
119
  .insight-card-header {
120
+ display: flex; align-items: center; gap: 0.5rem;
121
+ padding: 0.4rem 0.7rem;
122
+ background: var(--primary-deep); color: white;
123
+ border-radius: 4px 4px 0 0;
124
  }
125
  .insight-icon {
126
+ font-size: 0.55rem; font-weight: 700; background: rgba(255,255,255,0.2);
127
+ color: white; padding: 0.08rem 0.28rem; border-radius: 2px; letter-spacing: 0.5px;
128
  }
129
+ .insight-title { font-weight: 700; font-size: 0.75rem; flex: 1; color: white; }
130
  .insight-close {
131
+ background: none; border: none; color: rgba(255,255,255,0.6);
132
  font-size: 1rem; cursor: pointer; line-height: 1;
133
  }
134
+ .insight-close:hover { color: white; }
135
 
136
  .insight-body { padding: 0.55rem 0.7rem; }
137
 
 
255
  width: calc(100% - 2.5rem);
256
  }
257
  .table-panel-header {
258
+ padding: 0.4rem 0.7rem;
259
  background: var(--primary-deep); color: white;
260
+ font-size: 0.75rem; font-weight: 700;
261
+ display: flex; align-items: center; gap: 0.5rem;
262
  flex-shrink: 0;
263
+ border-radius: 4px 4px 0 0;
264
+ }
265
+ .table-panel-header .method-count {
266
+ font-size: 0.72rem; font-weight: 600;
267
+ color: rgba(255,255,255,0.6);
268
  }
269
  .hl-indicator { font-size: 0.72rem; color: #fcd97f; font-weight: 600; }
270
 
 
347
 
348
  /* ─── SCATTER + LEGEND ROW ─── */
349
  .scatter-section {
350
+ display: flex; flex-direction: column; gap: 0;
351
  margin: 0 1.25rem 0.6rem;
 
352
  }
353
+ .scatter-content .scatter-panel {
354
  flex: 3; min-width: 0;
 
355
  }
356
+ .scatter-content .legend-and-description {
357
+ flex: 0 0 300px; min-width: 220px; max-width: 300px;
358
  }
359
 
360
  .scatter-panel {
 
366
  }
367
 
368
  /* Viz toggle */
369
+ .viz-toolbar {
370
+ display: flex; gap: 0; align-items: stretch;
371
+ background: var(--primary-deep);
372
+ border-radius: 4px 4px 0 0;
373
+ overflow: hidden;
374
+ }
375
+ .scatter-content {
376
+ display: flex; gap: 0; align-items: stretch; flex: 1;
377
+ }
378
+ /* Color-by selector — header above legend */
379
+ .color-by-header {
380
+ display: flex; align-items: center; gap: 0.5rem;
381
+ padding: 0.45rem 0.7rem;
382
+ background: transparent;
383
+ flex: 0 0 300px; max-width: 300px;
384
+ white-space: nowrap;
385
+ border-left: 3px solid rgba(255,255,255,0.5);
386
+ }
387
+ .color-by-header label {
388
+ font-size: 0.75rem; font-weight: 700; color: #fff;
389
+ text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap;
390
+ }
391
+ .color-by-header select {
392
+ flex: 1; font-family: inherit; font-size: 0.75rem; font-weight: 600;
393
+ padding: 0.25rem 0.4rem; border: 1px solid rgba(255,255,255,0.3);
394
+ border-radius: 3px; background: rgba(255,255,255,0.12); color: #fff;
395
+ cursor: pointer;
396
+ }
397
+ .color-by-header select:hover {
398
+ background: rgba(255,255,255,0.2);
399
+ }
400
+ .color-by-header select option {
401
+ background: var(--primary-deep); color: #fff;
402
  }
403
  .viz-toggle-btn {
404
+ flex: 1; padding: 0.4rem 0.7rem;
405
+ background: transparent; border: none;
406
  font-family: inherit; font-size: 0.75rem; font-weight: 600;
407
+ color: rgba(255,255,255,0.6); cursor: pointer;
 
408
  transition: all 0.12s;
409
+ border-right: 1px solid rgba(255,255,255,0.15);
410
+ }
411
+ .viz-toggle-btn:last-of-type { border-right: none; }
412
+ .viz-toggle-btn:hover:not(.disabled) { color: #fff; background: rgba(255,255,255,0.1); }
413
+ .viz-toggle-btn.disabled {
414
+ color: rgba(255,255,255,0.25); cursor: not-allowed;
415
  }
 
416
  .viz-toggle-btn.active {
417
+ color: #fff; font-weight: 700;
418
+ background: rgba(255,255,255,0.15);
419
+ box-shadow: inset 0 -3px 0 var(--accent);
420
+ }
421
+ .weights-toggle {
422
+ border-left: 1px solid rgba(255,255,255,0.15);
423
+ border-right: 1px solid rgba(255,255,255,0.15);
424
+ flex: 0 0 auto;
425
+ background: rgba(217,90,62,0.25);
426
+ color: rgba(255,255,255,0.85);
427
+ }
428
+ .weights-toggle:hover:not(.disabled) { background: rgba(217,90,62,0.4); color: #fff; }
429
+ .weights-toggle.active {
430
+ background: var(--accent); color: #fff;
431
+ box-shadow: inset 0 -3px 0 rgba(0,0,0,0.2);
432
+ }
433
+ /* Weight sliders panel */
434
+ .weight-panel {
435
  background: var(--card);
436
+ border: 1px solid var(--border);
437
+ border-top: none;
438
+ }
439
+ .weight-panel-bar {
440
+ display: flex; align-items: center; gap: 0.4rem;
441
+ padding: 0.3rem 0.7rem;
442
+ background: var(--surface);
443
+ border-bottom: 1px solid var(--border-light);
444
  }
445
+ .weight-panel-label {
446
+ font-size: 0.68rem; font-weight: 700; color: var(--text-muted);
447
+ text-transform: uppercase; letter-spacing: 0.5px;
448
+ }
449
+ .weight-panel-bar .chart-help {
450
+ font-size: 0.55rem;
451
+ }
452
+ .weight-panel-bar .weight-reset-btn {
453
+ margin-left: auto;
454
+ }
455
+ .weight-panel-grid {
456
+ display: grid;
457
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
458
+ gap: 0.2rem 1.2rem;
459
+ padding: 0.45rem 0.7rem;
460
+ align-items: center;
461
+ }
462
+ .weight-slider-row {
463
+ display: flex; align-items: center; gap: 0.3rem;
464
+ min-height: 1.5rem;
465
+ }
466
+ .weight-slider-label {
467
+ flex: 0 0 85px; font-size: 0.68rem; font-weight: 600; color: var(--text);
468
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
469
+ }
470
+ .weight-slider-input {
471
+ flex: 1; height: 3px; accent-color: var(--primary);
472
+ cursor: pointer; min-width: 60px;
473
+ }
474
+ .weight-slider-value {
475
+ flex: 0 0 20px; text-align: right; font-weight: 700;
476
+ font-size: 0.68rem; color: var(--primary-deep);
477
+ min-width: 20px;
478
+ }
479
+ .weight-ai-badge {
480
+ font-size: 0.45rem; font-weight: 700;
481
+ background: var(--accent); color: white;
482
+ padding: 0.02rem 0.18rem; border-radius: 2px;
483
+ margin-left: 0.15rem; vertical-align: super;
484
+ }
485
+ .weight-reset-btn {
486
+ font-family: inherit; font-size: 0.68rem; font-weight: 600;
487
+ padding: 0.25rem 0.5rem;
488
+ background: var(--surface); border: 1px solid var(--border);
489
+ border-radius: 3px; color: var(--text-muted); cursor: pointer;
490
+ }
491
+ .weight-reset-btn:hover { background: var(--bg); color: var(--text); }
492
  /* Legend + description column */
493
  .legend-and-description {
494
  display: flex; flex-direction: column; gap: 0;
495
  flex: 1; min-width: 200px; max-width: 260px;
496
  }
497
  .legend-and-description .cluster-legend-compact {
498
+ border-radius: 0 0 4px 0;
499
  border-top: none;
500
  border-left: none;
501
  }
 
523
  border-top: none;
524
  border-radius: 0 0 4px 0;
525
  padding: 0.45rem 0.55rem;
526
+ max-height: 280px;
527
+ overflow-y: auto;
528
  }
529
  .cluster-legend-compact .cluster-legend-title {
530
+ font-size: 0.75rem; font-weight: 700; color: var(--text-muted);
531
+ text-transform: uppercase; letter-spacing: 0.6px;
532
  margin-bottom: 0.35rem; padding-bottom: 0.25rem;
533
  border-bottom: 1px solid var(--border-light);
534
  }
 
559
  }
560
  .cluster-legend-row .cluster-legend-label {
561
  flex: 1; color: var(--text); font-weight: 500;
562
+ font-size: 0.7rem; line-height: 1.3;
 
563
  }
564
  .cluster-legend-row .cluster-legend-count {
565
  color: var(--text-muted); font-size: 0.65rem; font-weight: 700;
 
572
  border-radius: 4px; margin: 0 1.25rem 0.5rem; overflow: hidden;
573
  }
574
  .cluster-insight-header {
575
+ display: flex; align-items: center; gap: 0.5rem;
576
+ padding: 0.4rem 0.7rem;
577
+ background: var(--primary-deep); color: white;
578
+ border-radius: 4px 4px 0 0;
579
  }
580
  .cluster-insight-icon {
581
+ font-size: 0.55rem; font-weight: 700; background: rgba(255,255,255,0.2);
582
  color: white; padding: 0.08rem 0.28rem; border-radius: 2px;
583
  }
584
+ .cluster-insight-title { font-weight: 700; font-size: 0.75rem; color: white; }
585
  .cluster-insight-body { padding: 0.45rem 0.65rem; }
586
  .cluster-insight-body p { font-size: 0.8rem; line-height: 1.55; color: var(--text); margin: 0; }
587
  .cluster-insight-loading { color: var(--primary); font-style: italic; }
 
662
 
663
  /* ─── ANALYTICS DASHBOARD ─── */
664
  .analytics-dashboard { margin: 0 1.25rem 0.5rem; }
665
+ .analytics-header {
666
+ display: flex; align-items: center; gap: 0.5rem;
667
+ padding: 0.4rem 0.7rem;
668
+ background: var(--primary-deep); color: white;
669
+ border-radius: 4px 4px 0 0;
670
+ margin-bottom: 0;
671
+ }
672
+ .analytics-header-icon {
673
+ font-size: 0.55rem; font-weight: 700; background: rgba(255,255,255,0.2);
674
+ color: white; padding: 0.08rem 0.28rem; border-radius: 2px;
675
+ }
676
+ .analytics-header-title { font-weight: 700; font-size: 0.75rem; }
677
  .analytics-grid {
678
  display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;
679
+ padding-top: 0.5rem;
680
  }
681
  .analytics-card {
682
  background: var(--card); border-radius: 4px;
 
1322
  /* ─── RESPONSIVE ─���─ */
1323
  @media (max-width: 960px) {
1324
  .analytics-grid { grid-template-columns: 1fr; }
1325
+ .scatter-content { flex-direction: column; }
1326
+ .scatter-content .cluster-legend-compact { max-width: none; }
1327
+ .viz-toolbar { flex-wrap: wrap; }
1328
  .table-panel, .scatter-section, .cluster-insight-card,
1329
  .query-explanation, .analytics-dashboard, .detail-panel {
1330
  margin-left: 0.5rem; margin-right: 0.5rem;
frontend/src/App.js CHANGED
@@ -2,7 +2,8 @@ import React, { useState, useEffect, useCallback } from 'react';
2
  import staticInsightData from './staticClusterInsight.json';
3
  import InsightCard from './components/InsightCard';
4
  import { ClusterLegend, ClusterInsight } from './components/ClusterOverview';
5
- import { SHORT_NAMES } from './constants';
 
6
  import ScatterPlot from './components/ScatterPlot';
7
  import MethodTable from './components/MethodTable';
8
  import DetailPanel from './components/DetailPanel';
@@ -39,17 +40,29 @@ function App() {
39
  const [clusterStats, setClusterStats] = useState(staticInsightData.clusterStats);
40
  const [activeCluster, setActiveCluster] = useState(null);
41
 
 
 
 
42
  const applyQueryResult = useCallback((queryText, result) => {
43
  setSuggestion(result);
44
  setData(result.umapData);
45
  setWeights(result.weights);
46
  setColorBy(result.colorBy);
 
 
 
47
  const highlights = result.filterMethods || result.highlightMethods || [];
48
  setHighlightedMethods(highlights);
49
  setSelectedPoint(null);
50
  setFilterActive(!!result.filterMethods);
51
  setFilterCount(result.filterMethods ? result.filterMethods.length : null);
52
  if (result.clusterStats) setClusterStats(result.clusterStats);
 
 
 
 
 
 
53
  setQuery(queryText);
54
  }, []);
55
 
@@ -69,6 +82,7 @@ function App() {
69
  if (result.success) {
70
  setData(result.data);
71
  setWeights(result.config.weights);
 
72
  if (result.filter) {
73
  setFilterActive(result.filter.active);
74
  setFilterCount(result.filter.active ? result.data.length : null);
@@ -89,9 +103,11 @@ function App() {
89
  useEffect(() => {
90
  setLoading(true);
91
 
92
- // Restore cached query state from a previous iframe load (e.g., ReVISit study sections 3/4)
93
- const cachedQuery = sessionStorage.getItem('grasp-explorer-query');
94
- const cachedResult = sessionStorage.getItem('grasp-explorer-result');
 
 
95
  if (cachedQuery && cachedResult) {
96
  try {
97
  const result = JSON.parse(cachedResult);
@@ -101,6 +117,11 @@ function App() {
101
  fetchUmap();
102
  }
103
  } else {
 
 
 
 
 
104
  fetchUmap();
105
  }
106
 
@@ -154,6 +175,9 @@ function App() {
154
  setFilterActive(false);
155
  setFilterCount(null);
156
  setActiveCluster(null);
 
 
 
157
  sessionStorage.removeItem('grasp-explorer-query');
158
  sessionStorage.removeItem('grasp-explorer-result');
159
  fetchUmap();
@@ -319,27 +343,64 @@ function App() {
319
 
320
  {/* Scatter plot + cluster legend */}
321
  <div className="scatter-section">
322
- <div className="scatter-panel">
323
- <div className="viz-toggle">
324
- <button
325
- className={`viz-toggle-btn ${vizMode === 'scatter' ? 'active' : ''}`}
326
- onClick={() => setVizMode('scatter')}
327
- >
328
- Similarity Map
329
- </button>
330
- <button
331
- className={`viz-toggle-btn ${vizMode === 'network' ? 'active' : ''}`}
332
- onClick={() => setVizMode('network')}
333
- >
334
- Method Connections
335
- </button>
336
- <button
337
- className={`viz-toggle-btn ${vizMode === 'clusters' ? 'active' : ''}`}
338
- onClick={() => setVizMode('clusters')}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  >
340
- Cluster Relations
341
- </button>
 
 
342
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
343
  {vizMode === 'scatter' && (
344
  <ScatterPlot
345
  data={data}
@@ -355,6 +416,7 @@ function App() {
355
  {vizMode === 'network' && (
356
  <NetworkGraph
357
  data={data}
 
358
  highlightedMethods={highlightedMethods}
359
  hoveredIndex={hoveredIndex}
360
  onPointClick={setSelectedPoint}
@@ -365,6 +427,7 @@ function App() {
365
  {vizMode === 'clusters' && (
366
  <ClusterGraph
367
  data={data}
 
368
  clusterStats={clusterStats}
369
  highlightedMethods={highlightedMethods}
370
  onPointClick={setSelectedPoint}
@@ -375,11 +438,13 @@ function App() {
375
  <ClusterLegend stats={clusterStats} activeCluster={activeCluster} onClusterClick={setActiveCluster} colorBy={colorBy} data={data} />
376
  <div className="viz-description">
377
  {vizMode === 'scatter' && (
378
- <p>Each dot is one grasp planning method. Methods that share similar attributes (planning approach, gripper type, sensor input) are placed close together.
379
  {colorBy === 'cluster'
380
  ? ' Colors indicate automatically discovered groups.'
381
- : ` Colors show ${SHORT_NAMES[colorBy] || colorBy}. Highlighted methods match your query.`
382
- } Hover to identify methods, click for details.</p>
 
 
383
  )}
384
  {vizMode === 'network' && (
385
  <p><strong>Colored lines</strong> connect methods in the same group. <strong>Gray dashed lines</strong> connect methods in different groups that share 3 or more attributes, revealing relationships the grouping alone does not show.</p>
@@ -389,14 +454,17 @@ function App() {
389
  )}
390
  </div>
391
  </div>
 
392
  </div>
393
 
394
- {!suggestion && (
 
395
  <ClusterInsight
396
  insight={clusterInsight}
397
  loading={false}
398
- stats={clusterStats}
399
  onMethodClick={handleMethodClick}
 
400
  />
401
  )}
402
 
 
2
  import staticInsightData from './staticClusterInsight.json';
3
  import InsightCard from './components/InsightCard';
4
  import { ClusterLegend, ClusterInsight } from './components/ClusterOverview';
5
+ import { SHORT_NAMES, COLOR_BY_OPTIONS, DEFAULT_WEIGHTS } from './constants';
6
+ import WeightSliders from './components/WeightSliders';
7
  import ScatterPlot from './components/ScatterPlot';
8
  import MethodTable from './components/MethodTable';
9
  import DetailPanel from './components/DetailPanel';
 
40
  const [clusterStats, setClusterStats] = useState(staticInsightData.clusterStats);
41
  const [activeCluster, setActiveCluster] = useState(null);
42
 
43
+ const [weightsOpen, setWeightsOpen] = useState(true);
44
+ const [aiAdjustedCols, setAiAdjustedCols] = useState(new Set());
45
+
46
  const applyQueryResult = useCallback((queryText, result) => {
47
  setSuggestion(result);
48
  setData(result.umapData);
49
  setWeights(result.weights);
50
  setColorBy(result.colorBy);
51
+ if (result.colorBy !== 'cluster') {
52
+ setVizMode('scatter');
53
+ }
54
  const highlights = result.filterMethods || result.highlightMethods || [];
55
  setHighlightedMethods(highlights);
56
  setSelectedPoint(null);
57
  setFilterActive(!!result.filterMethods);
58
  setFilterCount(result.filterMethods ? result.filterMethods.length : null);
59
  if (result.clusterStats) setClusterStats(result.clusterStats);
60
+ // Track which columns the AI adjusted
61
+ const adjusted = new Set();
62
+ for (const [col, val] of Object.entries(result.weights)) {
63
+ if (val !== DEFAULT_WEIGHTS[col]) adjusted.add(col);
64
+ }
65
+ setAiAdjustedCols(adjusted);
66
  setQuery(queryText);
67
  }, []);
68
 
 
82
  if (result.success) {
83
  setData(result.data);
84
  setWeights(result.config.weights);
85
+ if (result.clusterStats) setClusterStats(result.clusterStats);
86
  if (result.filter) {
87
  setFilterActive(result.filter.active);
88
  setFilterCount(result.filter.active ? result.data.length : null);
 
103
  useEffect(() => {
104
  setLoading(true);
105
 
106
+ // Restore cached query state only when embedded in an iframe (e.g., ReVISit study sections 3/4).
107
+ // Standalone HF Space usage always starts fresh.
108
+ const isIframe = window.self !== window.top;
109
+ const cachedQuery = isIframe && sessionStorage.getItem('grasp-explorer-query');
110
+ const cachedResult = isIframe && sessionStorage.getItem('grasp-explorer-result');
111
  if (cachedQuery && cachedResult) {
112
  try {
113
  const result = JSON.parse(cachedResult);
 
117
  fetchUmap();
118
  }
119
  } else {
120
+ // Clear any stale cache when loading standalone
121
+ if (!isIframe) {
122
+ sessionStorage.removeItem('grasp-explorer-query');
123
+ sessionStorage.removeItem('grasp-explorer-result');
124
+ }
125
  fetchUmap();
126
  }
127
 
 
175
  setFilterActive(false);
176
  setFilterCount(null);
177
  setActiveCluster(null);
178
+ setAiAdjustedCols(new Set());
179
+ setWeightsOpen(false);
180
+ setColorBy('cluster');
181
  sessionStorage.removeItem('grasp-explorer-query');
182
  sessionStorage.removeItem('grasp-explorer-result');
183
  fetchUmap();
 
343
 
344
  {/* Scatter plot + cluster legend */}
345
  <div className="scatter-section">
346
+ <div className="viz-toolbar">
347
+ <button
348
+ className={`viz-toggle-btn ${vizMode === 'scatter' ? 'active' : ''}`}
349
+ onClick={() => setVizMode('scatter')}
350
+ >
351
+ Similarity Map
352
+ </button>
353
+ <button
354
+ className={`viz-toggle-btn ${vizMode === 'network' ? 'active' : ''} ${colorBy !== 'cluster' ? 'disabled' : ''}`}
355
+ onClick={() => colorBy === 'cluster' && setVizMode('network')}
356
+ title={colorBy !== 'cluster' ? 'Switch to Cluster coloring to see method connections' : ''}
357
+ >
358
+ Method Connections
359
+ </button>
360
+ <button
361
+ className={`viz-toggle-btn ${vizMode === 'clusters' ? 'active' : ''} ${colorBy !== 'cluster' ? 'disabled' : ''}`}
362
+ onClick={() => colorBy === 'cluster' && setVizMode('clusters')}
363
+ title={colorBy !== 'cluster' ? 'Switch to Cluster coloring to see cluster relations' : ''}
364
+ >
365
+ Cluster Relations
366
+ </button>
367
+ <button
368
+ className={`viz-toggle-btn weights-toggle ${weightsOpen ? 'active' : ''}`}
369
+ onClick={() => setWeightsOpen(!weightsOpen)}
370
+ >
371
+ {weightsOpen ? 'Hide Weights' : 'Weights'}
372
+ </button>
373
+ <div className="color-by-header">
374
+ <label htmlFor="color-by-select">Color by</label>
375
+ <select
376
+ id="color-by-select"
377
+ value={colorBy}
378
+ onChange={(e) => {
379
+ const val = e.target.value;
380
+ setColorBy(val);
381
+ setActiveCluster(null);
382
+ if (val !== 'cluster' && (vizMode === 'network' || vizMode === 'clusters')) {
383
+ setVizMode('scatter');
384
+ }
385
+ }}
386
  >
387
+ {COLOR_BY_OPTIONS.map(opt => (
388
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
389
+ ))}
390
+ </select>
391
  </div>
392
+ </div>
393
+ {weightsOpen && (
394
+ <WeightSliders
395
+ weights={weights}
396
+ defaultWeights={DEFAULT_WEIGHTS}
397
+ aiAdjustedCols={aiAdjustedCols}
398
+ onChange={(newWeights) => { setWeights(newWeights); fetchUmap(newWeights); }}
399
+ onReset={() => { setAiAdjustedCols(new Set()); setWeights(DEFAULT_WEIGHTS); setWeightsOpen(false); fetchUmap(); }}
400
+ />
401
+ )}
402
+ <div className="scatter-content">
403
+ <div className="scatter-panel">
404
  {vizMode === 'scatter' && (
405
  <ScatterPlot
406
  data={data}
 
416
  {vizMode === 'network' && (
417
  <NetworkGraph
418
  data={data}
419
+ colorBy={colorBy}
420
  highlightedMethods={highlightedMethods}
421
  hoveredIndex={hoveredIndex}
422
  onPointClick={setSelectedPoint}
 
427
  {vizMode === 'clusters' && (
428
  <ClusterGraph
429
  data={data}
430
+ colorBy={colorBy}
431
  clusterStats={clusterStats}
432
  highlightedMethods={highlightedMethods}
433
  onPointClick={setSelectedPoint}
 
438
  <ClusterLegend stats={clusterStats} activeCluster={activeCluster} onClusterClick={setActiveCluster} colorBy={colorBy} data={data} />
439
  <div className="viz-description">
440
  {vizMode === 'scatter' && (
441
+ <p>Each dot is one grasp planning method, positioned by similarity across all attributes.
442
  {colorBy === 'cluster'
443
  ? ' Colors indicate automatically discovered groups.'
444
+ : ` Colors show ${SHORT_NAMES[colorBy] || colorBy}.`
445
+ }
446
+ {highlightedMethods.length > 0 && ' Highlighted methods match your query.'}
447
+ {' '}Hover to identify, click for details.</p>
448
  )}
449
  {vizMode === 'network' && (
450
  <p><strong>Colored lines</strong> connect methods in the same group. <strong>Gray dashed lines</strong> connect methods in different groups that share 3 or more attributes, revealing relationships the grouping alone does not show.</p>
 
454
  )}
455
  </div>
456
  </div>
457
+ </div>
458
  </div>
459
 
460
+ {!suggestion && colorBy === 'cluster' && aiAdjustedCols.size === 0 &&
461
+ Object.entries(weights).every(([col, val]) => val === DEFAULT_WEIGHTS[col]) && (
462
  <ClusterInsight
463
  insight={clusterInsight}
464
  loading={false}
465
+ stats={staticInsightData.clusterStats}
466
  onMethodClick={handleMethodClick}
467
+ data={data}
468
  />
469
  )}
470
 
frontend/src/components/AnalyticsDashboard.js CHANGED
@@ -339,6 +339,10 @@ export default function AnalyticsDashboard({ suggestion }) {
339
 
340
  return (
341
  <div className="analytics-dashboard">
 
 
 
 
342
  <div className="analytics-grid">
343
  <MethodRelevanceChart methodRelevance={methodRelevance} />
344
  <CitedReferencesChart citedReferences={analytics.citedReferences} />
 
339
 
340
  return (
341
  <div className="analytics-dashboard">
342
+ <div className="analytics-header">
343
+ <span className="analytics-header-icon">AI</span>
344
+ <span className="analytics-header-title">Analytics</span>
345
+ </div>
346
  <div className="analytics-grid">
347
  <MethodRelevanceChart methodRelevance={methodRelevance} />
348
  <CitedReferencesChart citedReferences={analytics.citedReferences} />
frontend/src/components/ClusterGraph.js CHANGED
@@ -1,9 +1,11 @@
1
  import React, { useMemo } from 'react';
2
  import Plot from 'react-plotly.js';
3
  import { CLUSTER_COLORS } from '../constants';
 
4
 
5
  export default function ClusterGraph({
6
  data,
 
7
  clusterStats,
8
  highlightedMethods,
9
  onPointClick,
@@ -133,7 +135,15 @@ export default function ClusterGraph({
133
  const count = (clusterMembers[c] || []).length;
134
  return Math.max(25, Math.sqrt(count) * 18);
135
  }),
136
- color: clusterIds.map(c => CLUSTER_COLORS[c] || '#999'),
 
 
 
 
 
 
 
 
137
  opacity: clusterIds.map(c => {
138
  if (highlightedClusters.size === 0) return 0.85;
139
  return highlightedClusters.has(c) ? 1 : 0.3;
 
1
  import React, { useMemo } from 'react';
2
  import Plot from 'react-plotly.js';
3
  import { CLUSTER_COLORS } from '../constants';
4
+ import { getPointColor, buildColorMap } from '../utils';
5
 
6
  export default function ClusterGraph({
7
  data,
8
+ colorBy = 'cluster',
9
  clusterStats,
10
  highlightedMethods,
11
  onPointClick,
 
135
  const count = (clusterMembers[c] || []).length;
136
  return Math.max(25, Math.sqrt(count) * 18);
137
  }),
138
+ color: clusterIds.map(c => {
139
+ if (colorBy === 'cluster') return CLUSTER_COLORS[c] || '#999';
140
+ // Use the most common value in this cluster for the color
141
+ const members = clusterMembers[c] || [];
142
+ if (members.length === 0) return '#999';
143
+ const cm = buildColorMap(data, colorBy);
144
+ // Pick color of the first member as representative
145
+ return getPointColor(data[members[0]], colorBy, cm);
146
+ }),
147
  opacity: clusterIds.map(c => {
148
  if (highlightedClusters.size === 0) return 0.85;
149
  return highlightedClusters.has(c) ? 1 : 0.3;
frontend/src/components/ClusterOverview.js CHANGED
@@ -1,7 +1,7 @@
1
  import React, { useMemo } from 'react';
2
  import InsightBullets from './InsightBullets';
3
  import { CLUSTER_COLORS, SHORT_NAMES } from '../constants';
4
- import { smartSplit } from '../utils';
5
 
6
  const COLUMN_COLORS = [
7
  '#0C3383', '#0A88BA', '#F2D338', '#F28F38', '#D91E1E',
@@ -75,38 +75,151 @@ export function ClusterLegend({ stats, activeCluster, onClusterClick, colorBy, d
75
  );
76
  }
77
 
78
- export function ClusterInsight({ insight, loading, stats, onMethodClick }) {
79
- if (!insight && !loading) return null;
 
80
 
81
- // Build method -> cluster map from stats for coloring
82
- const methodClusterMap = {};
83
- const clusterLabelMap = {};
84
- if (stats) {
85
- stats.forEach(cs => {
86
- cs.methods.forEach(name => {
87
- methodClusterMap[name] = cs.id;
88
- });
89
- if (cs.label) {
90
- clusterLabelMap[cs.label] = cs.id;
91
- }
92
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  }
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  return (
96
  <div className="cluster-insight-card">
97
  <div className="cluster-insight-header">
98
  <span className="cluster-insight-icon">AI</span>
99
- <span className="cluster-insight-title">Cluster Overview</span>
100
  </div>
101
  <div className="cluster-insight-body">
102
  {loading ? (
103
  <p className="cluster-insight-loading">Analyzing clusters...</p>
104
  ) : (
105
  <InsightBullets
106
- text={insight}
107
  methodClusterMap={methodClusterMap}
108
  clusterLabelMap={clusterLabelMap}
109
  onMethodClick={onMethodClick}
 
110
  />
111
  )}
112
  </div>
 
1
  import React, { useMemo } from 'react';
2
  import InsightBullets from './InsightBullets';
3
  import { CLUSTER_COLORS, SHORT_NAMES } from '../constants';
4
+ import { smartSplit, buildColorMap, getPointColor } from '../utils';
5
 
6
  const COLUMN_COLORS = [
7
  '#0C3383', '#0A88BA', '#F2D338', '#F28F38', '#D91E1E',
 
75
  );
76
  }
77
 
78
+ function buildColumnSummary(data, colorBy) {
79
+ if (!data || !data.length || !colorBy || colorBy === 'cluster') return null;
80
+ const displayName = SHORT_NAMES[colorBy] || colorBy;
81
 
82
+ // Group methods by their primary value for this column
83
+ const groups = {};
84
+ data.forEach(d => {
85
+ const parts = smartSplit(d.metadata?.[colorBy] || '');
86
+ const primary = parts.length > 0 ? parts[0] : 'N/A';
87
+ if (!groups[primary]) groups[primary] = [];
88
+ groups[primary].push(d);
89
+ });
90
+
91
+ // Compute spatial coherence: for each group, measure how tightly its members
92
+ // cluster in UMAP space vs. the overall spread. Low ratio = spatially concentrated,
93
+ // high ratio = dispersed across the landscape.
94
+ const allX = data.map(d => d.x);
95
+ const allY = data.map(d => d.y);
96
+ const globalSpread = Math.sqrt(variance(allX) + variance(allY));
97
+
98
+ const groupStats = Object.entries(groups)
99
+ .filter(([, members]) => members.length >= 2)
100
+ .map(([val, members]) => {
101
+ const gx = members.map(d => d.x);
102
+ const gy = members.map(d => d.y);
103
+ const groupSpread = Math.sqrt(variance(gx) + variance(gy));
104
+ const ratio = globalSpread > 0 ? groupSpread / globalSpread : 1;
105
+ return { val, count: members.length, ratio };
106
+ })
107
+ .sort((a, b) => a.ratio - b.ratio);
108
+
109
+ if (groupStats.length === 0) return null;
110
+
111
+ // Classify groups
112
+ const tight = groupStats.filter(g => g.ratio < 0.55);
113
+ const dispersed = groupStats.filter(g => g.ratio > 0.85);
114
+ const avgRatio = groupStats.reduce((s, g) => s + g.ratio, 0) / groupStats.length;
115
+
116
+ const bullets = [];
117
+
118
+ // Overall assessment
119
+ if (avgRatio < 0.5) {
120
+ bullets.push(`- **${displayName}** aligns strongly with UMAP similarity — methods with the same ${displayName.toLowerCase()} tend to be similar overall, suggesting this attribute correlates with other method characteristics.`);
121
+ } else if (avgRatio > 0.8) {
122
+ bullets.push(`- **${displayName}** is largely independent of overall method similarity — methods that are close in the scatter plot often differ on this attribute, making it a useful dimension to explore orthogonally.`);
123
+ } else {
124
+ bullets.push(`- **${displayName}** partially aligns with the UMAP layout — some values form spatial clusters while others are spread across the landscape.`);
125
+ }
126
+
127
+ // Highlight tightest group(s)
128
+ if (tight.length > 0) {
129
+ const top = tight.slice(0, 2).map(g => `"${g.val}" (${g.count})`).join(' and ');
130
+ bullets.push(`- Spatially concentrated: ${top} — these methods cluster together in UMAP space, meaning they also share other attributes.`);
131
  }
132
 
133
+ // Highlight most dispersed group(s)
134
+ if (dispersed.length > 0) {
135
+ const top = dispersed.slice(0, 2).map(g => `"${g.val}" (${g.count})`).join(' and ');
136
+ bullets.push(`- Spatially dispersed: ${top} — these methods appear across the landscape despite sharing this ${displayName.toLowerCase()} value, suggesting they differ on other attributes.`);
137
+ }
138
+
139
+ return bullets.join('\n');
140
+ }
141
+
142
+ function variance(arr) {
143
+ if (arr.length < 2) return 0;
144
+ const mean = arr.reduce((s, v) => s + v, 0) / arr.length;
145
+ return arr.reduce((s, v) => s + (v - mean) ** 2, 0) / arr.length;
146
+ }
147
+
148
+ function buildMethodColorMap(data, colorBy) {
149
+ if (!data || !data.length) return {};
150
+ const cm = buildColorMap(data, colorBy);
151
+ const map = {};
152
+ data.forEach(d => {
153
+ const color = getPointColor(d, colorBy, cm);
154
+ // Store as a pseudo cluster ID that InsightBullets can use
155
+ map[d.name] = color;
156
+ });
157
+ return map;
158
+ }
159
+
160
+ export function ClusterInsight({ insight, loading, stats, onMethodClick, colorBy = 'cluster', data }) {
161
+ const isColumnMode = colorBy && colorBy !== 'cluster' && colorBy !== 'index';
162
+
163
+ const columnSummary = useMemo(
164
+ () => isColumnMode ? buildColumnSummary(data, colorBy) : null,
165
+ [data, colorBy, isColumnMode]
166
+ );
167
+
168
+ const displayInsight = isColumnMode ? columnSummary : insight;
169
+ const title = isColumnMode ? `${SHORT_NAMES[colorBy] || colorBy} Overview` : 'Cluster Overview';
170
+
171
+ // Build method -> color map
172
+ const { methodClusterMap, clusterLabelMap, useDirectColors } = useMemo(() => {
173
+ if (isColumnMode && data) {
174
+ // Direct color strings instead of cluster IDs
175
+ const directMap = buildMethodColorMap(data, colorBy);
176
+ // Build group label map
177
+ const groups = {};
178
+ data.forEach(d => {
179
+ const parts = smartSplit(d.metadata?.[colorBy] || '');
180
+ const primary = parts.length > 0 ? parts[0] : 'N/A';
181
+ if (!groups[primary]) groups[primary] = getPointColor(d, colorBy, buildColorMap(data, colorBy));
182
+ });
183
+ return { methodClusterMap: directMap, clusterLabelMap: groups, useDirectColors: true };
184
+ }
185
+ const mcm = {};
186
+ const clm = {};
187
+ // Build from stats first (has emoji-prefixed names)
188
+ if (stats) {
189
+ stats.forEach(cs => {
190
+ cs.methods.forEach(name => { mcm[name] = cs.id; });
191
+ if (cs.label) clm[cs.label] = cs.id;
192
+ });
193
+ }
194
+ // Also build from live data (covers all 56 methods including those without papers)
195
+ if (data) {
196
+ data.forEach(d => {
197
+ if (d.name && d.cluster !== undefined && !mcm[d.name]) {
198
+ mcm[d.name] = d.cluster;
199
+ }
200
+ });
201
+ }
202
+ return { methodClusterMap: mcm, clusterLabelMap: clm, useDirectColors: false };
203
+ }, [stats, data, colorBy, isColumnMode]);
204
+
205
+ if (!displayInsight && !loading) return null;
206
+
207
  return (
208
  <div className="cluster-insight-card">
209
  <div className="cluster-insight-header">
210
  <span className="cluster-insight-icon">AI</span>
211
+ <span className="cluster-insight-title">{title}</span>
212
  </div>
213
  <div className="cluster-insight-body">
214
  {loading ? (
215
  <p className="cluster-insight-loading">Analyzing clusters...</p>
216
  ) : (
217
  <InsightBullets
218
+ text={displayInsight}
219
  methodClusterMap={methodClusterMap}
220
  clusterLabelMap={clusterLabelMap}
221
  onMethodClick={onMethodClick}
222
+ useDirectColors={useDirectColors}
223
  />
224
  )}
225
  </div>
frontend/src/components/InsightBullets.js CHANGED
@@ -184,7 +184,12 @@ function renderWithEntities(text, queryKeywords, entityLookup, longRegex, shortR
184
 
185
  // ─── FORMAT BULLET ───
186
 
187
- function formatBullet(text, methodClusterMap, clusterLabelMap, onMethodClick, queryKeywords, entityLookup, longRegex, shortRegex, shortTerms) {
 
 
 
 
 
188
  const quoteRegex = /("[^"]{3,}")/g;
189
  const segments = text.split(quoteRegex);
190
 
@@ -201,7 +206,7 @@ function formatBullet(text, methodClusterMap, clusterLabelMap, onMethodClick, qu
201
  );
202
  if (clusterKey !== undefined) {
203
  const cId = clusterLabelMap[clusterKey];
204
- const color = CLUSTER_COLORS[cId % CLUSTER_COLORS.length];
205
  return (
206
  <span key={i} className="entity-cluster-label"
207
  style={{ color, backgroundColor: color + '18', borderColor: color }}>
@@ -229,7 +234,7 @@ function formatBullet(text, methodClusterMap, clusterLabelMap, onMethodClick, qu
229
  }
230
  }
231
  }
232
- const clusterColor = clusterId !== undefined ? CLUSTER_COLORS[clusterId % CLUSTER_COLORS.length] : null;
233
  if (clusterColor && onMethodClick) {
234
  return (
235
  <span key={i} className="entity-method-clickable"
@@ -259,7 +264,7 @@ function formatBullet(text, methodClusterMap, clusterLabelMap, onMethodClick, qu
259
  const key = Object.keys(clusterLabelMap).find(l => l.toLowerCase() === part.toLowerCase());
260
  if (key) {
261
  const cId = clusterLabelMap[key];
262
- const color = CLUSTER_COLORS[cId % CLUSTER_COLORS.length];
263
  return (
264
  <span key={j} className="entity-cluster-label"
265
  style={{ color, backgroundColor: color + '18', borderColor: color }}>
@@ -268,7 +273,7 @@ function formatBullet(text, methodClusterMap, clusterLabelMap, onMethodClick, qu
268
  );
269
  }
270
  // Remaining text: check methods then entities
271
- return <span key={j}>{renderMethodsAndEntities(part, methodClusterMap, onMethodClick, queryKeywords, entityLookup, longRegex, shortRegex, shortTerms)}</span>;
272
  })}
273
  </span>
274
  );
@@ -276,11 +281,11 @@ function formatBullet(text, methodClusterMap, clusterLabelMap, onMethodClick, qu
276
  }
277
 
278
  // Unquoted: check method names then entities
279
- return <span key={i}>{renderMethodsAndEntities(seg, methodClusterMap, onMethodClick, queryKeywords, entityLookup, longRegex, shortRegex, shortTerms)}</span>;
280
  });
281
  }
282
 
283
- function renderMethodsAndEntities(text, methodClusterMap, onMethodClick, queryKeywords, entityLookup, longRegex, shortRegex, shortTerms) {
284
  if (!methodClusterMap || !onMethodClick) {
285
  return renderWithEntities(text, queryKeywords, entityLookup, longRegex, shortRegex, shortTerms);
286
  }
@@ -309,7 +314,7 @@ function renderMethodsAndEntities(text, methodClusterMap, onMethodClick, queryKe
309
  return parts.map((part, j) => {
310
  const variant = nameVariants[part.toLowerCase()];
311
  if (variant) {
312
- const color = CLUSTER_COLORS[variant.clusterId % CLUSTER_COLORS.length];
313
  return (
314
  <span key={j} className="entity-method-clickable"
315
  style={{ color, borderBottomColor: color }}
@@ -325,7 +330,7 @@ function renderMethodsAndEntities(text, methodClusterMap, onMethodClick, queryKe
325
 
326
  // ─── MAIN COMPONENT ───
327
 
328
- export default function InsightBullets({ text, methodClusterMap, clusterLabelMap, onMethodClick, query, termDictionary }) {
329
  const queryKeywords = useMemo(() => {
330
  const raw = extractQueryKeywords(query);
331
  return expandKeywordsWithAcronyms(raw);
@@ -341,10 +346,10 @@ export default function InsightBullets({ text, methodClusterMap, clusterLabelMap
341
  return (
342
  <ul className="insight-bullets">
343
  {bullets.map((line, i) => (
344
- <li key={i}>{formatBullet(line.replace(/^-\s*/, ''), methodClusterMap, clusterLabelMap, onMethodClick, queryKeywords, entityLookup, longRegex, shortRegex, shortTerms)}</li>
345
  ))}
346
  </ul>
347
  );
348
  }
349
- return <p>{formatBullet(text, methodClusterMap, clusterLabelMap, onMethodClick, queryKeywords, entityLookup, longRegex, shortRegex, shortTerms)}</p>;
350
  }
 
184
 
185
  // ─── FORMAT BULLET ───
186
 
187
+ function resolveColor(idOrColor, useDirectColors) {
188
+ if (useDirectColors) return idOrColor; // already a color string
189
+ return CLUSTER_COLORS[idOrColor % CLUSTER_COLORS.length];
190
+ }
191
+
192
+ function formatBullet(text, methodClusterMap, clusterLabelMap, onMethodClick, queryKeywords, entityLookup, longRegex, shortRegex, shortTerms, useDirectColors) {
193
  const quoteRegex = /("[^"]{3,}")/g;
194
  const segments = text.split(quoteRegex);
195
 
 
206
  );
207
  if (clusterKey !== undefined) {
208
  const cId = clusterLabelMap[clusterKey];
209
+ const color = resolveColor(cId, useDirectColors);
210
  return (
211
  <span key={i} className="entity-cluster-label"
212
  style={{ color, backgroundColor: color + '18', borderColor: color }}>
 
234
  }
235
  }
236
  }
237
+ const clusterColor = clusterId !== undefined ? resolveColor(clusterId, useDirectColors) : null;
238
  if (clusterColor && onMethodClick) {
239
  return (
240
  <span key={i} className="entity-method-clickable"
 
264
  const key = Object.keys(clusterLabelMap).find(l => l.toLowerCase() === part.toLowerCase());
265
  if (key) {
266
  const cId = clusterLabelMap[key];
267
+ const color = resolveColor(cId, useDirectColors);
268
  return (
269
  <span key={j} className="entity-cluster-label"
270
  style={{ color, backgroundColor: color + '18', borderColor: color }}>
 
273
  );
274
  }
275
  // Remaining text: check methods then entities
276
+ return <span key={j}>{renderMethodsAndEntities(part, methodClusterMap, onMethodClick, queryKeywords, entityLookup, longRegex, shortRegex, shortTerms, useDirectColors)}</span>;
277
  })}
278
  </span>
279
  );
 
281
  }
282
 
283
  // Unquoted: check method names then entities
284
+ return <span key={i}>{renderMethodsAndEntities(seg, methodClusterMap, onMethodClick, queryKeywords, entityLookup, longRegex, shortRegex, shortTerms, useDirectColors)}</span>;
285
  });
286
  }
287
 
288
+ function renderMethodsAndEntities(text, methodClusterMap, onMethodClick, queryKeywords, entityLookup, longRegex, shortRegex, shortTerms, useDirectColors) {
289
  if (!methodClusterMap || !onMethodClick) {
290
  return renderWithEntities(text, queryKeywords, entityLookup, longRegex, shortRegex, shortTerms);
291
  }
 
314
  return parts.map((part, j) => {
315
  const variant = nameVariants[part.toLowerCase()];
316
  if (variant) {
317
+ const color = resolveColor(variant.clusterId, useDirectColors);
318
  return (
319
  <span key={j} className="entity-method-clickable"
320
  style={{ color, borderBottomColor: color }}
 
330
 
331
  // ─── MAIN COMPONENT ───
332
 
333
+ export default function InsightBullets({ text, methodClusterMap, clusterLabelMap, onMethodClick, query, termDictionary, useDirectColors }) {
334
  const queryKeywords = useMemo(() => {
335
  const raw = extractQueryKeywords(query);
336
  return expandKeywordsWithAcronyms(raw);
 
346
  return (
347
  <ul className="insight-bullets">
348
  {bullets.map((line, i) => (
349
+ <li key={i}>{formatBullet(line.replace(/^-\s*/, ''), methodClusterMap, clusterLabelMap, onMethodClick, queryKeywords, entityLookup, longRegex, shortRegex, shortTerms, useDirectColors)}</li>
350
  ))}
351
  </ul>
352
  );
353
  }
354
+ return <p>{formatBullet(text, methodClusterMap, clusterLabelMap, onMethodClick, queryKeywords, entityLookup, longRegex, shortRegex, shortTerms, useDirectColors)}</p>;
355
  }
frontend/src/components/MethodTable.js CHANGED
@@ -103,7 +103,8 @@ export default function MethodTable({
103
  return (
104
  <div className="table-panel">
105
  <div className="table-panel-header">
106
- <span>{filteredData.length} of {data.length} Methods</span>
 
107
  {hasHighlights && (
108
  <span className="hl-indicator">{highlightedMethods.length} highlighted</span>
109
  )}
 
103
  return (
104
  <div className="table-panel">
105
  <div className="table-panel-header">
106
+ <span>Method Explorer</span>
107
+ <span className="method-count">{filteredData.length} of {data.length}</span>
108
  {hasHighlights && (
109
  <span className="hl-indicator">{highlightedMethods.length} highlighted</span>
110
  )}
frontend/src/components/NetworkGraph.js CHANGED
@@ -1,9 +1,11 @@
1
  import React, { useMemo } from 'react';
2
  import Plot from 'react-plotly.js';
3
  import { CLUSTER_COLORS } from '../constants';
 
4
 
5
  export default function NetworkGraph({
6
  data,
 
7
  highlightedMethods,
8
  hoveredIndex,
9
  onPointClick,
@@ -61,6 +63,8 @@ export default function NetworkGraph({
61
  return { intraEdges: intra, crossEdges: cross };
62
  }, [data]);
63
 
 
 
64
  if (!data || data.length === 0) return null;
65
 
66
  // Intra-cluster edge traces (one per cluster, cluster colored)
@@ -103,7 +107,7 @@ export default function NetworkGraph({
103
  return 10;
104
  });
105
 
106
- const markerColors = data.map(d => CLUSTER_COLORS[d.cluster] || '#999');
107
 
108
  const markerOpacity = data.map((d, i) => {
109
  if (i === hoveredIndex) return 1;
 
1
  import React, { useMemo } from 'react';
2
  import Plot from 'react-plotly.js';
3
  import { CLUSTER_COLORS } from '../constants';
4
+ import { getPointColor, buildColorMap } from '../utils';
5
 
6
  export default function NetworkGraph({
7
  data,
8
+ colorBy = 'cluster',
9
  highlightedMethods,
10
  hoveredIndex,
11
  onPointClick,
 
63
  return { intraEdges: intra, crossEdges: cross };
64
  }, [data]);
65
 
66
+ const colorMap = useMemo(() => buildColorMap(data || [], colorBy), [data, colorBy]);
67
+
68
  if (!data || data.length === 0) return null;
69
 
70
  // Intra-cluster edge traces (one per cluster, cluster colored)
 
107
  return 10;
108
  });
109
 
110
+ const markerColors = data.map(d => getPointColor(d, colorBy, colorMap));
111
 
112
  const markerOpacity = data.map((d, i) => {
113
  if (i === hoveredIndex) return 1;
frontend/src/components/ScatterPlot.js CHANGED
@@ -1,6 +1,6 @@
1
  import React, { useMemo } from 'react';
2
  import Plot from 'react-plotly.js';
3
- import { smartSplit } from '../utils';
4
  import { CLUSTER_COLORS, SHORT_NAMES } from '../constants';
5
 
6
  export default function ScatterPlot({
@@ -15,44 +15,14 @@ export default function ScatterPlot({
15
  }) {
16
  const hasHighlights = highlightedMethods.length > 0;
17
 
18
- const uniqueValues = useMemo(() => {
19
- if (!data.length || colorBy === 'index' || colorBy === 'cluster') return null;
20
- const valSet = new Set();
21
- data.forEach(d => {
22
- const parts = smartSplit(d.metadata[colorBy] || '');
23
- if (parts.length > 0) parts.forEach(p => valSet.add(p));
24
- else valSet.add('N/A');
25
- });
26
- return [...valSet].sort();
27
- }, [data, colorBy]);
28
-
29
- const colorMap = useMemo(() => {
30
- if (!uniqueValues) return null;
31
- const map = {};
32
- uniqueValues.forEach((val, i) => { map[val] = i; });
33
- return map;
34
- }, [uniqueValues]);
35
-
36
- const COLUMN_COLORS = [
37
- '#0C3383', '#0A88BA', '#F2D338', '#F28F38', '#D91E1E',
38
- '#7B2D8E', '#3F681C', '#E75480', '#1B998B', '#FF6B6B',
39
- '#4A90D9', '#D4A017', '#8B4513', '#2E8B57', '#CD5C5C',
40
- ];
41
 
42
  let markerColors, useDiscreteColors;
43
- if (colorBy === 'cluster') {
44
- markerColors = data.map(d => CLUSTER_COLORS[d.cluster] || '#999');
45
- useDiscreteColors = true;
46
- } else if (colorBy === 'index') {
47
  markerColors = data.map((_, i) => i);
48
  useDiscreteColors = false;
49
  } else {
50
- markerColors = data.map(d => {
51
- const parts = smartSplit(d.metadata[colorBy] || '');
52
- const primaryVal = parts.length > 0 ? parts[0] : 'N/A';
53
- const idx = colorMap[primaryVal] ?? colorMap['N/A'] ?? 0;
54
- return COLUMN_COLORS[idx % COLUMN_COLORS.length];
55
- });
56
  useDiscreteColors = true;
57
  }
58
 
@@ -116,8 +86,6 @@ export default function ScatterPlot({
116
  },
117
  colorbar: useDiscreteColors ? undefined : {
118
  title: SHORT_NAMES[colorBy] || colorBy,
119
- tickvals: uniqueValues ? uniqueValues.map((_, i) => i) : undefined,
120
- ticktext: uniqueValues ? uniqueValues.map(v => v.length > 15 ? v.slice(0, 15) + '...' : v) : undefined
121
  }
122
  },
123
  showlegend: false
@@ -126,19 +94,15 @@ export default function ScatterPlot({
126
  const layout = {
127
  xaxis: {
128
  zeroline: false, showgrid: false,
129
- showline: true, linecolor: '#d8dde4',
130
- title: 'UMAP 1', titlefont: { size: 11, color: '#8691a0' },
131
- tickfont: { size: 10, color: '#8691a0' },
132
  },
133
  yaxis: {
134
  zeroline: false, showgrid: false,
135
- showline: true, linecolor: '#d8dde4',
136
- title: 'UMAP 2', titlefont: { size: 11, color: '#8691a0' },
137
- tickfont: { size: 10, color: '#8691a0' },
138
  },
139
  hovermode: 'closest',
140
  height: 420,
141
- margin: { t: 8, b: 40, l: 45, r: 10 },
142
  paper_bgcolor: 'transparent',
143
  plot_bgcolor: '#fafcfd'
144
  };
@@ -159,7 +123,7 @@ export default function ScatterPlot({
159
  }}
160
  onUnhover={onUnhover}
161
  style={{ width: '100%' }}
162
- config={{ responsive: true, displayModeBar: true }}
163
  useResizeHandler={true}
164
  />
165
  );
 
1
  import React, { useMemo } from 'react';
2
  import Plot from 'react-plotly.js';
3
+ import { smartSplit, getPointColor, buildColorMap } from '../utils';
4
  import { CLUSTER_COLORS, SHORT_NAMES } from '../constants';
5
 
6
  export default function ScatterPlot({
 
15
  }) {
16
  const hasHighlights = highlightedMethods.length > 0;
17
 
18
+ const colorMap = useMemo(() => buildColorMap(data, colorBy), [data, colorBy]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  let markerColors, useDiscreteColors;
21
+ if (colorBy === 'index') {
 
 
 
22
  markerColors = data.map((_, i) => i);
23
  useDiscreteColors = false;
24
  } else {
25
+ markerColors = data.map(d => getPointColor(d, colorBy, colorMap));
 
 
 
 
 
26
  useDiscreteColors = true;
27
  }
28
 
 
86
  },
87
  colorbar: useDiscreteColors ? undefined : {
88
  title: SHORT_NAMES[colorBy] || colorBy,
 
 
89
  }
90
  },
91
  showlegend: false
 
94
  const layout = {
95
  xaxis: {
96
  zeroline: false, showgrid: false,
97
+ showline: false, showticklabels: false, title: '',
 
 
98
  },
99
  yaxis: {
100
  zeroline: false, showgrid: false,
101
+ showline: false, showticklabels: false, title: '',
 
 
102
  },
103
  hovermode: 'closest',
104
  height: 420,
105
+ margin: { t: 12, b: 16, l: 12, r: 12 },
106
  paper_bgcolor: 'transparent',
107
  plot_bgcolor: '#fafcfd'
108
  };
 
123
  }}
124
  onUnhover={onUnhover}
125
  style={{ width: '100%' }}
126
+ config={{ responsive: true, displayModeBar: 'hover' }}
127
  useResizeHandler={true}
128
  />
129
  );
frontend/src/components/WeightSliders.js ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { SHORT_NAMES, WEIGHT_COLUMNS } from '../constants';
3
+ import Tooltip from './Tooltip';
4
+
5
+ export default function WeightSliders({ weights, defaultWeights, aiAdjustedCols, onChange, onReset }) {
6
+ const [local, setLocal] = useState(weights);
7
+ const debounceRef = useRef(null);
8
+
9
+ // Sync local state when weights change externally (e.g., after query)
10
+ useEffect(() => {
11
+ setLocal(weights);
12
+ }, [weights]);
13
+
14
+ const handleChange = (col, value) => {
15
+ const next = { ...local, [col]: Number(value) };
16
+ setLocal(next);
17
+ if (debounceRef.current) clearTimeout(debounceRef.current);
18
+ debounceRef.current = setTimeout(() => onChange(next), 500);
19
+ };
20
+
21
+ return (
22
+ <div className="weight-panel">
23
+ <div className="weight-panel-bar">
24
+ <span className="weight-panel-label">Attribute Weights</span>
25
+ <Tooltip text="Weights control how much each attribute contributes to the UMAP distance calculation. A higher weight means methods that differ on that attribute will be pushed further apart, while methods that agree will be pulled closer. Cluster labels are generated from the 3 highest-weighted columns. When you ask a query, the AI adjusts these to emphasize the most relevant attributes." wide>
26
+ <span className="chart-help">?</span>
27
+ </Tooltip>
28
+ <button className="weight-reset-btn" onClick={onReset}>Reset Defaults</button>
29
+ </div>
30
+ <div className="weight-panel-grid">
31
+ {WEIGHT_COLUMNS.map(col => {
32
+ const val = local[col] ?? defaultWeights[col] ?? 10;
33
+ const isAi = aiAdjustedCols && aiAdjustedCols.has(col);
34
+ const label = SHORT_NAMES[col] || col;
35
+ return (
36
+ <div key={col} className="weight-slider-row">
37
+ <span className="weight-slider-label" title={col}>
38
+ {label}
39
+ {isAi && <span className="weight-ai-badge">AI</span>}
40
+ </span>
41
+ <input
42
+ type="range"
43
+ min="0"
44
+ max="20"
45
+ value={val}
46
+ onChange={(e) => handleChange(col, e.target.value)}
47
+ className="weight-slider-input"
48
+ />
49
+ <span className="weight-slider-value">{val}</span>
50
+ </div>
51
+ );
52
+ })}
53
+ </div>
54
+ </div>
55
+ );
56
+ }
frontend/src/constants.js CHANGED
@@ -30,6 +30,43 @@ export const SHORT_NAMES = {
30
  'Method Era': 'Era',
31
  };
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  export const TABLE_COLUMNS = [
34
  'Planning Method',
35
  'Training Data',
 
30
  'Method Era': 'Era',
31
  };
32
 
33
+ export const COLOR_BY_OPTIONS = [
34
+ { value: 'cluster', label: 'Cluster' },
35
+ { value: 'Planning Method', label: 'Planning Method' },
36
+ { value: 'End-effector Hardware', label: 'End-effector' },
37
+ { value: 'Object Configuration', label: 'Object Config' },
38
+ { value: 'Input Data', label: 'Input Data' },
39
+ { value: 'Training Data', label: 'Training Data' },
40
+ { value: 'Output Pose', label: 'Output Pose' },
41
+ { value: 'Backbone', label: 'Backbone' },
42
+ { value: 'Camera Position(s)', label: 'Camera' },
43
+ { value: 'Corresponding Dataset (see repository linked above)', label: 'Dataset' },
44
+ { value: 'Simulator (see repository linked above)', label: 'Simulator' },
45
+ { value: 'Learning Paradigm', label: 'Learning' },
46
+ { value: 'Sensor Complexity', label: 'Sensor' },
47
+ { value: 'Scene Difficulty', label: 'Scene' },
48
+ { value: 'Gripper Type', label: 'Gripper Type' },
49
+ { value: 'Method Era', label: 'Era' },
50
+ ];
51
+
52
+ export const DEFAULT_WEIGHTS = {
53
+ 'Planning Method': 10,
54
+ 'Training Data': 8,
55
+ 'End-effector Hardware': 6,
56
+ 'Object Configuration': 10,
57
+ 'Input Data': 6,
58
+ 'Output Pose': 10,
59
+ 'Corresponding Dataset (see repository linked above)': 5,
60
+ 'Simulator (see repository linked above)': 3,
61
+ 'Backbone': 5,
62
+ 'Metric(s) Used ': 5,
63
+ 'Camera Position(s)': 4,
64
+ 'Language': 4,
65
+ 'Description': 7,
66
+ };
67
+
68
+ export const WEIGHT_COLUMNS = Object.keys(DEFAULT_WEIGHTS);
69
+
70
  export const TABLE_COLUMNS = [
71
  'Planning Method',
72
  'Training Data',
frontend/src/staticClusterInsight.json CHANGED
@@ -96,11 +96,13 @@
96
  "methods": [
97
  "\ud83e\udd16 GraspVLA",
98
  "DeepRLManip",
 
99
  "Dex-Net 1.0 (MV-CNNs)",
100
  "Dex-Net 2.0 (GQ-CNN)",
101
  "Dex-Net 2.1",
102
  "Dex-Net 3.0",
103
  "Dex-Net 4.0",
 
104
  "Goal-Auxiliary Deep Deterministic Policy Gradient (GA-DDPG)",
105
  "GRAFF (Grasp-Affordances)",
106
  "GraspXL",
@@ -183,7 +185,9 @@
183
  "id": 2,
184
  "label": "Sampling / Three-finger / Singulated",
185
  "methods": [
 
186
  "Deep Dexterous Grasping (DDG)",
 
187
  "Deep Dexterous Grasping in Clutter (DDGC)",
188
  "Multi-FinGAN",
189
  "Robust Grasp Planning Over Uncertain Shape Completions"
@@ -318,6 +322,7 @@
318
  "3DAPNet",
319
  "AnyGrasp",
320
  "ggcnn_plus",
 
321
  "Grasp Proposal Network (GPNet)",
322
  "GraspSAM",
323
  "PointNet++ Grasping",
@@ -457,11 +462,15 @@
457
  "CaTGrasp",
458
  "Edge Grasp Network",
459
  "geometric-object-grasper",
 
460
  "Grasp detection via Implicit Geometry and Affordance (GIGA)",
 
461
  "Grasp Pose Detection (GPD)",
462
  "PointNetGPD",
463
  "ROI-GD",
 
464
  "Single-Shot SE(3) Grasp Detection (S4G)",
 
465
  "Volumetric Grasping Network (VGN)"
466
  ],
467
  "size": 9,
 
96
  "methods": [
97
  "\ud83e\udd16 GraspVLA",
98
  "DeepRLManip",
99
+ "Dex-Net",
100
  "Dex-Net 1.0 (MV-CNNs)",
101
  "Dex-Net 2.0 (GQ-CNN)",
102
  "Dex-Net 2.1",
103
  "Dex-Net 3.0",
104
  "Dex-Net 4.0",
105
+ "GA-DDPG",
106
  "Goal-Auxiliary Deep Deterministic Policy Gradient (GA-DDPG)",
107
  "GRAFF (Grasp-Affordances)",
108
  "GraspXL",
 
185
  "id": 2,
186
  "label": "Sampling / Three-finger / Singulated",
187
  "methods": [
188
+ "DDG",
189
  "Deep Dexterous Grasping (DDG)",
190
+ "DDGC",
191
  "Deep Dexterous Grasping in Clutter (DDGC)",
192
  "Multi-FinGAN",
193
  "Robust Grasp Planning Over Uncertain Shape Completions"
 
322
  "3DAPNet",
323
  "AnyGrasp",
324
  "ggcnn_plus",
325
+ "GPNet",
326
  "Grasp Proposal Network (GPNet)",
327
  "GraspSAM",
328
  "PointNet++ Grasping",
 
462
  "CaTGrasp",
463
  "Edge Grasp Network",
464
  "geometric-object-grasper",
465
+ "GIGA",
466
  "Grasp detection via Implicit Geometry and Affordance (GIGA)",
467
+ "GPD",
468
  "Grasp Pose Detection (GPD)",
469
  "PointNetGPD",
470
  "ROI-GD",
471
+ "S4G",
472
  "Single-Shot SE(3) Grasp Detection (S4G)",
473
+ "VGN",
474
  "Volumetric Grasping Network (VGN)"
475
  ],
476
  "size": 9,
frontend/src/utils.js CHANGED
@@ -4,6 +4,44 @@
4
  * 'Dexterous grasp, "6-DoF grasp pose (x, y, z, r, p, y)"'
5
  * -> ['Dexterous grasp', '6-DoF grasp pose (x, y, z, r, p, y)']
6
  */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  export function smartSplit(value) {
8
  if (!value) return [];
9
  const result = [];
 
4
  * 'Dexterous grasp, "6-DoF grasp pose (x, y, z, r, p, y)"'
5
  * -> ['Dexterous grasp', '6-DoF grasp pose (x, y, z, r, p, y)']
6
  */
7
+ import { CLUSTER_COLORS } from './constants';
8
+
9
+ const COLUMN_COLORS = [
10
+ '#0C3383', '#0A88BA', '#F2D338', '#F28F38', '#D91E1E',
11
+ '#7B2D8E', '#3F681C', '#E75480', '#1B998B', '#FF6B6B',
12
+ '#4A90D9', '#D4A017', '#8B4513', '#2E8B57', '#CD5C5C',
13
+ ];
14
+
15
+ /**
16
+ * Get the color for a data point based on the current colorBy mode.
17
+ */
18
+ export function getPointColor(d, colorBy, colorMap) {
19
+ if (colorBy === 'cluster') {
20
+ return CLUSTER_COLORS[d.cluster] || '#999';
21
+ }
22
+ const parts = smartSplit(d.metadata?.[colorBy] || '');
23
+ const primaryVal = parts.length > 0 ? parts[0] : 'N/A';
24
+ const idx = colorMap?.[primaryVal] ?? 0;
25
+ return COLUMN_COLORS[idx % COLUMN_COLORS.length];
26
+ }
27
+
28
+ /**
29
+ * Build a value -> index map for column-based coloring.
30
+ * Sorted by count descending to match the legend order.
31
+ */
32
+ export function buildColorMap(data, colorBy) {
33
+ if (!data.length || colorBy === 'cluster' || colorBy === 'index') return null;
34
+ const counts = {};
35
+ data.forEach(d => {
36
+ const parts = smartSplit(d.metadata?.[colorBy] || '');
37
+ const primary = parts.length > 0 ? parts[0] : 'N/A';
38
+ counts[primary] = (counts[primary] || 0) + 1;
39
+ });
40
+ const map = {};
41
+ Object.entries(counts).sort((a, b) => b[1] - a[1]).forEach(([val], i) => { map[val] = i; });
42
+ return map;
43
+ }
44
+
45
  export function smartSplit(value) {
46
  if (!value) return [];
47
  const result = [];