gramene-search 2.1.10 → 2.2.0
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/launch.json +11 -0
- package/.claude/settings.local.json +21 -1
- package/.env.example +6 -0
- package/.parcel-cache/13f2d5707e7af45c-RequestGraph +0 -0
- package/.parcel-cache/5ae0570a78c0dba3-AssetGraph +0 -0
- package/.parcel-cache/9ac092379278e465-BundleGraph +0 -0
- package/.parcel-cache/data.mdb +0 -0
- package/.parcel-cache/lock.mdb +0 -0
- package/.parcel-cache/snapshot-13f2d5707e7af45c.txt +2 -2
- package/dist/index.css +1 -4
- package/dist/index.css.map +1 -1
- package/dist/index.js +2308 -433
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
- package/src/bundles/api.js +24 -42
- package/src/bundles/docs.js +2 -1
- package/src/bundles/exprViz.js +97 -1
- package/src/bundles/index.js +4 -1
- package/src/bundles/ontologyEnrichment.js +14 -1
- package/src/bundles/savedViews.js +335 -0
- package/src/bundles/uiViewState.js +174 -0
- package/src/bundles/viewSnapshot.js +313 -0
- package/src/bundles/views.js +24 -2
- package/src/components/Auth.js +23 -3
- package/src/components/SaveView.js +157 -0
- package/src/components/exprViz/ExprVizView.js +16 -11
- package/src/components/exprViz/ParallelCoordsPlot.js +15 -0
- package/src/components/results/GeneList.js +45 -49
- package/src/components/results/OntologyEnrichment.js +13 -6
- package/src/components/results/TaxDist.js +11 -0
- package/src/components/results/details/BAR.js +148 -0
- package/src/components/results/details/Expression.js +50 -14
- package/src/components/results/details/Homology.js +171 -39
- package/src/components/results/details/Pathways.js +4 -2
- package/src/components/results/details/Sequences.js +24 -8
- package/src/components/results/details/VEP.js +65 -19
- package/src/components/styles.css +1 -4
- package/src/demo.js +30 -13
- package/src/index.js +2 -1
- package/src/suppressDevWarnings.js +13 -0
- package/src/utils/bootView.js +38 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useRef, useEffect, useState } from 'react'
|
|
2
2
|
import {connect} from "redux-bundler-react";
|
|
3
3
|
import {Tabs, Tab, Form, Row, Col} from 'react-bootstrap';
|
|
4
|
-
import BAR, {haveBAR} from "
|
|
4
|
+
import BAR, {haveBAR} from "./BAR";
|
|
5
5
|
|
|
6
6
|
function DynamicIframe(props) {
|
|
7
7
|
// Create a ref for the iframe element
|
|
@@ -31,16 +31,35 @@ function DynamicIframe(props) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const Detail = props => {
|
|
34
|
-
const
|
|
35
|
-
const
|
|
34
|
+
const geneId = props.searchResult.id;
|
|
35
|
+
const gene = props.geneDocs[geneId];
|
|
36
|
+
// User-selected view state (active sub-tab, chosen GXA experiment, chosen eFP
|
|
37
|
+
// study) lives in the uiViewState bundle keyed by geneId, so the shareable-
|
|
38
|
+
// views snapshot can round-trip it. Fetched data + the dev-only Local API
|
|
39
|
+
// toggle stay in local state. Defaults match the old local-state initials.
|
|
40
|
+
const expr = (props.uiViewState && props.uiViewState.byGene[geneId]
|
|
41
|
+
&& props.uiViewState.byGene[geneId].expression) || {};
|
|
42
|
+
const activeTab = expr.activeTab || 'gene';
|
|
43
|
+
const atlasExperiment = expr.atlasExperiment || null;
|
|
44
|
+
const setActiveTab = (k) => props.doSetExpressionState({geneId, patch: {activeTab: k}});
|
|
45
|
+
const setAtlasExperiment = (v) => props.doSetExpressionState({geneId, patch: {atlasExperiment: v}});
|
|
36
46
|
const [atlasExperimentList, setAtlasExperimentList] = useState([]);
|
|
37
47
|
const [atlasFacets, setAtlasFacets] = useState(null);
|
|
38
48
|
const [isLocal, setIsLocal] = useState(false);
|
|
39
|
-
const [activeTab, setActiveTab] = useState('gene');
|
|
40
49
|
|
|
41
50
|
const handleLocalAPIChange = (event) => {
|
|
42
51
|
setIsLocal(event.target.checked);
|
|
43
52
|
};
|
|
53
|
+
// The expressionStudies resource is otherwise fetched only when a top-level
|
|
54
|
+
// expression view (exprViz/expression/export) is on — but this per-gene
|
|
55
|
+
// Expression detail also needs it (the Paralogs sub-tab's experiment list and
|
|
56
|
+
// the atlasExperiment selection both derive from it). Self-fetch on mount so
|
|
57
|
+
// opening the detail populates the studies even when no such view is enabled.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!props.expressionStudies && props.doFetchExpressionStudies) {
|
|
60
|
+
props.doFetchExpressionStudies();
|
|
61
|
+
}
|
|
62
|
+
}, [props.expressionStudies]);
|
|
44
63
|
useEffect(() => {
|
|
45
64
|
if (!props.expressionStudies) return;
|
|
46
65
|
const tid = Math.floor(gene.taxon_id / 1000);
|
|
@@ -58,12 +77,17 @@ const Detail = props => {
|
|
|
58
77
|
eList.forEach(e => {e.factors.forEach(factor => facets[e.type][factor] = 1);});
|
|
59
78
|
setAtlasExperimentList(eList);
|
|
60
79
|
setAtlasFacets(facets);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
// Only pick a default experiment when the user (or a restored snapshot)
|
|
81
|
+
// hasn't already chosen one — otherwise we'd clobber a saved selection
|
|
82
|
+
// the moment the studies list loads.
|
|
83
|
+
if (!atlasExperiment) {
|
|
84
|
+
let refExp = eList.filter(e => e.isRef);
|
|
85
|
+
if (refExp.length === 1) {
|
|
86
|
+
setAtlasExperiment(refExp[0]._id);
|
|
87
|
+
} else {
|
|
88
|
+
// no reference experiment - choose first
|
|
89
|
+
setAtlasExperiment(eList[0]._id);
|
|
90
|
+
}
|
|
67
91
|
}
|
|
68
92
|
}
|
|
69
93
|
}, [props.expressionStudies]);
|
|
@@ -71,11 +95,15 @@ const Detail = props => {
|
|
|
71
95
|
let paralogs_url;
|
|
72
96
|
let gene_url = `https://dev.gramene.org/static/atlasWidget.html?genes=${gene.atlas_id || gene._id}&localAPI=${isLocal}`;
|
|
73
97
|
let paralogs = [];
|
|
74
|
-
|
|
98
|
+
const haveParalogs = props.grameneParalogs && props.grameneParalogs[gene._id];
|
|
99
|
+
if (haveParalogs) {
|
|
75
100
|
paralogs = props.grameneParalogs[gene._id];
|
|
76
|
-
} else if (gene.homology) {
|
|
77
|
-
props.doRequestParalogs(gene._id, gene.homology.supertree, gene.taxon_id);
|
|
78
101
|
}
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!haveParalogs && gene.homology) {
|
|
104
|
+
props.doRequestParalogs(gene._id, gene.homology.supertree, gene.taxon_id);
|
|
105
|
+
}
|
|
106
|
+
}, [gene._id, haveParalogs]);
|
|
79
107
|
// if (gene.homology && gene.homology.homologous_genes && gene.homology.homologous_genes.within_species_paralog) {
|
|
80
108
|
// paralogs = gene.homology.homologous_genes.within_species_paralog;
|
|
81
109
|
// }
|
|
@@ -87,6 +115,7 @@ const Detail = props => {
|
|
|
87
115
|
<Tab tabClassName="gxa" eventKey="paralogs" title={`Paralogs`} key="gxaparalogs">
|
|
88
116
|
<Form.Select aria-label='experiment selector'
|
|
89
117
|
placeholder='Select experiment'
|
|
118
|
+
value={atlasExperiment || ''}
|
|
90
119
|
onChange={(e) => setAtlasExperiment(e.target.value)}>
|
|
91
120
|
{ atlasExperimentList.map((e,idx) =>
|
|
92
121
|
<option key={idx} value={e._id}>{e.type}: {e.description || e._id}</option>
|
|
@@ -106,7 +135,11 @@ const Detail = props => {
|
|
|
106
135
|
{activeTab === "gene" && <DynamicIframe url={gene_url}/> }
|
|
107
136
|
</Tab>
|
|
108
137
|
{haveBAR(gene) &&
|
|
109
|
-
<Tab tabClassName="eFP" eventKey="eFP" title="eFP Browser" key="bar"
|
|
138
|
+
<Tab tabClassName="eFP" eventKey="eFP" title="eFP Browser" key="bar">
|
|
139
|
+
<BAR gene={gene}
|
|
140
|
+
study={expr.barStudy}
|
|
141
|
+
onStudyChange={v => props.doSetExpressionState({geneId, patch: {barStudy: v}})}/>
|
|
142
|
+
</Tab>
|
|
110
143
|
}
|
|
111
144
|
</Tabs>
|
|
112
145
|
};
|
|
@@ -114,7 +147,10 @@ const Detail = props => {
|
|
|
114
147
|
export default connect(
|
|
115
148
|
'selectGrameneParalogs',
|
|
116
149
|
'selectExpressionStudies',
|
|
150
|
+
'selectUiViewState',
|
|
117
151
|
'doRequestParalogs',
|
|
152
|
+
'doFetchExpressionStudies',
|
|
153
|
+
'doSetExpressionState',
|
|
118
154
|
//'doRequestParalogExpression',
|
|
119
155
|
Detail
|
|
120
156
|
);
|
|
@@ -2,7 +2,10 @@ import React from 'react'
|
|
|
2
2
|
import _ from 'lodash';
|
|
3
3
|
import {connect} from "redux-bundler-react";
|
|
4
4
|
import TreeVis from "gramene-genetree-vis";
|
|
5
|
-
|
|
5
|
+
// Subpath imports avoid gramene-trees-client/index.js, whose require chain
|
|
6
|
+
// fires a /swagger fetch at module load (see comment in bundles/api.js).
|
|
7
|
+
import taxonomy from "gramene-trees-client/src/taxonomy";
|
|
8
|
+
import genetree from "gramene-trees-client/src/genetree";
|
|
6
9
|
import {
|
|
7
10
|
TBrowse,
|
|
8
11
|
computePivotState,
|
|
@@ -17,7 +20,7 @@ import {
|
|
|
17
20
|
} from "tbrowse";
|
|
18
21
|
import {Detail, Title, Description, Content, Explore, Links} from "./generic";
|
|
19
22
|
import {suggestionToFilters} from "../../utils";
|
|
20
|
-
import {Spinner
|
|
23
|
+
import {Spinner} from "react-bootstrap";
|
|
21
24
|
import '../../../../node_modules/gramene-genetree-vis/src/styles/msa.less';
|
|
22
25
|
import '../../../../node_modules/gramene-genetree-vis/src/styles/tree.less';
|
|
23
26
|
import './tree-view.css';
|
|
@@ -28,15 +31,123 @@ const TBROWSE_ZONES = [treeZone, labelsZone, msaZone, neighborhoodZone, genomeZo
|
|
|
28
31
|
class Homology extends React.Component {
|
|
29
32
|
constructor(props) {
|
|
30
33
|
super(props);
|
|
31
|
-
|
|
34
|
+
// Async data caches stay in local state (per mount, per tree). The
|
|
35
|
+
// user-controlled view state (viewer toggle, resize height, tbrowse
|
|
36
|
+
// ViewState) is lifted into the uiViewState bundle, keyed by geneId.
|
|
37
|
+
//
|
|
38
|
+
// `*Status` fields track per-zone load lifecycle so the TBrowse
|
|
39
|
+
// toolbar can render loading/error affordances via its zoneStatus
|
|
40
|
+
// prop. They start `undefined` (no signal); flipped to 'loading'
|
|
41
|
+
// when the fetch kicks off and 'ready' / 'error' on settle.
|
|
42
|
+
this.state = {
|
|
43
|
+
neighborhood: null,
|
|
44
|
+
neighborhoodTreeId: null,
|
|
45
|
+
neighborhoodStatus: undefined,
|
|
46
|
+
geneStructures: null,
|
|
47
|
+
geneStructuresTreeId: null,
|
|
48
|
+
geneStructuresStatus: undefined,
|
|
49
|
+
};
|
|
32
50
|
if (!props.geneDocs.hasOwnProperty(props.searchResult.id)) {
|
|
33
51
|
props.requestGene(props.searchResult.id)
|
|
34
52
|
}
|
|
35
|
-
this.taxonomy =
|
|
53
|
+
this.taxonomy = taxonomy.tree(Object.values(props.grameneTaxonomy))
|
|
54
|
+
}
|
|
55
|
+
// ----- uiViewState accessors (with sensible defaults) -----
|
|
56
|
+
getGeneId() { return this.props.searchResult.id; }
|
|
57
|
+
getHomologySlice() {
|
|
58
|
+
const slice = this.props.uiViewState && this.props.uiViewState.byGene[this.getGeneId()];
|
|
59
|
+
return (slice && slice.homology) || {};
|
|
60
|
+
}
|
|
61
|
+
getViewer() { return this.getHomologySlice().viewer || 'treevis'; }
|
|
62
|
+
getHeight() {
|
|
63
|
+
const h = this.getHomologySlice().height;
|
|
64
|
+
return typeof h === 'number' ? h : 600;
|
|
65
|
+
}
|
|
66
|
+
setViewer(viewer) {
|
|
67
|
+
this.props.doSetHomologyViewer({geneId: this.getGeneId(), viewer});
|
|
68
|
+
}
|
|
69
|
+
setHeight(height) {
|
|
70
|
+
this.props.doSetHomologyHeight({geneId: this.getGeneId(), height});
|
|
71
|
+
}
|
|
72
|
+
setTbrowseViewState(viewState) {
|
|
73
|
+
this.props.doSetHomologyTbrowseViewState({geneId: this.getGeneId(), tbrowse: viewState});
|
|
74
|
+
}
|
|
75
|
+
// Seed the bundle with a pivot-computed initial tbrowse view state once the
|
|
76
|
+
// tree data is available and the user is actually looking at the tbrowse
|
|
77
|
+
// viewer. Called from lifecycle (not render) to avoid dispatching mid-render.
|
|
78
|
+
maybeSeedTbrowseViewState() {
|
|
79
|
+
if (this.getViewer() !== 'tbrowse') return;
|
|
80
|
+
if (this.getHomologySlice().tbrowse) return;
|
|
81
|
+
const id = this.getGeneId();
|
|
82
|
+
if (!this.props.geneDocs.hasOwnProperty(id)) return;
|
|
83
|
+
const gene = this.props.geneDocs[id];
|
|
84
|
+
if (!gene.homology) return;
|
|
85
|
+
const treeId = gene.homology.gene_tree.id;
|
|
86
|
+
const raw = this.props.grameneTrees[treeId];
|
|
87
|
+
if (!raw || !raw.taxon_id) return;
|
|
88
|
+
const adapted = fromGrameneGenetree([raw]);
|
|
89
|
+
const pivot = computePivotState(adapted.tree, gene._id);
|
|
90
|
+
this.setTbrowseViewState({
|
|
91
|
+
selectedNodeId: null,
|
|
92
|
+
collapsedNodeIds: pivot ? pivot.collapsedNodeIds : [],
|
|
93
|
+
prunedNodeIds: [],
|
|
94
|
+
swappedNodeIds: pivot ? pivot.swappedNodeIds : [],
|
|
95
|
+
compressedNodeIds: [],
|
|
96
|
+
nodeOfInterestId: pivot ? pivot.targetId : null,
|
|
97
|
+
// Use each zone's intended fr-share + initial visibility from its
|
|
98
|
+
// definition (tree:30 / labels:20 / msa:50 / neighborhood:30 /
|
|
99
|
+
// genome:32) instead of uniform 25 across the board. Gives the MSA
|
|
100
|
+
// a wider initial pane and the tree a narrower one.
|
|
101
|
+
//
|
|
102
|
+
// `defaultVisible ?? true` matches tbrowse's own buildInitialViewState
|
|
103
|
+
// logic — zones default to visible unless their definition opts out
|
|
104
|
+
// (e.g. neighborhood and genome opt out so they only appear once
|
|
105
|
+
// their async data lands; tbrowse's Layout auto-flips them on then).
|
|
106
|
+
zones: TBROWSE_ZONES.map(z => ({
|
|
107
|
+
id: z.id,
|
|
108
|
+
width: z.defaultWidth,
|
|
109
|
+
visible: z.defaultVisible ?? true
|
|
110
|
+
})),
|
|
111
|
+
zoneStates: {},
|
|
112
|
+
search: null,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
componentDidMount() {
|
|
116
|
+
this.maybeSeedTbrowseViewState();
|
|
117
|
+
this.maybeFetchTbrowseData();
|
|
118
|
+
}
|
|
119
|
+
componentDidUpdate() {
|
|
120
|
+
this.maybeSeedTbrowseViewState();
|
|
121
|
+
this.maybeFetchTbrowseData();
|
|
122
|
+
}
|
|
123
|
+
// Kick off the neighborhood + gene-structure fetches from lifecycle,
|
|
124
|
+
// not render. Each fetch internally dedupes on `_*FetchedFor === treeId`
|
|
125
|
+
// so calling on every commit is cheap. Gated on the user actually being
|
|
126
|
+
// on the tbrowse viewer + the tree data being loaded — otherwise we'd
|
|
127
|
+
// pay network cost for genes the user never looks at.
|
|
128
|
+
maybeFetchTbrowseData() {
|
|
129
|
+
if (this.getViewer() !== 'tbrowse') return;
|
|
130
|
+
const id = this.getGeneId();
|
|
131
|
+
if (!this.props.geneDocs.hasOwnProperty(id)) return;
|
|
132
|
+
const gene = this.props.geneDocs[id];
|
|
133
|
+
if (!gene.homology) return;
|
|
134
|
+
const treeId = gene.homology.gene_tree.id;
|
|
135
|
+
const raw = this.props.grameneTrees[treeId];
|
|
136
|
+
if (!raw || !raw.taxon_id) return;
|
|
137
|
+
// Lazily compute the adapted tbrowse data the same way renderTBrowse
|
|
138
|
+
// does, so fetchGeneStructures has the leaf ids without rerunning
|
|
139
|
+
// fromGrameneGenetree on every commit.
|
|
140
|
+
if (this._tbrowseTreeId !== treeId) {
|
|
141
|
+
this._tbrowseTreeId = treeId;
|
|
142
|
+
this._tbrowseData = fromGrameneGenetree([raw]);
|
|
143
|
+
}
|
|
144
|
+
this.fetchNeighborhood(treeId);
|
|
145
|
+
this.fetchGeneStructures(treeId, this._tbrowseData.tree);
|
|
36
146
|
}
|
|
37
147
|
fetchNeighborhood(treeId) {
|
|
38
148
|
if (this._neighborhoodFetchedFor === treeId) return;
|
|
39
149
|
this._neighborhoodFetchedFor = treeId;
|
|
150
|
+
this.setState({neighborhoodStatus: 'loading'});
|
|
40
151
|
const api = this.props.grameneAPI;
|
|
41
152
|
const url = new URL(`${api}/search`);
|
|
42
153
|
url.searchParams.set('fl', 'id,name,gene_tree,gene_idx,region,start,end,strand,biotype,system_name,description');
|
|
@@ -50,10 +161,19 @@ class Homology extends React.Component {
|
|
|
50
161
|
})
|
|
51
162
|
.then(json => {
|
|
52
163
|
if (this._neighborhoodFetchedFor !== treeId) return;
|
|
53
|
-
this.setState({
|
|
164
|
+
this.setState({
|
|
165
|
+
neighborhood: fromGrameneNeighborhood(json),
|
|
166
|
+
neighborhoodTreeId: treeId,
|
|
167
|
+
neighborhoodStatus: 'ready',
|
|
168
|
+
});
|
|
54
169
|
})
|
|
55
170
|
.catch(err => {
|
|
56
171
|
console.warn('tbrowse neighborhood fetch failed:', err);
|
|
172
|
+
// Surface the failure via the zoneStatus prop. Reset the
|
|
173
|
+
// dedupe key so a re-mount or tree-change can retry.
|
|
174
|
+
if (this._neighborhoodFetchedFor === treeId) {
|
|
175
|
+
this.setState({neighborhoodStatus: 'error'});
|
|
176
|
+
}
|
|
57
177
|
this._neighborhoodFetchedFor = null;
|
|
58
178
|
});
|
|
59
179
|
}
|
|
@@ -64,6 +184,7 @@ class Homology extends React.Component {
|
|
|
64
184
|
.filter(n => n.isLeaf && n.geneId)
|
|
65
185
|
.map(n => n.geneId);
|
|
66
186
|
if (ids.length === 0) return;
|
|
187
|
+
this.setState({geneStructuresStatus: 'loading'});
|
|
67
188
|
const api = this.props.grameneAPI;
|
|
68
189
|
const BATCH_SIZE = 50;
|
|
69
190
|
const batches = [];
|
|
@@ -84,21 +205,28 @@ class Homology extends React.Component {
|
|
|
84
205
|
.then(results => {
|
|
85
206
|
if (this._geneStructuresFetchedFor !== treeId) return;
|
|
86
207
|
const combined = [].concat(...results.map(r => Array.isArray(r) ? r : []));
|
|
87
|
-
this.setState({
|
|
208
|
+
this.setState({
|
|
209
|
+
geneStructures: fromGrameneGeneStructures(combined),
|
|
210
|
+
geneStructuresTreeId: treeId,
|
|
211
|
+
geneStructuresStatus: 'ready',
|
|
212
|
+
});
|
|
88
213
|
})
|
|
89
214
|
.catch(err => {
|
|
90
215
|
console.warn('tbrowse gene-structures fetch failed:', err);
|
|
216
|
+
if (this._geneStructuresFetchedFor === treeId) {
|
|
217
|
+
this.setState({geneStructuresStatus: 'error'});
|
|
218
|
+
}
|
|
91
219
|
this._geneStructuresFetchedFor = null;
|
|
92
220
|
});
|
|
93
221
|
}
|
|
94
222
|
startResize(e) {
|
|
95
223
|
e.preventDefault();
|
|
96
224
|
const startY = e.clientY;
|
|
97
|
-
const startHeight = this.
|
|
225
|
+
const startHeight = this.getHeight();
|
|
98
226
|
|
|
99
227
|
const onMouseMove = (moveEvent) => {
|
|
100
228
|
const newHeight = Math.max(200, startHeight + (moveEvent.clientY - startY));
|
|
101
|
-
this.
|
|
229
|
+
this.setHeight(newHeight);
|
|
102
230
|
};
|
|
103
231
|
|
|
104
232
|
const onMouseUp = () => {
|
|
@@ -122,7 +250,7 @@ class Homology extends React.Component {
|
|
|
122
250
|
renderTreeVis() {
|
|
123
251
|
return (
|
|
124
252
|
<>
|
|
125
|
-
<div className="gene-genetree" style={{height: this.
|
|
253
|
+
<div className="gene-genetree" style={{height: this.getHeight(), width: '100%'}}>
|
|
126
254
|
<TreeVis genetree={this.tree}
|
|
127
255
|
initialGeneOfInterest={this.gene}
|
|
128
256
|
genomesOfInterest={this.props.grameneGenomes.active}
|
|
@@ -140,32 +268,35 @@ class Homology extends React.Component {
|
|
|
140
268
|
}
|
|
141
269
|
renderTBrowse() {
|
|
142
270
|
const treeId = this.gene.homology.gene_tree.id;
|
|
271
|
+
// Adapted tbrowse data is computed lazily in maybeFetchTbrowseData
|
|
272
|
+
// (called from lifecycle). If the user just flipped to tbrowse and
|
|
273
|
+
// the lifecycle hasn't fired yet, compute it once here without
|
|
274
|
+
// calling any fetches — those will fire from componentDidUpdate.
|
|
143
275
|
if (this._tbrowseTreeId !== treeId) {
|
|
144
|
-
const raw = this.props.grameneTrees[treeId];
|
|
145
|
-
const adapted = fromGrameneGenetree([raw]);
|
|
146
276
|
this._tbrowseTreeId = treeId;
|
|
147
|
-
this._tbrowseData =
|
|
148
|
-
const pivot = computePivotState(adapted.tree, this.gene._id);
|
|
149
|
-
const zoneIds = TBROWSE_ZONES.map(z => z.id);
|
|
150
|
-
this._tbrowseInitialViewState = {
|
|
151
|
-
selectedNodeId: null,
|
|
152
|
-
collapsedNodeIds: pivot ? pivot.collapsedNodeIds : [],
|
|
153
|
-
prunedNodeIds: [],
|
|
154
|
-
swappedNodeIds: pivot ? pivot.swappedNodeIds : [],
|
|
155
|
-
compressedNodeIds: [],
|
|
156
|
-
nodeOfInterestId: pivot ? pivot.targetId : null,
|
|
157
|
-
zones: zoneIds.map(id => ({id, width: 25, visible: true})),
|
|
158
|
-
zoneStates: {},
|
|
159
|
-
search: null,
|
|
160
|
-
};
|
|
277
|
+
this._tbrowseData = fromGrameneGenetree([this.props.grameneTrees[treeId]]);
|
|
161
278
|
}
|
|
162
|
-
this.fetchNeighborhood(treeId);
|
|
163
|
-
this.fetchGeneStructures(treeId, this._tbrowseData.tree);
|
|
164
279
|
const neighborhood = this.state.neighborhoodTreeId === treeId ? this.state.neighborhood : undefined;
|
|
165
280
|
const geneStructures = this.state.geneStructuresTreeId === treeId ? this.state.geneStructures : undefined;
|
|
281
|
+
// Per-zone status for the TBrowse toolbar — pulses while a fetch
|
|
282
|
+
// is in flight, turns red on failure. Tracked per-tree so a
|
|
283
|
+
// tree-change resets any stale 'ready'/'error' from the prior gene.
|
|
284
|
+
const zoneStatus = {
|
|
285
|
+
neighborhood: this.state.neighborhoodTreeId === treeId
|
|
286
|
+
? this.state.neighborhoodStatus
|
|
287
|
+
: (this.state.neighborhoodStatus === 'loading' ? 'loading' : undefined),
|
|
288
|
+
genome: this.state.geneStructuresTreeId === treeId
|
|
289
|
+
? this.state.geneStructuresStatus
|
|
290
|
+
: (this.state.geneStructuresStatus === 'loading' ? 'loading' : undefined),
|
|
291
|
+
};
|
|
292
|
+
// Bundle-driven (controlled) view state. If we're rendering tbrowse before
|
|
293
|
+
// componentDidMount/Update has seeded the bundle slice, skip this turn and
|
|
294
|
+
// let the re-render with the seeded state do the work.
|
|
295
|
+
const tbrowseVS = this.getHomologySlice().tbrowse;
|
|
296
|
+
if (!tbrowseVS) return null;
|
|
166
297
|
return (
|
|
167
298
|
<>
|
|
168
|
-
<div className="gene-genetree" style={{height: this.
|
|
299
|
+
<div className="gene-genetree" style={{height: this.getHeight(), width: '100%'}}>
|
|
169
300
|
<TBrowse
|
|
170
301
|
tree={this._tbrowseData.tree}
|
|
171
302
|
taxonomy={this._tbrowseData.taxonomy}
|
|
@@ -177,7 +308,10 @@ class Homology extends React.Component {
|
|
|
177
308
|
geneStructures={geneStructures}
|
|
178
309
|
zones={TBROWSE_ZONES}
|
|
179
310
|
nodeOfInterest={this.gene._id}
|
|
180
|
-
|
|
311
|
+
viewState={tbrowseVS}
|
|
312
|
+
onViewStateChange={next => this.setTbrowseViewState(next)}
|
|
313
|
+
defaultOpenSections={{ zones: true, search: true }}
|
|
314
|
+
zoneStatus={zoneStatus}
|
|
181
315
|
/>
|
|
182
316
|
</div>
|
|
183
317
|
{this.renderResizeHandle()}
|
|
@@ -185,12 +319,12 @@ class Homology extends React.Component {
|
|
|
185
319
|
)
|
|
186
320
|
}
|
|
187
321
|
renderViewerToggle() {
|
|
188
|
-
const
|
|
322
|
+
const viewer = this.getViewer();
|
|
189
323
|
const btn = (id, label) => (
|
|
190
324
|
<button
|
|
191
325
|
key={id}
|
|
192
326
|
type="button"
|
|
193
|
-
onClick={() => this.
|
|
327
|
+
onClick={() => this.setViewer(id)}
|
|
194
328
|
style={{
|
|
195
329
|
padding: '4px 10px',
|
|
196
330
|
marginRight: 4,
|
|
@@ -336,25 +470,20 @@ class Homology extends React.Component {
|
|
|
336
470
|
else {
|
|
337
471
|
const tree = this.props.grameneTrees[treeId];
|
|
338
472
|
if (tree.hasOwnProperty('taxon_id')) {
|
|
339
|
-
this.tree =
|
|
473
|
+
this.tree = genetree.tree([this.props.grameneTrees[treeId]]);
|
|
340
474
|
this.orthologs = this.orthologList();
|
|
341
475
|
this.paralogs = this.paralogList();
|
|
342
476
|
}
|
|
343
477
|
}
|
|
344
|
-
let flagged=0;
|
|
345
|
-
// if (this.props.curation && this.props.curation.taxa.hasOwnProperty(this.gene.taxon_id)) {
|
|
346
|
-
// flagged = this.props.curatedGenes && this.props.curatedGenes[id] ? this.props.curatedGenes[id].flagged : 0;
|
|
347
|
-
// }
|
|
348
478
|
return (
|
|
349
479
|
<Detail>
|
|
350
480
|
{/*<Title key="title">Compara Gene Tree</Title>*/}
|
|
351
481
|
<Description key="description">
|
|
352
482
|
This phylogram shows the relationships between this gene and others similar to it, as determined by Ensembl Compara.
|
|
353
|
-
{flagged > 1 && <Alert variant={'warning'}>This gene was flagged for potential gene structural annotation issues by {flagged} curators</Alert>}
|
|
354
483
|
</Description>
|
|
355
484
|
{this.tree && <Content key="content">
|
|
356
485
|
{this.renderViewerToggle()}
|
|
357
|
-
{this.
|
|
486
|
+
{this.getViewer() === 'tbrowse' ? this.renderTBrowse() : this.renderTreeVis()}
|
|
358
487
|
</Content>}
|
|
359
488
|
{this.tree && <Explore key="explore" explorations={this.explorations()}/>}
|
|
360
489
|
<Links key="links" links={this.links()}/>
|
|
@@ -370,8 +499,11 @@ export default connect(
|
|
|
370
499
|
'selectGrameneAPI',
|
|
371
500
|
'selectConfiguration',
|
|
372
501
|
'selectCuration',
|
|
373
|
-
'
|
|
502
|
+
'selectUiViewState',
|
|
374
503
|
'doRequestGrameneTree',
|
|
504
|
+
'doSetHomologyViewer',
|
|
505
|
+
'doSetHomologyHeight',
|
|
506
|
+
'doSetHomologyTbrowseViewState',
|
|
375
507
|
'doAcceptGrameneSuggestion',
|
|
376
508
|
'doReplaceGrameneFilters',
|
|
377
509
|
Homology
|
|
@@ -2,7 +2,9 @@ import React from 'react'
|
|
|
2
2
|
import {connect} from "redux-bundler-react";
|
|
3
3
|
import FlatToNested from 'flat-to-nested';
|
|
4
4
|
import {Explore, Links} from "./generic";
|
|
5
|
-
import
|
|
5
|
+
// Subpath import avoids gramene-trees-client/index.js, whose require chain
|
|
6
|
+
// fires a /swagger fetch at module load (see comment in bundles/api.js).
|
|
7
|
+
import taxonomy from "gramene-trees-client/src/taxonomy";
|
|
6
8
|
import TreeMenu from "react-simple-tree-menu";
|
|
7
9
|
import '../../../../node_modules/react-simple-tree-menu/dist/main.css';
|
|
8
10
|
|
|
@@ -101,7 +103,7 @@ function buildIframeSrcDoc() {
|
|
|
101
103
|
class Pathways extends React.Component {
|
|
102
104
|
constructor(props) {
|
|
103
105
|
super(props);
|
|
104
|
-
this.taxonomy =
|
|
106
|
+
this.taxonomy = taxonomy.tree(Object.values(props.grameneTaxonomy))
|
|
105
107
|
this.gene = props.geneDocs[props.searchResult.id];
|
|
106
108
|
this.iframeRef = React.createRef();
|
|
107
109
|
// srcDoc is constant — the per-instance pathway/reaction state is
|
|
@@ -169,22 +169,36 @@ const buildId = (gene, geneSeq, up, down) => {
|
|
|
169
169
|
return `${geneSeq.genome}|${gene._id}|${gene.location.region}:${gs}..${ge} ${extras.join('|')}`
|
|
170
170
|
};
|
|
171
171
|
const Detail = props => {
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
const geneId = props.searchResult.id;
|
|
173
|
+
const gene = props.geneDocs[geneId];
|
|
174
|
+
// Sub-tab + selected isoform + flanking lengths live in the uiViewState
|
|
175
|
+
// bundle (keyed by geneId) so the shareable-views snapshot can round-trip
|
|
176
|
+
// them. Defaults match the old local-state initial values.
|
|
177
|
+
const slice = (props.uiViewState && props.uiViewState.byGene[geneId]
|
|
178
|
+
&& props.uiViewState.byGene[geneId].sequences) || {};
|
|
179
|
+
const tab = slice.tab || 'dna';
|
|
180
|
+
const tid = slice.tid || gene.gene_structure.canonical_transcript;
|
|
181
|
+
const upstream = slice.upstream || 0;
|
|
182
|
+
const downstream = slice.downstream || 0;
|
|
183
|
+
const setTab = (k) => props.doSetSequencesState({geneId, patch: {tab: k}});
|
|
184
|
+
const setTid = (v) => props.doSetSequencesState({geneId, patch: {tid: v}});
|
|
185
|
+
const setUpstream = (v) => props.doSetSequencesState({geneId, patch: {upstream: +v}});
|
|
186
|
+
const setDownstream = (v) => props.doSetSequencesState({geneId, patch: {downstream: +v}});
|
|
177
187
|
let geneSeq;
|
|
178
188
|
let rnaSeq;
|
|
179
189
|
let pepSeq;
|
|
180
|
-
|
|
190
|
+
// The *_SEQUENCE_REQUESTED reducer inserts a `{}` placeholder before the
|
|
191
|
+
// fetch resolves, so a key-only existence check passes while the body is
|
|
192
|
+
// still empty — leading to `seq.substring` of undefined downstream. Gate
|
|
193
|
+
// on the actual payload field instead.
|
|
194
|
+
if (props.geneSequences && props.geneSequences[gene._id] && props.geneSequences[gene._id].seq) {
|
|
181
195
|
geneSeq = props.geneSequences[gene._id];
|
|
182
196
|
}
|
|
183
197
|
else {
|
|
184
198
|
props.doRequestGeneSequence(gene)
|
|
185
199
|
return <pre>loading</pre>;
|
|
186
200
|
}
|
|
187
|
-
if (props.rnaSequences && props.rnaSequences[tid]) {
|
|
201
|
+
if (props.rnaSequences && props.rnaSequences[tid] && props.rnaSequences[tid].seq) {
|
|
188
202
|
rnaSeq = props.rnaSequences[tid]
|
|
189
203
|
}
|
|
190
204
|
else {
|
|
@@ -197,7 +211,7 @@ const Detail = props => {
|
|
|
197
211
|
let tl_id;
|
|
198
212
|
if (transcript.translation) {
|
|
199
213
|
tl_id = transcript.translation.id;
|
|
200
|
-
if (props.pepSequences && props.pepSequences[tl_id]) {
|
|
214
|
+
if (props.pepSequences && props.pepSequences[tl_id] && props.pepSequences[tl_id].seq) {
|
|
201
215
|
pepSeq = props.pepSequences[tl_id];
|
|
202
216
|
}
|
|
203
217
|
else {
|
|
@@ -329,9 +343,11 @@ export default connect(
|
|
|
329
343
|
'selectGeneSequences',
|
|
330
344
|
'selectRnaSequences',
|
|
331
345
|
'selectPepSequences',
|
|
346
|
+
'selectUiViewState',
|
|
332
347
|
'doRequestGeneSequence',
|
|
333
348
|
'doRequestRnaSequence',
|
|
334
349
|
'doRequestPepSequence',
|
|
350
|
+
'doSetSequencesState',
|
|
335
351
|
Detail
|
|
336
352
|
);
|
|
337
353
|
|
|
@@ -56,21 +56,40 @@ function compareGermplasm(a, b) {
|
|
|
56
56
|
}
|
|
57
57
|
function group_germplasm(gene, germplasmLUT, vep_obj) {
|
|
58
58
|
let accessionTable = [];
|
|
59
|
+
let missingMetadata = 0;
|
|
59
60
|
Object.entries(vep_obj).forEach(([key,accessions]) => {
|
|
60
61
|
const parts = key.split("__");
|
|
61
62
|
if (parts[0] === "VEP") {
|
|
62
63
|
if (parts[1] !== "merged") {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
const studyForSystem = study_info[parts[3]];
|
|
65
|
+
const pop = (studyForSystem && studyForSystem[parts[4]]) || {
|
|
66
|
+
label: `${parts[3]}/${parts[4]}`,
|
|
67
|
+
type: parts[4]
|
|
68
|
+
};
|
|
69
|
+
const conseq = parts[1].replaceAll("_"," ");
|
|
70
|
+
const status = parts[2] === "het" ? "heterozygous" : "homozygous";
|
|
71
|
+
accessions.forEach(ens_id => {
|
|
72
|
+
let germplasm;
|
|
73
|
+
if (germplasmLUT && germplasmLUT.hasOwnProperty(ens_id)) {
|
|
74
|
+
germplasm = germplasmLUT[ens_id][0];
|
|
75
|
+
} else {
|
|
76
|
+
// Fall back to a minimal record so the row is still rendered with
|
|
77
|
+
// whatever info we have from the VEP fields alone.
|
|
78
|
+
missingMetadata++;
|
|
79
|
+
germplasm = {
|
|
80
|
+
ens_id: ens_id,
|
|
81
|
+
pub_id: ens_id,
|
|
82
|
+
subpop: '?',
|
|
83
|
+
stock_center: null,
|
|
84
|
+
germplasm_dbid: null,
|
|
85
|
+
pop_id: null
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
accessionTable.push({
|
|
69
89
|
key: [pop.label,conseq,status].join('%%%'),
|
|
70
90
|
germplasm: germplasm,
|
|
71
91
|
pop: pop
|
|
72
|
-
};
|
|
73
|
-
accessionTable.push(accInfo);
|
|
92
|
+
});
|
|
74
93
|
});
|
|
75
94
|
}
|
|
76
95
|
}
|
|
@@ -106,6 +125,8 @@ function group_germplasm(gene, germplasmLUT, vep_obj) {
|
|
|
106
125
|
})
|
|
107
126
|
})
|
|
108
127
|
})
|
|
128
|
+
grouped.missingMetadata = missingMetadata;
|
|
129
|
+
grouped.totalAccessions = accessionTable.length;
|
|
109
130
|
return grouped;
|
|
110
131
|
}
|
|
111
132
|
const THRESHOLD = 5;
|
|
@@ -259,20 +280,45 @@ const GridWithGroups = ({groups,gene_id,doGrin}) => {
|
|
|
259
280
|
|
|
260
281
|
const Detail = props => {
|
|
261
282
|
const gene = props.geneDocs[props.searchResult.id];
|
|
262
|
-
|
|
263
|
-
|
|
283
|
+
const haveConsequences = props.grameneConsequences && props.grameneConsequences[gene._id];
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
if (!haveConsequences) {
|
|
286
|
+
props.doRequestVEP(gene._id);
|
|
287
|
+
}
|
|
288
|
+
}, [gene._id, haveConsequences]);
|
|
264
289
|
|
|
265
|
-
|
|
266
|
-
<h5>Predicted loss-of-function alleles were detected in these germplasm.</h5>
|
|
267
|
-
<div >Explore other variants within this gene in the <a target="_blank"
|
|
268
|
-
href={`${props.configuration.ensemblURL}/${gene.system_name}/Gene/Variation_Gene/Image?db=core;g=${props.searchResult.id}`}>
|
|
269
|
-
Variant image</a> page in the Ensembl genome browser.</div>
|
|
270
|
-
<GridWithGroups groups={...groups} gene_id={gene._id} doGrin={!props.configuration.hasOwnProperty('noGRIN')}/>
|
|
271
|
-
</div>
|
|
272
|
-
} else {
|
|
273
|
-
props.doRequestVEP(gene._id);
|
|
290
|
+
if (!haveConsequences) {
|
|
274
291
|
return <pre>loading</pre>;
|
|
275
292
|
}
|
|
293
|
+
|
|
294
|
+
// Render even if grameneGermplasm hasn't loaded — we'll fall back to
|
|
295
|
+
// VEP-only rows so the user sees something instead of a silent empty table.
|
|
296
|
+
const germplasmLUT = props.grameneGermplasm || {};
|
|
297
|
+
const groups = group_germplasm(gene, germplasmLUT, props.grameneConsequences[gene._id]);
|
|
298
|
+
const { missingMetadata, totalAccessions } = groups;
|
|
299
|
+
|
|
300
|
+
let notice = null;
|
|
301
|
+
if (totalAccessions === 0) {
|
|
302
|
+
notice = <div className="alert alert-warning" style={{padding: '8px', marginTop: '8px'}}>
|
|
303
|
+
VEP results were found for this gene but could not be grouped into accession-level rows.
|
|
304
|
+
</div>;
|
|
305
|
+
} else if (missingMetadata > 0) {
|
|
306
|
+
notice = <div className="alert alert-info" style={{padding: '8px', marginTop: '8px'}}>
|
|
307
|
+
Germplasm metadata could not be found for {missingMetadata} of {totalAccessions} accession{totalAccessions === 1 ? '' : 's'}.
|
|
308
|
+
Affected rows show the raw accession id without stock-center links or subpopulation info.
|
|
309
|
+
</div>;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return <div>
|
|
313
|
+
<h5>Predicted loss-of-function alleles were detected in these germplasm.</h5>
|
|
314
|
+
<div>Explore other variants within this gene in the <a target="_blank"
|
|
315
|
+
href={`${props.configuration.ensemblURL}/${gene.system_name}/Gene/Variation_Gene/Image?db=core;g=${props.searchResult.id}`}>
|
|
316
|
+
Variant image</a> page in the Ensembl genome browser.</div>
|
|
317
|
+
{notice}
|
|
318
|
+
{totalAccessions > 0 && (
|
|
319
|
+
<GridWithGroups groups={groups} gene_id={gene._id} doGrin={!props.configuration.hasOwnProperty('noGRIN')}/>
|
|
320
|
+
)}
|
|
321
|
+
</div>
|
|
276
322
|
};
|
|
277
323
|
|
|
278
324
|
export default connect(
|