gramene-search 2.0.5 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +1 -1
- package/.parcel-cache/83e7562660f7cc15-BundleGraph +0 -0
- package/.parcel-cache/d3a1b9507cb44047-AssetGraph +0 -0
- package/.parcel-cache/data.mdb +0 -0
- package/.parcel-cache/dc1da35000e13623-RequestGraph +0 -0
- package/.parcel-cache/lock.mdb +0 -0
- package/.parcel-cache/snapshot-dc1da35000e13623.txt +2 -2
- package/dist/index.css +176 -0
- package/dist/index.css.map +1 -1
- package/dist/index.js +2354 -817
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bundles/docs.js +42 -15
- package/src/bundles/gsea.js +568 -0
- package/src/bundles/index.js +2 -1
- package/src/bundles/ontologies.js +52 -60
- package/src/bundles/swaggerFields.js +5 -6
- package/src/bundles/views.js +7 -1
- package/src/components/exporter/expressionResolver.js +2 -2
- package/src/components/exprViz/ExprTable.js +13 -2
- package/src/components/exprViz/ExprVizView.js +43 -5
- package/src/components/geneSearchUI.js +2 -0
- package/src/components/results/GSEA.js +618 -0
- package/src/components/results/HelpDemo.js +30 -0
- package/src/components/results/gsea.css +177 -0
- package/src/fieldCatalog.overlay.json +1 -1
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { connect } from 'redux-bundler-react';
|
|
3
|
+
import { Accordion, Form, Badge } from 'react-bootstrap';
|
|
4
|
+
import { BsChevronDown, BsChevronRight } from 'react-icons/bs';
|
|
5
|
+
import './gsea.css';
|
|
6
|
+
|
|
7
|
+
function speciesTaxonId(tid) {
|
|
8
|
+
const n = +tid;
|
|
9
|
+
return n > 1000000 ? Math.floor(n / 1000) : n;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function genomeName(grameneMaps, tid) {
|
|
13
|
+
if (!grameneMaps) return String(tid);
|
|
14
|
+
const direct = grameneMaps[tid];
|
|
15
|
+
if (direct && direct.display_name) return direct.display_name;
|
|
16
|
+
const sp = grameneMaps[speciesTaxonId(tid)];
|
|
17
|
+
if (sp && sp.display_name) return sp.display_name;
|
|
18
|
+
return String(tid);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function fmtP(p) {
|
|
22
|
+
if (p == null) return '';
|
|
23
|
+
if (p === 0) return '0';
|
|
24
|
+
if (p < 1e-4) return p.toExponential(2);
|
|
25
|
+
return p.toPrecision(3);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fmtFold(f) {
|
|
29
|
+
if (!isFinite(f)) return '';
|
|
30
|
+
if (f >= 100) return f.toFixed(0);
|
|
31
|
+
return f.toFixed(2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------- species tree helpers ----------
|
|
35
|
+
|
|
36
|
+
function findRoots(tax) {
|
|
37
|
+
return Object.values(tax).filter(n =>
|
|
38
|
+
!n.is_a || n.is_a.length === 0 || !n.is_a.some(p => tax[p])
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Place each facet tid onto a taxonomy node id (fall back to species level
|
|
43
|
+
// when the exact subspecies tid isn't in grameneTaxonomy).
|
|
44
|
+
function placeTaxa(taxonomy, taxa) {
|
|
45
|
+
const placement = {};
|
|
46
|
+
const byPlace = {};
|
|
47
|
+
for (const { tid, count } of taxa) {
|
|
48
|
+
const place = taxonomy[tid] ? +tid : speciesTaxonId(tid);
|
|
49
|
+
placement[tid] = place;
|
|
50
|
+
if (!byPlace[place]) byPlace[place] = [];
|
|
51
|
+
byPlace[place].push({ tid, count });
|
|
52
|
+
}
|
|
53
|
+
return { placement, byPlace };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Set of taxonomy node ids on the path from any placed leaf back to its root.
|
|
57
|
+
function relevantNodeIds(taxonomy, byPlace) {
|
|
58
|
+
const out = new Set();
|
|
59
|
+
for (const placeId of Object.keys(byPlace)) {
|
|
60
|
+
let cur = taxonomy[placeId];
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
while (cur && !seen.has(+cur._id)) {
|
|
63
|
+
seen.add(+cur._id);
|
|
64
|
+
out.add(+cur._id);
|
|
65
|
+
const pid = cur.is_a && cur.is_a[0];
|
|
66
|
+
cur = pid ? taxonomy[pid] : null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function relevantChildren(node, taxonomy, relevant) {
|
|
73
|
+
return (node.children || [])
|
|
74
|
+
.map(cid => taxonomy[cid])
|
|
75
|
+
.filter(c => c && relevant.has(+c._id));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Walk down through single-relevant-child internal nodes to a branch / leaf.
|
|
79
|
+
function compressChain(node, taxonomy, relevant, byPlace) {
|
|
80
|
+
const chain = [node];
|
|
81
|
+
let cur = node;
|
|
82
|
+
while (true) {
|
|
83
|
+
if (byPlace[cur._id]) break; // node itself is a placed leaf
|
|
84
|
+
const kids = relevantChildren(cur, taxonomy, relevant);
|
|
85
|
+
if (kids.length !== 1) break;
|
|
86
|
+
cur = kids[0];
|
|
87
|
+
chain.push(cur);
|
|
88
|
+
}
|
|
89
|
+
return { chain, terminal: cur };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const CHAIN_DISPLAY_MAX = 3;
|
|
93
|
+
function renderChain(chain) {
|
|
94
|
+
const nameOf = n => n.short_name || n.name || String(n._id);
|
|
95
|
+
if (chain.length <= CHAIN_DISPLAY_MAX) {
|
|
96
|
+
return chain.map((n, i) => (
|
|
97
|
+
<React.Fragment key={n._id}>
|
|
98
|
+
{i > 0 && <span className="tax-sep"> › </span>}
|
|
99
|
+
{nameOf(n)}
|
|
100
|
+
</React.Fragment>
|
|
101
|
+
));
|
|
102
|
+
}
|
|
103
|
+
const first = chain[0];
|
|
104
|
+
const last = chain[chain.length - 1];
|
|
105
|
+
const middle = chain.slice(1, -1);
|
|
106
|
+
const fullTitle = chain.map(nameOf).join(' › ');
|
|
107
|
+
return (
|
|
108
|
+
<>
|
|
109
|
+
{nameOf(first)}
|
|
110
|
+
<span className="tax-sep"> › </span>
|
|
111
|
+
<span className="tax-ellipsis" title={fullTitle}>
|
|
112
|
+
<span className="tax-ellipsis-short">…</span>
|
|
113
|
+
<span className="tax-ellipsis-full">
|
|
114
|
+
{middle.map(n => (
|
|
115
|
+
<React.Fragment key={n._id}>
|
|
116
|
+
{nameOf(n)}
|
|
117
|
+
<span className="tax-sep"> › </span>
|
|
118
|
+
</React.Fragment>
|
|
119
|
+
))}
|
|
120
|
+
</span>
|
|
121
|
+
</span>
|
|
122
|
+
<span className="tax-sep"> › </span>
|
|
123
|
+
{nameOf(last)}
|
|
124
|
+
</>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function dedupAdjacent(chain) {
|
|
129
|
+
const out = [];
|
|
130
|
+
for (const n of chain) {
|
|
131
|
+
if (out.length && out[out.length - 1].name === n.name) {
|
|
132
|
+
out[out.length - 1] = n;
|
|
133
|
+
} else {
|
|
134
|
+
out.push(n);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const SpeciesTreeNode = ({
|
|
141
|
+
node, depth, taxonomy, relevant, byPlace, grameneMaps,
|
|
142
|
+
expanded, onToggleExpand, activeTaxon, onSelect
|
|
143
|
+
}) => {
|
|
144
|
+
const { chain: rawChain, terminal } = compressChain(node, taxonomy, relevant, byPlace);
|
|
145
|
+
const chain = dedupAdjacent(rawChain);
|
|
146
|
+
const placed = byPlace[terminal._id] || [];
|
|
147
|
+
const kids = relevantChildren(terminal, taxonomy, relevant);
|
|
148
|
+
const hasKids = kids.length > 0;
|
|
149
|
+
const isOpen = expanded.has(+terminal._id);
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div className="tax-node gsea-tree-node">
|
|
153
|
+
<div className="tax-row" style={{ paddingLeft: depth * 7 }}>
|
|
154
|
+
{hasKids ? (
|
|
155
|
+
<span className="tax-chevron" onClick={() => onToggleExpand(+terminal._id)}>
|
|
156
|
+
{isOpen ? <BsChevronDown/> : <BsChevronRight/>}
|
|
157
|
+
</span>
|
|
158
|
+
) : (
|
|
159
|
+
<span className="tax-chevron-spacer"/>
|
|
160
|
+
)}
|
|
161
|
+
<span className={hasKids ? 'tax-label tax-label-internal' : 'tax-label'}>
|
|
162
|
+
{renderChain(chain)}
|
|
163
|
+
</span>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{placed.map(({ tid, count }) => {
|
|
167
|
+
const isActive = String(activeTaxon) === String(tid);
|
|
168
|
+
return (
|
|
169
|
+
<div
|
|
170
|
+
key={tid}
|
|
171
|
+
className={'gsea-tree-leaf' + (isActive ? ' gsea-tree-leaf-active' : '')}
|
|
172
|
+
style={{ paddingLeft: (depth + 1) * 7 + 18 }}
|
|
173
|
+
onClick={() => onSelect(tid)}
|
|
174
|
+
>
|
|
175
|
+
<input
|
|
176
|
+
type="radio"
|
|
177
|
+
readOnly
|
|
178
|
+
checked={isActive}
|
|
179
|
+
onChange={() => onSelect(tid)}
|
|
180
|
+
/>
|
|
181
|
+
<span className="gsea-tree-leaf-name">{genomeName(grameneMaps, tid)}</span>
|
|
182
|
+
<span className="gsea-tree-leaf-count">{count.toLocaleString()}</span>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
})}
|
|
186
|
+
|
|
187
|
+
{hasKids && isOpen && (
|
|
188
|
+
<div className="tax-children">
|
|
189
|
+
{kids.map(c => (
|
|
190
|
+
<SpeciesTreeNode
|
|
191
|
+
key={c._id}
|
|
192
|
+
node={c}
|
|
193
|
+
depth={depth + 1}
|
|
194
|
+
taxonomy={taxonomy}
|
|
195
|
+
relevant={relevant}
|
|
196
|
+
byPlace={byPlace}
|
|
197
|
+
grameneMaps={grameneMaps}
|
|
198
|
+
expanded={expanded}
|
|
199
|
+
onToggleExpand={onToggleExpand}
|
|
200
|
+
activeTaxon={activeTaxon}
|
|
201
|
+
onSelect={onSelect}
|
|
202
|
+
/>
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const SpeciesTree = ({ taxonomy, grameneMaps, taxa, activeTaxon, onSelect }) => {
|
|
211
|
+
const { placement, byPlace } = useMemo(() => placeTaxa(taxonomy, taxa), [taxonomy, taxa]);
|
|
212
|
+
const relevant = useMemo(() => relevantNodeIds(taxonomy, byPlace), [taxonomy, byPlace]);
|
|
213
|
+
const roots = useMemo(
|
|
214
|
+
() => findRoots(taxonomy).filter(r => relevant.has(+r._id)),
|
|
215
|
+
[taxonomy, relevant]
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Default: every relevant internal node expanded so the user sees the
|
|
219
|
+
// full pruned tree on first paint.
|
|
220
|
+
const [expanded, setExpanded] = useState(() => new Set(relevant));
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
setExpanded(new Set(relevant));
|
|
223
|
+
}, [relevant]);
|
|
224
|
+
|
|
225
|
+
const handleToggle = (id) => {
|
|
226
|
+
setExpanded(prev => {
|
|
227
|
+
const next = new Set(prev);
|
|
228
|
+
next.has(id) ? next.delete(id) : next.add(id);
|
|
229
|
+
return next;
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Skip the root spine: if a root has only one relevant child and isn't
|
|
234
|
+
// itself a placed leaf, render from its child instead.
|
|
235
|
+
const topLevel = [];
|
|
236
|
+
for (const r of roots) {
|
|
237
|
+
const { terminal } = compressChain(r, taxonomy, relevant, byPlace);
|
|
238
|
+
if (byPlace[terminal._id]) {
|
|
239
|
+
topLevel.push(r);
|
|
240
|
+
} else {
|
|
241
|
+
const kids = relevantChildren(terminal, taxonomy, relevant);
|
|
242
|
+
kids.forEach(k => topLevel.push(k));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<div className="gsea-tree">
|
|
248
|
+
{topLevel.map(n => (
|
|
249
|
+
<SpeciesTreeNode
|
|
250
|
+
key={n._id}
|
|
251
|
+
node={n}
|
|
252
|
+
depth={0}
|
|
253
|
+
taxonomy={taxonomy}
|
|
254
|
+
relevant={relevant}
|
|
255
|
+
byPlace={byPlace}
|
|
256
|
+
grameneMaps={grameneMaps}
|
|
257
|
+
expanded={expanded}
|
|
258
|
+
onToggleExpand={handleToggle}
|
|
259
|
+
activeTaxon={activeTaxon}
|
|
260
|
+
onSelect={onSelect}
|
|
261
|
+
/>
|
|
262
|
+
))}
|
|
263
|
+
</div>
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// ---------- enrichment panel ----------
|
|
268
|
+
|
|
269
|
+
const ControlsBar = ({ ui, onChange }) => (
|
|
270
|
+
<div className="gsea-controls">
|
|
271
|
+
<Form.Group className="gsea-control">
|
|
272
|
+
<Form.Label>p_adj ≤</Form.Label>
|
|
273
|
+
<Form.Control
|
|
274
|
+
type="number" step="0.01" min="0" max="1"
|
|
275
|
+
value={ui.pAdjCutoff}
|
|
276
|
+
onChange={e => onChange({ pAdjCutoff: +e.target.value })}
|
|
277
|
+
/>
|
|
278
|
+
</Form.Group>
|
|
279
|
+
<Form.Group className="gsea-control">
|
|
280
|
+
<Form.Label>min k</Form.Label>
|
|
281
|
+
<Form.Control
|
|
282
|
+
type="number" step="1" min="1"
|
|
283
|
+
value={ui.minK}
|
|
284
|
+
onChange={e => onChange({ minK: Math.max(1, +e.target.value) })}
|
|
285
|
+
/>
|
|
286
|
+
</Form.Group>
|
|
287
|
+
<Form.Check
|
|
288
|
+
className="gsea-control"
|
|
289
|
+
type="switch"
|
|
290
|
+
id="gsea-most-specific"
|
|
291
|
+
label="Most-specific only"
|
|
292
|
+
checked={!!ui.mostSpecific}
|
|
293
|
+
onChange={e => onChange({ mostSpecific: e.target.checked })}
|
|
294
|
+
/>
|
|
295
|
+
<Form.Group className="gsea-control gsea-control-grow">
|
|
296
|
+
<Form.Label>Filter terms</Form.Label>
|
|
297
|
+
<Form.Control
|
|
298
|
+
type="text"
|
|
299
|
+
placeholder="term id or name…"
|
|
300
|
+
value={ui.search || ''}
|
|
301
|
+
onChange={e => onChange({ search: e.target.value })}
|
|
302
|
+
/>
|
|
303
|
+
</Form.Group>
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const TermRow = ({ row, showType, onAddFilter }) => {
|
|
308
|
+
const handleClick = () => onAddFilter && onAddFilter(row);
|
|
309
|
+
return (
|
|
310
|
+
<tr onClick={handleClick} title="Click to add as a filter">
|
|
311
|
+
<td className="gsea-term-id">{row.term_display_id}</td>
|
|
312
|
+
<td className="gsea-term-name">{row.term_name || <em>(loading…)</em>}</td>
|
|
313
|
+
{showType && <td className="gsea-term-type">{row.term_namespace || ''}</td>}
|
|
314
|
+
<td className="gsea-num">{row.k} / {row.n}</td>
|
|
315
|
+
<td className="gsea-num">{row.K} / {row.N}</td>
|
|
316
|
+
<td className="gsea-num">{fmtFold(row.fold)}</td>
|
|
317
|
+
<td className="gsea-num">{fmtP(row.p)}</td>
|
|
318
|
+
<td className="gsea-num">{fmtP(row.pAdj)}</td>
|
|
319
|
+
</tr>
|
|
320
|
+
);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// InterPro entry types and pathway types are useful per-row context
|
|
324
|
+
// (e.g. Domain vs Family vs Repeat for InterPro), and aren't reflected in
|
|
325
|
+
// the section title the way GO namespaces are. Show a Type column for
|
|
326
|
+
// those two ontologies only.
|
|
327
|
+
const ONTS_WITH_TYPE_COLUMN = new Set(['domains', 'pathways']);
|
|
328
|
+
|
|
329
|
+
const SORT_ACCESSORS = {
|
|
330
|
+
term: r => r.term_display_id || '',
|
|
331
|
+
name: r => (r.term_name || '').toLowerCase(),
|
|
332
|
+
type: r => (r.term_namespace || '').toLowerCase(),
|
|
333
|
+
k: r => r.k,
|
|
334
|
+
K: r => r.K,
|
|
335
|
+
fold: r => r.fold,
|
|
336
|
+
p: r => r.p,
|
|
337
|
+
pAdj: r => r.pAdj
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// Defaults chosen so a single click does what the user usually wants:
|
|
341
|
+
// numeric "more interesting" columns (k, K, fold) descend; p-values and
|
|
342
|
+
// text columns ascend.
|
|
343
|
+
const SORT_DEFAULT_DIR = {
|
|
344
|
+
term: 'asc', name: 'asc', type: 'asc',
|
|
345
|
+
k: 'desc', K: 'desc', fold: 'desc',
|
|
346
|
+
p: 'asc', pAdj: 'asc'
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const SortableTh = ({ label, sortKey, activeKey, activeDir, onSort, numeric }) => {
|
|
350
|
+
const active = activeKey === sortKey;
|
|
351
|
+
const arrow = active ? (activeDir === 'asc' ? ' ▲' : ' ▼') : '';
|
|
352
|
+
const cls = 'gsea-sort-th'
|
|
353
|
+
+ (active ? ' gsea-sort-th-active' : '')
|
|
354
|
+
+ (numeric ? ' gsea-num' : '');
|
|
355
|
+
return (
|
|
356
|
+
<th className={cls} onClick={() => onSort(sortKey)}>
|
|
357
|
+
{label}{arrow}
|
|
358
|
+
</th>
|
|
359
|
+
);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const OntologySection = ({ block, search, onAddFilter }) => {
|
|
363
|
+
const filtered = useMemo(() => {
|
|
364
|
+
if (!search) return block.rows;
|
|
365
|
+
const needle = search.toLowerCase();
|
|
366
|
+
return block.rows.filter(r =>
|
|
367
|
+
(r.term_display_id && r.term_display_id.toLowerCase().includes(needle)) ||
|
|
368
|
+
(r.term_name && r.term_name.toLowerCase().includes(needle))
|
|
369
|
+
);
|
|
370
|
+
}, [block.rows, search]);
|
|
371
|
+
|
|
372
|
+
const showType = ONTS_WITH_TYPE_COLUMN.has(block.ontology);
|
|
373
|
+
const [sortKey, setSortKey] = useState('pAdj');
|
|
374
|
+
const [sortDir, setSortDir] = useState('asc');
|
|
375
|
+
|
|
376
|
+
const handleSort = (key) => {
|
|
377
|
+
if (key === sortKey) {
|
|
378
|
+
setSortDir(d => (d === 'asc' ? 'desc' : 'asc'));
|
|
379
|
+
} else {
|
|
380
|
+
setSortKey(key);
|
|
381
|
+
setSortDir(SORT_DEFAULT_DIR[key] || 'asc');
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const sorted = useMemo(() => {
|
|
386
|
+
const accessor = SORT_ACCESSORS[sortKey];
|
|
387
|
+
if (!accessor) return filtered;
|
|
388
|
+
const sign = sortDir === 'desc' ? -1 : 1;
|
|
389
|
+
const arr = filtered.slice();
|
|
390
|
+
arr.sort((a, b) => {
|
|
391
|
+
const va = accessor(a);
|
|
392
|
+
const vb = accessor(b);
|
|
393
|
+
if (typeof va === 'number' && typeof vb === 'number') {
|
|
394
|
+
if (va === vb) return 0;
|
|
395
|
+
return (va - vb) * sign;
|
|
396
|
+
}
|
|
397
|
+
return String(va).localeCompare(String(vb)) * sign;
|
|
398
|
+
});
|
|
399
|
+
return arr;
|
|
400
|
+
}, [filtered, sortKey, sortDir]);
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<Accordion.Item eventKey={block.ontology}>
|
|
404
|
+
<Accordion.Header>
|
|
405
|
+
<span className="gsea-ont-title">{block.label}</span>
|
|
406
|
+
<Badge bg="secondary" className="gsea-ont-badge">
|
|
407
|
+
{block.passing} significant / {block.tested} tested
|
|
408
|
+
</Badge>
|
|
409
|
+
</Accordion.Header>
|
|
410
|
+
<Accordion.Body>
|
|
411
|
+
{sorted.length === 0
|
|
412
|
+
? <em>No terms pass the current cutoffs.</em>
|
|
413
|
+
: (
|
|
414
|
+
<table className="gsea-table">
|
|
415
|
+
<thead>
|
|
416
|
+
<tr>
|
|
417
|
+
<SortableTh label="Term" sortKey="term" activeKey={sortKey} activeDir={sortDir} onSort={handleSort}/>
|
|
418
|
+
<SortableTh label="Name" sortKey="name" activeKey={sortKey} activeDir={sortDir} onSort={handleSort}/>
|
|
419
|
+
{showType && <SortableTh label="Type" sortKey="type" activeKey={sortKey} activeDir={sortDir} onSort={handleSort}/>}
|
|
420
|
+
<SortableTh label="k / n" sortKey="k" activeKey={sortKey} activeDir={sortDir} onSort={handleSort} numeric/>
|
|
421
|
+
<SortableTh label="K / N" sortKey="K" activeKey={sortKey} activeDir={sortDir} onSort={handleSort} numeric/>
|
|
422
|
+
<SortableTh label="Fold" sortKey="fold" activeKey={sortKey} activeDir={sortDir} onSort={handleSort} numeric/>
|
|
423
|
+
<SortableTh label="p" sortKey="p" activeKey={sortKey} activeDir={sortDir} onSort={handleSort} numeric/>
|
|
424
|
+
<SortableTh label="p_adj" sortKey="pAdj" activeKey={sortKey} activeDir={sortDir} onSort={handleSort} numeric/>
|
|
425
|
+
</tr>
|
|
426
|
+
</thead>
|
|
427
|
+
<tbody>
|
|
428
|
+
{sorted.map(r => (
|
|
429
|
+
<TermRow
|
|
430
|
+
key={`${r.ontology}:${r.term_id}`}
|
|
431
|
+
row={r}
|
|
432
|
+
showType={showType}
|
|
433
|
+
onAddFilter={onAddFilter}
|
|
434
|
+
/>
|
|
435
|
+
))}
|
|
436
|
+
</tbody>
|
|
437
|
+
</table>
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
</Accordion.Body>
|
|
441
|
+
</Accordion.Item>
|
|
442
|
+
);
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const TaxonPanel = ({ taxon, gsea, results, ui, onUiChange, onAddFilter }) => {
|
|
446
|
+
if (!taxon) return <div className="gsea-panel"><em>Select a species on the left.</em></div>;
|
|
447
|
+
const t = gsea.byTaxon[taxon];
|
|
448
|
+
if (!t) return <div className="gsea-panel"><em>Initializing…</em></div>;
|
|
449
|
+
if (t.fg.status === 'loading' || t.bg.status === 'loading' || t.fg.status === 'idle' || t.bg.status === 'idle') {
|
|
450
|
+
return <div className="gsea-panel gsea-loading"><em>Loading enrichment…</em></div>;
|
|
451
|
+
}
|
|
452
|
+
if (t.fg.status === 'error') return <div className="gsea-panel"><em>Foreground error: {t.fg.error}</em></div>;
|
|
453
|
+
if (t.bg.status === 'error') return <div className="gsea-panel"><em>Background error: {t.bg.error}</em></div>;
|
|
454
|
+
if (!results) return <div className="gsea-panel"><em>No data.</em></div>;
|
|
455
|
+
|
|
456
|
+
const allBlocks = ui.ontology === 'all'
|
|
457
|
+
? Object.values(results)
|
|
458
|
+
: (results[ui.ontology] ? [results[ui.ontology]] : []);
|
|
459
|
+
// Hide ontologies that aren't used in this species at all.
|
|
460
|
+
const blocks = allBlocks.filter(b => b.tested > 0);
|
|
461
|
+
|
|
462
|
+
return (
|
|
463
|
+
<div className="gsea-panel">
|
|
464
|
+
<div className="gsea-summary">
|
|
465
|
+
Foreground: <b>{t.fg.numFound.toLocaleString()}</b> genes ·
|
|
466
|
+
Background: <b>{t.bg.numFound.toLocaleString()}</b> genes
|
|
467
|
+
</div>
|
|
468
|
+
<ControlsBar ui={ui} onChange={onUiChange} />
|
|
469
|
+
<Accordion alwaysOpen defaultActiveKey={blocks.length === 1 ? blocks[0].ontology : undefined}>
|
|
470
|
+
{blocks.map(b => (
|
|
471
|
+
<OntologySection
|
|
472
|
+
key={b.ontology}
|
|
473
|
+
block={b}
|
|
474
|
+
search={ui.search}
|
|
475
|
+
onAddFilter={onAddFilter}
|
|
476
|
+
/>
|
|
477
|
+
))}
|
|
478
|
+
</Accordion>
|
|
479
|
+
</div>
|
|
480
|
+
);
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const GseaViewCmp = props => {
|
|
484
|
+
const {
|
|
485
|
+
grameneSearch,
|
|
486
|
+
grameneMaps,
|
|
487
|
+
grameneTaxonomy,
|
|
488
|
+
gsea,
|
|
489
|
+
gseaUI: ui,
|
|
490
|
+
gseaResults: results,
|
|
491
|
+
doSetGseaActiveTaxon,
|
|
492
|
+
doSetGseaUI,
|
|
493
|
+
doFetchGseaForeground,
|
|
494
|
+
doFetchGseaBackground,
|
|
495
|
+
doAcceptGrameneSuggestion
|
|
496
|
+
} = props;
|
|
497
|
+
|
|
498
|
+
const taxa = useMemo(() => {
|
|
499
|
+
if (!grameneSearch || !grameneSearch.facet_counts) return [];
|
|
500
|
+
const arr = grameneSearch.facet_counts.facet_fields.taxon_id || [];
|
|
501
|
+
const ids = [];
|
|
502
|
+
const counts = {};
|
|
503
|
+
for (let i = 0; i < arr.length; i += 2) {
|
|
504
|
+
ids.push(arr[i]);
|
|
505
|
+
counts[arr[i]] = +arr[i + 1];
|
|
506
|
+
}
|
|
507
|
+
if (grameneMaps) {
|
|
508
|
+
ids.sort((a, b) => {
|
|
509
|
+
const ma = grameneMaps[a] || grameneMaps[speciesTaxonId(a)];
|
|
510
|
+
const mb = grameneMaps[b] || grameneMaps[speciesTaxonId(b)];
|
|
511
|
+
return ((ma && ma.left_index) || 0) - ((mb && mb.left_index) || 0);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
return ids.map(tid => ({ tid, count: counts[tid] }));
|
|
515
|
+
}, [grameneSearch, grameneMaps]);
|
|
516
|
+
|
|
517
|
+
const activeTaxon = gsea.activeTaxon;
|
|
518
|
+
|
|
519
|
+
const [treeWidth, setTreeWidth] = useState(280);
|
|
520
|
+
const beginResize = (e) => {
|
|
521
|
+
e.preventDefault();
|
|
522
|
+
const startX = e.clientX;
|
|
523
|
+
const startWidth = treeWidth;
|
|
524
|
+
const onMove = (ev) => {
|
|
525
|
+
const next = Math.max(150, Math.min(800, startWidth + (ev.clientX - startX)));
|
|
526
|
+
setTreeWidth(next);
|
|
527
|
+
};
|
|
528
|
+
const onUp = () => {
|
|
529
|
+
document.removeEventListener('mousemove', onMove);
|
|
530
|
+
document.removeEventListener('mouseup', onUp);
|
|
531
|
+
document.body.style.userSelect = '';
|
|
532
|
+
document.body.style.cursor = '';
|
|
533
|
+
};
|
|
534
|
+
document.body.style.userSelect = 'none';
|
|
535
|
+
document.body.style.cursor = 'col-resize';
|
|
536
|
+
document.addEventListener('mousemove', onMove);
|
|
537
|
+
document.addEventListener('mouseup', onUp);
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
useEffect(() => {
|
|
541
|
+
if (taxa.length === 0) return;
|
|
542
|
+
if (!activeTaxon || !taxa.find(t => String(t.tid) === String(activeTaxon))) {
|
|
543
|
+
doSetGseaActiveTaxon(taxa[0].tid);
|
|
544
|
+
}
|
|
545
|
+
}, [taxa, activeTaxon, doSetGseaActiveTaxon]);
|
|
546
|
+
|
|
547
|
+
useEffect(() => {
|
|
548
|
+
if (!activeTaxon) return;
|
|
549
|
+
const t = gsea.byTaxon[activeTaxon];
|
|
550
|
+
if (!t) return;
|
|
551
|
+
if (t.fg.status === 'idle') doFetchGseaForeground(activeTaxon);
|
|
552
|
+
if (t.bg.status === 'idle') doFetchGseaBackground(activeTaxon);
|
|
553
|
+
}, [activeTaxon, gsea, doFetchGseaForeground, doFetchGseaBackground]);
|
|
554
|
+
|
|
555
|
+
if (taxa.length === 0) {
|
|
556
|
+
return <div className="gsea-view"><em>No species in the current results.</em></div>;
|
|
557
|
+
}
|
|
558
|
+
if (!grameneTaxonomy) {
|
|
559
|
+
return <div className="gsea-view"><em>Loading taxonomy…</em></div>;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const handleAddFilter = (row) => {
|
|
563
|
+
if (!doAcceptGrameneSuggestion) return;
|
|
564
|
+
const label = row.term_name
|
|
565
|
+
? `${row.term_display_id} ${row.term_name}`
|
|
566
|
+
: row.term_display_id;
|
|
567
|
+
doAcceptGrameneSuggestion({
|
|
568
|
+
fq_field: row.field,
|
|
569
|
+
fq_value: String(row.term_id),
|
|
570
|
+
name: label,
|
|
571
|
+
category: row.ontology_label
|
|
572
|
+
});
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
return (
|
|
576
|
+
<div className="gsea-view gsea-layout">
|
|
577
|
+
<div className="gsea-layout-tree" style={{ flex: `0 0 ${treeWidth}px` }}>
|
|
578
|
+
<SpeciesTree
|
|
579
|
+
taxonomy={grameneTaxonomy}
|
|
580
|
+
grameneMaps={grameneMaps}
|
|
581
|
+
taxa={taxa}
|
|
582
|
+
activeTaxon={activeTaxon}
|
|
583
|
+
onSelect={tid => doSetGseaActiveTaxon(String(tid))}
|
|
584
|
+
/>
|
|
585
|
+
</div>
|
|
586
|
+
<div
|
|
587
|
+
className="gsea-splitter"
|
|
588
|
+
onMouseDown={beginResize}
|
|
589
|
+
title="Drag to resize"
|
|
590
|
+
/>
|
|
591
|
+
<div className="gsea-layout-panel">
|
|
592
|
+
<TaxonPanel
|
|
593
|
+
taxon={activeTaxon ? String(activeTaxon) : null}
|
|
594
|
+
gsea={gsea}
|
|
595
|
+
results={results}
|
|
596
|
+
ui={ui}
|
|
597
|
+
onUiChange={doSetGseaUI}
|
|
598
|
+
onAddFilter={handleAddFilter}
|
|
599
|
+
/>
|
|
600
|
+
</div>
|
|
601
|
+
</div>
|
|
602
|
+
);
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
export default connect(
|
|
606
|
+
'selectGrameneSearch',
|
|
607
|
+
'selectGrameneMaps',
|
|
608
|
+
'selectGrameneTaxonomy',
|
|
609
|
+
'selectGsea',
|
|
610
|
+
'selectGseaUI',
|
|
611
|
+
'selectGseaResults',
|
|
612
|
+
'doSetGseaActiveTaxon',
|
|
613
|
+
'doSetGseaUI',
|
|
614
|
+
'doFetchGseaForeground',
|
|
615
|
+
'doFetchGseaBackground',
|
|
616
|
+
'doAcceptGrameneSuggestion',
|
|
617
|
+
GseaViewCmp
|
|
618
|
+
);
|
|
@@ -42,6 +42,36 @@ const examples = [
|
|
|
42
42
|
]
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
|
+
{
|
|
46
|
+
subsite: {
|
|
47
|
+
maize:1,
|
|
48
|
+
sorghum:1,
|
|
49
|
+
grapevine:1,
|
|
50
|
+
rice:1,
|
|
51
|
+
main:1,
|
|
52
|
+
},
|
|
53
|
+
text: "Which genes are curated in the scientific literature?",
|
|
54
|
+
filters: {
|
|
55
|
+
status: 'init',
|
|
56
|
+
rows: 20,
|
|
57
|
+
operation: 'AND',
|
|
58
|
+
negate: false,
|
|
59
|
+
leftIdx: 0,
|
|
60
|
+
rightIdx: 3,
|
|
61
|
+
children: [
|
|
62
|
+
{
|
|
63
|
+
fq_field: 'capabilities',
|
|
64
|
+
fq_value: 'pubs',
|
|
65
|
+
name: 'publication',
|
|
66
|
+
category: 'Curated',
|
|
67
|
+
leftIdx: 1,
|
|
68
|
+
rightIdx: 2,
|
|
69
|
+
negate: false,
|
|
70
|
+
marked: false
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
},
|
|
45
75
|
{
|
|
46
76
|
subsite: {
|
|
47
77
|
grapevine:1
|