richiam commited on
Commit
4e2e836
·
verified ·
1 Parent(s): 67d1729

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +139 -12
app.py CHANGED
@@ -562,6 +562,27 @@ def proteins_tab():
562
  ], width=6),
563
  ], className="mb-2"),
564
  dbc.Row([
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
  dbc.Col([
566
  html.Label("Show columns", className="fw-semibold small mb-1"),
567
  dcc.Dropdown(
@@ -572,7 +593,7 @@ def proteins_tab():
572
  placeholder="Select columns…",
573
  style={"fontSize": "13px"},
574
  ),
575
- ], width=12),
576
  ]),
577
  ], className="filter-panel"),
578
  dbc.Row([
@@ -645,19 +666,41 @@ def clustering_tab():
645
  placeholder="All groups",
646
  style={"fontSize": "13px"},
647
  ),
648
- ], width=4),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
649
  dbc.Col([
650
  dbc.RadioItems(
651
  id="plot-type",
652
  options=[
653
- {"label": " UMAP cluster plot", "value": "cluster"},
654
- {"label": " Cluster distribution", "value": "distribution"},
655
  ],
656
  value="cluster",
657
  inline=True,
658
  className="mb-2 mt-4",
659
  ),
660
- ], width=4),
661
  dbc.Col([
662
  html.Label("Show top N clusters", className="fw-semibold small mb-1"),
663
  dcc.Dropdown(
@@ -1183,6 +1226,14 @@ def readme_tab():
1183
  html.Span("✕ Clear ", className="fw-semibold"),
1184
  "Resets the search value and group filter, returning to the full dataset.",
1185
  ]),
 
 
 
 
 
 
 
 
1186
  ], flush=True, className="mb-3"),
1187
  html.H6("Table", className="fw-bold mt-2"),
1188
  dbc.ListGroup([
@@ -1254,6 +1305,16 @@ def readme_tab():
1254
  "are merged into a light-gray \"Other\" trace. Choose \"All\" to colour every "
1255
  "cluster individually (may be slow for large fields).",
1256
  ]),
 
 
 
 
 
 
 
 
 
 
1257
  ], flush=True, className="mb-3"),
1258
  html.H6("UMAP cluster plot", className="fw-bold mt-2"),
1259
  dbc.ListGroup([
@@ -1487,7 +1548,7 @@ def render_tab(tab):
1487
  return html.Div()
1488
 
1489
 
1490
- def _apply_filters(groups, search_field, search_value):
1491
  filtered = df.copy()
1492
  if groups:
1493
  filtered = filtered[filtered["groups"].apply(
@@ -1497,9 +1558,32 @@ def _apply_filters(groups, search_field, search_value):
1497
  filtered = filtered[
1498
  filtered[search_field].astype(str).str.contains(search_value, case=False, na=False)
1499
  ]
 
 
 
 
1500
  return filtered
1501
 
1502
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1503
  @app.callback(
1504
  Output("protein-table", "data"),
1505
  Output("protein-table", "hidden_columns"),
@@ -1509,9 +1593,11 @@ def _apply_filters(groups, search_field, search_value):
1509
  Input("search-field", "value"),
1510
  Input("search-value", "value"),
1511
  Input("col-selector", "value"),
 
 
1512
  )
1513
- def filter_proteins(groups, search_field, search_value, selected_cols):
1514
- filtered = _apply_filters(groups, search_field, search_value)
1515
  cols = selected_cols if selected_cols else DEFAULT_COLS
1516
  hidden = [c for c in TABLE_FIELDS if c not in cols]
1517
  records = filtered[TABLE_FIELDS].to_dict("records")
@@ -1529,12 +1615,14 @@ def filter_proteins(groups, search_field, search_value, selected_cols):
1529
  Input("filter-group", "value"),
1530
  Input("search-field", "value"),
1531
  Input("search-value", "value"),
 
 
1532
  prevent_initial_call=True,
1533
  )
1534
- def show_detail(selected_rows, groups, search_field, search_value):
1535
  if not selected_rows:
1536
  return html.Div()
1537
- filtered = _apply_filters(groups, search_field, search_value)
1538
  clicked = selected_rows[0]
1539
  if clicked >= len(filtered):
1540
  return dash.no_update
@@ -1999,6 +2087,31 @@ def _distribution_figure(cdf, field_name, top_n=20):
1999
  return fig
2000
 
2001
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2002
  @app.callback(
2003
  Output("cluster-graph", "figure"),
2004
  Output("plot-status", "children"),
@@ -2009,8 +2122,10 @@ def _distribution_figure(cdf, field_name, top_n=20):
2009
  Input("plot-type", "value"),
2010
  Input("top-n-clusters", "value"),
2011
  Input("cluster-group-filter", "value"),
 
 
2012
  )
2013
- def update_cluster_plot(model, min_val, threshold, field, plot_type, top_n, groups):
2014
  empty_fig = go.Figure()
2015
  empty_fig.update_layout(paper_bgcolor="#ffffff", plot_bgcolor="#f9fafc")
2016
  if not all([model, min_val, threshold, field, plot_type]):
@@ -2020,6 +2135,13 @@ def update_cluster_plot(model, min_val, threshold, field, plot_type, top_n, grou
2020
  if cdf is None:
2021
  return empty_fig, f"Data file not found for model={model} min={min_val} t={threshold}"
2022
 
 
 
 
 
 
 
 
2023
  field_df = cdf[cdf["field"] == field].copy()
2024
  if field_df.empty:
2025
  return empty_fig, f"No data for field '{field}' in this parameter combination."
@@ -2036,7 +2158,12 @@ def update_cluster_plot(model, min_val, threshold, field, plot_type, top_n, grou
2036
  return empty_fig, f"No data for the selected group(s) in this field."
2037
 
2038
  n = top_n or 0
2039
- suffix = f" — {', '.join(groups)}" if groups else ""
 
 
 
 
 
2040
  if plot_type == "cluster":
2041
  return _umap_figure(field_df, field + suffix, top_n=n), ""
2042
  else:
 
562
  ], width=6),
563
  ], className="mb-2"),
564
  dbc.Row([
565
+ dbc.Col([
566
+ html.Label("Cross-filter: field", className="fw-semibold small mb-1"),
567
+ dcc.Dropdown(
568
+ id="table-cross-field",
569
+ options=[{"label": f.replace("_"," ").title(), "value": f}
570
+ for f in EXTRACTION_FIELDS[1:]],
571
+ placeholder="Select a field…",
572
+ clearable=True,
573
+ style={"fontSize": "13px"},
574
+ ),
575
+ ], width=3),
576
+ dbc.Col([
577
+ html.Label("Cross-filter: value", className="fw-semibold small mb-1"),
578
+ dcc.Dropdown(
579
+ id="table-cross-value",
580
+ options=[],
581
+ placeholder="Select a value…",
582
+ clearable=True,
583
+ style={"fontSize": "13px"},
584
+ ),
585
+ ], width=5),
586
  dbc.Col([
587
  html.Label("Show columns", className="fw-semibold small mb-1"),
588
  dcc.Dropdown(
 
593
  placeholder="Select columns…",
594
  style={"fontSize": "13px"},
595
  ),
596
+ ], width=4),
597
  ]),
598
  ], className="filter-panel"),
599
  dbc.Row([
 
666
  placeholder="All groups",
667
  style={"fontSize": "13px"},
668
  ),
669
+ ], width=3),
670
+ dbc.Col([
671
+ html.Label("Cross-filter: field", className="fw-semibold small mb-1"),
672
+ dcc.Dropdown(
673
+ id="dd-cross-field",
674
+ options=[{"label": f.replace("_"," ").title(), "value": f}
675
+ for f in CLUSTER_FIELDS],
676
+ placeholder="No cross-filter",
677
+ clearable=True,
678
+ style={"fontSize": "13px"},
679
+ ),
680
+ ], width=3),
681
+ dbc.Col([
682
+ html.Label("Cross-filter: value", className="fw-semibold small mb-1"),
683
+ dcc.Dropdown(
684
+ id="dd-cross-value",
685
+ options=[],
686
+ placeholder="Select a value…",
687
+ clearable=True,
688
+ disabled=True,
689
+ style={"fontSize": "13px"},
690
+ ),
691
+ ], width=3),
692
  dbc.Col([
693
  dbc.RadioItems(
694
  id="plot-type",
695
  options=[
696
+ {"label": " UMAP", "value": "cluster"},
697
+ {"label": " Distribution", "value": "distribution"},
698
  ],
699
  value="cluster",
700
  inline=True,
701
  className="mb-2 mt-4",
702
  ),
703
+ ], width=2),
704
  dbc.Col([
705
  html.Label("Show top N clusters", className="fw-semibold small mb-1"),
706
  dcc.Dropdown(
 
1226
  html.Span("✕ Clear ", className="fw-semibold"),
1227
  "Resets the search value and group filter, returning to the full dataset.",
1228
  ]),
1229
+ dbc.ListGroupItem([
1230
+ html.Span("Cross-filter: field / value ", className="fw-semibold"),
1231
+ "Select a purification field and one of its most common values to restrict "
1232
+ "the table to proteins where that field matches. For example, set field = "
1233
+ "Organism Source and value = 'Escherichia coli', then add Elution Buffer "
1234
+ "to the visible columns to see all elution buffers used with E. coli proteins. "
1235
+ "Combines with group and search filters.",
1236
+ ]),
1237
  ], flush=True, className="mb-3"),
1238
  html.H6("Table", className="fw-bold mt-2"),
1239
  dbc.ListGroup([
 
1305
  "are merged into a light-gray \"Other\" trace. Choose \"All\" to colour every "
1306
  "cluster individually (may be slow for large fields).",
1307
  ]),
1308
+ dbc.ListGroupItem([
1309
+ html.Span("Cross-filter: field / value ", className="fw-semibold"),
1310
+ "Restrict the plot to proteins that belong to a specific cluster in a "
1311
+ "different field. For example, set cross-filter field = Organism Source "
1312
+ "and value = 'Escherichia coli', then set Field = Elution Buffer — the "
1313
+ "UMAP and distribution chart will show elution buffer clusters only for "
1314
+ "E. coli proteins. The value dropdown is populated with the top 200 most "
1315
+ "common cluster labels for the selected cross-filter field and current "
1316
+ "model / threshold settings.",
1317
+ ]),
1318
  ], flush=True, className="mb-3"),
1319
  html.H6("UMAP cluster plot", className="fw-bold mt-2"),
1320
  dbc.ListGroup([
 
1548
  return html.Div()
1549
 
1550
 
1551
+ def _apply_filters(groups, search_field, search_value, cross_field=None, cross_value=None):
1552
  filtered = df.copy()
1553
  if groups:
1554
  filtered = filtered[filtered["groups"].apply(
 
1558
  filtered = filtered[
1559
  filtered[search_field].astype(str).str.contains(search_value, case=False, na=False)
1560
  ]
1561
+ if cross_field and cross_value and cross_field in filtered.columns:
1562
+ filtered = filtered[
1563
+ filtered[cross_field].astype(str).str.contains(cross_value, case=False, na=False)
1564
+ ]
1565
  return filtered
1566
 
1567
 
1568
+ @app.callback(
1569
+ Output("table-cross-value", "options"),
1570
+ Output("table-cross-value", "disabled"),
1571
+ Output("table-cross-value", "value"),
1572
+ Input("table-cross-field", "value"),
1573
+ )
1574
+ def populate_table_cross_values(cross_field):
1575
+ if not cross_field or cross_field not in df.columns:
1576
+ return [], True, None
1577
+ vals = (
1578
+ df[cross_field].dropna().astype(str)
1579
+ .loc[lambda s: s.str.strip() != ""]
1580
+ .value_counts()
1581
+ .head(200)
1582
+ .index.tolist()
1583
+ )
1584
+ return [{"label": v, "value": v} for v in vals], False, None
1585
+
1586
+
1587
  @app.callback(
1588
  Output("protein-table", "data"),
1589
  Output("protein-table", "hidden_columns"),
 
1593
  Input("search-field", "value"),
1594
  Input("search-value", "value"),
1595
  Input("col-selector", "value"),
1596
+ Input("table-cross-field", "value"),
1597
+ Input("table-cross-value", "value"),
1598
  )
1599
+ def filter_proteins(groups, search_field, search_value, selected_cols, cross_field, cross_value):
1600
+ filtered = _apply_filters(groups, search_field, search_value, cross_field, cross_value)
1601
  cols = selected_cols if selected_cols else DEFAULT_COLS
1602
  hidden = [c for c in TABLE_FIELDS if c not in cols]
1603
  records = filtered[TABLE_FIELDS].to_dict("records")
 
1615
  Input("filter-group", "value"),
1616
  Input("search-field", "value"),
1617
  Input("search-value", "value"),
1618
+ Input("table-cross-field", "value"),
1619
+ Input("table-cross-value", "value"),
1620
  prevent_initial_call=True,
1621
  )
1622
+ def show_detail(selected_rows, groups, search_field, search_value, cross_field, cross_value):
1623
  if not selected_rows:
1624
  return html.Div()
1625
+ filtered = _apply_filters(groups, search_field, search_value, cross_field, cross_value)
1626
  clicked = selected_rows[0]
1627
  if clicked >= len(filtered):
1628
  return dash.no_update
 
2087
  return fig
2088
 
2089
 
2090
+ @app.callback(
2091
+ Output("dd-cross-value", "options"),
2092
+ Output("dd-cross-value", "disabled"),
2093
+ Output("dd-cross-value", "value"),
2094
+ Input("dd-cross-field", "value"),
2095
+ Input("dd-model", "value"),
2096
+ Input("dd-min", "value"),
2097
+ Input("dd-threshold", "value"),
2098
+ )
2099
+ def populate_cross_filter_values(cross_field, model, min_val, threshold):
2100
+ if not cross_field or not all([model, min_val, threshold]):
2101
+ return [], True, None
2102
+ cdf = _load_cluster_csv(model, min_val, threshold)
2103
+ if cdf is None:
2104
+ return [], True, None
2105
+ labels = (
2106
+ cdf[(cdf["field"] == cross_field) & (cdf["cluster_id"] != -1)]["cluster_label"]
2107
+ .value_counts()
2108
+ .head(200)
2109
+ .index.tolist()
2110
+ )
2111
+ options = [{"label": l, "value": l} for l in labels]
2112
+ return options, False, None
2113
+
2114
+
2115
  @app.callback(
2116
  Output("cluster-graph", "figure"),
2117
  Output("plot-status", "children"),
 
2122
  Input("plot-type", "value"),
2123
  Input("top-n-clusters", "value"),
2124
  Input("cluster-group-filter", "value"),
2125
+ Input("dd-cross-field", "value"),
2126
+ Input("dd-cross-value", "value"),
2127
  )
2128
+ def update_cluster_plot(model, min_val, threshold, field, plot_type, top_n, groups, cross_field, cross_value):
2129
  empty_fig = go.Figure()
2130
  empty_fig.update_layout(paper_bgcolor="#ffffff", plot_bgcolor="#f9fafc")
2131
  if not all([model, min_val, threshold, field, plot_type]):
 
2135
  if cdf is None:
2136
  return empty_fig, f"Data file not found for model={model} min={min_val} t={threshold}"
2137
 
2138
+ # Apply cross-field filter: keep only (key, protein_index) in the selected cross-field cluster
2139
+ if cross_field and cross_value:
2140
+ keep = cdf[(cdf["field"] == cross_field) & (cdf["cluster_label"] == cross_value)][["key", "protein_index"]]
2141
+ if keep.empty:
2142
+ return empty_fig, f"No proteins found for {cross_field} = '{cross_value}'."
2143
+ cdf = cdf.merge(keep, on=["key", "protein_index"], how="inner")
2144
+
2145
  field_df = cdf[cdf["field"] == field].copy()
2146
  if field_df.empty:
2147
  return empty_fig, f"No data for field '{field}' in this parameter combination."
 
2158
  return empty_fig, f"No data for the selected group(s) in this field."
2159
 
2160
  n = top_n or 0
2161
+ suffix_parts = []
2162
+ if groups:
2163
+ suffix_parts.append(", ".join(groups))
2164
+ if cross_field and cross_value:
2165
+ suffix_parts.append(f"{cross_field.replace('_',' ')}={cross_value}")
2166
+ suffix = f" — {' | '.join(suffix_parts)}" if suffix_parts else ""
2167
  if plot_type == "cluster":
2168
  return _umap_figure(field_df, field + suffix, top_n=n), ""
2169
  else: