Spaces:
Sleeping
Sleeping
Commit ·
9d586f0
1
Parent(s): aa1f862
slider for weight and interaction. color by for scatter plot, imrpovement sto UX
Browse files- backend/app.py +7 -2
- backend/rag/query_engine.py +15 -4
- chroma_db/chroma.sqlite3 +1 -1
- frontend/src/App.css +165 -41
- frontend/src/App.js +96 -28
- frontend/src/components/AnalyticsDashboard.js +4 -0
- frontend/src/components/ClusterGraph.js +11 -1
- frontend/src/components/ClusterOverview.js +130 -17
- frontend/src/components/InsightBullets.js +16 -11
- frontend/src/components/MethodTable.js +2 -1
- frontend/src/components/NetworkGraph.js +5 -1
- frontend/src/components/ScatterPlot.js +8 -44
- frontend/src/components/WeightSliders.js +56 -0
- frontend/src/constants.js +37 -0
- frontend/src/staticClusterInsight.json +9 -0
- frontend/src/utils.js +38 -0
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
|
|
|
|
|
|
|
| 782 |
dominant_attrs = []
|
| 783 |
-
for col in
|
| 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:
|
| 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:
|
| 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.
|
| 121 |
-
padding: 0.4rem 0.7rem;
|
| 122 |
-
|
|
|
|
| 123 |
}
|
| 124 |
.insight-icon {
|
| 125 |
-
font-size: 0.
|
| 126 |
-
color: white; padding: 0.
|
| 127 |
}
|
| 128 |
-
.insight-title { font-weight: 700; font-size: 0.
|
| 129 |
.insight-close {
|
| 130 |
-
background: none; border: none; color:
|
| 131 |
font-size: 1rem; cursor: pointer; line-height: 1;
|
| 132 |
}
|
| 133 |
-
.insight-close:hover { color:
|
| 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.
|
| 258 |
background: var(--primary-deep); color: white;
|
| 259 |
-
font-size: 0.
|
| 260 |
-
display: flex;
|
| 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-
|
| 349 |
flex: 3; min-width: 0;
|
| 350 |
-
border-radius: 0 0 0 4px;
|
| 351 |
}
|
| 352 |
-
.scatter-
|
| 353 |
-
flex:
|
| 354 |
}
|
| 355 |
|
| 356 |
.scatter-panel {
|
|
@@ -362,31 +366,136 @@ body {
|
|
| 362 |
}
|
| 363 |
|
| 364 |
/* Viz toggle */
|
| 365 |
-
.viz-
|
| 366 |
-
display: flex; gap: 0;
|
| 367 |
-
|
| 368 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
}
|
| 370 |
.viz-toggle-btn {
|
| 371 |
-
flex: 1; padding: 0.
|
| 372 |
-
background:
|
| 373 |
font-family: inherit; font-size: 0.75rem; font-weight: 600;
|
| 374 |
-
color:
|
| 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:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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.
|
| 420 |
-
text-transform: uppercase; letter-spacing: 0.
|
| 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 |
-
|
| 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.
|
| 466 |
-
padding: 0.
|
| 467 |
-
background: var(--primary-
|
| 468 |
-
border-
|
| 469 |
}
|
| 470 |
.cluster-insight-icon {
|
| 471 |
-
font-size: 0.55rem; font-weight: 700; background:
|
| 472 |
color: white; padding: 0.08rem 0.28rem; border-radius: 2px;
|
| 473 |
}
|
| 474 |
-
.cluster-insight-title { font-weight: 700; font-size: 0.
|
| 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-
|
| 1203 |
-
.scatter-
|
|
|
|
| 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
|
| 93 |
-
|
| 94 |
-
const
|
|
|
|
|
|
|
| 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="
|
| 323 |
-
<
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
>
|
| 340 |
-
|
| 341 |
-
|
|
|
|
|
|
|
| 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
|
| 379 |
{colorBy === 'cluster'
|
| 380 |
? ' Colors indicate automatically discovered groups.'
|
| 381 |
-
: ` Colors show ${SHORT_NAMES[colorBy] || colorBy}.
|
| 382 |
-
}
|
|
|
|
|
|
|
| 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 =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 79 |
-
if (!
|
|
|
|
| 80 |
|
| 81 |
-
//
|
| 82 |
-
const
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 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">
|
| 100 |
</div>
|
| 101 |
<div className="cluster-insight-body">
|
| 102 |
{loading ? (
|
| 103 |
<p className="cluster-insight-loading">Analyzing clusters...</p>
|
| 104 |
) : (
|
| 105 |
<InsightBullets
|
| 106 |
-
text={
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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 ?
|
| 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 =
|
| 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 =
|
| 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>
|
|
|
|
| 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 =>
|
| 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
|
| 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 === '
|
| 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:
|
| 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:
|
| 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:
|
| 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:
|
| 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 = [];
|