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
|
@@ -1,81 +1,73 @@
|
|
|
1
|
+
import { getConfiguredCache } from 'money-clip';
|
|
2
|
+
|
|
1
3
|
const ONT_KEYS = ['GO', 'PO', 'TO', 'domains'];
|
|
2
|
-
|
|
4
|
+
|
|
5
|
+
// Dedicated IndexedDB store for ontology records. The full term set per
|
|
6
|
+
// ontology is stable enough to keep around indefinitely (default maxAge of
|
|
7
|
+
// `Infinity`), and we don't want it to share the short TTL of the app-wide
|
|
8
|
+
// cache configured in demo.js.
|
|
9
|
+
const cache = getConfiguredCache({
|
|
10
|
+
version: 1,
|
|
11
|
+
name: 'gramene_ontologies'
|
|
12
|
+
});
|
|
3
13
|
|
|
4
14
|
const inflight = {};
|
|
5
15
|
|
|
6
16
|
const ontologies = {
|
|
7
17
|
name: 'ontologies',
|
|
18
|
+
|
|
8
19
|
getReducer: () => {
|
|
9
|
-
const initialState = { GO: {}, PO: {}, TO: {}, domains: {} };
|
|
20
|
+
const initialState = { GO: {}, PO: {}, TO: {}, domains: {}, loaded: {} };
|
|
10
21
|
return (state = initialState, { type, payload }) => {
|
|
11
22
|
switch (type) {
|
|
12
|
-
case '
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
return { ...state, [key]: bucket };
|
|
19
|
-
}
|
|
20
|
-
case 'ONTOLOGY_RECORDS_RECEIVED': {
|
|
21
|
-
const { key, records } = payload;
|
|
22
|
-
return { ...state, [key]: { ...state[key], ...records } };
|
|
23
|
-
}
|
|
23
|
+
case 'ONTOLOGY_BULK_LOADED':
|
|
24
|
+
return {
|
|
25
|
+
...state,
|
|
26
|
+
[payload.key]: payload.records,
|
|
27
|
+
loaded: { ...state.loaded, [payload.key]: true }
|
|
28
|
+
};
|
|
24
29
|
default:
|
|
25
30
|
return state;
|
|
26
31
|
}
|
|
27
32
|
};
|
|
28
33
|
},
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
// Signature kept for backward compatibility — the `ids` argument is
|
|
36
|
+
// ignored. On first call per ontology key we load the full term set
|
|
37
|
+
// (cache hit if available, otherwise `${api}/${key}?rows=-1`) and
|
|
38
|
+
// dispatch a single bulk load. Subsequent calls are no-ops.
|
|
39
|
+
doEnsureOntologyRecords: (key, _ids) => ({ dispatch, store }) => {
|
|
31
40
|
if (!ONT_KEYS.includes(key)) return Promise.resolve();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
for (const id of ids) {
|
|
36
|
-
if (id == null) continue;
|
|
37
|
-
const idNum = +id;
|
|
38
|
-
if (!existing.hasOwnProperty(idNum) && !(inflight[key] && inflight[key].has(idNum))) {
|
|
39
|
-
missing.push(idNum);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if (missing.length === 0) return Promise.resolve();
|
|
43
|
-
|
|
44
|
-
if (!inflight[key]) inflight[key] = new Set();
|
|
45
|
-
for (const id of missing) inflight[key].add(id);
|
|
46
|
-
dispatch({ type: 'ONTOLOGY_RECORDS_REQUESTED', payload: { key, ids: missing } });
|
|
47
|
-
|
|
48
|
-
const api = store.selectGrameneAPI();
|
|
49
|
-
const batches = [];
|
|
50
|
-
for (let i = 0; i < missing.length; i += BATCH_SIZE) {
|
|
51
|
-
batches.push(missing.slice(i, i + BATCH_SIZE));
|
|
52
|
-
}
|
|
41
|
+
const state = store.selectOntologies();
|
|
42
|
+
if (state.loaded && state.loaded[key]) return Promise.resolve();
|
|
43
|
+
if (inflight[key]) return inflight[key];
|
|
53
44
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
45
|
+
inflight[key] = cache.get(key)
|
|
46
|
+
.then(cached => {
|
|
47
|
+
if (cached) {
|
|
48
|
+
dispatch({ type: 'ONTOLOGY_BULK_LOADED', payload: { key, records: cached } });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const api = store.selectGrameneAPI();
|
|
52
|
+
return fetch(`${api}/${key}?rows=-1`)
|
|
53
|
+
.then(r => r.json())
|
|
54
|
+
.then(docs => {
|
|
55
|
+
const records = {};
|
|
56
|
+
for (const d of (docs || [])) {
|
|
57
|
+
if (d && d._id != null) records[d._id] = d;
|
|
58
|
+
}
|
|
59
|
+
dispatch({ type: 'ONTOLOGY_BULK_LOADED', payload: { key, records } });
|
|
60
|
+
cache.set(key, records).catch(e => console.warn('Failed to cache ontology', key, e));
|
|
61
|
+
});
|
|
62
|
+
})
|
|
63
|
+
.catch(err => {
|
|
64
|
+
console.error(`Failed to load ontology ${key}`, err);
|
|
65
|
+
})
|
|
66
|
+
.finally(() => {
|
|
67
|
+
delete inflight[key];
|
|
68
|
+
});
|
|
77
69
|
|
|
78
|
-
return
|
|
70
|
+
return inflight[key];
|
|
79
71
|
},
|
|
80
72
|
|
|
81
73
|
selectOntologies: state => state.ontologies
|
|
@@ -268,12 +268,12 @@ function buildSpeciesNameIndex(grameneMaps) {
|
|
|
268
268
|
function fieldExperimentId(name) {
|
|
269
269
|
let m = name.match(/^(\w+?)_g\d+__expr$/);
|
|
270
270
|
if (m) return m[1].replace(/_/g, '-');
|
|
271
|
-
m = name.match(/^(\w+?)_g\d+_g\d+_(pval|
|
|
271
|
+
m = name.match(/^(\w+?)_g\d+_g\d+_(pval|l2fc)_attr_[a-z]$/);
|
|
272
272
|
if (m) return m[1].replace(/_/g, '-');
|
|
273
273
|
return null;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
const STAT_RANK = { l2fc: 0,
|
|
276
|
+
const STAT_RANK = { l2fc: 0, pval: 1 };
|
|
277
277
|
|
|
278
278
|
function collapseDiffExprInSubgroups(subgroups, fieldsOut, collator) {
|
|
279
279
|
for (const taxGroup of subgroups) {
|
|
@@ -300,7 +300,7 @@ function collapseDiffExprInSubgroups(subgroups, fieldsOut, collator) {
|
|
|
300
300
|
});
|
|
301
301
|
const rep = names[0];
|
|
302
302
|
const repEntry = fieldsOut[rep];
|
|
303
|
-
const label = (repEntry.label || rep).replace(/\s+\((?:pval|
|
|
303
|
+
const label = (repEntry.label || rep).replace(/\s+\((?:pval|l2fc)\)/, '');
|
|
304
304
|
fieldsOut[rep] = { ...repEntry, label, linkedFields: names.slice() };
|
|
305
305
|
newFields.push(rep);
|
|
306
306
|
}
|
|
@@ -577,14 +577,13 @@ grameneFieldCatalog.selectFieldCatalog = createSelector(
|
|
|
577
577
|
const present = new Set(fieldNames);
|
|
578
578
|
// Build a synthetic doc from the discovered field names; values carry the
|
|
579
579
|
// right JS type so inferType picks the correct multiValued/type (arrays
|
|
580
|
-
// for multi-valued fields, scalars otherwise). Derive pval
|
|
581
|
-
//
|
|
580
|
+
// for multi-valued fields, scalars otherwise). Derive the pval companion
|
|
581
|
+
// from every l2fc field.
|
|
582
582
|
const doc = {};
|
|
583
583
|
for (const name of present) {
|
|
584
584
|
doc[name] = synthesizedValue(name);
|
|
585
585
|
if (/_l2fc_attr_f$/.test(name)) {
|
|
586
586
|
doc[name.replace('_l2fc_', '_pval_')] = 0;
|
|
587
|
-
doc[name.replace('_l2fc_', '_logfc_')] = 0;
|
|
588
587
|
}
|
|
589
588
|
}
|
|
590
589
|
const catalog = buildCatalog([doc]);
|
package/src/bundles/views.js
CHANGED
|
@@ -29,6 +29,12 @@ const grameneViews = {
|
|
|
29
29
|
show: 'off',
|
|
30
30
|
shouldScroll: false
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
id: 'gsea',
|
|
34
|
+
name: 'Gene set enrichment',
|
|
35
|
+
show: 'off',
|
|
36
|
+
shouldScroll: false
|
|
37
|
+
},
|
|
32
38
|
{
|
|
33
39
|
id: 'userLists',
|
|
34
40
|
name: 'User Gene Lists',
|
|
@@ -121,7 +127,7 @@ const grameneViews = {
|
|
|
121
127
|
const touched = raw.touched || {};
|
|
122
128
|
const numFound = (search && search.response && search.response.numFound) || 0;
|
|
123
129
|
const hasFilters = !!(filters && filters.rightIdx > 1);
|
|
124
|
-
const resultDependentIds = new Set(['taxonomy', 'list', 'export']);
|
|
130
|
+
const resultDependentIds = new Set(['taxonomy', 'list', 'export', 'exprViz', 'gsea']);
|
|
125
131
|
const autoDisable = (numFound === 0) || !hasFilters;
|
|
126
132
|
const hasFirebase = !!(config && config.firebaseConfig);
|
|
127
133
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const EXPR_FIELD_RE = /^(E[-_][A-Za-z0-9_-]+?)_g(\d+)__expr$/;
|
|
2
|
-
const DIFFEXPR_FIELD_RE = /^(E[-_][A-Za-z0-9_-]+?)_g(\d+)_g(\d+)_(pval|
|
|
2
|
+
const DIFFEXPR_FIELD_RE = /^(E[-_][A-Za-z0-9_-]+?)_g(\d+)_g(\d+)_(pval|l2fc)_attr_([a-z])$/;
|
|
3
3
|
|
|
4
4
|
export const EXPRESSION_EXTRA_COLUMNS = [
|
|
5
5
|
'experiment',
|
|
@@ -132,7 +132,7 @@ export function resolveDiffExpressionForDoc(doc, diffExpressionFields, expressio
|
|
|
132
132
|
byContrast.set(key, entry);
|
|
133
133
|
}
|
|
134
134
|
if (parsed.stat === 'pval') entry.pval = val;
|
|
135
|
-
else entry.l2fc = val;
|
|
135
|
+
else entry.l2fc = val;
|
|
136
136
|
}
|
|
137
137
|
const maxPval = cutoffs && cutoffs.diffMaxPval;
|
|
138
138
|
const maxPvalActive = maxPval !== null && maxPval !== undefined && maxPval !== '' && Number.isFinite(+maxPval);
|
|
@@ -8,13 +8,24 @@ const baseColDefs = [
|
|
|
8
8
|
{ field: 'name', headerName: 'Name', pinned: 'left', width: 140, suppressMovable: true }
|
|
9
9
|
];
|
|
10
10
|
|
|
11
|
+
// Hoisted so the reference is stable across renders. ag-grid otherwise sees a
|
|
12
|
+
// "new" defaultColDef on every parent re-render (e.g. when hovering a row
|
|
13
|
+
// triggers setHoveredId in the parent) and re-applies column state, which
|
|
14
|
+
// snaps any user-resized columns back to their original widths.
|
|
15
|
+
const DEFAULT_COL_DEF = {
|
|
16
|
+
resizable: true,
|
|
17
|
+
sortable: true,
|
|
18
|
+
filter: false,
|
|
19
|
+
suppressMenu: true
|
|
20
|
+
};
|
|
21
|
+
|
|
11
22
|
function arraysEqual(a, b) {
|
|
12
23
|
if (a.length !== b.length) return false;
|
|
13
24
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
14
25
|
return true;
|
|
15
26
|
}
|
|
16
27
|
|
|
17
|
-
function buildFieldInfo(fields, studies, expressionSamples) {
|
|
28
|
+
export function buildFieldInfo(fields, studies, expressionSamples) {
|
|
18
29
|
const info = {};
|
|
19
30
|
if (!fields || !studies || !expressionSamples) return info;
|
|
20
31
|
const wanted = new Set(fields);
|
|
@@ -322,7 +333,7 @@ const ExprTable = ({ rows, fields, onReorder, studies, expressionSamples, onHove
|
|
|
322
333
|
<AgGridReact
|
|
323
334
|
rowData={rows}
|
|
324
335
|
columnDefs={columnDefs}
|
|
325
|
-
defaultColDef={
|
|
336
|
+
defaultColDef={DEFAULT_COL_DEF}
|
|
326
337
|
animateRows={false}
|
|
327
338
|
suppressFieldDotNotation={true}
|
|
328
339
|
suppressDragLeaveHidesColumns={true}
|
|
@@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
2
2
|
import { connect } from 'redux-bundler-react';
|
|
3
3
|
import { Tabs, Tab, Button, ToggleButton, ToggleButtonGroup } from 'react-bootstrap';
|
|
4
4
|
import FieldsModal from './FieldsModal';
|
|
5
|
-
import ExprTable from './ExprTable';
|
|
5
|
+
import ExprTable, { buildFieldInfo } from './ExprTable';
|
|
6
6
|
import ParallelCoordsPlot from './ParallelCoordsPlot';
|
|
7
7
|
import './styles.css';
|
|
8
8
|
|
|
@@ -193,11 +193,49 @@ function tsvCell(v) {
|
|
|
193
193
|
return s.replace(/[\t\r\n]+/g, ' ');
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
|
|
196
|
+
// Mirror the on-screen table header in the TSV: one row per metadata level
|
|
197
|
+
// the table is showing (Study, then one row per distinct factor type, then
|
|
198
|
+
// one row per distinct characteristic type), followed by the leaf header
|
|
199
|
+
// (Gene ID / Name / per-sample group). The first two columns are repurposed
|
|
200
|
+
// to carry the row category and the row's specific name, matching the
|
|
201
|
+
// pinned-column labels in ExprTable.
|
|
202
|
+
function downloadTsv(filename, rows, fields, studies, expressionSamples) {
|
|
197
203
|
const cols = ['id', 'name', ...fields];
|
|
198
|
-
const
|
|
199
|
-
|
|
204
|
+
const fieldInfo = buildFieldInfo(fields, studies, expressionSamples);
|
|
205
|
+
|
|
206
|
+
const factorTypes = new Set();
|
|
207
|
+
const charTypes = new Set();
|
|
208
|
+
for (const f of fields) {
|
|
209
|
+
const info = fieldInfo[f];
|
|
210
|
+
if (!info) continue;
|
|
211
|
+
Object.keys(info.factors || {}).forEach(t => factorTypes.add(t));
|
|
212
|
+
Object.keys(info.characteristics || {}).forEach(t => charTypes.add(t));
|
|
213
|
+
}
|
|
214
|
+
const factorTypeList = Array.from(factorTypes).sort();
|
|
215
|
+
const charTypeList = Array.from(charTypes).sort();
|
|
216
|
+
|
|
217
|
+
const metaRow = (cat, label, getValue) => {
|
|
218
|
+
const cells = [cat, label];
|
|
219
|
+
for (const f of fields) cells.push(tsvCell(getValue(fieldInfo[f] || {})));
|
|
220
|
+
return cells.join('\t');
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const lines = [];
|
|
224
|
+
lines.push(metaRow('Study', 'Title', info => info.studyDescription || ''));
|
|
225
|
+
for (const t of factorTypeList) {
|
|
226
|
+
lines.push(metaRow('Factor', t, info => (info.factors && info.factors[t]) || ''));
|
|
227
|
+
}
|
|
228
|
+
for (const t of charTypeList) {
|
|
229
|
+
lines.push(metaRow('Characteristic', t, info => (info.characteristics && info.characteristics[t]) || ''));
|
|
230
|
+
}
|
|
231
|
+
// Leaf header — column ids for the data rows.
|
|
232
|
+
lines.push(['Gene ID', 'Name', ...fields.map(f => {
|
|
233
|
+
const info = fieldInfo[f];
|
|
234
|
+
return (info && info.group) || f.replace(/__expr$/, '');
|
|
235
|
+
})].join('\t'));
|
|
236
|
+
|
|
200
237
|
for (const r of rows) lines.push(cols.map(c => tsvCell(r[c])).join('\t'));
|
|
238
|
+
|
|
201
239
|
const blob = new Blob([lines.join('\n') + '\n'], { type: 'text/tab-separated-values' });
|
|
202
240
|
const url = URL.createObjectURL(blob);
|
|
203
241
|
const a = document.createElement('a');
|
|
@@ -317,7 +355,7 @@ const TaxonPanel = ({ taxon, studies, expressionSamples, tabState, onOpenFields,
|
|
|
317
355
|
size="sm"
|
|
318
356
|
variant="outline-secondary"
|
|
319
357
|
disabled={filteredRows.length === 0 || visibleFields.length === 0}
|
|
320
|
-
onClick={() => downloadTsv(`expression_${taxon}.tsv`, filteredRows, visibleFields)}
|
|
358
|
+
onClick={() => downloadTsv(`expression_${taxon}.tsv`, filteredRows, visibleFields, studies, expressionSamples)}
|
|
321
359
|
title="Download the visible rows and columns as tab-delimited text"
|
|
322
360
|
>
|
|
323
361
|
Download TSV
|
|
@@ -13,6 +13,7 @@ import Expression from './results/Expression'
|
|
|
13
13
|
import UserGeneLists from './results/UserGeneLists'
|
|
14
14
|
import ExporterView from './exporter/ExporterView'
|
|
15
15
|
import ExprVizView from './exprViz/ExprVizView'
|
|
16
|
+
import GSEA from './results/GSEA'
|
|
16
17
|
import Auth from './Auth'
|
|
17
18
|
import ReactGA from 'react-ga4'
|
|
18
19
|
import './styles.css';
|
|
@@ -42,6 +43,7 @@ const inventory = {
|
|
|
42
43
|
attribs: GeneAttribs,
|
|
43
44
|
expression: Expression,
|
|
44
45
|
exprViz: ExprVizView,
|
|
46
|
+
gsea: GSEA,
|
|
45
47
|
userLists: UserGeneLists,
|
|
46
48
|
export: ExporterView
|
|
47
49
|
};
|