sequenceserver 2.0.0.beta4 → 2.0.0.rc1
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.
Potentially problematic release.
This version of sequenceserver might be problematic. Click here for more details.
- 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'));
|