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
package/package.json
CHANGED
package/src/bundles/docs.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
import { getConfiguredCache } from 'money-clip';
|
|
2
|
+
|
|
3
|
+
// Dedicated IndexedDB store for the full pathway corpus, persisted
|
|
4
|
+
// indefinitely (default maxAge of `Infinity`). The pathway set is small
|
|
5
|
+
// and stable enough to keep around across sessions, so we bulk-load it
|
|
6
|
+
// once with `?rows=-1` and reuse it instead of issuing per-id requests.
|
|
7
|
+
const pathwayCache = getConfiguredCache({
|
|
8
|
+
version: 1,
|
|
9
|
+
name: 'gramene_pathways'
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
let pathwaysBulkPromise = null;
|
|
13
|
+
|
|
1
14
|
const grameneDocs = {
|
|
2
15
|
name: 'grameneDocs',
|
|
3
16
|
getReducer: () => {
|
|
@@ -201,22 +214,36 @@ const grameneDocs = {
|
|
|
201
214
|
})
|
|
202
215
|
}
|
|
203
216
|
},
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
217
|
+
// Signature kept for backward compatibility; the `ids` argument is
|
|
218
|
+
// ignored. On first call we bulk-load every pathway record from the
|
|
219
|
+
// dedicated IndexedDB cache (or `${api}/pathways?rows=-1` on miss),
|
|
220
|
+
// dispatch a single GRAMENE_PATHWAYS_RECEIVED, and short-circuit all
|
|
221
|
+
// subsequent calls.
|
|
222
|
+
doRequestGramenePathways: _ids => ({dispatch, store}) => {
|
|
223
|
+
if (pathwaysBulkPromise) return pathwaysBulkPromise;
|
|
224
|
+
|
|
225
|
+
pathwaysBulkPromise = pathwayCache.get('all')
|
|
226
|
+
.then(cached => {
|
|
227
|
+
if (cached) {
|
|
228
|
+
dispatch({type: 'GRAMENE_PATHWAYS_RECEIVED', payload: cached});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
return fetch(`${store.selectGrameneAPI()}/pathways?rows=-1`)
|
|
232
|
+
.then(res => res.json())
|
|
233
|
+
.then(res => {
|
|
234
|
+
const pathways = {};
|
|
235
|
+
(res || []).forEach(p => {
|
|
236
|
+
if (p && p._id != null) pathways[p._id] = p;
|
|
237
|
+
});
|
|
238
|
+
dispatch({type: 'GRAMENE_PATHWAYS_RECEIVED', payload: pathways});
|
|
239
|
+
pathwayCache.set('all', pathways).catch(e => console.warn('Failed to cache pathways', e));
|
|
216
240
|
});
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
241
|
+
})
|
|
242
|
+
.catch(err => {
|
|
243
|
+
console.error('Failed to load pathways', err);
|
|
244
|
+
pathwaysBulkPromise = null; // allow retry on next call
|
|
245
|
+
});
|
|
246
|
+
return pathwaysBulkPromise;
|
|
220
247
|
},
|
|
221
248
|
doRequestExpressionStudies: id => ({dispatch, store}) => {
|
|
222
249
|
fetch(`${store.selectGrameneAPI()}/experiments?rows=-1`)
|
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import { createSelector } from 'redux-bundler';
|
|
2
|
+
|
|
3
|
+
// Gene Set Enrichment Analysis bundle.
|
|
4
|
+
//
|
|
5
|
+
// For each species tab we run:
|
|
6
|
+
// foreground: q=<filters>&fq=taxon_id:<tid>&rows=0 + facet on six __ancestors fields
|
|
7
|
+
// background: q=taxon_id:<tid>&rows=0 + same facets (cached forever per tid)
|
|
8
|
+
//
|
|
9
|
+
// Per-ontology denominators (n_ont, N_ont) come from the root term's facet
|
|
10
|
+
// count, since every annotated gene carries the root in __ancestors. Roots
|
|
11
|
+
// are identified once ontology records are loaded by picking the ancestor
|
|
12
|
+
// (or self) with the highest background facet count — this works for
|
|
13
|
+
// multi-rooted GO (BP/MF/CC) and for the rice-rooted pathway tree.
|
|
14
|
+
//
|
|
15
|
+
// Enrichment uses the upper-tail Fisher exact (hypergeometric); p-values
|
|
16
|
+
// are corrected per ontology with Benjamini–Hochberg. The "most-specific"
|
|
17
|
+
// collapse is applied AFTER BH so a parent term that's significant on its
|
|
18
|
+
// own is preserved even when none of its children clear the cutoff.
|
|
19
|
+
|
|
20
|
+
const ONTOLOGIES = [
|
|
21
|
+
{ key: 'GO', field: 'GO__ancestors', label: 'Gene Ontology', bucket: 'GO' },
|
|
22
|
+
{ key: 'PO', field: 'PO__ancestors', label: 'Plant Ontology', bucket: 'PO' },
|
|
23
|
+
{ key: 'TO', field: 'TO__ancestors', label: 'Trait Ontology', bucket: 'TO' },
|
|
24
|
+
{ key: 'QTL_TO', field: 'QTL_TO__ancestors', label: 'QTL Traits (TO)', bucket: 'TO' },
|
|
25
|
+
{ key: 'domains', field: 'domains__ancestors', label: 'InterPro Domains', bucket: 'domains' },
|
|
26
|
+
{ key: 'pathways', field: 'pathways__ancestors', label: 'Pathways', bucket: null }
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const FACET_PARAMS = ONTOLOGIES.map(o =>
|
|
30
|
+
`facet.field=${encodeURIComponent(`{!facet.limit=10000 facet.mincount=1 key=${o.key}}${o.field}`)}`
|
|
31
|
+
).join('&');
|
|
32
|
+
|
|
33
|
+
const fgPending = {};
|
|
34
|
+
const bgPending = {};
|
|
35
|
+
|
|
36
|
+
function fgSig(q, taxon) { return `${q}|${taxon}`; }
|
|
37
|
+
function bgSig(taxon) { return `bg|${taxon}`; }
|
|
38
|
+
|
|
39
|
+
function parseFacets(json) {
|
|
40
|
+
const out = {};
|
|
41
|
+
const ff = (json && json.facet_counts && json.facet_counts.facet_fields) || {};
|
|
42
|
+
for (const o of ONTOLOGIES) {
|
|
43
|
+
const arr = ff[o.key] || [];
|
|
44
|
+
const map = {};
|
|
45
|
+
for (let i = 0; i < arr.length; i += 2) {
|
|
46
|
+
map[+arr[i]] = +arr[i + 1];
|
|
47
|
+
}
|
|
48
|
+
out[o.key] = map;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const gsea = {
|
|
54
|
+
name: 'gsea',
|
|
55
|
+
|
|
56
|
+
// Background facet counts depend only on the species — they're invariant
|
|
57
|
+
// across filter changes and across sessions, so we persist whenever a bg
|
|
58
|
+
// fetch completes. Foreground state piggybacks on the same write but is
|
|
59
|
+
// self-invalidated by the signature check in the reactor.
|
|
60
|
+
persistActions: ['GSEA_BG_SUCCEEDED'],
|
|
61
|
+
|
|
62
|
+
getReducer: () => {
|
|
63
|
+
const initialState = {
|
|
64
|
+
activeTaxon: null,
|
|
65
|
+
byTaxon: {},
|
|
66
|
+
ui: {
|
|
67
|
+
pAdjCutoff: 0.05,
|
|
68
|
+
minK: 2,
|
|
69
|
+
mostSpecific: true,
|
|
70
|
+
ontology: 'all',
|
|
71
|
+
search: ''
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function ensureTaxon(state, tid) {
|
|
76
|
+
if (state.byTaxon[tid]) return state;
|
|
77
|
+
return {
|
|
78
|
+
...state,
|
|
79
|
+
byTaxon: {
|
|
80
|
+
...state.byTaxon,
|
|
81
|
+
[tid]: {
|
|
82
|
+
fg: { status: 'idle', signature: null, requestId: 0, terms: null, numFound: 0, error: null },
|
|
83
|
+
bg: { status: 'idle', signature: null, requestId: 0, terms: null, numFound: 0, error: null }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (state = initialState, { type, payload }) => {
|
|
90
|
+
switch (type) {
|
|
91
|
+
case 'GSEA_ACTIVE_TAXON_SET':
|
|
92
|
+
return { ...ensureTaxon(state, payload), activeTaxon: payload };
|
|
93
|
+
|
|
94
|
+
case 'GSEA_UI_SET':
|
|
95
|
+
return { ...state, ui: { ...state.ui, ...(payload || {}) } };
|
|
96
|
+
|
|
97
|
+
case 'GSEA_FG_STARTED': {
|
|
98
|
+
const next = ensureTaxon(state, payload.taxon);
|
|
99
|
+
const t = next.byTaxon[payload.taxon];
|
|
100
|
+
return {
|
|
101
|
+
...next,
|
|
102
|
+
byTaxon: {
|
|
103
|
+
...next.byTaxon,
|
|
104
|
+
[payload.taxon]: {
|
|
105
|
+
...t,
|
|
106
|
+
fg: { status: 'loading', signature: payload.signature, requestId: payload.requestId, terms: t.fg.terms, numFound: t.fg.numFound, error: null }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
case 'GSEA_FG_SUCCEEDED': {
|
|
112
|
+
const t = state.byTaxon[payload.taxon];
|
|
113
|
+
if (!t || payload.requestId !== t.fg.requestId) return state;
|
|
114
|
+
return {
|
|
115
|
+
...state,
|
|
116
|
+
byTaxon: {
|
|
117
|
+
...state.byTaxon,
|
|
118
|
+
[payload.taxon]: { ...t, fg: { ...t.fg, status: 'ready', terms: payload.terms, numFound: payload.numFound } }
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
case 'GSEA_FG_FAILED': {
|
|
123
|
+
const t = state.byTaxon[payload.taxon];
|
|
124
|
+
if (!t || payload.requestId !== t.fg.requestId) return state;
|
|
125
|
+
return {
|
|
126
|
+
...state,
|
|
127
|
+
byTaxon: {
|
|
128
|
+
...state.byTaxon,
|
|
129
|
+
[payload.taxon]: { ...t, fg: { ...t.fg, status: 'error', error: payload.error } }
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case 'GSEA_BG_STARTED': {
|
|
135
|
+
const next = ensureTaxon(state, payload.taxon);
|
|
136
|
+
const t = next.byTaxon[payload.taxon];
|
|
137
|
+
return {
|
|
138
|
+
...next,
|
|
139
|
+
byTaxon: {
|
|
140
|
+
...next.byTaxon,
|
|
141
|
+
[payload.taxon]: {
|
|
142
|
+
...t,
|
|
143
|
+
bg: { status: 'loading', signature: payload.signature, requestId: payload.requestId, terms: t.bg.terms, numFound: t.bg.numFound, error: null }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
case 'GSEA_BG_SUCCEEDED': {
|
|
149
|
+
const t = state.byTaxon[payload.taxon];
|
|
150
|
+
if (!t || payload.requestId !== t.bg.requestId) return state;
|
|
151
|
+
return {
|
|
152
|
+
...state,
|
|
153
|
+
byTaxon: {
|
|
154
|
+
...state.byTaxon,
|
|
155
|
+
[payload.taxon]: { ...t, bg: { ...t.bg, status: 'ready', terms: payload.terms, numFound: payload.numFound } }
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
case 'GSEA_BG_FAILED': {
|
|
160
|
+
const t = state.byTaxon[payload.taxon];
|
|
161
|
+
if (!t || payload.requestId !== t.bg.requestId) return state;
|
|
162
|
+
return {
|
|
163
|
+
...state,
|
|
164
|
+
byTaxon: {
|
|
165
|
+
...state.byTaxon,
|
|
166
|
+
[payload.taxon]: { ...t, bg: { ...t.bg, status: 'error', error: payload.error } }
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case 'GRAMENE_SEARCH_CLEARED': {
|
|
172
|
+
// Filters changed — invalidate foreground but keep background
|
|
173
|
+
// (it depends only on taxon).
|
|
174
|
+
const newByTaxon = {};
|
|
175
|
+
for (const tid of Object.keys(state.byTaxon)) {
|
|
176
|
+
const t = state.byTaxon[tid];
|
|
177
|
+
newByTaxon[tid] = {
|
|
178
|
+
...t,
|
|
179
|
+
fg: { status: 'idle', signature: null, requestId: 0, terms: null, numFound: 0, error: null }
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return { ...state, byTaxon: newByTaxon };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
default:
|
|
186
|
+
return state;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
doSetGseaActiveTaxon: tid => ({ dispatch }) =>
|
|
192
|
+
dispatch({ type: 'GSEA_ACTIVE_TAXON_SET', payload: tid }),
|
|
193
|
+
|
|
194
|
+
doSetGseaUI: patch => ({ dispatch }) =>
|
|
195
|
+
dispatch({ type: 'GSEA_UI_SET', payload: patch }),
|
|
196
|
+
|
|
197
|
+
doFetchGseaForeground: taxon => ({ dispatch, store }) => {
|
|
198
|
+
const q = store.selectGrameneFiltersQueryString();
|
|
199
|
+
const signature = fgSig(q, taxon);
|
|
200
|
+
const state = store.selectGsea();
|
|
201
|
+
const t = state.byTaxon[taxon];
|
|
202
|
+
if (t && t.fg.signature === signature && (t.fg.status === 'loading' || t.fg.status === 'ready')) return;
|
|
203
|
+
const requestId = (fgPending[taxon] = (fgPending[taxon] || 0) + 1);
|
|
204
|
+
dispatch({ type: 'GSEA_FG_STARTED', payload: { taxon, signature, requestId } });
|
|
205
|
+
|
|
206
|
+
const api = store.selectGrameneAPI();
|
|
207
|
+
const url = `${api}/search?q=${q}&fq=taxon_id:${taxon}&rows=0&facet=true&${FACET_PARAMS}`;
|
|
208
|
+
fetch(url)
|
|
209
|
+
.then(r => r.json())
|
|
210
|
+
.then(json => {
|
|
211
|
+
if (requestId !== fgPending[taxon]) return;
|
|
212
|
+
const terms = parseFacets(json);
|
|
213
|
+
const numFound = (json && json.response && json.response.numFound) || 0;
|
|
214
|
+
dispatch({ type: 'GSEA_FG_SUCCEEDED', payload: { taxon, requestId, terms, numFound } });
|
|
215
|
+
store.doEnsureGseaTermRecords(taxon);
|
|
216
|
+
})
|
|
217
|
+
.catch(err => {
|
|
218
|
+
if (requestId !== fgPending[taxon]) return;
|
|
219
|
+
dispatch({ type: 'GSEA_FG_FAILED', payload: { taxon, requestId, error: String(err) } });
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
doFetchGseaBackground: taxon => ({ dispatch, store }) => {
|
|
224
|
+
const signature = bgSig(taxon);
|
|
225
|
+
const state = store.selectGsea();
|
|
226
|
+
const t = state.byTaxon[taxon];
|
|
227
|
+
if (t && t.bg.signature === signature && (t.bg.status === 'loading' || t.bg.status === 'ready')) return;
|
|
228
|
+
const requestId = (bgPending[taxon] = (bgPending[taxon] || 0) + 1);
|
|
229
|
+
dispatch({ type: 'GSEA_BG_STARTED', payload: { taxon, signature, requestId } });
|
|
230
|
+
|
|
231
|
+
const api = store.selectGrameneAPI();
|
|
232
|
+
const url = `${api}/search?q=taxon_id:${taxon}&rows=0&facet=true&${FACET_PARAMS}`;
|
|
233
|
+
fetch(url)
|
|
234
|
+
.then(r => r.json())
|
|
235
|
+
.then(json => {
|
|
236
|
+
if (requestId !== bgPending[taxon]) return;
|
|
237
|
+
const terms = parseFacets(json);
|
|
238
|
+
const numFound = (json && json.response && json.response.numFound) || 0;
|
|
239
|
+
dispatch({ type: 'GSEA_BG_SUCCEEDED', payload: { taxon, requestId, terms, numFound } });
|
|
240
|
+
store.doEnsureGseaTermRecords(taxon);
|
|
241
|
+
})
|
|
242
|
+
.catch(err => {
|
|
243
|
+
if (requestId !== bgPending[taxon]) return;
|
|
244
|
+
dispatch({ type: 'GSEA_BG_FAILED', payload: { taxon, requestId, error: String(err) } });
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
doEnsureGseaTermRecords: taxon => ({ store }) => {
|
|
249
|
+
const state = store.selectGsea();
|
|
250
|
+
if (!state.byTaxon[taxon]) return;
|
|
251
|
+
// The ontologies and pathways bundles bulk-load + persist on first
|
|
252
|
+
// request; we just need to nudge each one once.
|
|
253
|
+
const buckets = new Set();
|
|
254
|
+
for (const o of ONTOLOGIES) {
|
|
255
|
+
if (o.bucket) buckets.add(o.bucket);
|
|
256
|
+
}
|
|
257
|
+
for (const k of buckets) {
|
|
258
|
+
store.doEnsureOntologyRecords(k);
|
|
259
|
+
}
|
|
260
|
+
if (store.doRequestGramenePathways) {
|
|
261
|
+
store.doRequestGramenePathways();
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
reactGseaFetch: createSelector(
|
|
266
|
+
'selectGsea',
|
|
267
|
+
'selectGrameneFiltersStatus',
|
|
268
|
+
'selectGrameneFiltersQueryString',
|
|
269
|
+
'selectGrameneViewsOn',
|
|
270
|
+
(gs, fStatus, q, viewsOn) => {
|
|
271
|
+
if (!viewsOn || !viewsOn.has('gsea')) return;
|
|
272
|
+
if (fStatus === 'init') return;
|
|
273
|
+
const tid = gs.activeTaxon;
|
|
274
|
+
if (!tid) return;
|
|
275
|
+
const t = gs.byTaxon[tid];
|
|
276
|
+
if (!t) return;
|
|
277
|
+
const sig = fgSig(q, tid);
|
|
278
|
+
// A 'loading' status from a rehydrated state with no live request is
|
|
279
|
+
// treated as idle — otherwise we'd deadlock waiting on a fetch that
|
|
280
|
+
// ended when the previous tab closed.
|
|
281
|
+
const fgInFlight = t.fg.status === 'loading' && (fgPending[tid] || 0) === t.fg.requestId && t.fg.requestId > 0;
|
|
282
|
+
if (t.fg.signature !== sig && !fgInFlight) {
|
|
283
|
+
return { actionCreator: 'doFetchGseaForeground', args: [tid] };
|
|
284
|
+
}
|
|
285
|
+
const bgInFlight = t.bg.status === 'loading' && (bgPending[tid] || 0) === t.bg.requestId && t.bg.requestId > 0;
|
|
286
|
+
if (t.bg.status !== 'ready' && !bgInFlight) {
|
|
287
|
+
return { actionCreator: 'doFetchGseaBackground', args: [tid] };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
),
|
|
291
|
+
|
|
292
|
+
selectGsea: state => state.gsea,
|
|
293
|
+
selectGseaUI: state => state.gsea.ui,
|
|
294
|
+
selectGseaOntologyDefs: () => ONTOLOGIES,
|
|
295
|
+
|
|
296
|
+
selectGseaResults: createSelector(
|
|
297
|
+
'selectGsea',
|
|
298
|
+
'selectOntologies',
|
|
299
|
+
'selectGramenePathways',
|
|
300
|
+
(gs, ontoBuckets, pathwayDocs) => {
|
|
301
|
+
const tid = gs.activeTaxon;
|
|
302
|
+
if (!tid) return null;
|
|
303
|
+
const t = gs.byTaxon[tid];
|
|
304
|
+
if (!t || !t.fg.terms || !t.bg.terms) return null;
|
|
305
|
+
const ui = gs.ui;
|
|
306
|
+
const out = {};
|
|
307
|
+
for (const o of ONTOLOGIES) {
|
|
308
|
+
const fg = t.fg.terms[o.key] || {};
|
|
309
|
+
const bg = t.bg.terms[o.key] || {};
|
|
310
|
+
const recs = o.key === 'pathways' ? (pathwayDocs || {}) : ((ontoBuckets && ontoBuckets[o.bucket]) || {});
|
|
311
|
+
|
|
312
|
+
// Forest-fallback denominators: when a term is itself a forest root
|
|
313
|
+
// (no parents in `recs` — common for InterPro), root finding returns
|
|
314
|
+
// the term and we'd get fold=1 by construction. Use the maximum
|
|
315
|
+
// counts across the ontology as a proxy for "annotated in this
|
|
316
|
+
// ontology" instead. For ontologies with a true synthetic root,
|
|
317
|
+
// these maxima equal the root counts and the answer is unchanged.
|
|
318
|
+
let maxFg = 0, maxBg = 0;
|
|
319
|
+
for (const idStr of Object.keys(bg)) {
|
|
320
|
+
const v = bg[idStr];
|
|
321
|
+
if (v > maxBg) maxBg = v;
|
|
322
|
+
}
|
|
323
|
+
for (const idStr of Object.keys(fg)) {
|
|
324
|
+
const v = fg[idStr];
|
|
325
|
+
if (v > maxFg) maxFg = v;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const rootCache = {};
|
|
329
|
+
const rootOf = (id) => {
|
|
330
|
+
if (rootCache.hasOwnProperty(id)) return rootCache[id];
|
|
331
|
+
const r = findRoot(o.key, id, recs, bg);
|
|
332
|
+
rootCache[id] = r;
|
|
333
|
+
return r;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const rows = [];
|
|
337
|
+
for (const idStr of Object.keys(fg)) {
|
|
338
|
+
const id = +idStr;
|
|
339
|
+
const k = fg[id];
|
|
340
|
+
const K = bg[id] || 0;
|
|
341
|
+
if (K < k) continue; // bg should always be >= fg
|
|
342
|
+
if (k < ui.minK) continue;
|
|
343
|
+
const termRec = recs && recs[id];
|
|
344
|
+
if (termRec && (termRec.is_obsolete || termRec.obsolete)) continue;
|
|
345
|
+
const root = rootOf(id);
|
|
346
|
+
// If the "root" is the term itself, the term is a forest root in
|
|
347
|
+
// this ontology — fall back to ontology-wide maxima.
|
|
348
|
+
const fellBack = (+root === id);
|
|
349
|
+
const n = fellBack ? maxFg : ((root != null && fg[root]) ? fg[root] : k);
|
|
350
|
+
const N = fellBack ? maxBg : ((root != null && bg[root]) ? bg[root] : K);
|
|
351
|
+
if (n <= 0 || N <= 0) continue;
|
|
352
|
+
if (k > n || K > N) continue;
|
|
353
|
+
const fold = (k / n) / (K / N);
|
|
354
|
+
const p = fisherUpperTail(k, n, K, N);
|
|
355
|
+
rows.push({
|
|
356
|
+
ontology: o.key,
|
|
357
|
+
ontology_label: o.label,
|
|
358
|
+
term_id: id,
|
|
359
|
+
field: o.field,
|
|
360
|
+
k, n, K, N, fold, p, pAdj: 1,
|
|
361
|
+
root,
|
|
362
|
+
denomFallback: fellBack
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// GO is split into its three top-level namespaces (BP / MF / CC)
|
|
367
|
+
// and each is tested as its own ontology — both BH correction and
|
|
368
|
+
// most-specific collapse run within a namespace. We only split once
|
|
369
|
+
// ontology records have arrived and root finding has produced
|
|
370
|
+
// canonical roots; otherwise we'd see a swarm of singleton groups
|
|
371
|
+
// during the brief loading window between bg landing and records
|
|
372
|
+
// being fetched.
|
|
373
|
+
if (o.key === 'GO' && Object.keys(recs).length > 0) {
|
|
374
|
+
const byRoot = {};
|
|
375
|
+
for (const r of rows) {
|
|
376
|
+
const k = String(r.root);
|
|
377
|
+
if (!byRoot[k]) byRoot[k] = [];
|
|
378
|
+
byRoot[k].push(r);
|
|
379
|
+
}
|
|
380
|
+
const rootKeys = Object.keys(byRoot).sort((a, b) => {
|
|
381
|
+
const na = goRootName(recs[+a]) || a;
|
|
382
|
+
const nb = goRootName(recs[+b]) || b;
|
|
383
|
+
return na.localeCompare(nb);
|
|
384
|
+
});
|
|
385
|
+
for (const rootKey of rootKeys) {
|
|
386
|
+
const rootRec = recs[+rootKey];
|
|
387
|
+
const rootName = goRootName(rootRec);
|
|
388
|
+
const sectionKey = `GO:${rootKey}`;
|
|
389
|
+
const sectionLabel = rootName ? `GO: ${titleCase(rootName)}` : `GO: ${rootKey}`;
|
|
390
|
+
out[sectionKey] = finalizeBlock(
|
|
391
|
+
byRoot[rootKey], o.key, o.field, recs, ui, sectionKey, sectionLabel
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
out[o.key] = finalizeBlock(rows, o.key, o.field, recs, ui, o.key, o.label);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
)
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
function goRootName(rec) {
|
|
404
|
+
if (!rec) return '';
|
|
405
|
+
return rec.name || rec.display_name || rec.namespace || '';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function titleCase(s) {
|
|
409
|
+
return String(s).split('_').map(w => w ? w[0].toUpperCase() + w.slice(1) : '').join(' ');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// BH correction + most-specific collapse + metadata + final sort, returning
|
|
413
|
+
// the block descriptor consumed by the view layer.
|
|
414
|
+
function finalizeBlock(rows, ontKey, ontField, recs, ui, sectionKey, sectionLabel) {
|
|
415
|
+
rows.sort((a, b) => a.p - b.p);
|
|
416
|
+
const m = rows.length;
|
|
417
|
+
let prev = 1;
|
|
418
|
+
for (let i = m - 1; i >= 0; i--) {
|
|
419
|
+
const adj = Math.min(prev, rows[i].p * m / (i + 1));
|
|
420
|
+
rows[i].pAdj = adj;
|
|
421
|
+
prev = adj;
|
|
422
|
+
}
|
|
423
|
+
const passing = rows.filter(r => r.pAdj <= ui.pAdjCutoff);
|
|
424
|
+
let display = passing;
|
|
425
|
+
if (ui.mostSpecific) {
|
|
426
|
+
display = collapseToMostSpecific(ontKey, passing, recs);
|
|
427
|
+
}
|
|
428
|
+
for (const r of display) {
|
|
429
|
+
const rec = recs && recs[r.term_id];
|
|
430
|
+
if (rec) {
|
|
431
|
+
r.term_name = rec.name || rec.display_name || '';
|
|
432
|
+
r.term_namespace = rec.namespace || rec.type || '';
|
|
433
|
+
r.term_display_id = rec.id != null ? String(rec.id) : String(r.term_id);
|
|
434
|
+
} else {
|
|
435
|
+
r.term_name = '';
|
|
436
|
+
r.term_namespace = '';
|
|
437
|
+
r.term_display_id = String(r.term_id);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
display.sort((a, b) => a.pAdj - b.pAdj || b.fold - a.fold);
|
|
441
|
+
return {
|
|
442
|
+
ontology: sectionKey,
|
|
443
|
+
label: sectionLabel,
|
|
444
|
+
field: ontField,
|
|
445
|
+
tested: rows.length,
|
|
446
|
+
passing: passing.length,
|
|
447
|
+
rows: display
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ---------- math helpers ----------
|
|
452
|
+
|
|
453
|
+
const LF_CACHE = [0, 0];
|
|
454
|
+
function logFactorial(n) {
|
|
455
|
+
if (n < LF_CACHE.length) return LF_CACHE[n];
|
|
456
|
+
let lf = LF_CACHE[LF_CACHE.length - 1];
|
|
457
|
+
for (let i = LF_CACHE.length; i <= n; i++) {
|
|
458
|
+
lf += Math.log(i);
|
|
459
|
+
LF_CACHE.push(lf);
|
|
460
|
+
}
|
|
461
|
+
return LF_CACHE[n];
|
|
462
|
+
}
|
|
463
|
+
function logChoose(n, k) {
|
|
464
|
+
if (k < 0 || k > n) return -Infinity;
|
|
465
|
+
return logFactorial(n) - logFactorial(k) - logFactorial(n - k);
|
|
466
|
+
}
|
|
467
|
+
function logHypergeom(x, n, K, N) {
|
|
468
|
+
return logChoose(K, x) + logChoose(N - K, n - x) - logChoose(N, n);
|
|
469
|
+
}
|
|
470
|
+
function logSumExp(a, b) {
|
|
471
|
+
if (a === -Infinity) return b;
|
|
472
|
+
if (b === -Infinity) return a;
|
|
473
|
+
const m = Math.max(a, b);
|
|
474
|
+
return m + Math.log(Math.exp(a - m) + Math.exp(b - m));
|
|
475
|
+
}
|
|
476
|
+
function fisherUpperTail(k, n, K, N) {
|
|
477
|
+
const upper = Math.min(n, K);
|
|
478
|
+
let logP = -Infinity;
|
|
479
|
+
for (let x = k; x <= upper; x++) {
|
|
480
|
+
logP = logSumExp(logP, logHypergeom(x, n, K, N));
|
|
481
|
+
}
|
|
482
|
+
const p = Math.exp(logP);
|
|
483
|
+
if (!isFinite(p)) return 1;
|
|
484
|
+
return Math.min(1, Math.max(0, p));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ---------- ontology graph helpers ----------
|
|
488
|
+
|
|
489
|
+
// Pick the "root" for a term as the ancestor (or self) with the highest
|
|
490
|
+
// background facet count. The true root has every annotated gene under it,
|
|
491
|
+
// so this selects BP/MF/CC for GO terms automatically and the rice root
|
|
492
|
+
// for pathways without needing per-ontology hardcoded ids.
|
|
493
|
+
function findRoot(ontKey, id, recs, bgCounts) {
|
|
494
|
+
const rec = recs && recs[id];
|
|
495
|
+
const candidates = new Set([+id]);
|
|
496
|
+
if (rec) {
|
|
497
|
+
if (ontKey === 'pathways') {
|
|
498
|
+
for (const k of Object.keys(rec)) {
|
|
499
|
+
if (k.startsWith('ancestors_') && Array.isArray(rec[k])) {
|
|
500
|
+
for (const a of rec[k]) candidates.add(+a);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
} else if (Array.isArray(rec.ancestors)) {
|
|
504
|
+
for (const a of rec.ancestors) candidates.add(+a);
|
|
505
|
+
} else if (Array.isArray(rec.is_a)) {
|
|
506
|
+
const stack = rec.is_a.slice();
|
|
507
|
+
while (stack.length) {
|
|
508
|
+
const cur = +stack.pop();
|
|
509
|
+
if (candidates.has(cur)) continue;
|
|
510
|
+
candidates.add(cur);
|
|
511
|
+
const r = recs[cur];
|
|
512
|
+
if (r && Array.isArray(r.is_a)) for (const p of r.is_a) stack.push(p);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
let best = +id;
|
|
517
|
+
let bestCount = bgCounts[+id] || 0;
|
|
518
|
+
for (const c of candidates) {
|
|
519
|
+
const cnt = bgCounts[c] || 0;
|
|
520
|
+
if (cnt > bestCount) { bestCount = cnt; best = c; }
|
|
521
|
+
}
|
|
522
|
+
return best;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function ancestorsOf(ontKey, id, recs) {
|
|
526
|
+
const out = new Set();
|
|
527
|
+
const rec = recs && recs[id];
|
|
528
|
+
if (!rec) return out;
|
|
529
|
+
if (ontKey === 'pathways') {
|
|
530
|
+
for (const k of Object.keys(rec)) {
|
|
531
|
+
if (k.startsWith('ancestors_') && Array.isArray(rec[k])) {
|
|
532
|
+
for (const a of rec[k]) if (+a !== +id) out.add(+a);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return out;
|
|
536
|
+
}
|
|
537
|
+
if (Array.isArray(rec.ancestors)) {
|
|
538
|
+
for (const a of rec.ancestors) if (+a !== +id) out.add(+a);
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
541
|
+
if (Array.isArray(rec.is_a)) {
|
|
542
|
+
const stack = rec.is_a.slice();
|
|
543
|
+
while (stack.length) {
|
|
544
|
+
const cur = +stack.pop();
|
|
545
|
+
if (out.has(cur)) continue;
|
|
546
|
+
out.add(cur);
|
|
547
|
+
const r = recs[cur];
|
|
548
|
+
if (r && Array.isArray(r.is_a)) for (const p of r.is_a) stack.push(p);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return out;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Drop any term that is an ancestor of another term in the same passing set.
|
|
555
|
+
// Applied AFTER BH so a parent term can survive when none of its children
|
|
556
|
+
// pass the p_adj cutoff.
|
|
557
|
+
function collapseToMostSpecific(ontKey, rows, recs) {
|
|
558
|
+
if (!rows || rows.length <= 1) return rows;
|
|
559
|
+
const inSet = new Set(rows.map(r => +r.term_id));
|
|
560
|
+
const covered = new Set();
|
|
561
|
+
for (const r of rows) {
|
|
562
|
+
const ancs = ancestorsOf(ontKey, +r.term_id, recs);
|
|
563
|
+
for (const a of ancs) if (inSet.has(a)) covered.add(a);
|
|
564
|
+
}
|
|
565
|
+
return rows.filter(r => !covered.has(+r.term_id));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export default gsea;
|
package/src/bundles/index.js
CHANGED
|
@@ -7,5 +7,6 @@ import fieldCatalogBundle from './swaggerFields'
|
|
|
7
7
|
import exporterBundle from './exporter'
|
|
8
8
|
import ontologiesBundle from './ontologies'
|
|
9
9
|
import exprVizBundle from './exprViz'
|
|
10
|
+
import gseaBundle from './gsea'
|
|
10
11
|
|
|
11
|
-
export default [...apiBundles, docsBundle, filterBundle, viewsBundle, genomesBundle, fieldCatalogBundle, exporterBundle, ontologiesBundle, exprVizBundle];
|
|
12
|
+
export default [...apiBundles, docsBundle, filterBundle, viewsBundle, genomesBundle, fieldCatalogBundle, exporterBundle, ontologiesBundle, exprVizBundle, gseaBundle];
|