Upload folder using huggingface_hub
Browse files
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=
|
| 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=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 649 |
dbc.Col([
|
| 650 |
dbc.RadioItems(
|
| 651 |
id="plot-type",
|
| 652 |
options=[
|
| 653 |
-
{"label": " UMAP
|
| 654 |
-
{"label": "
|
| 655 |
],
|
| 656 |
value="cluster",
|
| 657 |
inline=True,
|
| 658 |
className="mb-2 mt-4",
|
| 659 |
),
|
| 660 |
-
], width=
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|