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.
@@ -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 &middot;
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