Samad14 commited on
Commit
d2ea003
·
1 Parent(s): bde6aea

fix(interactions): add evidence-type filter, JSON export, and learn tooltips to STRING-DB viewer

Browse files
bioai-platform/frontend/src/components/interactions/StringDBViewer.tsx CHANGED
@@ -1,6 +1,7 @@
1
  "use client";
2
  import { useEffect, useState, useRef } from "react";
3
  import { useAuditTrail } from "@/hooks/useAuditTrail";
 
4
 
5
  type Interaction = {
6
  partner_gene: string;
@@ -11,6 +12,10 @@ type Interaction = {
11
  ascore: number;
12
  };
13
 
 
 
 
 
14
  export function StringDBViewer({ geneName, initialData }: { geneName: string; initialData?: { interactions: Interaction[] } | null }) {
15
  const audit = useAuditTrail();
16
  const [data, setData] = useState<{ interactions: Interaction[] } | null>(initialData ?? null);
@@ -18,6 +23,7 @@ export function StringDBViewer({ geneName, initialData }: { geneName: string; in
18
  const [error, setError] = useState<string | null>(null);
19
  const [imgError, setImgError] = useState(false);
20
  const [copied, setCopied] = useState(false);
 
21
  const auditedRef = useRef(false);
22
 
23
  const species = 9606;
@@ -34,14 +40,20 @@ export function StringDBViewer({ geneName, initialData }: { geneName: string; in
34
  }, [geneName, initialData, audit]);
35
 
36
  const scoreChannels = [
37
- { key: "escore" as const, label: "Experimental", color: "#2DD4BF" },
38
- { key: "dscore" as const, label: "Database", color: "#7C3AED" },
39
- { key: "ascore" as const, label: "Co-expression", color: "#E0A94E" },
40
- { key: "tscore" as const, label: "Text mining", color: "#FBBF24" },
41
  ];
42
 
43
  const stringDbUrl = `https://string-db.org/api/image/network?identifiers=${geneName}&species=${species}`;
44
- const allGenes = data?.interactions?.map(i => i.partner_gene).join("%0d") ?? "";
 
 
 
 
 
 
45
 
46
  const exportPng = () => {
47
  const img = new window.Image();
@@ -62,7 +74,7 @@ export function StringDBViewer({ geneName, initialData }: { geneName: string; in
62
  img.src = stringDbUrl;
63
  };
64
 
65
- const exportSvgTable = () => {
66
  if (!data?.interactions) return;
67
  const rows = data.interactions.map(i =>
68
  `${i.partner_gene}\t${i.combined_score.toFixed(3)}\t${i.escore.toFixed(3)}\t${i.dscore.toFixed(3)}\t${i.ascore.toFixed(3)}\t${i.tscore.toFixed(3)}`
@@ -74,6 +86,19 @@ export function StringDBViewer({ geneName, initialData }: { geneName: string; in
74
  a.click();
75
  };
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  const copyGeneList = () => {
78
  if (!data?.interactions) return;
79
  const genes = data.interactions.map(i => i.partner_gene).join("\n");
@@ -98,10 +123,14 @@ export function StringDBViewer({ geneName, initialData }: { geneName: string; in
98
  className="text-xs px-2.5 py-1 rounded bg-surface-1 border border-glass-border text-text-secondary hover:text-accent-cyan transition-colors">
99
  Export PNG
100
  </button>
101
- <button onClick={exportSvgTable}
102
  className="text-xs px-2.5 py-1 rounded bg-surface-1 border border-glass-border text-text-secondary hover:text-accent-cyan transition-colors">
103
  Export TSV
104
  </button>
 
 
 
 
105
  <button onClick={copyGeneList}
106
  className="text-xs px-2.5 py-1 rounded bg-surface-1 border border-glass-border text-text-secondary hover:text-accent-cyan transition-colors">
107
  {copied ? "Copied!" : "Copy genes"}
@@ -128,19 +157,46 @@ export function StringDBViewer({ geneName, initialData }: { geneName: string; in
128
  )}
129
  </div>
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  <div className="overflow-x-auto rounded-xl border border-glass-border">
132
  <table className="w-full text-xs">
133
  <thead className="bg-surface-1">
134
  <tr>
135
  <th className="px-3 py-2 text-left text-text-muted">Partner</th>
136
- <th className="px-3 py-2 text-left text-text-muted">Combined</th>
 
 
 
 
 
137
  {scoreChannels.map(c => (
138
- <th key={c.key} className="px-3 py-2 text-left" style={{ color: c.color + "99" }}>{c.label}</th>
 
 
 
 
139
  ))}
140
  </tr>
141
  </thead>
142
  <tbody>
143
- {interactions.map((inter, i) => (
144
  <tr key={i} className="border-t border-glass-border hover:bg-surface-1">
145
  <td className="px-3 py-2 text-accent-cyan font-mono font-bold">{inter.partner_gene}</td>
146
  <td className="px-3 py-2 text-text-secondary">{inter.combined_score.toFixed(3)}</td>
 
1
  "use client";
2
  import { useEffect, useState, useRef } from "react";
3
  import { useAuditTrail } from "@/hooks/useAuditTrail";
4
+ import { LearnPopover } from "@/components/LearnPopover";
5
 
6
  type Interaction = {
7
  partner_gene: string;
 
12
  ascore: number;
13
  };
14
 
15
+ type EvidenceFilter = "all" | "experimental" | "database" | "coexpression" | "textmining";
16
+
17
+ const EVIDENCE_THRESHOLD = 0.3;
18
+
19
  export function StringDBViewer({ geneName, initialData }: { geneName: string; initialData?: { interactions: Interaction[] } | null }) {
20
  const audit = useAuditTrail();
21
  const [data, setData] = useState<{ interactions: Interaction[] } | null>(initialData ?? null);
 
23
  const [error, setError] = useState<string | null>(null);
24
  const [imgError, setImgError] = useState(false);
25
  const [copied, setCopied] = useState(false);
26
+ const [filter, setFilter] = useState<EvidenceFilter>("all");
27
  const auditedRef = useRef(false);
28
 
29
  const species = 9606;
 
40
  }, [geneName, initialData, audit]);
41
 
42
  const scoreChannels = [
43
+ { key: "escore" as const, label: "Experimental", color: "#2DD4BF", filter: "experimental" as const, explain: "Support from physical interaction experiments (yeast two-hybrid, affinity capture, co-crystallisation)." },
44
+ { key: "dscore" as const, label: "Database", color: "#7C3AED", filter: "database" as const, explain: "Support from curated interaction databases that collect evidence from published literature." },
45
+ { key: "ascore" as const, label: "Co-expression", color: "#E0A94E", filter: "coexpression" as const, explain: "Support from correlated mRNA expression across many experiments — proteins that move together often work together." },
46
+ { key: "tscore" as const, label: "Text mining", color: "#FBBF24", filter: "textmining" as const, explain: "Support from automated scanning of the scientific literature for co-occurrence of the two genes." },
47
  ];
48
 
49
  const stringDbUrl = `https://string-db.org/api/image/network?identifiers=${geneName}&species=${species}`;
50
+
51
+ const visibleInteractions = (data?.interactions ?? []).filter(i => {
52
+ if (filter === "all") return true;
53
+ const channel = scoreChannels.find(c => c.filter === filter);
54
+ if (!channel) return true;
55
+ return i[channel.key] >= EVIDENCE_THRESHOLD;
56
+ });
57
 
58
  const exportPng = () => {
59
  const img = new window.Image();
 
74
  img.src = stringDbUrl;
75
  };
76
 
77
+ const exportTsv = () => {
78
  if (!data?.interactions) return;
79
  const rows = data.interactions.map(i =>
80
  `${i.partner_gene}\t${i.combined_score.toFixed(3)}\t${i.escore.toFixed(3)}\t${i.dscore.toFixed(3)}\t${i.ascore.toFixed(3)}\t${i.tscore.toFixed(3)}`
 
86
  a.click();
87
  };
88
 
89
+ const exportJson = () => {
90
+ if (!data?.interactions) return;
91
+ const a = document.createElement("a");
92
+ a.download = `${geneName}_stringdb_interactions.json`;
93
+ a.href = "data:application/json;charset=utf-8," + encodeURIComponent(JSON.stringify({
94
+ gene: geneName,
95
+ species,
96
+ source: "STRING-DB",
97
+ interactions: data.interactions,
98
+ }, null, 2));
99
+ a.click();
100
+ };
101
+
102
  const copyGeneList = () => {
103
  if (!data?.interactions) return;
104
  const genes = data.interactions.map(i => i.partner_gene).join("\n");
 
123
  className="text-xs px-2.5 py-1 rounded bg-surface-1 border border-glass-border text-text-secondary hover:text-accent-cyan transition-colors">
124
  Export PNG
125
  </button>
126
+ <button onClick={exportTsv}
127
  className="text-xs px-2.5 py-1 rounded bg-surface-1 border border-glass-border text-text-secondary hover:text-accent-cyan transition-colors">
128
  Export TSV
129
  </button>
130
+ <button onClick={exportJson}
131
+ className="text-xs px-2.5 py-1 rounded bg-surface-1 border border-glass-border text-text-secondary hover:text-accent-cyan transition-colors">
132
+ Export JSON
133
+ </button>
134
  <button onClick={copyGeneList}
135
  className="text-xs px-2.5 py-1 rounded bg-surface-1 border border-glass-border text-text-secondary hover:text-accent-cyan transition-colors">
136
  {copied ? "Copied!" : "Copy genes"}
 
157
  )}
158
  </div>
159
 
160
+ <div className="flex flex-wrap items-center gap-2">
161
+ <span className="text-xs text-text-muted">Evidence filter:</span>
162
+ {[{ key: "all" as const, label: "All" }, ...scoreChannels.map(c => ({ key: c.filter, label: c.label }))].map(opt => (
163
+ <button
164
+ key={opt.key}
165
+ onClick={() => setFilter(opt.key)}
166
+ className={`text-xs px-2.5 py-1 rounded border transition-colors ${
167
+ filter === opt.key
168
+ ? "bg-accent-cyan/10 border-accent-cyan/40 text-accent-cyan"
169
+ : "bg-surface-1 border-glass-border text-text-secondary hover:text-text-primary"
170
+ }`}
171
+ >
172
+ {opt.label}
173
+ </button>
174
+ ))}
175
+ <span className="text-xs text-text-muted ml-auto">Showing {visibleInteractions.length} of {interactions.length} partners</span>
176
+ </div>
177
+
178
  <div className="overflow-x-auto rounded-xl border border-glass-border">
179
  <table className="w-full text-xs">
180
  <thead className="bg-surface-1">
181
  <tr>
182
  <th className="px-3 py-2 text-left text-text-muted">Partner</th>
183
+ <th className="px-3 py-2 text-left text-text-muted">
184
+ <LearnPopover term="Combined score" topic="interactions"
185
+ explanation="STRING's confidence (0-1) that the interaction is real, integrating all evidence channels below.">
186
+ Combined
187
+ </LearnPopover>
188
+ </th>
189
  {scoreChannels.map(c => (
190
+ <th key={c.key} className="px-3 py-2 text-left" style={{ color: c.color + "99" }}>
191
+ <LearnPopover term={`${c.label} evidence`} topic="interactions" explanation={c.explain}>
192
+ {c.label}
193
+ </LearnPopover>
194
+ </th>
195
  ))}
196
  </tr>
197
  </thead>
198
  <tbody>
199
+ {visibleInteractions.map((inter, i) => (
200
  <tr key={i} className="border-t border-glass-border hover:bg-surface-1">
201
  <td className="px-3 py-2 text-accent-cyan font-mono font-bold">{inter.partner_gene}</td>
202
  <td className="px-3 py-2 text-text-secondary">{inter.combined_score.toFixed(3)}</td>