sequenceserver 2.0.0.beta4 → 2.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +7 -4
- data/AppImage/sequenceserver.sh +5 -0
- data/lib/sequenceserver.rb +9 -5
- data/lib/sequenceserver/blast/job.rb +7 -24
- data/lib/sequenceserver/blast/report.rb +66 -33
- data/lib/sequenceserver/routes.rb +28 -2
- data/lib/sequenceserver/version.rb +1 -1
- data/public/SequenceServer_logo.png +0 -0
- data/public/css/grapher.css +8 -15
- data/public/css/sequenceserver.css +115 -55
- data/public/css/sequenceserver.min.css +3 -3
- data/public/js/circos.js +1 -1
- data/public/js/download_fasta.js +17 -0
- data/public/js/grapher.js +7 -9
- data/public/js/hit.js +217 -0
- data/public/js/hits_overview.js +12 -13
- data/public/js/hsp.js +104 -84
- data/public/js/{sequenceserver.js → jquery_world.js} +1 -18
- data/public/js/kablammo.js +337 -334
- data/public/js/length_distribution.js +1 -1
- data/public/js/query.js +147 -0
- data/public/js/report.js +203 -830
- data/public/js/search.js +176 -169
- data/public/js/sequence_modal.js +167 -0
- data/public/js/sidebar.js +210 -0
- data/public/js/utils.js +2 -19
- data/public/js/visualisation_helpers.js +2 -2
- data/public/sequenceserver-report.min.js +19 -19
- data/public/sequenceserver-search.min.js +11 -11
- data/public/vendor/github/twbs/bootstrap@3.3.5/js/bootstrap.js +2 -2
- data/spec/blast_versions/blast_2.2.30/import_spec_capybara_local_2.2.30.rb +5 -5
- data/spec/blast_versions/blast_2.2.31/import_spec_capybara_local_2.2.31.rb +5 -5
- data/spec/blast_versions/blast_2.3.0/import_spec_capybara_local_2.3.0.rb +5 -5
- data/spec/blast_versions/blast_2.4.0/import_spec_capybara_local_2.4.0.rb +5 -5
- data/spec/blast_versions/blast_2.5.0/import_spec_capybara_local_2.5.0.rb +5 -5
- data/spec/blast_versions/blast_2.6.0/import_spec_capybara_local_2.6.0.rb +5 -5
- data/spec/blast_versions/blast_2.7.1/import_spec_capybara_local_2.7.1.rb +5 -5
- data/spec/blast_versions/blast_2.8.1/import_spec_capybara_local_2.8.1.rb +5 -5
- data/spec/blast_versions/blast_2.9.0/import_spec_capybara_local_2.9.0.rb +5 -5
- data/spec/blast_versions/diamond_0.9.24/import_spec_capybara_local_0.9.24.rb +2 -2
- data/spec/capybara_spec.rb +1 -1
- data/views/layout.erb +1 -1
- metadata +9 -3
data/public/js/query.js
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import _ from 'underscore';
|
3
|
+
|
4
|
+
import HitsOverview from './hits_overview';
|
5
|
+
import LengthDistribution from './length_distribution'; // length distribution of hits
|
6
|
+
import Utils from './utils'; // to use as mixin in HitsTable
|
7
|
+
|
8
|
+
/**
|
9
|
+
* Query component displays query defline, graphical overview, length
|
10
|
+
* distribution, and hits table.
|
11
|
+
*/
|
12
|
+
export default React.createClass({
|
13
|
+
|
14
|
+
// Kind of public API //
|
15
|
+
|
16
|
+
/**
|
17
|
+
* Returns the id of query.
|
18
|
+
*/
|
19
|
+
domID: function () {
|
20
|
+
return 'Query_' + this.props.query.number;
|
21
|
+
},
|
22
|
+
|
23
|
+
queryLength: function () {
|
24
|
+
return this.props.query.length;
|
25
|
+
},
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Returns number of hits.
|
29
|
+
*/
|
30
|
+
numhits: function () {
|
31
|
+
return this.props.query.hits.length;
|
32
|
+
},
|
33
|
+
|
34
|
+
// Life cycle methods //
|
35
|
+
|
36
|
+
render: function () {
|
37
|
+
return (
|
38
|
+
<div className="resultn" id={this.domID()}
|
39
|
+
data-query-len={this.props.query.length}
|
40
|
+
data-algorithm={this.props.program}>
|
41
|
+
{ this.headerJSX() }
|
42
|
+
{ this.numhits() && this.hitsListJSX() || this.noHitsJSX() }
|
43
|
+
</div>
|
44
|
+
);
|
45
|
+
},
|
46
|
+
|
47
|
+
headerJSX: function () {
|
48
|
+
var meta = `length: ${this.queryLength().toLocaleString()}`;
|
49
|
+
if (this.props.showQueryCrumbs) {
|
50
|
+
meta = `query ${this.props.query.number}, ` + meta;
|
51
|
+
}
|
52
|
+
return <div className="section-header">
|
53
|
+
<h3>
|
54
|
+
<strong>Query= {this.props.query.id}</strong>
|
55
|
+
{this.props.query.title}
|
56
|
+
</h3>
|
57
|
+
<span className="label label-reset pos-label">{ meta }</span>
|
58
|
+
</div>;
|
59
|
+
},
|
60
|
+
|
61
|
+
hitsListJSX: function () {
|
62
|
+
return <div className="section-content">
|
63
|
+
<HitsOverview key={'GO_' + this.props.query.number} query={this.props.query} program={this.props.program} collapsed={this.props.veryBig} />
|
64
|
+
<LengthDistribution key={'LD_' + this.props.query.id} query={this.props.query} algorithm={this.props.program} collapsed="true" />
|
65
|
+
<HitsTable key={'HT_' + this.props.query.number} query={this.props.query} imported_xml={this.props.imported_xml} />
|
66
|
+
</div>;
|
67
|
+
},
|
68
|
+
|
69
|
+
noHitsJSX: function () {
|
70
|
+
return <div className="section-content">
|
71
|
+
<strong> ****** No hits found ****** </strong>
|
72
|
+
</div>;
|
73
|
+
},
|
74
|
+
|
75
|
+
// Each update cycle will cause all previous queries to be re-rendered.
|
76
|
+
// We avoid that by implementing shouldComponentUpdate life-cycle hook.
|
77
|
+
// The trick is to simply check if the components has recieved props
|
78
|
+
// before.
|
79
|
+
shouldComponentUpdate: function () {
|
80
|
+
// If the component has received props before, query property will
|
81
|
+
// be set on it. If it is, we return false so that the component
|
82
|
+
// is not re-rendered. If the query property is not set, we return
|
83
|
+
// true: this must be the first time react is trying to render the
|
84
|
+
// component.
|
85
|
+
return !this.props.query;
|
86
|
+
}
|
87
|
+
});
|
88
|
+
|
89
|
+
/**
|
90
|
+
* Renders summary of all hits per query in a tabular form.
|
91
|
+
*/
|
92
|
+
var HitsTable = React.createClass({
|
93
|
+
mixins: [Utils],
|
94
|
+
render: function () {
|
95
|
+
var count = 0,
|
96
|
+
hasName = _.every(this.props.query.hits, function(hit) {
|
97
|
+
return hit.sciname !== '';
|
98
|
+
});
|
99
|
+
|
100
|
+
return (
|
101
|
+
<div className="table-hit-overview">
|
102
|
+
<h4 className="caption" data-toggle="collapse" data-target={'#Query_'+this.props.query.number+'HT_'+this.props.query.number}>
|
103
|
+
<i className="fa fa-minus-square-o"></i>
|
104
|
+
<span>Summary table of hits</span>
|
105
|
+
</h4>
|
106
|
+
<div className="collapsed in"id={'Query_'+ this.props.query.number + 'HT_'+ this.props.query.number}>
|
107
|
+
<table
|
108
|
+
className="table table-hover table-condensed tabular-view ">
|
109
|
+
<thead>
|
110
|
+
<th className="text-left">#</th>
|
111
|
+
<th>Similar sequences</th>
|
112
|
+
{hasName && <th className="text-left">Species</th>}
|
113
|
+
{!this.props.imported_xml && <th className="text-right">Query coverage (%)</th>}
|
114
|
+
<th className="text-right">Total score</th>
|
115
|
+
<th className="text-right">E value</th>
|
116
|
+
<th className="text-right" data-toggle="tooltip"
|
117
|
+
data-placement="left" title="Total identity of all hsps / total length of all hsps">
|
118
|
+
Identity (%)
|
119
|
+
</th>
|
120
|
+
</thead>
|
121
|
+
<tbody>
|
122
|
+
{
|
123
|
+
_.map(this.props.query.hits, _.bind(function (hit) {
|
124
|
+
return (
|
125
|
+
<tr key={hit.number}>
|
126
|
+
<td className="text-left">{hit.number + '.'}</td>
|
127
|
+
<td>
|
128
|
+
<a href={'#Query_' + this.props.query.number + '_hit_' + hit.number} className="btn-link">
|
129
|
+
{hit.id}
|
130
|
+
</a>
|
131
|
+
</td>
|
132
|
+
{hasName && <td className="text-left">{hit.sciname}</td>}
|
133
|
+
{!this.props.imported_xml && <td className="text-right">{hit.qcovs}</td>}
|
134
|
+
<td className="text-right">{hit.score}</td>
|
135
|
+
<td className="text-right">{this.inExponential(hit.hsps[0].evalue)}</td>
|
136
|
+
<td className="text-right">{hit.identity}</td>
|
137
|
+
</tr>
|
138
|
+
);
|
139
|
+
}, this))
|
140
|
+
}
|
141
|
+
</tbody>
|
142
|
+
</table>
|
143
|
+
</div>
|
144
|
+
</div>
|
145
|
+
);
|
146
|
+
}
|
147
|
+
});
|
data/public/js/report.js
CHANGED
@@ -1,35 +1,16 @@
|
|
1
|
-
import './
|
1
|
+
import './jquery_world'; // for custom $.tooltip function
|
2
2
|
import React from 'react';
|
3
3
|
import _ from 'underscore';
|
4
4
|
|
5
|
+
import Sidebar from './sidebar';
|
5
6
|
import Circos from './circos';
|
6
|
-
import
|
7
|
-
import
|
8
|
-
import HSPOverview from './kablammo';
|
9
|
-
import AlignmentExporter from './alignment_exporter'; // to download textual alignment
|
7
|
+
import Query from './query';
|
8
|
+
import Hit from './hit';
|
10
9
|
import HSP from './hsp';
|
11
|
-
import './sequence';
|
12
10
|
|
13
|
-
import
|
14
|
-
import Utils from './utils'; // to use as mixin in Hit and HitsTable
|
11
|
+
import SequenceModal from './sequence_modal';
|
15
12
|
import showErrorModal from './error_modal';
|
16
13
|
|
17
|
-
/**
|
18
|
-
* Dynamically create form and submit.
|
19
|
-
*/
|
20
|
-
var downloadFASTA = function (sequence_ids, database_ids) {
|
21
|
-
var form = $('<form/>').attr('method', 'post').attr('action', 'get_sequence');
|
22
|
-
addField('sequence_ids', sequence_ids);
|
23
|
-
addField('database_ids', database_ids);
|
24
|
-
form.appendTo('body').submit().remove();
|
25
|
-
|
26
|
-
function addField(name, val) {
|
27
|
-
form.append(
|
28
|
-
$('<input>').attr('type', 'hidden').attr('name', name).val(val)
|
29
|
-
);
|
30
|
-
}
|
31
|
-
};
|
32
|
-
|
33
14
|
/**
|
34
15
|
* Base component of report page. This component is later rendered into page's
|
35
16
|
* '#view' element.
|
@@ -40,12 +21,39 @@ var Page = React.createClass({
|
|
40
21
|
<div>
|
41
22
|
{/* Provide bootstrap .container element inside the #view for
|
42
23
|
the Report component to render itself in. */}
|
43
|
-
<div className="container"
|
24
|
+
<div className="container">
|
25
|
+
<Report showSequenceModal={ _ => this.showSequenceModal(_) }
|
26
|
+
getCharacterWidth={ () => this.getCharacterWidth() } />
|
27
|
+
</div>
|
28
|
+
|
29
|
+
{/* Add a hidden span tag containing chars used in HSPs */}
|
30
|
+
<pre className="pre-reset hsp-lines" ref="hspChars" hidden>
|
31
|
+
ABCDEFGHIJKLMNOPQRSTUVWXYZ +-
|
32
|
+
</pre>
|
44
33
|
|
45
34
|
{/* Required by Grapher for SVG and PNG download */}
|
46
35
|
<canvas id="png-exporter" hidden></canvas>
|
36
|
+
|
37
|
+
<SequenceModal ref="sequenceModal" />
|
47
38
|
</div>
|
48
39
|
);
|
40
|
+
},
|
41
|
+
|
42
|
+
componentDidMount: function () {
|
43
|
+
var job_id = location.pathname.substr(1);
|
44
|
+
sessionStorage.setItem('job_id', job_id);
|
45
|
+
},
|
46
|
+
|
47
|
+
showSequenceModal: function (url) {
|
48
|
+
this.refs.sequenceModal.show(url);
|
49
|
+
},
|
50
|
+
|
51
|
+
getCharacterWidth: function () {
|
52
|
+
if (!this.characterWidth) {
|
53
|
+
var $hspChars = $(React.findDOMNode(this.refs.hspChars));
|
54
|
+
this.characterWidth = $hspChars.width() / 29;
|
55
|
+
}
|
56
|
+
return this.characterWidth;
|
49
57
|
}
|
50
58
|
});
|
51
59
|
|
@@ -60,15 +68,22 @@ var Report = React.createClass({
|
|
60
68
|
|
61
69
|
getInitialState: function () {
|
62
70
|
this.fetchResults();
|
63
|
-
|
71
|
+
|
72
|
+
// Properties below are internal state used to render results in small
|
73
|
+
// slices (see updateState).
|
74
|
+
this.numUpdates = 0;
|
75
|
+
this.nextQuery = 0;
|
76
|
+
this.nextHit = 0;
|
77
|
+
this.nextHSP = 0;
|
78
|
+
this.maxHSPs = 3; // max HSPs to render in a cycle
|
64
79
|
|
65
80
|
return {
|
66
81
|
search_id: '',
|
67
82
|
program: '',
|
68
83
|
program_version: '',
|
69
84
|
submitted_at: '',
|
70
|
-
num_queries: 0,
|
71
85
|
queries: [],
|
86
|
+
results: [],
|
72
87
|
querydb: [],
|
73
88
|
params: [],
|
74
89
|
stats: []
|
@@ -97,7 +112,7 @@ var Report = React.createClass({
|
|
97
112
|
setTimeout(poll, interval);
|
98
113
|
break;
|
99
114
|
case 200:
|
100
|
-
component.
|
115
|
+
component.setStateFromJSON(jqXHR.responseJSON);
|
101
116
|
break;
|
102
117
|
case 404:
|
103
118
|
case 400:
|
@@ -112,38 +127,139 @@ var Report = React.createClass({
|
|
112
127
|
},
|
113
128
|
|
114
129
|
/**
|
115
|
-
*
|
116
|
-
* process is not overwhelemed when there are too many queries.
|
130
|
+
* Calls setState after any required modification to responseJSON.
|
117
131
|
*/
|
118
|
-
|
119
|
-
|
120
|
-
responseJSON.num_queries = queries.length;
|
121
|
-
responseJSON.veryBig = queries.length > 250;
|
122
|
-
responseJSON.queries = queries.splice(0, 50);
|
132
|
+
setStateFromJSON: function(responseJSON) {
|
133
|
+
this.lastTimeStamp = Date.now();
|
123
134
|
this.setState(responseJSON);
|
124
|
-
|
125
|
-
// Render results for remaining queries.
|
126
|
-
var update = function () {
|
127
|
-
if (queries.length > 0) {
|
128
|
-
this.setState({
|
129
|
-
queries: this.state.queries.concat(queries.splice(0, 50))
|
130
|
-
});
|
131
|
-
setTimeout(update.bind(this), 500);
|
132
|
-
}
|
133
|
-
else {
|
134
|
-
this.componentFinishedUpdating();
|
135
|
-
}
|
136
|
-
};
|
137
|
-
setTimeout(update.bind(this), 500);
|
138
135
|
},
|
139
136
|
|
140
|
-
|
141
|
-
// View //
|
137
|
+
// Life-cycle methods //
|
142
138
|
render: function () {
|
143
139
|
return this.isResultAvailable() ?
|
144
140
|
this.resultsJSX() : this.loadingJSX();
|
145
141
|
},
|
146
142
|
|
143
|
+
/**
|
144
|
+
* Called as soon as the page has loaded and the user sees the loading spinner.
|
145
|
+
* We use this opportunity to setup services that make use of delegated events
|
146
|
+
* bound to the window, document, or body.
|
147
|
+
*/
|
148
|
+
componentDidMount: function () {
|
149
|
+
// This sets up an event handler which enables users to select text from
|
150
|
+
// hit header without collapsing the hit.
|
151
|
+
this.preventCollapseOnSelection();
|
152
|
+
this.toggleTable();
|
153
|
+
},
|
154
|
+
|
155
|
+
/**
|
156
|
+
* Called for the first time after as BLAST results have been retrieved from
|
157
|
+
* the server and added to this.state by fetchResults. Only summary overview
|
158
|
+
* and circos would have been rendered at this point. At this stage we kick
|
159
|
+
* start iteratively updating 10 HSPs (and as many hits and queries) every
|
160
|
+
* 25 milli-seconds.
|
161
|
+
*/
|
162
|
+
componentDidUpdate: function () {
|
163
|
+
// Log to console how long the last update take?
|
164
|
+
console.log((Date.now() - this.lastTimeStamp)/1000);
|
165
|
+
|
166
|
+
// Lock sidebar in its position on the first update.
|
167
|
+
if (this.nextQuery == 0 && this.nextHit == 0 && this.nextHSP == 0) {
|
168
|
+
this.affixSidebar();
|
169
|
+
}
|
170
|
+
|
171
|
+
// Queue next update if we have not rendered all results yet.
|
172
|
+
if (this.nextQuery < this.state.queries.length) {
|
173
|
+
// setTimeout is used to clear call stack and space out
|
174
|
+
// the updates giving the browser a chance to respond
|
175
|
+
// to user interactions.
|
176
|
+
setTimeout(() => this.updateState(), 25);
|
177
|
+
}
|
178
|
+
else {
|
179
|
+
this.componentFinishedUpdating();
|
180
|
+
}
|
181
|
+
},
|
182
|
+
|
183
|
+
/**
|
184
|
+
* Push next slice of results to React for rendering.
|
185
|
+
*/
|
186
|
+
updateState: function() {
|
187
|
+
var results = [];
|
188
|
+
var numHSPsProcessed = 0;
|
189
|
+
while (this.nextQuery < this.state.queries.length) {
|
190
|
+
var query = this.state.queries[this.nextQuery];
|
191
|
+
// We may see a query multiple times during rendering because only
|
192
|
+
// 3 hsps or are rendered in each cycle, but we want to create the
|
193
|
+
// corresponding Query component only the first time we see it.
|
194
|
+
if (this.nextHit == 0 && this.nextHSP == 0) {
|
195
|
+
results.push(<Query key={'Query_'+query.number} query={query}
|
196
|
+
program={this.state.program} querydb={this.state.querydb}
|
197
|
+
showQueryCrumbs={this.state.queries.length > 1}
|
198
|
+
imported_xml={this.state.imported_xml}
|
199
|
+
veryBig={this.state.veryBig} />);
|
200
|
+
}
|
201
|
+
|
202
|
+
while (this.nextHit < query.hits.length) {
|
203
|
+
var hit = query.hits[this.nextHit];
|
204
|
+
// We may see a hit multiple times during rendering because only
|
205
|
+
// 10 hsps are rendered in each cycle, but we want to create the
|
206
|
+
// corresponding Hit component only the first time we see it.
|
207
|
+
if (this.nextHSP == 0) {
|
208
|
+
results.push(<Hit key={'Query_'+query.number+'_Hit_'+hit.number} query={query}
|
209
|
+
hit={hit} algorithm={this.state.program} querydb={this.state.querydb}
|
210
|
+
selectHit={this.selectHit} imported_xml={this.state.imported_xml}
|
211
|
+
showQueryCrumbs={this.state.queries.length > 1}
|
212
|
+
showHitCrumbs={query.hits.length > 1}
|
213
|
+
veryBig={this.state.veryBig}
|
214
|
+
{... this.props} />
|
215
|
+
);
|
216
|
+
}
|
217
|
+
|
218
|
+
while (this.nextHSP < hit.hsps.length) {
|
219
|
+
// Get nextHSP and increment the counter.
|
220
|
+
var hsp = hit.hsps[this.nextHSP++];
|
221
|
+
results.push(
|
222
|
+
<HSP key={'Query_'+query.number+'_Hit_'+hit.number+'_HSP_'+hsp.number}
|
223
|
+
query={query} hit={hit} hsp={hsp} algorithm={this.state.program}
|
224
|
+
showHSPNumbers={hit.hsps.length > 1} {... this.props} />
|
225
|
+
);
|
226
|
+
numHSPsProcessed++;
|
227
|
+
if (numHSPsProcessed == this.maxHSPs) break;
|
228
|
+
}
|
229
|
+
// Are we here because we have iterated over all hsps of a hit,
|
230
|
+
// or because of the break clause in the inner loop?
|
231
|
+
if (this.nextHSP == hit.hsps.length) {
|
232
|
+
this.nextHit = this.nextHit + 1;
|
233
|
+
this.nextHSP = 0;
|
234
|
+
}
|
235
|
+
if (numHSPsProcessed == this.maxHSPs) break;
|
236
|
+
}
|
237
|
+
|
238
|
+
// Are we here because we have iterated over all hits of a query,
|
239
|
+
// or because of the break clause in the inner loop?
|
240
|
+
if (this.nextHit == query.hits.length) {
|
241
|
+
this.nextQuery = this.nextQuery + 1;
|
242
|
+
this.nextHit = 0;
|
243
|
+
}
|
244
|
+
if (numHSPsProcessed == this.maxHSPs) break;
|
245
|
+
}
|
246
|
+
|
247
|
+
// Push the components to react for rendering.
|
248
|
+
this.numUpdates++;
|
249
|
+
this.lastTimeStamp = Date.now();
|
250
|
+
this.setState({
|
251
|
+
results: this.state.results.concat(results),
|
252
|
+
veryBig: this.numUpdates >= 250
|
253
|
+
});
|
254
|
+
},
|
255
|
+
|
256
|
+
/**
|
257
|
+
* Called after all results have been rendered.
|
258
|
+
*/
|
259
|
+
componentFinishedUpdating: function () {
|
260
|
+
this.shouldShowIndex() && this.setupScrollSpy();
|
261
|
+
},
|
262
|
+
|
147
263
|
/**
|
148
264
|
* Returns loading message
|
149
265
|
*/
|
@@ -179,25 +295,15 @@ var Report = React.createClass({
|
|
179
295
|
<div className="row">
|
180
296
|
{ this.shouldShowSidebar() &&
|
181
297
|
(
|
182
|
-
<div
|
183
|
-
|
184
|
-
<SideBar data={this.state} shouldShowIndex={this.shouldShowIndex()}/>
|
298
|
+
<div className="col-md-3 hidden-sm hidden-xs">
|
299
|
+
<Sidebar data={this.state} shouldShowIndex={this.shouldShowIndex()}/>
|
185
300
|
</div>
|
186
301
|
)
|
187
302
|
}
|
188
|
-
<div className={this.shouldShowSidebar() ?
|
189
|
-
'col-md-9' : 'col-md-12'}>
|
303
|
+
<div className={this.shouldShowSidebar() ? 'col-md-9' : 'col-md-12'}>
|
190
304
|
{ this.overviewJSX() }
|
191
305
|
{ this.circosJSX() }
|
192
|
-
{
|
193
|
-
_.map(this.state.queries, _.bind(function (query) {
|
194
|
-
return (
|
195
|
-
<Query key={'Query_'+query.id} query={query} showQueryCrumbs={this.state.num_queries > 1}
|
196
|
-
selectHit={this.selectHit} program={this.state.program} querydb={this.state.querydb}
|
197
|
-
veryBig={this.state.veryBig} imported_xml={this.state.imported_xml} />
|
198
|
-
);
|
199
|
-
}, this))
|
200
|
-
}
|
306
|
+
{ this.state.results }
|
201
307
|
</div>
|
202
308
|
</div>
|
203
309
|
);
|
@@ -209,18 +315,18 @@ var Report = React.createClass({
|
|
209
315
|
overviewJSX: function () {
|
210
316
|
return (
|
211
317
|
<div className="overview">
|
212
|
-
<p
|
213
|
-
{this.state.program_version}{this.state.submitted_at
|
318
|
+
<p>
|
319
|
+
<strong>{this.state.program_version}</strong>{this.state.submitted_at
|
214
320
|
&& `, query submitted on ${this.state.submitted_at}`}
|
215
321
|
</p>
|
216
|
-
<p
|
217
|
-
Databases: {
|
322
|
+
<p>
|
323
|
+
<strong> Databases: </strong>{
|
218
324
|
this.state.querydb.map((db) => { return db.title; }).join(', ')
|
219
325
|
} ({this.state.stats.nsequences} sequences,
|
220
326
|
{this.state.stats.ncharacters} characters)
|
221
327
|
</p>
|
222
|
-
<p
|
223
|
-
Parameters: {
|
328
|
+
<p>
|
329
|
+
<strong>Parameters: </strong> {
|
224
330
|
_.map(this.state.params, function (val, key) {
|
225
331
|
return key + ' ' + val;
|
226
332
|
}).join(', ')
|
@@ -258,6 +364,10 @@ var Report = React.createClass({
|
|
258
364
|
return this.state.queries.some(query => query.hits.length > 0);
|
259
365
|
},
|
260
366
|
|
367
|
+
/**
|
368
|
+
* Does the report have at least two hits? This is used to determine
|
369
|
+
* whether Circos should be enabled or not.
|
370
|
+
*/
|
261
371
|
atLeastTwoHits: function () {
|
262
372
|
var hit_num = 0;
|
263
373
|
return this.state.queries.some(query => {
|
@@ -283,38 +393,7 @@ var Report = React.createClass({
|
|
283
393
|
*/
|
284
394
|
shouldShowIndex: function () {
|
285
395
|
var num_queries = this.state.queries.length;
|
286
|
-
return num_queries >= 2 && num_queries <=
|
287
|
-
},
|
288
|
-
|
289
|
-
/**
|
290
|
-
* Called after first call to render. The results may not be available at
|
291
|
-
* this stage and thus results DOM cannot be scripted here, unless using
|
292
|
-
* delegated events bound to the window, document, or body.
|
293
|
-
*/
|
294
|
-
componentDidMount: function () {
|
295
|
-
// This sets up an event handler which enables users to select text
|
296
|
-
// from hit header without collapsing the hit.
|
297
|
-
this.preventCollapseOnSelection();
|
298
|
-
},
|
299
|
-
|
300
|
-
/**
|
301
|
-
* Called after each state change. Only a part of results DOM may be
|
302
|
-
* available after a state change.
|
303
|
-
*/
|
304
|
-
componentDidUpdate: function () {
|
305
|
-
// We track the number of updates to the component.
|
306
|
-
this.updateCycle += 1;
|
307
|
-
|
308
|
-
// Lock sidebar in its position on first update of
|
309
|
-
// results DOM.
|
310
|
-
if (this.updateCycle === 1 ) this.affixSidebar();
|
311
|
-
},
|
312
|
-
|
313
|
-
/**
|
314
|
-
* Called after all results have been rendered.
|
315
|
-
*/
|
316
|
-
componentFinishedUpdating: function () {
|
317
|
-
this.shouldShowIndex() && this.setupScrollSpy();
|
396
|
+
return num_queries >= 2 && num_queries <= 12;
|
318
397
|
},
|
319
398
|
|
320
399
|
/**
|
@@ -326,12 +405,9 @@ var Report = React.createClass({
|
|
326
405
|
$this.on('mouseup mousemove', function handler(event) {
|
327
406
|
if (event.type === 'mouseup') {
|
328
407
|
// user wants to toggle
|
329
|
-
$this.
|
330
|
-
|
331
|
-
|
332
|
-
var target = $('#' + $this.attr('data-target'));
|
333
|
-
target.toggleClass('in');
|
334
|
-
$this.find('.fa-chevron-down').toggleClass('fa-rotate-270');
|
408
|
+
var hitID = $this.parents('.hit').attr('id');
|
409
|
+
$(`div[data-parent-hit=${hitID}]`).toggle();
|
410
|
+
$this.find('i').toggleClass('fa-minus-square-o fa-plus-square-o');
|
335
411
|
} else {
|
336
412
|
// user wants to select
|
337
413
|
$this.attr('data-toggle', '');
|
@@ -341,6 +417,19 @@ var Report = React.createClass({
|
|
341
417
|
});
|
342
418
|
},
|
343
419
|
|
420
|
+
/* Handling the fa icon when Hit Table is collapsed */
|
421
|
+
toggleTable: function () {
|
422
|
+
$('body').on('mousedown', '.resultn > .section-content > .table-hit-overview > .caption', function (event) {
|
423
|
+
var $this = $(this);
|
424
|
+
$this.on('mouseup mousemove', function handler(event) {
|
425
|
+
$this.find('i').toggleClass('fa-minus-square-o fa-plus-square-o');
|
426
|
+
$this.off('mouseup mousemove', handler);
|
427
|
+
});
|
428
|
+
});
|
429
|
+
},
|
430
|
+
|
431
|
+
|
432
|
+
|
344
433
|
/**
|
345
434
|
* Affixes the sidebar.
|
346
435
|
*/
|
@@ -379,12 +468,14 @@ var Report = React.createClass({
|
|
379
468
|
// Highlight selected hit and enable 'Download FASTA/Alignment of
|
380
469
|
// selected' links.
|
381
470
|
if (checkbox.is(':checked')) {
|
382
|
-
$hit.
|
383
|
-
$('.
|
471
|
+
$hit.addClass('glow');
|
472
|
+
$hit.next('.hsp').addClass('glow');
|
384
473
|
$('.download-fasta-of-selected').enable();
|
474
|
+
$('.download-alignment-of-selected').enable();
|
385
475
|
}
|
386
476
|
else {
|
387
|
-
$hit.
|
477
|
+
$hit.removeClass('glow');
|
478
|
+
$hit.next('.hsp').removeClass('glow');
|
388
479
|
}
|
389
480
|
|
390
481
|
if (num_checked >= 1)
|
@@ -404,722 +495,4 @@ var Report = React.createClass({
|
|
404
495
|
},
|
405
496
|
});
|
406
497
|
|
407
|
-
/**
|
408
|
-
* Renders report for each query sequence.
|
409
|
-
*
|
410
|
-
* Composed of graphical overview, tabular summary (HitsTable),
|
411
|
-
* and a list of Hits.
|
412
|
-
*/
|
413
|
-
var Query = React.createClass({
|
414
|
-
|
415
|
-
// Kind of public API //
|
416
|
-
|
417
|
-
/**
|
418
|
-
* Returns the id of query.
|
419
|
-
*/
|
420
|
-
domID: function () {
|
421
|
-
return 'Query_' + this.props.query.number;
|
422
|
-
},
|
423
|
-
|
424
|
-
queryLength: function () {
|
425
|
-
return this.props.query.length;
|
426
|
-
},
|
427
|
-
|
428
|
-
/**
|
429
|
-
* Returns number of hits.
|
430
|
-
*/
|
431
|
-
numhits: function () {
|
432
|
-
return this.props.query.hits.length;
|
433
|
-
},
|
434
|
-
|
435
|
-
// Life cycle methods //
|
436
|
-
|
437
|
-
render: function () {
|
438
|
-
return (
|
439
|
-
<div className="resultn" id={this.domID()}
|
440
|
-
data-query-len={this.props.query.length}
|
441
|
-
data-algorithm={this.props.program}>
|
442
|
-
{ this.headerJSX() }
|
443
|
-
{ this.numhits() && this.hitsListJSX() || this.noHitsJSX() }
|
444
|
-
</div>
|
445
|
-
);
|
446
|
-
},
|
447
|
-
|
448
|
-
headerJSX: function () {
|
449
|
-
var meta = `length: ${this.queryLength().toLocaleString()}`;
|
450
|
-
if (this.props.showQueryCrumbs) {
|
451
|
-
meta = `query ${this.props.query.number}, ` + meta;
|
452
|
-
}
|
453
|
-
return <div className="section-header">
|
454
|
-
<h3>
|
455
|
-
Query= {this.props.query.id}
|
456
|
-
<small>{this.props.query.title}</small>
|
457
|
-
</h3>
|
458
|
-
<span className="label label-reset pos-label">{ meta }</span>
|
459
|
-
</div>;
|
460
|
-
},
|
461
|
-
|
462
|
-
hitsListJSX: function () {
|
463
|
-
return <div className="section-content">
|
464
|
-
<HitsOverview key={'GO_' + this.props.query.number} query={this.props.query} program={this.props.program} collapsed={this.props.veryBig} />
|
465
|
-
<LengthDistribution key={'LD_' + this.props.query.id} query={this.props.query} algorithm={this.props.program} collapsed="true" />
|
466
|
-
<HitsTable key={'HT_' + this.props.query.number} query={this.props.query} imported_xml={this.props.imported_xml} />
|
467
|
-
<div id="hits">
|
468
|
-
{
|
469
|
-
_.map(this.props.query.hits, _.bind(function (hit) {
|
470
|
-
return (
|
471
|
-
<Hit key={'HIT_' + hit.number} hit={hit}
|
472
|
-
algorithm={this.props.program}
|
473
|
-
querydb={this.props.querydb}
|
474
|
-
query={this.props.query}
|
475
|
-
imported_xml={this.props.imported_xml}
|
476
|
-
selectHit={this.props.selectHit}
|
477
|
-
showHitCrumbs={this.numhits() > 1}
|
478
|
-
showQueryCrumbs={this.props.showQueryCrumbs} />
|
479
|
-
);
|
480
|
-
}, this))
|
481
|
-
}
|
482
|
-
</div>
|
483
|
-
</div>;
|
484
|
-
},
|
485
|
-
|
486
|
-
noHitsJSX: function () {
|
487
|
-
return <div className="section-content">
|
488
|
-
<br />
|
489
|
-
<p>
|
490
|
-
<strong> ****** No hits found ****** </strong>
|
491
|
-
</p>
|
492
|
-
</div>;
|
493
|
-
},
|
494
|
-
|
495
|
-
shouldComponentUpdate: function (nextProps, nextState) {
|
496
|
-
if (!this.props.query) return true;
|
497
|
-
}
|
498
|
-
});
|
499
|
-
|
500
|
-
/**
|
501
|
-
* Renders summary of all hits per query in a tabular form.
|
502
|
-
*/
|
503
|
-
var HitsTable = React.createClass({
|
504
|
-
mixins: [Utils],
|
505
|
-
render: function () {
|
506
|
-
var count = 0,
|
507
|
-
hasName = _.every(this.props.query.hits, function(hit) {
|
508
|
-
return hit.sciname !== '';
|
509
|
-
});
|
510
|
-
|
511
|
-
return (
|
512
|
-
<table
|
513
|
-
className="table table-hover table-condensed tabular-view">
|
514
|
-
<thead>
|
515
|
-
<th className="text-left">#</th>
|
516
|
-
<th>Similar sequences</th>
|
517
|
-
{hasName && <th className="text-left">Species</th>}
|
518
|
-
{!this.props.imported_xml && <th className="text-right">Query coverage (%)</th>}
|
519
|
-
<th className="text-right">Total score</th>
|
520
|
-
<th className="text-right">E value</th>
|
521
|
-
<th className="text-right" data-toggle="tooltip"
|
522
|
-
data-placement="left" title="Total identity of all hsps / total length of all hsps">
|
523
|
-
Identity (%)
|
524
|
-
</th>
|
525
|
-
</thead>
|
526
|
-
<tbody>
|
527
|
-
{
|
528
|
-
_.map(this.props.query.hits, _.bind(function (hit) {
|
529
|
-
return (
|
530
|
-
<tr key={hit.number}>
|
531
|
-
<td className="text-left">{hit.number + '.'}</td>
|
532
|
-
<td>
|
533
|
-
<a href={'#Query_' + this.props.query.number + '_hit_' + hit.number}>
|
534
|
-
{hit.id}
|
535
|
-
</a>
|
536
|
-
</td>
|
537
|
-
{hasName && <td className="text-left">{hit.sciname}</td>}
|
538
|
-
{!this.props.imported_xml && <td className="text-right">{hit.qcovs}</td>}
|
539
|
-
<td className="text-right">{hit.score}</td>
|
540
|
-
<td className="text-right">{this.inExponential(hit.hsps[0].evalue)}</td>
|
541
|
-
<td className="text-right">{hit.identity}</td>
|
542
|
-
</tr>
|
543
|
-
);
|
544
|
-
}, this))
|
545
|
-
}
|
546
|
-
</tbody>
|
547
|
-
</table>
|
548
|
-
);
|
549
|
-
}
|
550
|
-
});
|
551
|
-
|
552
|
-
/**
|
553
|
-
* Component for each hit.
|
554
|
-
*/
|
555
|
-
var Hit = React.createClass({
|
556
|
-
mixins: [Utils],
|
557
|
-
|
558
|
-
/**
|
559
|
-
* Returns accession number of the hit sequence.
|
560
|
-
*/
|
561
|
-
accession: function () {
|
562
|
-
return this.props.hit.accession;
|
563
|
-
},
|
564
|
-
|
565
|
-
/**
|
566
|
-
* Returns length of the hit sequence.
|
567
|
-
*/
|
568
|
-
hitLength: function () {
|
569
|
-
return this.props.hit.length;
|
570
|
-
},
|
571
|
-
|
572
|
-
// Internal helpers. //
|
573
|
-
|
574
|
-
/**
|
575
|
-
* Returns id that will be used for the DOM node corresponding to the hit.
|
576
|
-
*/
|
577
|
-
domID: function () {
|
578
|
-
return 'Query_' + this.props.query.number + '_hit_' + this.props.hit.number;
|
579
|
-
},
|
580
|
-
|
581
|
-
databaseIDs: function () {
|
582
|
-
return _.map(this.props.querydb, _.iteratee('id'));
|
583
|
-
},
|
584
|
-
|
585
|
-
showSequenceViewer: function (event) {
|
586
|
-
this.setState({ showSequenceViewer: true });
|
587
|
-
event && event.preventDefault();
|
588
|
-
},
|
589
|
-
|
590
|
-
hideSequenceViewer: function () {
|
591
|
-
this.setState({ showSequenceViewer: false });
|
592
|
-
},
|
593
|
-
|
594
|
-
viewSequenceLink: function () {
|
595
|
-
return encodeURI(`get_sequence/?sequence_ids=${this.accession()}&database_ids=${this.databaseIDs()}`);
|
596
|
-
},
|
597
|
-
|
598
|
-
downloadFASTA: function (event) {
|
599
|
-
var accessions = [this.accession()];
|
600
|
-
downloadFASTA(accessions, this.databaseIDs());
|
601
|
-
},
|
602
|
-
|
603
|
-
// Event-handler for exporting alignments.
|
604
|
-
// Calls relevant method on AlignmentExporter defined in alignment_exporter.js.
|
605
|
-
downloadAlignment: function (event) {
|
606
|
-
var hsps = _.map(this.props.hit.hsps, _.bind(function (hsp) {
|
607
|
-
hsp.query_id = this.props.query.id;
|
608
|
-
hsp.hit_id = this.props.hit.id;
|
609
|
-
return hsp;
|
610
|
-
}, this));
|
611
|
-
|
612
|
-
var aln_exporter = new AlignmentExporter();
|
613
|
-
aln_exporter.export_alignments(hsps, this.props.query.id+'_'+this.props.hit.id);
|
614
|
-
},
|
615
|
-
|
616
|
-
|
617
|
-
// Life cycle methods //
|
618
|
-
|
619
|
-
getInitialState: function () {
|
620
|
-
return { showSequenceViewer: false };
|
621
|
-
},
|
622
|
-
|
623
|
-
// Return JSX for view sequence button.
|
624
|
-
viewSequenceButton: function () {
|
625
|
-
if (this.hitLength() > 10000) {
|
626
|
-
return (
|
627
|
-
<button
|
628
|
-
className="btn btn-link view-sequence disabled"
|
629
|
-
title="Sequence too long" disabled="true">
|
630
|
-
<i className="fa fa-eye"></i> Sequence
|
631
|
-
</button>
|
632
|
-
);
|
633
|
-
}
|
634
|
-
else {
|
635
|
-
return (
|
636
|
-
<button
|
637
|
-
className="btn btn-link view-sequence"
|
638
|
-
onClick={this.showSequenceViewer}>
|
639
|
-
<i className="fa fa-eye"></i> Sequence
|
640
|
-
</button>
|
641
|
-
);
|
642
|
-
}
|
643
|
-
},
|
644
|
-
|
645
|
-
render: function () {
|
646
|
-
return (
|
647
|
-
<div className="hit" id={this.domID()} data-hit-def={this.props.hit.id}
|
648
|
-
data-hit-len={this.props.hit.length} data-hit-evalue={this.props.hit.evalue}>
|
649
|
-
{ this.headerJSX() } { this.contentJSX() }
|
650
|
-
</div>
|
651
|
-
);
|
652
|
-
},
|
653
|
-
|
654
|
-
headerJSX: function () {
|
655
|
-
var meta = `length: ${this.hitLength().toLocaleString()}`;
|
656
|
-
|
657
|
-
if (this.props.showQueryCrumbs && this.props.showHitCrumbs) {
|
658
|
-
// Multiper queries, multiple hits
|
659
|
-
meta = `hit ${this.props.hit.number} of query ${this.props.query.number}, ` + meta;
|
660
|
-
}
|
661
|
-
else if (this.props.showQueryCrumbs && !this.props.showHitCrumbs) {
|
662
|
-
// Multiple queries, single hit
|
663
|
-
meta = `the only hit of query ${this.props.query.number}, ` + meta;
|
664
|
-
}
|
665
|
-
else if (!this.props.showQueryCrumbs && this.props.showHitCrumbs) {
|
666
|
-
// Single query, multiple hits
|
667
|
-
meta = `hit ${this.props.hit.number}, ` + meta;
|
668
|
-
}
|
669
|
-
|
670
|
-
return <div className="section-header">
|
671
|
-
<h4 data-toggle="collapse" data-target={this.domID() + '_content'}>
|
672
|
-
<i className="fa fa-chevron-down"></i>
|
673
|
-
<span>
|
674
|
-
{this.props.hit.id}
|
675
|
-
<small>{this.props.hit.title}</small>
|
676
|
-
</span>
|
677
|
-
</h4>
|
678
|
-
<span className="label label-reset pos-label">{ meta }</span>
|
679
|
-
</div>;
|
680
|
-
},
|
681
|
-
|
682
|
-
contentJSX: function () {
|
683
|
-
return <div id={this.domID() + '_content'} className="section-content collapse in">
|
684
|
-
{ this.hitLinks() }
|
685
|
-
<HSPOverview key={'kablammo' + this.props.query.id} query={this.props.query}
|
686
|
-
hit={this.props.hit} algorithm={this.props.algorithm} />
|
687
|
-
{ this.hspListJSX() }
|
688
|
-
</div>;
|
689
|
-
},
|
690
|
-
|
691
|
-
hitLinks: function () {
|
692
|
-
return (
|
693
|
-
<div className="hit-links">
|
694
|
-
<label>
|
695
|
-
<input type="checkbox" id={this.domID() + '_checkbox'}
|
696
|
-
value={this.accession()} onChange={function () {
|
697
|
-
this.props.selectHit(this.domID() + '_checkbox');
|
698
|
-
}.bind(this)} data-target={'#' + this.domID()}
|
699
|
-
/> Select
|
700
|
-
</label>
|
701
|
-
{
|
702
|
-
!this.props.imported_xml && [
|
703
|
-
<span> | </span>,
|
704
|
-
this.viewSequenceButton(),
|
705
|
-
this.state.showSequenceViewer && <SequenceViewer
|
706
|
-
url={this.viewSequenceLink()} onHide={this.hideSequenceViewer} />
|
707
|
-
]
|
708
|
-
}
|
709
|
-
{
|
710
|
-
!this.props.imported_xml && [
|
711
|
-
<span> | </span>,
|
712
|
-
<button className='btn btn-link download-fa'
|
713
|
-
onClick={this.downloadFASTA}>
|
714
|
-
<i className="fa fa-download"></i> FASTA
|
715
|
-
</button>
|
716
|
-
]
|
717
|
-
}
|
718
|
-
<span> | </span>
|
719
|
-
<button className='btn btn-link download-aln'
|
720
|
-
onClick={this.downloadAlignment}>
|
721
|
-
<i className="fa fa-download"></i> Alignment
|
722
|
-
</button>
|
723
|
-
{
|
724
|
-
_.map(this.props.hit.links, _.bind(function (link) {
|
725
|
-
return [<span> | </span>, this.a(link)];
|
726
|
-
}, this))
|
727
|
-
}
|
728
|
-
</div>
|
729
|
-
);
|
730
|
-
},
|
731
|
-
|
732
|
-
hspListJSX: function () {
|
733
|
-
return <div className="hsps">
|
734
|
-
{
|
735
|
-
this.props.hit.hsps.map((hsp) => {
|
736
|
-
return <HSP key={hsp.number}
|
737
|
-
algorithm={this.props.algorithm}
|
738
|
-
queryNumber={this.props.query.number}
|
739
|
-
hitNumber={this.props.hit.number} hsp={hsp}/>;
|
740
|
-
}, this)
|
741
|
-
}
|
742
|
-
</div>;
|
743
|
-
}
|
744
|
-
});
|
745
|
-
|
746
|
-
|
747
|
-
/**
|
748
|
-
* Component for sequence-viewer links.
|
749
|
-
*/
|
750
|
-
var SequenceViewer = (function () {
|
751
|
-
|
752
|
-
var Viewer = React.createClass({
|
753
|
-
|
754
|
-
/**
|
755
|
-
* The CSS class name that will be assigned to the widget container. ID
|
756
|
-
* assigned to the widget container is derived from the same.
|
757
|
-
*/
|
758
|
-
widgetClass: 'biojs-vis-sequence',
|
759
|
-
|
760
|
-
// Lifecycle methods. //
|
761
|
-
|
762
|
-
render: function () {
|
763
|
-
this.widgetID =
|
764
|
-
this.widgetClass + '-' + (new Date().getUTCMilliseconds());
|
765
|
-
|
766
|
-
return (
|
767
|
-
<div
|
768
|
-
className="fastan">
|
769
|
-
<div
|
770
|
-
className="section-header">
|
771
|
-
<h4>
|
772
|
-
{this.props.sequence.id}
|
773
|
-
<small>
|
774
|
-
{this.props.sequence.title}
|
775
|
-
</small>
|
776
|
-
</h4>
|
777
|
-
</div>
|
778
|
-
<div
|
779
|
-
className="section-content">
|
780
|
-
<div
|
781
|
-
className={this.widgetClass} id={this.widgetID}>
|
782
|
-
</div>
|
783
|
-
</div>
|
784
|
-
</div>
|
785
|
-
);
|
786
|
-
},
|
787
|
-
|
788
|
-
componentDidMount: function () {
|
789
|
-
// attach BioJS sequence viewer
|
790
|
-
var widget = new Sequence({
|
791
|
-
sequence: this.props.sequence.value,
|
792
|
-
target: this.widgetID,
|
793
|
-
format: 'PRIDE',
|
794
|
-
columns: {
|
795
|
-
size: 40,
|
796
|
-
spacedEach: 0
|
797
|
-
},
|
798
|
-
formatOptions: {
|
799
|
-
title: false,
|
800
|
-
footer: false
|
801
|
-
}
|
802
|
-
});
|
803
|
-
widget.hideFormatSelector();
|
804
|
-
}
|
805
|
-
});
|
806
|
-
|
807
|
-
return React.createClass({
|
808
|
-
|
809
|
-
// Kind of public API. //
|
810
|
-
|
811
|
-
/**
|
812
|
-
* Shows sequence viewer.
|
813
|
-
*/
|
814
|
-
show: function () {
|
815
|
-
this.modal().modal('show');
|
816
|
-
},
|
817
|
-
|
818
|
-
|
819
|
-
// Internal helpers. //
|
820
|
-
|
821
|
-
modal: function () {
|
822
|
-
return $(React.findDOMNode(this.refs.modal));
|
823
|
-
},
|
824
|
-
|
825
|
-
resultsJSX: function () {
|
826
|
-
return (
|
827
|
-
<div className="modal-body">
|
828
|
-
{
|
829
|
-
_.map(this.state.error_msgs, _.bind(function (error_msg) {
|
830
|
-
return (
|
831
|
-
<div
|
832
|
-
className="fastan">
|
833
|
-
<div
|
834
|
-
className="section-header">
|
835
|
-
<h4>
|
836
|
-
{error_msg[0]}
|
837
|
-
</h4>
|
838
|
-
</div>
|
839
|
-
<div
|
840
|
-
className="section-content">
|
841
|
-
<pre
|
842
|
-
className="pre-reset">
|
843
|
-
{error_msg[1]}
|
844
|
-
</pre>
|
845
|
-
</div>
|
846
|
-
</div>
|
847
|
-
);
|
848
|
-
}, this))
|
849
|
-
}
|
850
|
-
{
|
851
|
-
_.map(this.state.sequences, _.bind(function (sequence) {
|
852
|
-
return (<Viewer sequence={sequence}/>);
|
853
|
-
}, this))
|
854
|
-
}
|
855
|
-
</div>
|
856
|
-
);
|
857
|
-
},
|
858
|
-
|
859
|
-
loadingJSX: function () {
|
860
|
-
return (
|
861
|
-
<div className="modal-body text-center">
|
862
|
-
<i className="fa fa-spinner fa-3x fa-spin"></i>
|
863
|
-
</div>
|
864
|
-
);
|
865
|
-
},
|
866
|
-
|
867
|
-
|
868
|
-
// Lifecycle methods. //
|
869
|
-
|
870
|
-
getInitialState: function () {
|
871
|
-
return {
|
872
|
-
error_msgs: [],
|
873
|
-
sequences: [],
|
874
|
-
requestCompleted: false
|
875
|
-
};
|
876
|
-
},
|
877
|
-
|
878
|
-
render: function () {
|
879
|
-
return (
|
880
|
-
<div
|
881
|
-
className="modal sequence-viewer"
|
882
|
-
ref="modal" tabIndex="-1">
|
883
|
-
<div
|
884
|
-
className="modal-dialog">
|
885
|
-
<div
|
886
|
-
className="modal-content">
|
887
|
-
<div
|
888
|
-
className="modal-header">
|
889
|
-
<h3>View sequence</h3>
|
890
|
-
</div>
|
891
|
-
|
892
|
-
{ this.state.requestCompleted &&
|
893
|
-
this.resultsJSX() || this.loadingJSX() }
|
894
|
-
</div>
|
895
|
-
</div>
|
896
|
-
</div>
|
897
|
-
);
|
898
|
-
},
|
899
|
-
|
900
|
-
componentDidMount: function () {
|
901
|
-
// Display modal with a spinner.
|
902
|
-
this.show();
|
903
|
-
|
904
|
-
// Fetch sequence and update state.
|
905
|
-
$.getJSON(this.props.url)
|
906
|
-
.done(_.bind(function (response) {
|
907
|
-
this.setState({
|
908
|
-
sequences: response.sequences,
|
909
|
-
error_msgs: response.error_msgs,
|
910
|
-
requestCompleted: true
|
911
|
-
});
|
912
|
-
}, this))
|
913
|
-
.fail(function (jqXHR, status, error) {
|
914
|
-
showErrorModal(jqXHR, function () {
|
915
|
-
this.hide();
|
916
|
-
});
|
917
|
-
});
|
918
|
-
|
919
|
-
this.modal().on('hidden.bs.modal', this.props.onHide);
|
920
|
-
},
|
921
|
-
});
|
922
|
-
})();
|
923
|
-
|
924
|
-
/**
|
925
|
-
* Renders links for downloading hit information in different formats.
|
926
|
-
* Renders links for navigating to each query.
|
927
|
-
*/
|
928
|
-
var SideBar = React.createClass({
|
929
|
-
|
930
|
-
/**
|
931
|
-
* Event-handler for downloading fasta of all hits.
|
932
|
-
*/
|
933
|
-
downloadFastaOfAll: function () {
|
934
|
-
var sequence_ids = $('.hit-links :checkbox').map(function () {
|
935
|
-
return this.value;
|
936
|
-
}).get();
|
937
|
-
var database_ids = _.map(this.props.data.querydb, _.iteratee('id'));
|
938
|
-
downloadFASTA(sequence_ids, database_ids);
|
939
|
-
return false;
|
940
|
-
},
|
941
|
-
|
942
|
-
/**
|
943
|
-
* Handles downloading fasta of selected hits.
|
944
|
-
*/
|
945
|
-
downloadFastaOfSelected: function () {
|
946
|
-
var sequence_ids = $('.hit-links :checkbox:checked').map(function () {
|
947
|
-
return this.value;
|
948
|
-
}).get();
|
949
|
-
var database_ids = _.map(this.props.data.querydb, _.iteratee('id'));
|
950
|
-
downloadFASTA(sequence_ids, database_ids);
|
951
|
-
return false;
|
952
|
-
},
|
953
|
-
|
954
|
-
downloadAlignmentOfAll: function() {
|
955
|
-
var sequence_ids = $('.hit-links :checkbox').map(function () {
|
956
|
-
return this.value;
|
957
|
-
}).get();
|
958
|
-
var hsps_arr = [];
|
959
|
-
var aln_exporter = new AlignmentExporter();
|
960
|
-
_.each(this.props.data.queries, _.bind(function (query) {
|
961
|
-
_.each(query.hits, function (hit) {
|
962
|
-
_.each(hit.hsps, function (hsp) {
|
963
|
-
hsp.hit_id = hit.id;
|
964
|
-
hsp.query_id = query.id;
|
965
|
-
hsps_arr.push(hsp);
|
966
|
-
});
|
967
|
-
});
|
968
|
-
}, this));
|
969
|
-
console.log('len '+hsps_arr.length);
|
970
|
-
aln_exporter.export_alignments(hsps_arr, 'alignment-'+sequence_ids.length+'_hits');
|
971
|
-
return false;
|
972
|
-
},
|
973
|
-
|
974
|
-
downloadAlignmentOfSelected: function () {
|
975
|
-
var sequence_ids = $('.hit-links :checkbox:checked').map(function () {
|
976
|
-
return this.value;
|
977
|
-
}).get();
|
978
|
-
var hsps_arr = [];
|
979
|
-
var aln_exporter = new AlignmentExporter();
|
980
|
-
console.log('check '+sequence_ids.toString());
|
981
|
-
_.each(this.props.data.queries, _.bind(function (query) {
|
982
|
-
_.each(query.hits, function (hit) {
|
983
|
-
if (_.indexOf(sequence_ids, hit.accession) != -1) {
|
984
|
-
_.each(hit.hsps, function (hsp) {
|
985
|
-
hsp.hit_id = hit.id;
|
986
|
-
hsp.query_id = query.id;
|
987
|
-
hsps_arr.push(hsp);
|
988
|
-
});
|
989
|
-
}
|
990
|
-
});
|
991
|
-
}, this));
|
992
|
-
aln_exporter.export_alignments(hsps_arr, 'alignment-'+sequence_ids.length+'_hits');
|
993
|
-
return false;
|
994
|
-
},
|
995
|
-
|
996
|
-
|
997
|
-
// JSX //
|
998
|
-
render: function () {
|
999
|
-
return (
|
1000
|
-
<div className="sidebar">
|
1001
|
-
{ this.props.shouldShowIndex && this.index() }
|
1002
|
-
{ this.downloads() }
|
1003
|
-
</div>
|
1004
|
-
);
|
1005
|
-
},
|
1006
|
-
|
1007
|
-
index: function () {
|
1008
|
-
return (
|
1009
|
-
<div className="index">
|
1010
|
-
<div
|
1011
|
-
className="section-header">
|
1012
|
-
<h4>
|
1013
|
-
{ this.summary() }
|
1014
|
-
</h4>
|
1015
|
-
</div>
|
1016
|
-
<ul
|
1017
|
-
className="nav hover-reset active-bold">
|
1018
|
-
{
|
1019
|
-
_.map(this.props.data.queries, _.bind(function (query) {
|
1020
|
-
return (
|
1021
|
-
<li key={'Side_bar_'+query.id}>
|
1022
|
-
<a
|
1023
|
-
className="nowrap-ellipsis hover-bold"
|
1024
|
-
href={'#Query_' + query.number}
|
1025
|
-
title={'Query= ' + query.id + ' ' + query.title}>
|
1026
|
-
{'Query= ' + query.id}
|
1027
|
-
</a>
|
1028
|
-
</li>
|
1029
|
-
);
|
1030
|
-
}, this))
|
1031
|
-
}
|
1032
|
-
</ul>
|
1033
|
-
</div>
|
1034
|
-
);
|
1035
|
-
},
|
1036
|
-
|
1037
|
-
summary: function () {
|
1038
|
-
var program = this.props.data.program;
|
1039
|
-
var numqueries = this.props.data.queries.length;
|
1040
|
-
var numquerydb = this.props.data.querydb.length;
|
1041
|
-
|
1042
|
-
return (
|
1043
|
-
program.toUpperCase() + ': ' +
|
1044
|
-
numqueries + ' ' + (numqueries > 1 ? 'queries' : 'query') + ', ' +
|
1045
|
-
numquerydb + ' ' + (numquerydb > 1 ? 'databases' : 'database')
|
1046
|
-
);
|
1047
|
-
},
|
1048
|
-
|
1049
|
-
downloads: function () {
|
1050
|
-
return (
|
1051
|
-
<div className="downloads">
|
1052
|
-
<div className="section-header">
|
1053
|
-
<h4>
|
1054
|
-
Download FASTA, XML, TSV
|
1055
|
-
</h4>
|
1056
|
-
</div>
|
1057
|
-
<ul className="nav">
|
1058
|
-
{
|
1059
|
-
!this.props.data.imported_xml && <li>
|
1060
|
-
<a href="#" className="btn-link download-fasta-of-all"
|
1061
|
-
onClick={this.downloadFastaOfAll}>
|
1062
|
-
FASTA of all hits
|
1063
|
-
</a>
|
1064
|
-
</li>
|
1065
|
-
}
|
1066
|
-
{
|
1067
|
-
!this.props.data.imported_xml && <li>
|
1068
|
-
<a href="#" className="btn-link download-fasta-of-selected disabled"
|
1069
|
-
onClick={this.downloadFastaOfSelected}>
|
1070
|
-
FASTA of <span className="text-bold"></span> selected hit(s)
|
1071
|
-
</a>
|
1072
|
-
</li>
|
1073
|
-
}
|
1074
|
-
<li>
|
1075
|
-
<a href="#" className="btn-link download-alignment-of-all"
|
1076
|
-
onClick={this.downloadAlignmentOfAll}>
|
1077
|
-
Alignment of all hits
|
1078
|
-
</a>
|
1079
|
-
</li>
|
1080
|
-
<li>
|
1081
|
-
<a href="#" className="btn-link download-alignment-of-selected disabled"
|
1082
|
-
onClick={this.downloadAlignmentOfSelected}>
|
1083
|
-
Alignment of <span className="text-bold"></span> selected hit(s)
|
1084
|
-
</a>
|
1085
|
-
</li>
|
1086
|
-
{
|
1087
|
-
!this.props.data.imported_xml && <li>
|
1088
|
-
<a className="download" data-toggle="tooltip"
|
1089
|
-
title="15 columns: query and subject ID; scientific
|
1090
|
-
name, alignment length, mismatches, gaps, identity,
|
1091
|
-
start and end coordinates, e value, bitscore, query
|
1092
|
-
coverage per subject and per HSP."
|
1093
|
-
href={'download/' + this.props.data.search_id + '.std_tsv'}>
|
1094
|
-
Standard tabular report
|
1095
|
-
</a>
|
1096
|
-
</li>
|
1097
|
-
}
|
1098
|
-
{
|
1099
|
-
!this.props.data.imported_xml && <li>
|
1100
|
-
<a className="download" data-toggle="tooltip"
|
1101
|
-
title="44 columns: query and subject ID, GI,
|
1102
|
-
accessions, and length; alignment details;
|
1103
|
-
taxonomy details of subject sequence(s) and
|
1104
|
-
query coverage per subject and per HSP."
|
1105
|
-
href={'download/' + this.props.data.search_id + '.full_tsv'}>
|
1106
|
-
Full tabular report
|
1107
|
-
</a>
|
1108
|
-
</li>
|
1109
|
-
}
|
1110
|
-
{
|
1111
|
-
!this.props.data.imported_xml && <li>
|
1112
|
-
<a className="download" data-toggle="tooltip"
|
1113
|
-
title="Results in XML format."
|
1114
|
-
href={'download/' + this.props.data.search_id + '.xml'}>
|
1115
|
-
Full XML report
|
1116
|
-
</a>
|
1117
|
-
</li>
|
1118
|
-
}
|
1119
|
-
</ul>
|
1120
|
-
</div>
|
1121
|
-
);
|
1122
|
-
},
|
1123
|
-
});
|
1124
|
-
|
1125
498
|
React.render(<Page/>, document.getElementById('view'));
|