gramene-search 2.0.2 → 2.0.4
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 +5 -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 +314 -0
- package/dist/index.css.map +1 -1
- package/dist/index.js +2022 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bundles/exprViz.js +422 -0
- package/src/bundles/index.js +2 -1
- package/src/bundles/views.js +18 -12
- package/src/components/exprViz/ExprTable.js +194 -0
- package/src/components/exprViz/ExprVizView.js +294 -0
- package/src/components/exprViz/FieldsModal.js +414 -0
- package/src/components/exprViz/ParallelCoordsPlot.js +314 -0
- package/src/components/exprViz/styles.css +316 -0
- package/src/components/geneSearchUI.js +2 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { connect } from 'redux-bundler-react';
|
|
3
|
+
import { Tabs, Tab, Button, ToggleButton, ToggleButtonGroup } from 'react-bootstrap';
|
|
4
|
+
import FieldsModal from './FieldsModal';
|
|
5
|
+
import ExprTable from './ExprTable';
|
|
6
|
+
import ParallelCoordsPlot from './ParallelCoordsPlot';
|
|
7
|
+
import './styles.css';
|
|
8
|
+
|
|
9
|
+
function speciesTaxonId(tid) {
|
|
10
|
+
const n = +tid;
|
|
11
|
+
return n > 1000000 ? Math.floor(n / 1000) : n;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function genomeName(grameneMaps, tid) {
|
|
15
|
+
if (!grameneMaps) return tid;
|
|
16
|
+
const direct = grameneMaps[tid];
|
|
17
|
+
if (direct && direct.display_name) return direct.display_name;
|
|
18
|
+
const sp = grameneMaps[speciesTaxonId(tid)];
|
|
19
|
+
if (sp && sp.display_name) return sp.display_name;
|
|
20
|
+
return tid;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ExprVizViewCmp = props => {
|
|
24
|
+
const {
|
|
25
|
+
exprVizPivot: pivot,
|
|
26
|
+
exprViz,
|
|
27
|
+
exprVizActiveTaxon: activeTaxon,
|
|
28
|
+
grameneMaps,
|
|
29
|
+
expressionStudies,
|
|
30
|
+
expressionSamples,
|
|
31
|
+
doSetExprVizActiveTaxon,
|
|
32
|
+
doToggleExprVizFieldsModal,
|
|
33
|
+
doFetchExprVizData
|
|
34
|
+
} = props;
|
|
35
|
+
|
|
36
|
+
const studiesFor = tid => {
|
|
37
|
+
if (!expressionStudies) return [];
|
|
38
|
+
return expressionStudies[tid] || expressionStudies[speciesTaxonId(tid)] || [];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const taxa = useMemo(() => {
|
|
42
|
+
const ids = Object.keys(pivot.data || {});
|
|
43
|
+
if (!grameneMaps) return ids;
|
|
44
|
+
return ids.sort((a, b) => {
|
|
45
|
+
const ma = grameneMaps[a] || grameneMaps[speciesTaxonId(a)];
|
|
46
|
+
const mb = grameneMaps[b] || grameneMaps[speciesTaxonId(b)];
|
|
47
|
+
return ((ma && ma.left_index) || 0) - ((mb && mb.left_index) || 0);
|
|
48
|
+
});
|
|
49
|
+
}, [pivot.data, grameneMaps]);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (taxa.length === 0) return;
|
|
53
|
+
if (!activeTaxon || !taxa.includes(String(activeTaxon))) {
|
|
54
|
+
doSetExprVizActiveTaxon(taxa[0]);
|
|
55
|
+
}
|
|
56
|
+
}, [taxa, activeTaxon, doSetExprVizActiveTaxon]);
|
|
57
|
+
|
|
58
|
+
if (pivot.status === 'loading') {
|
|
59
|
+
return <div className="exprviz-view"><em>Loading studies…</em></div>;
|
|
60
|
+
}
|
|
61
|
+
if (pivot.status === 'error') {
|
|
62
|
+
return <div className="exprviz-view"><em>Error: {pivot.error}</em></div>;
|
|
63
|
+
}
|
|
64
|
+
if (taxa.length === 0) {
|
|
65
|
+
return <div className="exprviz-view"><em>No expression studies for current results.</em></div>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="exprviz-view">
|
|
70
|
+
<Tabs
|
|
71
|
+
activeKey={activeTaxon || taxa[0]}
|
|
72
|
+
onSelect={k => doSetExprVizActiveTaxon(k)}
|
|
73
|
+
className="exprviz-tabs"
|
|
74
|
+
>
|
|
75
|
+
{taxa.map(tid => {
|
|
76
|
+
const studies = studiesFor(tid);
|
|
77
|
+
const taxName = genomeName(grameneMaps, tid);
|
|
78
|
+
const geneCount = pivot.data[tid] || 0;
|
|
79
|
+
return (
|
|
80
|
+
<Tab
|
|
81
|
+
key={tid}
|
|
82
|
+
eventKey={tid}
|
|
83
|
+
title={`${taxName} (${studies.length} studies · ${geneCount} genes)`}
|
|
84
|
+
>
|
|
85
|
+
<TaxonPanel
|
|
86
|
+
taxon={tid}
|
|
87
|
+
studies={studies}
|
|
88
|
+
expressionSamples={expressionSamples}
|
|
89
|
+
tabState={exprViz.byTaxon[tid]}
|
|
90
|
+
onOpenFields={() => doToggleExprVizFieldsModal(tid, true)}
|
|
91
|
+
onLoad={() => doFetchExprVizData(tid)}
|
|
92
|
+
onReorder={(next) => props.doReorderExprVizFields(tid, next)}
|
|
93
|
+
onAddRangeQuery={props.doAddGrameneRangeQuery}
|
|
94
|
+
/>
|
|
95
|
+
</Tab>
|
|
96
|
+
);
|
|
97
|
+
})}
|
|
98
|
+
</Tabs>
|
|
99
|
+
<FieldsModal/>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function rowMatchesSelections(row, selections) {
|
|
105
|
+
for (const f of Object.keys(selections)) {
|
|
106
|
+
const v = row[f];
|
|
107
|
+
if (v == null || Array.isArray(v)) return false;
|
|
108
|
+
const n = +v;
|
|
109
|
+
if (!Number.isFinite(n)) return false;
|
|
110
|
+
const [lo, hi] = selections[f];
|
|
111
|
+
if (n < lo || n > hi) return false;
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function fmt(n) {
|
|
117
|
+
if (!Number.isFinite(n)) return String(n);
|
|
118
|
+
const a = Math.abs(n);
|
|
119
|
+
if (a !== 0 && (a < 0.001 || a >= 1e6)) return n.toExponential(3);
|
|
120
|
+
return Number(n.toFixed(4)).toString();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function tsvCell(v) {
|
|
124
|
+
if (v == null) return '';
|
|
125
|
+
if (Array.isArray(v)) return v.join(',');
|
|
126
|
+
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
127
|
+
return s.replace(/[\t\r\n]+/g, ' ');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function downloadTsv(filename, rows, fields) {
|
|
131
|
+
const cols = ['id', 'name', ...fields];
|
|
132
|
+
const headerLabels = ['id', 'name', ...fields.map(f => f.replace(/__expr$/, ''))];
|
|
133
|
+
const lines = [headerLabels.join('\t')];
|
|
134
|
+
for (const r of rows) lines.push(cols.map(c => tsvCell(r[c])).join('\t'));
|
|
135
|
+
const blob = new Blob([lines.join('\n') + '\n'], { type: 'text/tab-separated-values' });
|
|
136
|
+
const url = URL.createObjectURL(blob);
|
|
137
|
+
const a = document.createElement('a');
|
|
138
|
+
a.href = url;
|
|
139
|
+
a.download = filename;
|
|
140
|
+
document.body.appendChild(a);
|
|
141
|
+
a.click();
|
|
142
|
+
document.body.removeChild(a);
|
|
143
|
+
URL.revokeObjectURL(url);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const TaxonPanel = ({ taxon, studies, expressionSamples, tabState, onOpenFields, onLoad, onReorder, onAddRangeQuery }) => {
|
|
147
|
+
const selected = (tabState && tabState.selectedFields) || [];
|
|
148
|
+
const rows = (tabState && tabState.rows) || [];
|
|
149
|
+
const fetchInfo = (tabState && tabState.fetch) || { status: 'idle', total: 0 };
|
|
150
|
+
const [scale, setScale] = useState('linear');
|
|
151
|
+
const [selections, setSelections] = useState({});
|
|
152
|
+
const [clearVersion, setClearVersion] = useState(0);
|
|
153
|
+
const [hoveredId, setHoveredId] = useState(null);
|
|
154
|
+
|
|
155
|
+
const hasBrush = Object.keys(selections).length > 0;
|
|
156
|
+
const filteredRows = useMemo(() => {
|
|
157
|
+
if (!hasBrush) return rows;
|
|
158
|
+
return rows.filter(r => rowMatchesSelections(r, selections));
|
|
159
|
+
}, [rows, selections, hasBrush]);
|
|
160
|
+
|
|
161
|
+
// Drop fields with no numeric data in the loaded rows so empty axes/columns
|
|
162
|
+
// don't clutter the visualization. Selected-but-empty fields stay in the
|
|
163
|
+
// underlying selection so a future load can repopulate them.
|
|
164
|
+
const visibleFields = useMemo(() => {
|
|
165
|
+
if (rows.length === 0 || selected.length === 0) return selected;
|
|
166
|
+
return selected.filter(f => rows.some(r => {
|
|
167
|
+
const v = r[f];
|
|
168
|
+
return v != null && !Array.isArray(v) && Number.isFinite(+v);
|
|
169
|
+
}));
|
|
170
|
+
}, [rows, selected]);
|
|
171
|
+
|
|
172
|
+
const handleReorder = onReorder
|
|
173
|
+
? (newVisibleOrder) => {
|
|
174
|
+
const visibleSet = new Set(newVisibleOrder);
|
|
175
|
+
const hidden = selected.filter(f => !visibleSet.has(f));
|
|
176
|
+
onReorder([...newVisibleOrder, ...hidden]);
|
|
177
|
+
}
|
|
178
|
+
: undefined;
|
|
179
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (rows.length === 0 && hasBrush) {
|
|
182
|
+
setSelections({});
|
|
183
|
+
setClearVersion(v => v + 1);
|
|
184
|
+
}
|
|
185
|
+
}, [rows.length, hasBrush]);
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<div className="exprviz-tab-panel">
|
|
189
|
+
<div className="exprviz-toolbar">
|
|
190
|
+
<Button size="sm" onClick={onOpenFields}>
|
|
191
|
+
Select fields ({selected.length} selected, {studies.length} studies)
|
|
192
|
+
</Button>
|
|
193
|
+
<Button
|
|
194
|
+
size="sm"
|
|
195
|
+
variant="primary"
|
|
196
|
+
disabled={selected.length === 0 || fetchInfo.status === 'loading'}
|
|
197
|
+
onClick={onLoad}
|
|
198
|
+
>
|
|
199
|
+
{fetchInfo.status === 'loading' ? 'Loading…' : 'Load data'}
|
|
200
|
+
</Button>
|
|
201
|
+
<ToggleButtonGroup
|
|
202
|
+
type="radio"
|
|
203
|
+
name={`exprviz-scale-${taxon}`}
|
|
204
|
+
size="sm"
|
|
205
|
+
value={scale}
|
|
206
|
+
onChange={setScale}
|
|
207
|
+
>
|
|
208
|
+
<ToggleButton id={`exprviz-scale-${taxon}-lin`} value="linear" variant="outline-secondary">Linear</ToggleButton>
|
|
209
|
+
<ToggleButton id={`exprviz-scale-${taxon}-log`} value="log" variant="outline-secondary">Log</ToggleButton>
|
|
210
|
+
</ToggleButtonGroup>
|
|
211
|
+
<Button
|
|
212
|
+
size="sm"
|
|
213
|
+
variant="outline-secondary"
|
|
214
|
+
disabled={!hasBrush}
|
|
215
|
+
onClick={() => { setClearVersion(v => v + 1); setSelections({}); }}
|
|
216
|
+
>
|
|
217
|
+
Clear brushes
|
|
218
|
+
</Button>
|
|
219
|
+
<Button
|
|
220
|
+
size="sm"
|
|
221
|
+
variant="outline-secondary"
|
|
222
|
+
disabled={filteredRows.length === 0 || visibleFields.length === 0}
|
|
223
|
+
onClick={() => downloadTsv(`expression_${taxon}.tsv`, filteredRows, visibleFields)}
|
|
224
|
+
title="Download the visible rows and columns as tab-delimited text"
|
|
225
|
+
>
|
|
226
|
+
Download TSV
|
|
227
|
+
</Button>
|
|
228
|
+
<Button
|
|
229
|
+
size="sm"
|
|
230
|
+
variant="success"
|
|
231
|
+
disabled={!hasBrush || !onAddRangeQuery}
|
|
232
|
+
onClick={() => {
|
|
233
|
+
const terms = Object.keys(selections).map(field => {
|
|
234
|
+
const [lo, hi] = selections[field];
|
|
235
|
+
return {
|
|
236
|
+
category: 'Expression',
|
|
237
|
+
name: `${field}: ${fmt(lo)}–${fmt(hi)}`,
|
|
238
|
+
fq_field: field,
|
|
239
|
+
fq_value: `[${lo} TO ${hi}]`
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
onAddRangeQuery(terms);
|
|
243
|
+
}}
|
|
244
|
+
title="Add brush ranges as an AND-conjunction filter on the search"
|
|
245
|
+
>
|
|
246
|
+
Apply as filter
|
|
247
|
+
</Button>
|
|
248
|
+
<span className="exprviz-status">
|
|
249
|
+
{hasBrush ? `${filteredRows.length} of ${rows.length}` : rows.length}
|
|
250
|
+
{fetchInfo.total ? ` / ${fetchInfo.total}` : ''} genes
|
|
251
|
+
{hasBrush ? ' (brushed)' : ' loaded'}
|
|
252
|
+
</span>
|
|
253
|
+
</div>
|
|
254
|
+
<div className="exprviz-body">
|
|
255
|
+
<div className="exprviz-plot">
|
|
256
|
+
<ParallelCoordsPlot
|
|
257
|
+
rows={rows}
|
|
258
|
+
fields={visibleFields}
|
|
259
|
+
scale={scale}
|
|
260
|
+
onBrushChange={setSelections}
|
|
261
|
+
onReorder={handleReorder}
|
|
262
|
+
clearVersion={clearVersion}
|
|
263
|
+
hoveredId={hoveredId}
|
|
264
|
+
/>
|
|
265
|
+
</div>
|
|
266
|
+
<div className="exprviz-table">
|
|
267
|
+
<ExprTable
|
|
268
|
+
rows={filteredRows}
|
|
269
|
+
fields={visibleFields}
|
|
270
|
+
onReorder={handleReorder}
|
|
271
|
+
studies={studies}
|
|
272
|
+
expressionSamples={expressionSamples}
|
|
273
|
+
onHoverRow={setHoveredId}
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export default connect(
|
|
282
|
+
'selectExprViz',
|
|
283
|
+
'selectExprVizPivot',
|
|
284
|
+
'selectExprVizActiveTaxon',
|
|
285
|
+
'selectGrameneMaps',
|
|
286
|
+
'selectExpressionStudies',
|
|
287
|
+
'selectExpressionSamples',
|
|
288
|
+
'doSetExprVizActiveTaxon',
|
|
289
|
+
'doToggleExprVizFieldsModal',
|
|
290
|
+
'doFetchExprVizData',
|
|
291
|
+
'doReorderExprVizFields',
|
|
292
|
+
'doAddGrameneRangeQuery',
|
|
293
|
+
ExprVizViewCmp
|
|
294
|
+
);
|