gramene-search 2.0.2 → 2.0.5
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 +8 -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 +431 -0
- package/dist/index.css.map +1 -1
- package/dist/index.js +3555 -1119
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bundles/api.js +33 -26
- package/src/bundles/exporter.js +4 -1
- package/src/bundles/exprViz.js +422 -0
- package/src/bundles/index.js +2 -1
- package/src/bundles/swaggerFields.js +13 -0
- package/src/bundles/views.js +30 -12
- package/src/components/exprViz/ExprTable.js +339 -0
- package/src/components/exprViz/ExprVizView.js +400 -0
- package/src/components/exprViz/FieldsModal.js +441 -0
- package/src/components/exprViz/ParallelCoordsPlot.js +407 -0
- package/src/components/exprViz/styles.css +471 -0
- package/src/components/geneSearchUI.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gramene-search",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "search wrapper for gramene",
|
|
5
5
|
"source": "src/index.js",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"ag-grid-community": "^31.1.1",
|
|
28
28
|
"ag-grid-react": "^31.0.3",
|
|
29
29
|
"axios": "^1.6.8",
|
|
30
|
+
"d3": "^7.9.0",
|
|
30
31
|
"firebase": "^11.0.1",
|
|
31
32
|
"flat-to-nested": "^1.1.1",
|
|
32
33
|
"gramene-bins-client": "^2.3.3",
|
package/src/bundles/api.js
CHANGED
|
@@ -172,12 +172,15 @@ const expressionStudies = createAsyncResourceBundle( {
|
|
|
172
172
|
.then(res => _.groupBy(res, 'taxon_id'))
|
|
173
173
|
}
|
|
174
174
|
});
|
|
175
|
+
// Only fetch when a view that consumes expression studies is enabled.
|
|
176
|
+
const EXPRESSION_VIEWS = ['exprViz', 'expression', 'export'];
|
|
175
177
|
expressionStudies.reactExpressionStudies = createSelector(
|
|
176
178
|
'selectExpressionStudiesShouldUpdate',
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
'selectGrameneViewsOn',
|
|
180
|
+
(shouldUpdate, viewsOn) => {
|
|
181
|
+
if (!shouldUpdate) return;
|
|
182
|
+
if (!viewsOn || !EXPRESSION_VIEWS.some(id => viewsOn.has(id))) return;
|
|
183
|
+
return { actionCreator: 'doFetchExpressionStudies' }
|
|
181
184
|
}
|
|
182
185
|
);
|
|
183
186
|
|
|
@@ -202,10 +205,11 @@ const expressionSamples = createAsyncResourceBundle( {
|
|
|
202
205
|
});
|
|
203
206
|
expressionSamples.reactExpressionSamples = createSelector(
|
|
204
207
|
'selectExpressionSamplesShouldUpdate',
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
208
|
+
'selectGrameneViewsOn',
|
|
209
|
+
(shouldUpdate, viewsOn) => {
|
|
210
|
+
if (!shouldUpdate) return;
|
|
211
|
+
if (!viewsOn || !EXPRESSION_VIEWS.some(id => viewsOn.has(id))) return;
|
|
212
|
+
return { actionCreator: 'doFetchExpressionSamples' }
|
|
209
213
|
}
|
|
210
214
|
);
|
|
211
215
|
|
|
@@ -219,12 +223,15 @@ const curatedGenes = createAsyncResourceBundle( {
|
|
|
219
223
|
.then(curation => _.keyBy(curation.genes, 'gene_id'))
|
|
220
224
|
}
|
|
221
225
|
});
|
|
226
|
+
// Curated annotations are consumed by GeneList rows and Homology details,
|
|
227
|
+
// both inside the gene-list view.
|
|
222
228
|
curatedGenes.reactCuratedGenes = createSelector(
|
|
223
229
|
'selectCuratedGenesShouldUpdate',
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
230
|
+
'selectGrameneViewsOn',
|
|
231
|
+
(shouldUpdate, viewsOn) => {
|
|
232
|
+
if (!shouldUpdate) return;
|
|
233
|
+
if (!viewsOn || !viewsOn.has('list')) return;
|
|
234
|
+
return { actionCreator: 'doFetchCuratedGenes' }
|
|
228
235
|
}
|
|
229
236
|
);
|
|
230
237
|
|
|
@@ -245,12 +252,14 @@ const grameneGermplasm = createAsyncResourceBundle( {
|
|
|
245
252
|
})
|
|
246
253
|
}
|
|
247
254
|
});
|
|
255
|
+
// Germplasm metadata is consumed by the VEP detail panel inside the gene list.
|
|
248
256
|
grameneGermplasm.reactGrameneGermplasm = createSelector(
|
|
249
257
|
'selectGrameneGermplasmShouldUpdate',
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
258
|
+
'selectGrameneViewsOn',
|
|
259
|
+
(shouldUpdate, viewsOn) => {
|
|
260
|
+
if (!shouldUpdate) return;
|
|
261
|
+
if (!viewsOn || !viewsOn.has('list')) return;
|
|
262
|
+
return { actionCreator: 'doFetchGrameneGermplasm' }
|
|
254
263
|
}
|
|
255
264
|
);
|
|
256
265
|
//
|
|
@@ -334,14 +343,12 @@ const grameneGeneAttribs = createAsyncResourceBundle( {
|
|
|
334
343
|
grameneGeneAttribs.reactGrameneGeneAttribs = createSelector(
|
|
335
344
|
'selectGrameneGeneAttribsShouldUpdate',
|
|
336
345
|
'selectGrameneFiltersStatus',
|
|
337
|
-
'
|
|
338
|
-
(shouldUpdate, status,
|
|
339
|
-
if (shouldUpdate
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
}
|
|
346
|
+
'selectGrameneViewsOn',
|
|
347
|
+
(shouldUpdate, status, viewsOn) => {
|
|
348
|
+
if (!shouldUpdate) return;
|
|
349
|
+
if (status !== 'finished' && status !== 'ready') return;
|
|
350
|
+
if (!viewsOn || !viewsOn.has('attribs')) return;
|
|
351
|
+
return { actionCreator: 'doFetchGrameneGeneAttribs' }
|
|
345
352
|
}
|
|
346
353
|
);
|
|
347
354
|
|
|
@@ -353,8 +360,8 @@ const grameneSearch = createAsyncResourceBundle({
|
|
|
353
360
|
const offset = store.selectGrameneSearchOffset();
|
|
354
361
|
const rows = store.selectGrameneSearchRows();
|
|
355
362
|
const g = store.selectGrameneGenomes();
|
|
356
|
-
const m = store.selectGrameneMaps();
|
|
357
|
-
const taxa = Object.keys(g.active).filter(tid => !m[tid].hidden);
|
|
363
|
+
const m = store.selectGrameneMaps() || {};
|
|
364
|
+
const taxa = Object.keys((g && g.active) || {}).filter(tid => m[tid] && !m[tid].hidden);
|
|
358
365
|
let fq='';
|
|
359
366
|
if (taxa.length) {
|
|
360
367
|
console.log('search add a fq for ',taxa);
|
package/src/bundles/exporter.js
CHANGED
|
@@ -181,10 +181,13 @@ const exporter = {
|
|
|
181
181
|
'selectGrameneFiltersQueryString',
|
|
182
182
|
'selectGrameneGenomes',
|
|
183
183
|
'selectGrameneMaps',
|
|
184
|
-
|
|
184
|
+
'selectGrameneViewsOn',
|
|
185
|
+
(exp, filtersStatus, q, g, m, viewsOn) => {
|
|
185
186
|
if (!exp || !exp.preview) return;
|
|
186
187
|
if (filtersStatus === 'init') return;
|
|
187
188
|
if (exp.preview.status === 'loading') return;
|
|
189
|
+
// Don't run the preview query unless the user has the exporter view open.
|
|
190
|
+
if (!viewsOn || !viewsOn.has('export')) return;
|
|
188
191
|
const maps = m || {};
|
|
189
192
|
const taxa = Object.keys((g && g.active) || {}).filter(tid => maps[tid] && !maps[tid].hidden);
|
|
190
193
|
const sig = `${q}|${taxa.sort().join(',')}`;
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { createSelector } from 'redux-bundler';
|
|
2
|
+
|
|
3
|
+
// State shape:
|
|
4
|
+
// {
|
|
5
|
+
// pivot: { status, signature, data: { [taxon_id]: <gene count> }, error, requestId },
|
|
6
|
+
// activeTaxon: <taxon_id|null>,
|
|
7
|
+
// byTaxon: {
|
|
8
|
+
// [taxon_id]: {
|
|
9
|
+
// selectedFields: [<solr field name>...],
|
|
10
|
+
// fieldsModalOpen: <bool>,
|
|
11
|
+
// fetch: { status, offset, total, signature, requestId, error },
|
|
12
|
+
// rows: [<doc>...]
|
|
13
|
+
// }
|
|
14
|
+
// }
|
|
15
|
+
// }
|
|
16
|
+
// Per-taxon study lists are derived in the view layer from expressionStudies.
|
|
17
|
+
|
|
18
|
+
const PAGE_SIZE = 200;
|
|
19
|
+
|
|
20
|
+
let pivotPendingId = 0;
|
|
21
|
+
const fetchPending = {}; // per-taxon request id
|
|
22
|
+
const attrsPending = {}; // per-taxon request id for available attrs
|
|
23
|
+
const existencePending = {}; // per-taxon request id for field-existence check
|
|
24
|
+
|
|
25
|
+
function pivotSignature(store) {
|
|
26
|
+
const q = store.selectGrameneFiltersQueryString();
|
|
27
|
+
const g = store.selectGrameneGenomes();
|
|
28
|
+
const m = store.selectGrameneMaps() || {};
|
|
29
|
+
const taxa = Object.keys(g.active || {}).filter(tid => m[tid] && !m[tid].hidden);
|
|
30
|
+
return `${q}|${taxa.sort().join(',')}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fetchSignature(store, taxon) {
|
|
34
|
+
const q = store.selectGrameneFiltersQueryString();
|
|
35
|
+
const ev = store.selectExprViz();
|
|
36
|
+
const sel = (ev.byTaxon[taxon] && ev.byTaxon[taxon].selectedFields) || [];
|
|
37
|
+
return `${q}|${taxon}|${sel.slice().sort().join(',')}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ALWAYS_FL = ['id', 'name', 'system_name', 'taxon_id'];
|
|
41
|
+
|
|
42
|
+
const exprViz = {
|
|
43
|
+
name: 'exprViz',
|
|
44
|
+
|
|
45
|
+
getReducer: () => {
|
|
46
|
+
const initialState = {
|
|
47
|
+
pivot: { status: 'idle', signature: null, data: {}, error: null, requestId: 0 },
|
|
48
|
+
activeTaxon: null,
|
|
49
|
+
byTaxon: {}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function ensureTaxon(state, taxon) {
|
|
53
|
+
if (state.byTaxon[taxon]) return state.byTaxon[taxon];
|
|
54
|
+
return {
|
|
55
|
+
selectedFields: [],
|
|
56
|
+
fieldsModalOpen: false,
|
|
57
|
+
fetch: { status: 'idle', offset: 0, total: 0, signature: null, requestId: 0, error: null },
|
|
58
|
+
rows: [],
|
|
59
|
+
availableAttrs: null,
|
|
60
|
+
attrsSignature: null,
|
|
61
|
+
attrsRequestId: 0,
|
|
62
|
+
fieldExistence: null,
|
|
63
|
+
existenceStatus: 'idle',
|
|
64
|
+
existenceSignature: null,
|
|
65
|
+
existenceRequestId: 0
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (state = initialState, { type, payload }) => {
|
|
70
|
+
switch (type) {
|
|
71
|
+
case 'EXPRVIZ_PIVOT_STARTED':
|
|
72
|
+
return { ...state, pivot: { status: 'loading', signature: payload.signature, data: state.pivot.data, error: null, requestId: payload.requestId } };
|
|
73
|
+
case 'EXPRVIZ_PIVOT_SUCCEEDED':
|
|
74
|
+
if (payload.requestId !== state.pivot.requestId) return state;
|
|
75
|
+
return { ...state, pivot: { status: 'ready', signature: state.pivot.signature, data: payload.data, error: null, requestId: payload.requestId } };
|
|
76
|
+
case 'EXPRVIZ_PIVOT_FAILED':
|
|
77
|
+
if (payload.requestId !== state.pivot.requestId) return state;
|
|
78
|
+
return { ...state, pivot: { ...state.pivot, status: 'error', error: payload.error } };
|
|
79
|
+
|
|
80
|
+
case 'EXPRVIZ_ACTIVE_TAXON_SET':
|
|
81
|
+
return { ...state, activeTaxon: payload };
|
|
82
|
+
|
|
83
|
+
case 'EXPRVIZ_FIELDS_MODAL_TOGGLED': {
|
|
84
|
+
const t = ensureTaxon(state, payload.taxon);
|
|
85
|
+
return {
|
|
86
|
+
...state,
|
|
87
|
+
byTaxon: { ...state.byTaxon, [payload.taxon]: { ...t, fieldsModalOpen: payload.open } }
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
case 'EXPRVIZ_FIELDS_SET': {
|
|
91
|
+
const t = ensureTaxon(state, payload.taxon);
|
|
92
|
+
return {
|
|
93
|
+
...state,
|
|
94
|
+
byTaxon: {
|
|
95
|
+
...state.byTaxon,
|
|
96
|
+
[payload.taxon]: {
|
|
97
|
+
...t,
|
|
98
|
+
selectedFields: payload.fields,
|
|
99
|
+
rows: [],
|
|
100
|
+
fetch: { status: 'idle', offset: 0, total: 0, signature: null, requestId: 0, error: null }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
case 'EXPRVIZ_FIELDS_REORDERED': {
|
|
106
|
+
const t = state.byTaxon[payload.taxon];
|
|
107
|
+
if (!t) return state;
|
|
108
|
+
// Same set of fields — preserve rows; only the column/axis order changes.
|
|
109
|
+
const setA = new Set(t.selectedFields);
|
|
110
|
+
const setB = new Set(payload.fields);
|
|
111
|
+
if (setA.size !== setB.size || ![...setA].every(f => setB.has(f))) {
|
|
112
|
+
return state;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
...state,
|
|
116
|
+
byTaxon: {
|
|
117
|
+
...state.byTaxon,
|
|
118
|
+
[payload.taxon]: { ...t, selectedFields: payload.fields }
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
case 'EXPRVIZ_FETCH_STARTED': {
|
|
124
|
+
const t = ensureTaxon(state, payload.taxon);
|
|
125
|
+
return {
|
|
126
|
+
...state,
|
|
127
|
+
byTaxon: {
|
|
128
|
+
...state.byTaxon,
|
|
129
|
+
[payload.taxon]: {
|
|
130
|
+
...t,
|
|
131
|
+
fetch: { ...t.fetch, status: 'loading', signature: payload.signature, requestId: payload.requestId, error: null }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
case 'EXPRVIZ_FETCH_BATCH': {
|
|
137
|
+
const t = state.byTaxon[payload.taxon];
|
|
138
|
+
if (!t || payload.requestId !== t.fetch.requestId) return state;
|
|
139
|
+
const rows = t.rows.concat(payload.docs);
|
|
140
|
+
return {
|
|
141
|
+
...state,
|
|
142
|
+
byTaxon: {
|
|
143
|
+
...state.byTaxon,
|
|
144
|
+
[payload.taxon]: {
|
|
145
|
+
...t,
|
|
146
|
+
rows,
|
|
147
|
+
fetch: { ...t.fetch, offset: rows.length, total: payload.total, status: rows.length >= payload.total ? 'done' : 'loading' }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
case 'EXPRVIZ_FETCH_FAILED': {
|
|
153
|
+
const t = state.byTaxon[payload.taxon];
|
|
154
|
+
if (!t || payload.requestId !== t.fetch.requestId) return state;
|
|
155
|
+
return {
|
|
156
|
+
...state,
|
|
157
|
+
byTaxon: {
|
|
158
|
+
...state.byTaxon,
|
|
159
|
+
[payload.taxon]: { ...t, fetch: { ...t.fetch, status: 'error', error: payload.error } }
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
case 'EXPRVIZ_ATTRS_STARTED': {
|
|
165
|
+
const t = ensureTaxon(state, payload.taxon);
|
|
166
|
+
return {
|
|
167
|
+
...state,
|
|
168
|
+
byTaxon: {
|
|
169
|
+
...state.byTaxon,
|
|
170
|
+
[payload.taxon]: { ...t, attrsRequestId: payload.requestId, attrsSignature: payload.signature }
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
case 'EXPRVIZ_ATTRS_SUCCEEDED': {
|
|
175
|
+
const t = state.byTaxon[payload.taxon];
|
|
176
|
+
if (!t || payload.requestId !== t.attrsRequestId) return state;
|
|
177
|
+
return {
|
|
178
|
+
...state,
|
|
179
|
+
byTaxon: {
|
|
180
|
+
...state.byTaxon,
|
|
181
|
+
[payload.taxon]: { ...t, availableAttrs: payload.attrs }
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
case 'EXPRVIZ_EXISTENCE_STARTED': {
|
|
187
|
+
const t = ensureTaxon(state, payload.taxon);
|
|
188
|
+
return {
|
|
189
|
+
...state,
|
|
190
|
+
byTaxon: {
|
|
191
|
+
...state.byTaxon,
|
|
192
|
+
[payload.taxon]: {
|
|
193
|
+
...t,
|
|
194
|
+
existenceStatus: 'loading',
|
|
195
|
+
existenceRequestId: payload.requestId,
|
|
196
|
+
existenceSignature: payload.signature
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
case 'EXPRVIZ_EXISTENCE_SUCCEEDED': {
|
|
202
|
+
const t = state.byTaxon[payload.taxon];
|
|
203
|
+
if (!t || payload.requestId !== t.existenceRequestId) return state;
|
|
204
|
+
return {
|
|
205
|
+
...state,
|
|
206
|
+
byTaxon: {
|
|
207
|
+
...state.byTaxon,
|
|
208
|
+
[payload.taxon]: { ...t, existenceStatus: 'ready', fieldExistence: payload.counts }
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
case 'GRAMENE_SEARCH_CLEARED': {
|
|
214
|
+
// Search context changed — invalidate pivot and any loaded rows.
|
|
215
|
+
// Selected fields are kept so the user can re-run the load.
|
|
216
|
+
const newByTaxon = {};
|
|
217
|
+
for (const tid of Object.keys(state.byTaxon)) {
|
|
218
|
+
newByTaxon[tid] = {
|
|
219
|
+
...state.byTaxon[tid],
|
|
220
|
+
rows: [],
|
|
221
|
+
fetch: { status: 'idle', offset: 0, total: 0, signature: null, requestId: 0, error: null },
|
|
222
|
+
availableAttrs: null,
|
|
223
|
+
attrsSignature: null,
|
|
224
|
+
fieldExistence: null,
|
|
225
|
+
existenceStatus: 'idle',
|
|
226
|
+
existenceSignature: null
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
...state,
|
|
231
|
+
pivot: { status: 'idle', signature: null, data: {}, error: null, requestId: 0 },
|
|
232
|
+
byTaxon: newByTaxon
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
default:
|
|
237
|
+
return state;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
doFetchExprVizPivot: () => ({ dispatch, store }) => {
|
|
243
|
+
const requestId = ++pivotPendingId;
|
|
244
|
+
const signature = pivotSignature(store);
|
|
245
|
+
dispatch({ type: 'EXPRVIZ_PIVOT_STARTED', payload: { requestId, signature } });
|
|
246
|
+
|
|
247
|
+
const api = store.selectGrameneAPI();
|
|
248
|
+
const q = store.selectGrameneFiltersQueryString();
|
|
249
|
+
const g = store.selectGrameneGenomes();
|
|
250
|
+
const m = store.selectGrameneMaps() || {};
|
|
251
|
+
const taxa = Object.keys(g.active || {}).filter(tid => m[tid] && !m[tid].hidden);
|
|
252
|
+
const fq = taxa.length ? `&fq=taxon_id:(${taxa.join(' OR ')})` : '';
|
|
253
|
+
const facetField = "{!facet.limit='300' facet.mincount='1' key='taxon_id'}taxon_id";
|
|
254
|
+
const url = `${api}/search?q=${q}${fq}&fq=expressed_in_gxa_attr_ss:*&rows=0&facet=true&facet.field=${facetField}`;
|
|
255
|
+
|
|
256
|
+
fetch(url)
|
|
257
|
+
.then(r => r.json())
|
|
258
|
+
.then(json => {
|
|
259
|
+
const arr = (json && json.facet_counts && json.facet_counts.facet_fields && json.facet_counts.facet_fields.taxon_id) || [];
|
|
260
|
+
const data = {};
|
|
261
|
+
for (let i = 0; i < arr.length; i += 2) {
|
|
262
|
+
data[arr[i]] = arr[i + 1];
|
|
263
|
+
}
|
|
264
|
+
if (Object.keys(data).length === 0) {
|
|
265
|
+
console.warn('[exprViz] no taxon_ids found with expression', { url, json });
|
|
266
|
+
}
|
|
267
|
+
dispatch({ type: 'EXPRVIZ_PIVOT_SUCCEEDED', payload: { requestId, data } });
|
|
268
|
+
})
|
|
269
|
+
.catch(err => {
|
|
270
|
+
dispatch({ type: 'EXPRVIZ_PIVOT_FAILED', payload: { requestId, error: String(err) } });
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
doSetExprVizActiveTaxon: tid => ({ dispatch }) =>
|
|
275
|
+
dispatch({ type: 'EXPRVIZ_ACTIVE_TAXON_SET', payload: tid }),
|
|
276
|
+
|
|
277
|
+
doToggleExprVizFieldsModal: (taxon, open) => ({ dispatch, store }) => {
|
|
278
|
+
dispatch({ type: 'EXPRVIZ_FIELDS_MODAL_TOGGLED', payload: { taxon, open: !!open } });
|
|
279
|
+
if (open) store.doFetchExprVizAvailableAttrs(taxon);
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
doFetchExprVizAvailableAttrs: taxon => ({ dispatch, store }) => {
|
|
283
|
+
const q = store.selectGrameneFiltersQueryString();
|
|
284
|
+
const signature = `${q}|${taxon}`;
|
|
285
|
+
const ev = store.selectExprViz();
|
|
286
|
+
const t = ev.byTaxon[taxon];
|
|
287
|
+
if (t && t.availableAttrs && t.attrsSignature === signature) return;
|
|
288
|
+
const requestId = (attrsPending[taxon] = (attrsPending[taxon] || 0) + 1);
|
|
289
|
+
dispatch({ type: 'EXPRVIZ_ATTRS_STARTED', payload: { taxon, requestId, signature } });
|
|
290
|
+
|
|
291
|
+
const api = store.selectGrameneAPI();
|
|
292
|
+
const facetField = "{!facet.limit='2000' facet.mincount='1' key='attrs'}expressed_in_gxa_attr_ss";
|
|
293
|
+
const url = `${api}/search?q=${q}&fq=taxon_id:${taxon}&fq=expressed_in_gxa_attr_ss:*&rows=0&facet=true&facet.field=${facetField}`;
|
|
294
|
+
fetch(url)
|
|
295
|
+
.then(r => r.json())
|
|
296
|
+
.then(json => {
|
|
297
|
+
if (requestId !== attrsPending[taxon]) return;
|
|
298
|
+
const arr = (json && json.facet_counts && json.facet_counts.facet_fields && json.facet_counts.facet_fields.attrs) || [];
|
|
299
|
+
const attrs = [];
|
|
300
|
+
for (let i = 0; i < arr.length; i += 2) attrs.push(arr[i]);
|
|
301
|
+
dispatch({ type: 'EXPRVIZ_ATTRS_SUCCEEDED', payload: { taxon, requestId, attrs } });
|
|
302
|
+
})
|
|
303
|
+
.catch(() => { /* swallow — non-fatal; modal still works without filtering */ });
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
doSetExprVizFields: (taxon, fields) => ({ dispatch }) =>
|
|
307
|
+
dispatch({ type: 'EXPRVIZ_FIELDS_SET', payload: { taxon, fields } }),
|
|
308
|
+
|
|
309
|
+
doReorderExprVizFields: (taxon, fields) => ({ dispatch }) =>
|
|
310
|
+
dispatch({ type: 'EXPRVIZ_FIELDS_REORDERED', payload: { taxon, fields } }),
|
|
311
|
+
|
|
312
|
+
doFetchExprVizData: taxon => ({ dispatch, store }) => {
|
|
313
|
+
const ev = store.selectExprViz();
|
|
314
|
+
const t = ev.byTaxon[taxon];
|
|
315
|
+
if (!t || t.selectedFields.length === 0) return;
|
|
316
|
+
const requestId = (fetchPending[taxon] = (fetchPending[taxon] || 0) + 1);
|
|
317
|
+
const signature = fetchSignature(store, taxon);
|
|
318
|
+
dispatch({ type: 'EXPRVIZ_FETCH_STARTED', payload: { taxon, requestId, signature } });
|
|
319
|
+
|
|
320
|
+
const api = store.selectGrameneAPI();
|
|
321
|
+
const q = store.selectGrameneFiltersQueryString();
|
|
322
|
+
const fl = [...new Set([...ALWAYS_FL, ...t.selectedFields])].join(',');
|
|
323
|
+
|
|
324
|
+
const fetchPage = (offset) => {
|
|
325
|
+
if (requestId !== fetchPending[taxon]) return; // superseded
|
|
326
|
+
const url = `${api}/search?q=${q}&fq=taxon_id:${taxon}&fl=${fl}&rows=${PAGE_SIZE}&start=${offset}`;
|
|
327
|
+
fetch(url)
|
|
328
|
+
.then(r => r.json())
|
|
329
|
+
.then(json => {
|
|
330
|
+
if (requestId !== fetchPending[taxon]) return;
|
|
331
|
+
const docs = (json.response && json.response.docs) || [];
|
|
332
|
+
const total = (json.response && json.response.numFound) || 0;
|
|
333
|
+
dispatch({ type: 'EXPRVIZ_FETCH_BATCH', payload: { taxon, requestId, docs, total } });
|
|
334
|
+
const next = offset + docs.length;
|
|
335
|
+
if (docs.length > 0 && next < total) {
|
|
336
|
+
fetchPage(next);
|
|
337
|
+
}
|
|
338
|
+
})
|
|
339
|
+
.catch(err => {
|
|
340
|
+
dispatch({ type: 'EXPRVIZ_FETCH_FAILED', payload: { taxon, requestId, error: String(err) } });
|
|
341
|
+
});
|
|
342
|
+
};
|
|
343
|
+
fetchPage(0);
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
// Per-candidate-field non-null counts under the current q + taxon. Used by
|
|
347
|
+
// FieldsModal to hide fields that have no data in the current result set
|
|
348
|
+
// before the user picks. Uses stats.field (the swagger whitelist drops
|
|
349
|
+
// facet.query) batched across multiple GETs to stay under URL limits.
|
|
350
|
+
doFetchExprVizFieldExistence: (taxon, fields) => ({ dispatch, store }) => {
|
|
351
|
+
if (!fields || fields.length === 0) return;
|
|
352
|
+
const q = store.selectGrameneFiltersQueryString();
|
|
353
|
+
const fieldsKey = fields.slice().sort().join(',');
|
|
354
|
+
const signature = `${q}|${taxon}|${fieldsKey}`;
|
|
355
|
+
const ev = store.selectExprViz();
|
|
356
|
+
const t = ev.byTaxon[taxon];
|
|
357
|
+
if (t && t.fieldExistence && t.existenceSignature === signature) return;
|
|
358
|
+
|
|
359
|
+
const requestId = (existencePending[taxon] = (existencePending[taxon] || 0) + 1);
|
|
360
|
+
dispatch({ type: 'EXPRVIZ_EXISTENCE_STARTED', payload: { taxon, requestId, signature } });
|
|
361
|
+
|
|
362
|
+
const api = store.selectGrameneAPI();
|
|
363
|
+
const BATCH_SIZE = 40;
|
|
364
|
+
const batches = [];
|
|
365
|
+
for (let i = 0; i < fields.length; i += BATCH_SIZE) {
|
|
366
|
+
batches.push(fields.slice(i, i + BATCH_SIZE));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
Promise.all(batches.map(batch => {
|
|
370
|
+
const params = new URLSearchParams();
|
|
371
|
+
params.append('q', q);
|
|
372
|
+
params.append('fq', `taxon_id:${taxon}`);
|
|
373
|
+
params.append('rows', '0');
|
|
374
|
+
params.append('stats', 'true');
|
|
375
|
+
batch.forEach(f => params.append('stats.field', f));
|
|
376
|
+
return fetch(`${api}/search?${params.toString()}`)
|
|
377
|
+
.then(r => r.json())
|
|
378
|
+
.then(json => {
|
|
379
|
+
const sf = (json && json.stats && json.stats.stats_fields) || {};
|
|
380
|
+
const out = {};
|
|
381
|
+
for (const f of Object.keys(sf)) {
|
|
382
|
+
const s = sf[f];
|
|
383
|
+
out[f] = s && typeof s.count === 'number' ? s.count : 0;
|
|
384
|
+
}
|
|
385
|
+
return out;
|
|
386
|
+
});
|
|
387
|
+
}))
|
|
388
|
+
.then(results => {
|
|
389
|
+
if (requestId !== existencePending[taxon]) return;
|
|
390
|
+
const counts = Object.assign({}, ...results);
|
|
391
|
+
dispatch({ type: 'EXPRVIZ_EXISTENCE_SUCCEEDED', payload: { taxon, requestId, counts } });
|
|
392
|
+
})
|
|
393
|
+
.catch(() => { /* swallow — non-fatal; modal falls back to showing all */ });
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
reactExprVizPivot: createSelector(
|
|
397
|
+
'selectExprViz',
|
|
398
|
+
'selectGrameneFiltersStatus',
|
|
399
|
+
'selectGrameneViews',
|
|
400
|
+
'selectGrameneFiltersQueryString',
|
|
401
|
+
'selectGrameneGenomes',
|
|
402
|
+
'selectGrameneMaps',
|
|
403
|
+
(ev, filtersStatus, views, q, g, m) => {
|
|
404
|
+
if (!ev || filtersStatus === 'init') return;
|
|
405
|
+
const onView = views && views.options && views.options.find(v => v.id === 'exprViz');
|
|
406
|
+
if (!onView || onView.show !== 'on') return;
|
|
407
|
+
const maps = m || {};
|
|
408
|
+
const taxa = Object.keys((g && g.active) || {}).filter(tid => maps[tid] && !maps[tid].hidden);
|
|
409
|
+
const sig = `${q}|${taxa.sort().join(',')}`;
|
|
410
|
+
if (ev.pivot.status === 'loading') return;
|
|
411
|
+
if (ev.pivot.signature === sig && ev.pivot.status === 'ready') return;
|
|
412
|
+
return { actionCreator: 'doFetchExprVizPivot' };
|
|
413
|
+
}
|
|
414
|
+
),
|
|
415
|
+
|
|
416
|
+
selectExprViz: state => state.exprViz,
|
|
417
|
+
selectExprVizPivot: state => state.exprViz.pivot,
|
|
418
|
+
selectExprVizActiveTaxon: state => state.exprViz.activeTaxon,
|
|
419
|
+
selectExprVizByTaxon: state => state.exprViz.byTaxon
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
export default exprViz;
|
package/src/bundles/index.js
CHANGED
|
@@ -6,5 +6,6 @@ import genomesBundle from './genomes'
|
|
|
6
6
|
import fieldCatalogBundle from './swaggerFields'
|
|
7
7
|
import exporterBundle from './exporter'
|
|
8
8
|
import ontologiesBundle from './ontologies'
|
|
9
|
+
import exprVizBundle from './exprViz'
|
|
9
10
|
|
|
10
|
-
export default [...apiBundles, docsBundle, filterBundle, viewsBundle, genomesBundle, fieldCatalogBundle, exporterBundle, ontologiesBundle];
|
|
11
|
+
export default [...apiBundles, docsBundle, filterBundle, viewsBundle, genomesBundle, fieldCatalogBundle, exporterBundle, ontologiesBundle, exprVizBundle];
|
|
@@ -188,6 +188,19 @@ const grameneFieldCatalog = createAsyncResourceBundle({
|
|
|
188
188
|
}
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
// Auto-fetch the field catalog only when a view that needs it (exprViz fields
|
|
192
|
+
// modal, exporter field tree) is enabled. Other views never read it.
|
|
193
|
+
const FIELD_CATALOG_VIEWS = ['exprViz', 'export'];
|
|
194
|
+
grameneFieldCatalog.reactGrameneFieldCatalog = createSelector(
|
|
195
|
+
'selectGrameneFieldCatalogShouldUpdate',
|
|
196
|
+
'selectGrameneViewsOn',
|
|
197
|
+
(shouldUpdate, viewsOn) => {
|
|
198
|
+
if (!shouldUpdate) return;
|
|
199
|
+
if (!viewsOn || !FIELD_CATALOG_VIEWS.some(id => viewsOn.has(id))) return;
|
|
200
|
+
return { actionCreator: 'doFetchGrameneFieldCatalog' };
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
|
|
191
204
|
function assayFactorLabel(assay) {
|
|
192
205
|
if (!assay) return '';
|
|
193
206
|
const factors = Array.isArray(assay.factor) ? assay.factor : [];
|
package/src/bundles/views.js
CHANGED
|
@@ -11,12 +11,6 @@ const grameneViews = {
|
|
|
11
11
|
show: 'off',
|
|
12
12
|
shouldScroll: false
|
|
13
13
|
},
|
|
14
|
-
{
|
|
15
|
-
id: 'userLists',
|
|
16
|
-
name: 'User Gene Lists',
|
|
17
|
-
show: 'off',
|
|
18
|
-
shouldScroll: false
|
|
19
|
-
},
|
|
20
14
|
{
|
|
21
15
|
id: 'taxonomy',
|
|
22
16
|
name: 'Taxonomic distribution',
|
|
@@ -30,15 +24,14 @@ const grameneViews = {
|
|
|
30
24
|
shouldScroll: false
|
|
31
25
|
},
|
|
32
26
|
{
|
|
33
|
-
id: '
|
|
34
|
-
name: '
|
|
27
|
+
id: 'exprViz',
|
|
28
|
+
name: 'Expression visualization',
|
|
35
29
|
show: 'off',
|
|
36
|
-
shouldScroll: false
|
|
37
|
-
desiredSamples: {}
|
|
30
|
+
shouldScroll: false
|
|
38
31
|
},
|
|
39
32
|
{
|
|
40
|
-
id: '
|
|
41
|
-
name: 'Gene
|
|
33
|
+
id: 'userLists',
|
|
34
|
+
name: 'User Gene Lists',
|
|
42
35
|
show: 'off',
|
|
43
36
|
shouldScroll: false
|
|
44
37
|
},
|
|
@@ -47,6 +40,19 @@ const grameneViews = {
|
|
|
47
40
|
name: 'Data exporter',
|
|
48
41
|
show: 'off',
|
|
49
42
|
shouldScroll: false
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'expression',
|
|
46
|
+
name: 'Gene expression',
|
|
47
|
+
show: 'off',
|
|
48
|
+
shouldScroll: false,
|
|
49
|
+
desiredSamples: {}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'attribs',
|
|
53
|
+
name: 'Gene attributes',
|
|
54
|
+
show: 'off',
|
|
55
|
+
shouldScroll: false
|
|
50
56
|
}
|
|
51
57
|
// {
|
|
52
58
|
// id: 'domains',
|
|
@@ -143,6 +149,18 @@ const grameneViews = {
|
|
|
143
149
|
}
|
|
144
150
|
return { ...raw, options };
|
|
145
151
|
}
|
|
152
|
+
),
|
|
153
|
+
// Set of view ids whose `show` is currently 'on'. Used by data bundles to
|
|
154
|
+
// gate their auto-fetch reactors so we don't pay the cost of loading data
|
|
155
|
+
// for views the user has turned off.
|
|
156
|
+
selectGrameneViewsOn: createSelector(
|
|
157
|
+
'selectGrameneViews',
|
|
158
|
+
(views) => {
|
|
159
|
+
const ids = new Set();
|
|
160
|
+
const opts = (views && views.options) || [];
|
|
161
|
+
for (const v of opts) if (v && v.show === 'on') ids.add(v.id);
|
|
162
|
+
return ids;
|
|
163
|
+
}
|
|
146
164
|
)
|
|
147
165
|
};
|
|
148
166
|
|