sequenceserver 1.1.0.beta8 → 1.1.0.beta10
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/lib/sequenceserver/blast/hsp.rb +2 -174
- data/lib/sequenceserver/blast/report.rb +4 -3
- data/lib/sequenceserver/job.rb +2 -1
- data/lib/sequenceserver/version.rb +1 -1
- data/public/css/grapher.css +50 -35
- data/public/css/sequenceserver.css +25 -16
- data/public/css/sequenceserver.min.css +3 -3
- data/public/js/circos.js +1 -1
- data/public/js/grapher.js +8 -3
- data/public/js/hits_overview.js +4 -6
- data/public/js/hsp.js +283 -0
- data/public/js/kablammo.js +3 -3
- data/public/js/report.js +668 -850
- data/public/js/search.js +208 -180
- data/public/js/utils.js +77 -0
- data/public/sequenceserver-report.min.js +14 -14
- data/public/sequenceserver-search.min.js +2 -2
- metadata +4 -2
data/public/js/circos.js
CHANGED
data/public/js/grapher.js
CHANGED
@@ -18,8 +18,9 @@ export default function Grapher(Graph) {
|
|
18
18
|
}
|
19
19
|
|
20
20
|
render () {
|
21
|
+
var cssClasses = Graph.className() + ' grapher';
|
21
22
|
return (
|
22
|
-
<div
|
23
|
+
<div ref="grapher" className={cssClasses}>
|
23
24
|
<div className="grapher-header">
|
24
25
|
<h5 className="caption" data-toggle="collapse"
|
25
26
|
data-target={"#"+this.collapseId()}>
|
@@ -113,9 +114,13 @@ $(window).resize(_.debounce(function () {
|
|
113
114
|
// Swap-icon and toggle .graph-links on collapse.
|
114
115
|
$('body').on('hidden.bs.collapse', ".collapse", function () {
|
115
116
|
var component = Graphers[$(this).attr('id')];
|
116
|
-
component
|
117
|
+
if (component) {
|
118
|
+
component.setState({ collapsed: true });
|
119
|
+
}
|
117
120
|
});
|
118
121
|
$('body').on('shown.bs.collapse', ".collapse", function () {
|
119
122
|
var component = Graphers[$(this).attr('id')];
|
120
|
-
component
|
123
|
+
if (component) {
|
124
|
+
component.setState({ collapsed: false });
|
125
|
+
}
|
121
126
|
});
|
data/public/js/hits_overview.js
CHANGED
@@ -6,7 +6,7 @@ import * as Helpers from './visualisation_helpers';
|
|
6
6
|
class Graph {
|
7
7
|
|
8
8
|
static name() {
|
9
|
-
return '
|
9
|
+
return 'Graphical overview of hits';
|
10
10
|
}
|
11
11
|
|
12
12
|
static className() {
|
@@ -207,14 +207,12 @@ class Graph {
|
|
207
207
|
|
208
208
|
graphIt($queryDiv, $graphDiv, index, howMany, opts, inhits) {
|
209
209
|
/* barHeight: Height of each hit track.
|
210
|
-
* barPadding: Padding around each hit track.
|
211
210
|
* legend: Height reserved for the overview legend.
|
212
211
|
* margin: Margin around the svg element.
|
213
212
|
*/
|
214
213
|
var defaults = {
|
215
|
-
barHeight:
|
216
|
-
|
217
|
-
legend: 5,
|
214
|
+
barHeight: 3,
|
215
|
+
legend: inhits.length > 1 ? 3 : 0,
|
218
216
|
margin: 20
|
219
217
|
},
|
220
218
|
options = $.extend(defaults, opts);
|
@@ -233,7 +231,7 @@ class Graph {
|
|
233
231
|
var q_i = $queryDiv.attr('id');
|
234
232
|
|
235
233
|
var width = $graphDiv.width();
|
236
|
-
var height = hits.length * (options.barHeight
|
234
|
+
var height = hits.length * (options.barHeight) +
|
237
235
|
2 * options.legend + 5 * options.margin;
|
238
236
|
// var height = $graphDiv.height();
|
239
237
|
|
data/public/js/hsp.js
ADDED
@@ -0,0 +1,283 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import _ from 'underscore';
|
3
|
+
|
4
|
+
import Utils from './utils';
|
5
|
+
import * as Helpers from './visualisation_helpers';
|
6
|
+
|
7
|
+
var HSPComponents = {};
|
8
|
+
|
9
|
+
/**
|
10
|
+
* Alignment viewer.
|
11
|
+
*/
|
12
|
+
export default class HSP extends React.Component {
|
13
|
+
|
14
|
+
constructor(props) {
|
15
|
+
super(props);
|
16
|
+
this.hsp = props.hsp;
|
17
|
+
}
|
18
|
+
|
19
|
+
domID() {
|
20
|
+
return "Query_" + this.props.query.number + "_hit_" +
|
21
|
+
this.props.hit.number + "_" + this.props.hsp.number;
|
22
|
+
}
|
23
|
+
|
24
|
+
// Renders pretty formatted alignment.
|
25
|
+
render () {
|
26
|
+
return (
|
27
|
+
<div className="hsp" id={this.domID()} key={this.domID()} ref="hsp">
|
28
|
+
<pre className="pre-reset hsp-stats">
|
29
|
+
{Helpers.toLetters(this.hsp.number) + "."} {this.hspStats()}
|
30
|
+
</pre>
|
31
|
+
{this.hspLines()}
|
32
|
+
</div>
|
33
|
+
);
|
34
|
+
}
|
35
|
+
|
36
|
+
componentDidMount () {
|
37
|
+
HSPComponents[this.domID()] = this;
|
38
|
+
this.draw();
|
39
|
+
}
|
40
|
+
|
41
|
+
draw () {
|
42
|
+
this.chars = $(React.findDOMNode(this.refs.hsp)).width() / 7.35;
|
43
|
+
this.forceUpdate();
|
44
|
+
}
|
45
|
+
|
46
|
+
/**
|
47
|
+
* Return prettified stats for the given hsp and based on the BLAST
|
48
|
+
* algorithm.
|
49
|
+
*/
|
50
|
+
hspStats () {
|
51
|
+
let line = [];
|
52
|
+
|
53
|
+
// Bit score and total score.
|
54
|
+
line.push(`Score: ${Utils.inTwoDecimal(this.hsp.bit_score)} (${this.hsp.score}), `);
|
55
|
+
|
56
|
+
// E value
|
57
|
+
line.push(`E value: `); line.push(Utils.inExponential(this.hsp.evalue)); line.push(', ');
|
58
|
+
|
59
|
+
// Identity
|
60
|
+
line.push([`Identities: ${Utils.inFraction(this.hsp.identity, this.hsp.length)} (${Utils.inPercentage(this.hsp.identity, this.hsp.length)}), `]);
|
61
|
+
|
62
|
+
// Positives (if this is a protein alignment).
|
63
|
+
if (this.props.algorithm === 'blastp' ||
|
64
|
+
this.props.algorithm === 'tblastx') {
|
65
|
+
line.push(`Positives: ${Utils.inFraction(this.hsp.positives, this.hsp.length)} (${Utils.inPercentage(this.hsp.positives, this.hsp.length)}), `)
|
66
|
+
}
|
67
|
+
|
68
|
+
// Gaps
|
69
|
+
line.push(`Gaps: ${Utils.inFraction(this.hsp.gaps, this.hsp.length)} (${Utils.inPercentage(this.hsp.gaps, this.hsp.length)}), `);
|
70
|
+
|
71
|
+
// Query coverage
|
72
|
+
//line.push(`Query coverage: ${this.hsp.qcovhsp}%, `)
|
73
|
+
|
74
|
+
switch (this.props.algorithm) {
|
75
|
+
case 'tblastx':
|
76
|
+
line.push(`Frame: ${Utils.inFraction(this.hsp.qframe, this.hsp.sframe)}`)
|
77
|
+
break;
|
78
|
+
case 'blastn':
|
79
|
+
line.push(`Strand: ${(this.hsp.qframe > 0 ? '+' : '-')} / ${(this.hsp.sframe > 0 ? '+' : '-')}`)
|
80
|
+
break;
|
81
|
+
case 'blastx':
|
82
|
+
line.push(`Query Frame: ${this.hsp.qframe}`)
|
83
|
+
break;
|
84
|
+
case 'tblastn':
|
85
|
+
line.push(`Hit Frame: ${this.hsp.sframe}`)
|
86
|
+
break;
|
87
|
+
}
|
88
|
+
|
89
|
+
return line;
|
90
|
+
}
|
91
|
+
|
92
|
+
hspLines () {
|
93
|
+
var pp = [];
|
94
|
+
var lines = this.lines();
|
95
|
+
var nqseq = this.nqseq();
|
96
|
+
var nsseq = this.nsseq();
|
97
|
+
var width = this.width();
|
98
|
+
var chars = this.chars - 2 * width - 8;
|
99
|
+
|
100
|
+
for (let i = 1; i <= lines; i++) {
|
101
|
+
let line = [];
|
102
|
+
let seq_start_index = chars * (i - 1);
|
103
|
+
let seq_stop_index = seq_start_index + chars;
|
104
|
+
|
105
|
+
let lqstart = nqseq;
|
106
|
+
let lqseq = this.hsp.qseq.slice(seq_start_index, seq_stop_index);
|
107
|
+
let lqend = nqseq + (lqseq.length - lqseq.split('-').length) *
|
108
|
+
this.qframe_unit() * this.qframe_sign();
|
109
|
+
nqseq = lqend + this.qframe_unit() * this.qframe_sign();
|
110
|
+
|
111
|
+
let lmseq = this.hsp.midline.slice(seq_start_index, seq_stop_index);
|
112
|
+
|
113
|
+
let lsstart = nsseq;
|
114
|
+
let lsseq = this.hsp.sseq.slice(seq_start_index, seq_stop_index);
|
115
|
+
let lsend = nsseq + (lsseq.length - lsseq.split('-').length) *
|
116
|
+
this.sframe_unit() * this.sframe_sign();
|
117
|
+
nsseq = lsend + this.sframe_unit() * this.sframe_sign();
|
118
|
+
|
119
|
+
line.push(this.spanCoords('Query ' + this.formatCoords(lqstart, width) + ' '));
|
120
|
+
line.push(lqseq);
|
121
|
+
line.push(this.spanCoords(' ' + lqend));
|
122
|
+
line.push(<br/>);
|
123
|
+
|
124
|
+
line.push(this.formatCoords('', width + 8) + ' ');
|
125
|
+
line.push(lmseq);
|
126
|
+
line.push(<br/>);
|
127
|
+
|
128
|
+
line.push(this.spanCoords('Subject ' + this.formatCoords(lsstart, width) + ' '));
|
129
|
+
line.push(lsseq);
|
130
|
+
line.push(this.spanCoords(' ' + lsend))
|
131
|
+
line.push(<br/>);
|
132
|
+
|
133
|
+
pp.push((<pre className="pre-reset hsp-lines">{line}</pre>));
|
134
|
+
}
|
135
|
+
|
136
|
+
return pp;
|
137
|
+
}
|
138
|
+
|
139
|
+
// Number of lines of pairwise-alignment (i.e., each line consists of 3
|
140
|
+
// lines). We draw as many pre tags.
|
141
|
+
lines() {
|
142
|
+
return Math.ceil(this.hsp.length / this.chars);
|
143
|
+
}
|
144
|
+
|
145
|
+
// Width of each line of alignment.
|
146
|
+
width() {
|
147
|
+
return _.max(_.map([this.hsp.qstart, this.hsp.qend,
|
148
|
+
this.hsp.sstart, this.hsp.send],
|
149
|
+
(n) => { return n.toString().length }));
|
150
|
+
}
|
151
|
+
|
152
|
+
// Alignment start coordinate for query sequence.
|
153
|
+
//
|
154
|
+
// This will be qstart or qend depending on the direction in which the
|
155
|
+
// (translated) query sequence aligned.
|
156
|
+
nqseq () {
|
157
|
+
switch (this.props.algorithm) {
|
158
|
+
case 'blastp':
|
159
|
+
case 'blastx':
|
160
|
+
case 'tblastn':
|
161
|
+
case 'tblastx':
|
162
|
+
return this.hsp.qframe >= 0 ? this.hsp.qstart : this.hsp.qend;
|
163
|
+
case 'blastn':
|
164
|
+
// BLASTN is a bit weird in that, no matter which direction the query
|
165
|
+
// sequence aligned in, qstart is taken as alignment start coordinate
|
166
|
+
// for query.
|
167
|
+
return this.hsp.qstart;
|
168
|
+
}
|
169
|
+
}
|
170
|
+
|
171
|
+
// Alignment start coordinate for subject sequence.
|
172
|
+
//
|
173
|
+
// This will be sstart or send depending on the direction in which the
|
174
|
+
// (translated) subject sequence aligned.
|
175
|
+
nsseq () {
|
176
|
+
switch (this.props.algorithm) {
|
177
|
+
case 'blastp':
|
178
|
+
case 'blastx':
|
179
|
+
case 'tblastn':
|
180
|
+
case 'tblastx':
|
181
|
+
return this.hsp.sframe >= 0 ? this.hsp.sstart : this.hsp.send;
|
182
|
+
case 'blastn':
|
183
|
+
// BLASTN is a bit weird in that, no matter which direction the
|
184
|
+
// subject sequence aligned in, sstart is taken as alignment
|
185
|
+
// start coordinate for subject.
|
186
|
+
return this.hsp.sstart
|
187
|
+
}
|
188
|
+
}
|
189
|
+
|
190
|
+
// Jump in query coordinate.
|
191
|
+
//
|
192
|
+
// Roughly,
|
193
|
+
//
|
194
|
+
// qend = qstart + n * qframe_unit
|
195
|
+
//
|
196
|
+
// This will be 1 or 3 depending on whether the query sequence was
|
197
|
+
// translated or not.
|
198
|
+
qframe_unit () {
|
199
|
+
switch (this.props.algorithm) {
|
200
|
+
case 'blastp':
|
201
|
+
case 'blastn':
|
202
|
+
case 'tblastn':
|
203
|
+
return 1;
|
204
|
+
case 'blastx':
|
205
|
+
// _Translated_ nucleotide query against protein database.
|
206
|
+
case 'tblastx':
|
207
|
+
// _Translated_ nucleotide query against translated
|
208
|
+
// nucleotide database.
|
209
|
+
return 3;
|
210
|
+
}
|
211
|
+
}
|
212
|
+
|
213
|
+
// Jump in subject coordinate.
|
214
|
+
//
|
215
|
+
// Roughly,
|
216
|
+
//
|
217
|
+
// send = sstart + n * sframe_unit
|
218
|
+
//
|
219
|
+
// This will be 1 or 3 depending on whether the subject sequence was
|
220
|
+
// translated or not.
|
221
|
+
sframe_unit () {
|
222
|
+
switch (this.props.algorithm) {
|
223
|
+
case 'blastp':
|
224
|
+
case 'blastx':
|
225
|
+
case 'blastn':
|
226
|
+
return 1;
|
227
|
+
case 'tblastn':
|
228
|
+
// Protein query against _translated_ nucleotide database.
|
229
|
+
return 3;
|
230
|
+
case 'tblastx':
|
231
|
+
// Translated nucleotide query against _translated_
|
232
|
+
// nucleotide database.
|
233
|
+
return 3;
|
234
|
+
}
|
235
|
+
}
|
236
|
+
|
237
|
+
// If we should add or subtract qframe_unit from qstart to arrive at qend.
|
238
|
+
//
|
239
|
+
// Roughly,
|
240
|
+
//
|
241
|
+
// qend = qstart + (qframe_sign) * n * qframe_unit
|
242
|
+
//
|
243
|
+
// This will be +1 or -1, depending on the direction in which the
|
244
|
+
// (translated) query sequence aligned.
|
245
|
+
qframe_sign () {
|
246
|
+
return this.hsp.qframe >= 0 ? 1 : -1;
|
247
|
+
}
|
248
|
+
|
249
|
+
// If we should add or subtract sframe_unit from sstart to arrive at send.
|
250
|
+
//
|
251
|
+
// Roughly,
|
252
|
+
//
|
253
|
+
// send = sstart + (sframe_sign) * n * sframe_unit
|
254
|
+
//
|
255
|
+
// This will be +1 or -1, depending on the direction in which the
|
256
|
+
// (translated) subject sequence aligned.
|
257
|
+
sframe_sign () {
|
258
|
+
return this.hsp.sframe >= 0 ? 1 : -1;
|
259
|
+
}
|
260
|
+
|
261
|
+
|
262
|
+
/**
|
263
|
+
* Pad given coord with ' ' till its length == width. Returns undefined if
|
264
|
+
* width is not supplied.
|
265
|
+
*/
|
266
|
+
formatCoords (coord, width) {
|
267
|
+
if (width) {
|
268
|
+
let padding = width - coord.toString().length;
|
269
|
+
return Array(padding + 1).join(' ').concat([coord]);
|
270
|
+
}
|
271
|
+
}
|
272
|
+
|
273
|
+
spanCoords (text) {
|
274
|
+
return <span className="hsp-coords">{text}</span>
|
275
|
+
}
|
276
|
+
}
|
277
|
+
|
278
|
+
// Redraw if window resized.
|
279
|
+
$(window).resize(_.debounce(function () {
|
280
|
+
_.each(HSPComponents, (comp) => {
|
281
|
+
comp.draw();
|
282
|
+
});
|
283
|
+
}, 100));
|
data/public/js/kablammo.js
CHANGED
@@ -19,7 +19,7 @@ import * as Helpers from './visualisation_helpers';
|
|
19
19
|
|
20
20
|
class Graph {
|
21
21
|
static name() {
|
22
|
-
return '
|
22
|
+
return 'Graphical overview of aligning region(s)';
|
23
23
|
}
|
24
24
|
|
25
25
|
static className() {
|
@@ -36,7 +36,7 @@ class Graph {
|
|
36
36
|
|
37
37
|
constructor($svgContainer, props) {
|
38
38
|
this._zoom_scale_by = 1.4;
|
39
|
-
this._padding_x =
|
39
|
+
this._padding_x = 12;
|
40
40
|
this._padding_y = 50;
|
41
41
|
|
42
42
|
this._canvas_height = $svgContainer.height();
|
@@ -212,7 +212,7 @@ class Graph {
|
|
212
212
|
var a = self._scales.query.height;
|
213
213
|
var b = self._scales.subject.height;
|
214
214
|
var middle = ( b - a ) / 2;
|
215
|
-
return a + middle +
|
215
|
+
return a + middle + 2; // for font-height 10px
|
216
216
|
})
|
217
217
|
.text(function(hsp) {
|
218
218
|
return Helpers.toLetters(hsp.number)
|
data/public/js/report.js
CHANGED
@@ -7,9 +7,11 @@ import HitsOverview from './hits_overview';
|
|
7
7
|
import LengthDistribution from './length_distribution'; // length distribution of hits
|
8
8
|
import HSPOverview from './kablammo';
|
9
9
|
import AlignmentExporter from './alignment_exporter'; // to download textual alignment
|
10
|
+
import HSP from './hsp';
|
10
11
|
import './sequence';
|
11
12
|
|
12
13
|
import * as Helpers from './visualisation_helpers'; // for toLetters
|
14
|
+
import Utils from './utils'; // to use as mixin in Hit and HitsTable
|
13
15
|
import showErrorModal from './error_modal';
|
14
16
|
|
15
17
|
/**
|
@@ -29,296 +31,557 @@ var downloadFASTA = function (sequence_ids, database_ids) {
|
|
29
31
|
}
|
30
32
|
|
31
33
|
/**
|
32
|
-
*
|
34
|
+
* Base component of report page. This component is later rendered into page's
|
35
|
+
* '#view' element.
|
33
36
|
*/
|
34
|
-
var
|
37
|
+
var Page = React.createClass({
|
38
|
+
render: function () {
|
39
|
+
return (
|
40
|
+
<div>
|
41
|
+
{/* Provide bootstrap .container element inside the #view for
|
42
|
+
the Report component to render itself in. */}
|
43
|
+
<div className="container"><Report ref="report"/></div>
|
44
|
+
|
45
|
+
{/* Required by Grapher for SVG and PNG download */}
|
46
|
+
<canvas id="png-exporter" hidden></canvas>
|
47
|
+
</div>
|
48
|
+
);
|
49
|
+
}
|
50
|
+
});
|
51
|
+
|
52
|
+
/**
|
53
|
+
* Renders entire report.
|
54
|
+
*
|
55
|
+
* Composed of Query and Sidebar components.
|
56
|
+
*/
|
57
|
+
var Report = React.createClass({
|
58
|
+
|
59
|
+
// Model //
|
60
|
+
|
61
|
+
getInitialState: function () {
|
62
|
+
this.fetchResults();
|
63
|
+
this.updateCycle = 0;
|
64
|
+
|
65
|
+
return {
|
66
|
+
search_id: '',
|
67
|
+
program: '',
|
68
|
+
program_version: '',
|
69
|
+
submitted_at: '',
|
70
|
+
queries: [],
|
71
|
+
querydb: [],
|
72
|
+
params: [],
|
73
|
+
stats: []
|
74
|
+
};
|
75
|
+
},
|
35
76
|
|
36
77
|
/**
|
37
|
-
*
|
78
|
+
* Fetch results.
|
38
79
|
*/
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
80
|
+
fetchResults: function () {
|
81
|
+
var intervals = [200, 400, 800, 1200, 2000, 3000, 5000];
|
82
|
+
var component = this;
|
83
|
+
|
84
|
+
function poll () {
|
85
|
+
$.getJSON(location.pathname + '.json')
|
86
|
+
.complete(function (jqXHR) {
|
87
|
+
switch (jqXHR.status) {
|
88
|
+
case 202:
|
89
|
+
var interval;
|
90
|
+
if (intervals.length === 1) {
|
91
|
+
interval = intervals[0];
|
92
|
+
}
|
93
|
+
else {
|
94
|
+
interval = intervals.shift();
|
95
|
+
}
|
96
|
+
setTimeout(poll, interval);
|
97
|
+
break;
|
98
|
+
case 200:
|
99
|
+
component.updateState(jqXHR.responseJSON);
|
100
|
+
break;
|
101
|
+
case 404:
|
102
|
+
case 400:
|
103
|
+
case 500:
|
104
|
+
showErrorModal(jqXHR.responseJSON);
|
105
|
+
break;
|
106
|
+
}
|
107
|
+
});
|
48
108
|
}
|
109
|
+
|
110
|
+
poll();
|
49
111
|
},
|
50
112
|
|
113
|
+
/**
|
114
|
+
* Incrementally update state so that the rendering process is
|
115
|
+
* not overwhelemed when there are too many queries.
|
116
|
+
*/
|
117
|
+
updateState: function(responseJSON) {
|
118
|
+
var queries = responseJSON.queries;
|
119
|
+
|
120
|
+
// Render results for first 50 queries and set flag if total queries is
|
121
|
+
// more than 250.
|
122
|
+
var numHits = 0;
|
123
|
+
responseJSON.veryBig = queries.length > 250;
|
124
|
+
//responseJSON.veryBig = !_.every(queries, (query) => {
|
125
|
+
//numHits += query.hits.length;
|
126
|
+
//return (numHits <= 500);
|
127
|
+
//});
|
128
|
+
responseJSON.queries = queries.splice(0, 50);
|
129
|
+
this.setState(responseJSON);
|
130
|
+
|
131
|
+
// Render results for remaining queries.
|
132
|
+
var update = function () {
|
133
|
+
if (queries.length > 0) {
|
134
|
+
this.setState({
|
135
|
+
queries: this.state.queries.concat(queries.splice(0, 50))
|
136
|
+
});
|
137
|
+
setTimeout(update.bind(this), 500);
|
138
|
+
}
|
139
|
+
else {
|
140
|
+
this.componentFinishedUpdating();
|
141
|
+
}
|
142
|
+
};
|
143
|
+
setTimeout(update.bind(this), 500);
|
144
|
+
},
|
51
145
|
|
52
|
-
/***********************************
|
53
|
-
* Formatters for hits & hsp table *
|
54
|
-
***********************************/
|
55
146
|
|
56
|
-
//
|
57
|
-
|
58
|
-
return (
|
147
|
+
// View //
|
148
|
+
render: function () {
|
149
|
+
return this.isResultAvailable() ?
|
150
|
+
this.resultsJSX() : this.loadingJSX();
|
59
151
|
},
|
60
152
|
|
61
153
|
/**
|
62
|
-
* Returns
|
154
|
+
* Returns loading message
|
63
155
|
*/
|
64
|
-
|
65
|
-
return (
|
156
|
+
loadingJSX: function () {
|
157
|
+
return (
|
158
|
+
<div
|
159
|
+
className="row">
|
160
|
+
<div
|
161
|
+
className="col-md-6 col-md-offset-3 text-center">
|
162
|
+
<h1>
|
163
|
+
<i className="fa fa-cog fa-spin"></i> BLAST-ing
|
164
|
+
</h1>
|
165
|
+
<p>
|
166
|
+
<br/>
|
167
|
+
This can take some time depending on the size of your query and
|
168
|
+
database(s). The page will update automatically when BLAST is
|
169
|
+
done.
|
170
|
+
<br/>
|
171
|
+
<br/>
|
172
|
+
You can bookmark the page and come back to it later or share
|
173
|
+
the link with someone.
|
174
|
+
</p>
|
175
|
+
</div>
|
176
|
+
</div>
|
177
|
+
);
|
66
178
|
},
|
67
179
|
|
68
180
|
/**
|
69
|
-
*
|
181
|
+
* Return results JSX.
|
70
182
|
*/
|
71
|
-
|
72
|
-
return
|
183
|
+
resultsJSX: function () {
|
184
|
+
return (
|
185
|
+
<div className="row">
|
186
|
+
{ this.shouldShowSidebar() &&
|
187
|
+
(
|
188
|
+
<div
|
189
|
+
className="col-md-3 hidden-sm hidden-xs">
|
190
|
+
<SideBar data={this.state} shouldShowIndex={this.shouldShowIndex()}/>
|
191
|
+
</div>
|
192
|
+
)
|
193
|
+
}
|
194
|
+
<div className={this.shouldShowSidebar() ?
|
195
|
+
'col-md-9' : 'col-md-12'}>
|
196
|
+
{ this.overviewJSX() }
|
197
|
+
{ this.isHitsAvailable()
|
198
|
+
? <Circos queries={this.state.queries}
|
199
|
+
program={this.state.program} collapsed="true"/>
|
200
|
+
: <span></span> }
|
201
|
+
{
|
202
|
+
_.map(this.state.queries, _.bind(function (query) {
|
203
|
+
return (
|
204
|
+
<Query key={"Query_"+query.id} query={query} data={this.state}
|
205
|
+
selectHit={this.selectHit}/>
|
206
|
+
);
|
207
|
+
}, this))
|
208
|
+
}
|
209
|
+
</div>
|
210
|
+
</div>
|
211
|
+
);
|
73
212
|
},
|
74
213
|
|
75
214
|
/**
|
76
|
-
*
|
215
|
+
* Renders report overview.
|
77
216
|
*/
|
78
|
-
|
79
|
-
return
|
217
|
+
overviewJSX: function () {
|
218
|
+
return (
|
219
|
+
<div className="overview">
|
220
|
+
<pre className="pre-reset">
|
221
|
+
{this.state.program_version}{this.state.submitted_at
|
222
|
+
&& `; query submitted on ${this.state.submitted_at}`}
|
223
|
+
<br/>
|
224
|
+
Databases ({this.state.stats.nsequences} sequences,
|
225
|
+
{this.state.stats.ncharacters} characters): {
|
226
|
+
this.state.querydb.map((db) => { return db.title }).join(", ")
|
227
|
+
}
|
228
|
+
<br/>
|
229
|
+
Parameters: {
|
230
|
+
_.map(this.state.params, function (val, key) {
|
231
|
+
return key + " " + val;
|
232
|
+
}).join(", ")
|
233
|
+
}
|
234
|
+
</pre>
|
235
|
+
</div>
|
236
|
+
);
|
80
237
|
},
|
81
238
|
|
239
|
+
|
240
|
+
// Controller //
|
241
|
+
|
82
242
|
/**
|
83
|
-
* Returns
|
84
|
-
*
|
243
|
+
* Returns true if results have been fetched.
|
244
|
+
*
|
245
|
+
* A holding message is shown till results are fetched.
|
85
246
|
*/
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
return 0
|
90
|
-
}
|
247
|
+
isResultAvailable: function () {
|
248
|
+
return this.state.queries.length >= 1;
|
249
|
+
},
|
91
250
|
|
92
|
-
|
93
|
-
|
94
|
-
{
|
95
|
-
|
96
|
-
}
|
251
|
+
isHitsAvailable: function () {
|
252
|
+
var cnt = 0;
|
253
|
+
_.each(this.state.queries, function (query) {
|
254
|
+
if(query.hits.length == 0) cnt++;
|
255
|
+
});
|
256
|
+
return !(cnt == this.state.queries.length);
|
257
|
+
},
|
97
258
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
259
|
+
/**
|
260
|
+
* Returns true if sidebar should be shown.
|
261
|
+
*
|
262
|
+
* Sidebar is not shown if there is only one query and there are no hits
|
263
|
+
* corresponding to the query.
|
264
|
+
*/
|
265
|
+
shouldShowSidebar: function () {
|
266
|
+
return !(this.state.queries.length == 1 &&
|
267
|
+
this.state.queries[0].hits.length == 0);
|
268
|
+
},
|
107
269
|
|
108
|
-
/**
|
109
|
-
|
110
|
-
|
111
|
-
|
270
|
+
/**
|
271
|
+
* Returns true if index should be shown in the sidebar.
|
272
|
+
*
|
273
|
+
* Index is not shown in the sidebar if there are more than eight queries
|
274
|
+
* in total.
|
275
|
+
*/
|
276
|
+
shouldShowIndex: function () {
|
277
|
+
return this.state.queries.length <= 8;
|
278
|
+
},
|
112
279
|
|
113
|
-
|
280
|
+
/**
|
281
|
+
* Called after first call to render. The results may not be available at
|
282
|
+
* this stage and thus results DOM cannot be scripted here, unless using
|
283
|
+
* delegated events bound to the window, document, or body.
|
284
|
+
*/
|
285
|
+
componentDidMount: function () {
|
286
|
+
// This sets up an event handler which enables users to select text
|
287
|
+
// from hit header without collapsing the hit.
|
288
|
+
this.preventCollapseOnSelection();
|
289
|
+
},
|
114
290
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
291
|
+
/**
|
292
|
+
* Called after each state change. Only a part of results DOM may be
|
293
|
+
* available after a state change.
|
294
|
+
*/
|
295
|
+
componentDidUpdate: function () {
|
296
|
+
// We track the number of updates to the component.
|
297
|
+
this.updateCycle += 1;
|
120
298
|
|
121
|
-
//
|
299
|
+
// Lock sidebar in its position on first update of
|
300
|
+
// results DOM.
|
301
|
+
if (this.updateCycle === 1 ) this.affixSidebar();
|
302
|
+
},
|
122
303
|
|
123
|
-
|
124
|
-
|
125
|
-
|
304
|
+
/**
|
305
|
+
* Prevents folding of hits during text-selection, etc.
|
306
|
+
*/
|
126
307
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
{this.props.sequence.id}
|
134
|
-
<small>
|
135
|
-
{this.props.sequence.title}
|
136
|
-
</small>
|
137
|
-
</h4>
|
138
|
-
</div>
|
139
|
-
<div
|
140
|
-
className="section-content">
|
141
|
-
<div
|
142
|
-
className={this.widgetClass} id={this.widgetID}>
|
143
|
-
</div>
|
144
|
-
</div>
|
145
|
-
</div>
|
146
|
-
);
|
147
|
-
},
|
308
|
+
/**
|
309
|
+
* Called after all results have been rendered.
|
310
|
+
*/
|
311
|
+
componentFinishedUpdating: function () {
|
312
|
+
this.shouldShowIndex() && this.setupScrollSpy();
|
313
|
+
},
|
148
314
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
315
|
+
/**
|
316
|
+
* Prevents folding of hits during text-selection.
|
317
|
+
*/
|
318
|
+
preventCollapseOnSelection: function () {
|
319
|
+
$('body').on('mousedown', ".hit > .section-header > h4", function (event) {
|
320
|
+
var $this = $(this);
|
321
|
+
$this.on('mouseup mousemove', function handler(event) {
|
322
|
+
if (event.type === 'mouseup') {
|
323
|
+
// user wants to toggle
|
324
|
+
$this.attr('data-toggle', 'collapse');
|
325
|
+
$this.find('.fa-chevron-down').toggleClass('fa-rotate-270');
|
326
|
+
} else {
|
327
|
+
// user wants to select
|
328
|
+
$this.attr('data-toggle', '');
|
162
329
|
}
|
330
|
+
$this.off('mouseup mousemove', handler);
|
163
331
|
});
|
164
|
-
|
165
|
-
|
166
|
-
});
|
167
|
-
|
168
|
-
return React.createClass({
|
169
|
-
|
170
|
-
// Kind of public API. //
|
171
|
-
|
172
|
-
/**
|
173
|
-
* Shows sequence viewer.
|
174
|
-
*/
|
175
|
-
show: function () {
|
176
|
-
this.modal().modal('show');
|
177
|
-
},
|
178
|
-
|
179
|
-
|
180
|
-
// Internal helpers. //
|
181
|
-
|
182
|
-
modal: function () {
|
183
|
-
return $(React.findDOMNode(this.refs.modal));
|
184
|
-
},
|
332
|
+
});
|
333
|
+
},
|
185
334
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
{error_msg[0]}
|
198
|
-
</h4>
|
199
|
-
</div>
|
200
|
-
<div
|
201
|
-
className="section-content">
|
202
|
-
<pre
|
203
|
-
className="pre-reset">
|
204
|
-
{error_msg[1]}
|
205
|
-
</pre>
|
206
|
-
</div>
|
207
|
-
</div>
|
208
|
-
);
|
209
|
-
}, this))
|
210
|
-
}
|
211
|
-
{
|
212
|
-
_.map(this.state.sequences, _.bind(function (sequence) {
|
213
|
-
return (<Viewer sequence={sequence}/>);
|
214
|
-
}, this))
|
215
|
-
}
|
216
|
-
</div>
|
217
|
-
);
|
218
|
-
},
|
335
|
+
/**
|
336
|
+
* Affixes the sidebar.
|
337
|
+
*/
|
338
|
+
affixSidebar: function () {
|
339
|
+
var $sidebar = $('.sidebar');
|
340
|
+
$sidebar.affix({
|
341
|
+
offset: {
|
342
|
+
top: $sidebar.offset().top
|
343
|
+
}
|
344
|
+
});
|
345
|
+
},
|
219
346
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
},
|
347
|
+
/**
|
348
|
+
* For the query in viewport, highlights corresponding entry in the index.
|
349
|
+
*/
|
350
|
+
setupScrollSpy: function () {
|
351
|
+
$('body').scrollspy({target: '.sidebar'});
|
352
|
+
},
|
227
353
|
|
354
|
+
/**
|
355
|
+
* Event-handler when hit is selected
|
356
|
+
* Adds glow to hit component.
|
357
|
+
* Updates number of Fasta that can be downloaded
|
358
|
+
*/
|
359
|
+
selectHit: function (id) {
|
228
360
|
|
229
|
-
|
361
|
+
var checkbox = $("#" + id);
|
362
|
+
var num_checked = $('.hit-links :checkbox:checked').length;
|
230
363
|
|
231
|
-
|
232
|
-
return
|
233
|
-
|
234
|
-
sequences: [],
|
235
|
-
requestCompleted: false
|
236
|
-
};
|
237
|
-
},
|
364
|
+
if (!checkbox || !checkbox.val()) {
|
365
|
+
return;
|
366
|
+
}
|
238
367
|
|
239
|
-
|
240
|
-
return (
|
241
|
-
<div
|
242
|
-
className="modal sequence-viewer"
|
243
|
-
ref="modal" tabIndex="-1">
|
244
|
-
<div
|
245
|
-
className="modal-dialog">
|
246
|
-
<div
|
247
|
-
className="modal-content">
|
248
|
-
<div
|
249
|
-
className="modal-header">
|
250
|
-
<h3>View sequence</h3>
|
251
|
-
</div>
|
368
|
+
var $hit = $(checkbox.data('target'));
|
252
369
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
);
|
259
|
-
|
370
|
+
// Highlight selected hit and sync checkboxes if sequence viewer is open.
|
371
|
+
if (checkbox.is(":checked")) {
|
372
|
+
$hit
|
373
|
+
.addClass('glow')
|
374
|
+
.find(":checkbox").not(checkbox).check();
|
375
|
+
var $a = $('.download-fasta-of-selected');
|
376
|
+
var $b = $('.download-alignment-of-selected');
|
377
|
+
$b.enable()
|
378
|
+
var $n = $a.find('span');
|
379
|
+
$a
|
380
|
+
.enable()
|
381
|
+
}
|
260
382
|
|
261
|
-
|
262
|
-
|
263
|
-
|
383
|
+
else {
|
384
|
+
$hit
|
385
|
+
.removeClass('glow')
|
386
|
+
.find(":checkbox").not(checkbox).uncheck();
|
387
|
+
}
|
264
388
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
})
|
273
|
-
}, this))
|
274
|
-
.fail(function (jqXHR, status, error) {
|
275
|
-
showErrorModal(jqXHR, function () {
|
276
|
-
this.hide();
|
277
|
-
});
|
278
|
-
});
|
389
|
+
if (num_checked >= 1)
|
390
|
+
{
|
391
|
+
var $a = $('.download-fasta-of-selected');
|
392
|
+
var $b = $('.download-alignment-of-selected');
|
393
|
+
$a.find('.text-bold').html(num_checked);
|
394
|
+
$b.find('.text-bold').html(num_checked);
|
395
|
+
}
|
279
396
|
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
397
|
+
if (num_checked == 0) {
|
398
|
+
var $a = $('.download-fasta-of-selected');
|
399
|
+
var $b = $('.download-alignment-of-selected');
|
400
|
+
$a.addClass('disabled').find('.text-bold').html('');
|
401
|
+
$b.addClass('disabled').find('.text-bold').html('');
|
402
|
+
}
|
403
|
+
},
|
404
|
+
});
|
284
405
|
|
285
406
|
/**
|
286
|
-
*
|
407
|
+
* Renders report for each query sequence.
|
408
|
+
*
|
409
|
+
* Composed of graphical overview, tabular summary (HitsTable),
|
410
|
+
* and a list of Hits.
|
287
411
|
*/
|
288
|
-
var
|
289
|
-
mixins: [Utils],
|
412
|
+
var Query = React.createClass({
|
290
413
|
|
291
|
-
|
292
|
-
* Returns accession number of the hit sequence.
|
293
|
-
*/
|
294
|
-
accession: function () {
|
295
|
-
return this.props.hit.accession;
|
296
|
-
},
|
414
|
+
// Kind of public API //
|
297
415
|
|
298
416
|
/**
|
299
|
-
* Returns
|
417
|
+
* Returns the id of query.
|
300
418
|
*/
|
301
|
-
|
302
|
-
return this.props.
|
419
|
+
domID: function () {
|
420
|
+
return "Query_" + this.props.query.number;
|
303
421
|
},
|
304
422
|
|
305
|
-
// Internal helpers. //
|
306
|
-
|
307
423
|
/**
|
308
|
-
* Returns
|
424
|
+
* Returns number of hits.
|
309
425
|
*/
|
310
|
-
|
311
|
-
return
|
312
|
-
},
|
313
|
-
|
314
|
-
databaseIDs: function () {
|
315
|
-
return _.map(this.props.querydb, _.iteratee('id'));
|
426
|
+
numhits: function () {
|
427
|
+
return this.props.query.hits.length;
|
316
428
|
},
|
317
429
|
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
430
|
+
// Life cycle methods //
|
431
|
+
|
432
|
+
render: function () {
|
433
|
+
return (
|
434
|
+
<div
|
435
|
+
className="resultn" id={this.domID()}
|
436
|
+
data-query-len={this.props.query.length}
|
437
|
+
data-algorithm={this.props.data.program}>
|
438
|
+
<div
|
439
|
+
className="section-header">
|
440
|
+
<h3>
|
441
|
+
Query= {this.props.query.id}
|
442
|
+
|
443
|
+
<small>
|
444
|
+
{this.props.query.title}
|
445
|
+
</small>
|
446
|
+
</h3>
|
447
|
+
<span
|
448
|
+
className="label label-reset pos-label"
|
449
|
+
title={"Query" + this.props.query.number + "."}
|
450
|
+
data-toggle="tooltip">
|
451
|
+
{this.props.query.number + "/" + this.props.data.queries.length}
|
452
|
+
</span>
|
453
|
+
</div>
|
454
|
+
{this.numhits() &&
|
455
|
+
(
|
456
|
+
<div className="section-content">
|
457
|
+
<HitsOverview key={"GO_"+this.props.query.number} query={this.props.query} program={this.props.data.program} collapsed={this.props.data.veryBig}/>
|
458
|
+
<LengthDistribution key={"LD_"+this.props.query.id} query={this.props.query} algorithm={this.props.data.program} collapsed="true"/>
|
459
|
+
<HitsTable key={"HT_"+this.props.query.number} query={this.props.query}/>
|
460
|
+
<div
|
461
|
+
id="hits">
|
462
|
+
{
|
463
|
+
_.map(this.props.query.hits, _.bind(function (hit) {
|
464
|
+
return (
|
465
|
+
<Hit
|
466
|
+
hit={hit}
|
467
|
+
key={"HIT_"+hit.number}
|
468
|
+
algorithm={this.props.data.program}
|
469
|
+
querydb={this.props.data.querydb}
|
470
|
+
query={this.props.query}
|
471
|
+
selectHit={this.props.selectHit}/>
|
472
|
+
);
|
473
|
+
}, this))
|
474
|
+
}
|
475
|
+
</div>
|
476
|
+
</div>
|
477
|
+
) || (
|
478
|
+
<div
|
479
|
+
className="section-content">
|
480
|
+
<p>
|
481
|
+
Query length: {this.props.query.length}
|
482
|
+
</p>
|
483
|
+
<br/>
|
484
|
+
<br/>
|
485
|
+
<p>
|
486
|
+
<strong> ****** No hits found ****** </strong>
|
487
|
+
</p>
|
488
|
+
</div>
|
489
|
+
)
|
490
|
+
}
|
491
|
+
</div>
|
492
|
+
)
|
493
|
+
},
|
494
|
+
});
|
495
|
+
|
496
|
+
/**
|
497
|
+
* Renders summary of all hits per query in a tabular form.
|
498
|
+
*/
|
499
|
+
var HitsTable = React.createClass({
|
500
|
+
mixins: [Utils],
|
501
|
+
render: function () {
|
502
|
+
var count = 0,
|
503
|
+
hasName = _.every(this.props.query.hits, function(hit) {
|
504
|
+
return hit.sciname !== '';
|
505
|
+
});
|
506
|
+
|
507
|
+
return (
|
508
|
+
<table
|
509
|
+
className="table table-hover table-condensed tabular-view">
|
510
|
+
<thead>
|
511
|
+
<th className="text-left">#</th>
|
512
|
+
<th>Similar sequences</th>
|
513
|
+
{hasName && <th className="text-left">Species</th>}
|
514
|
+
<th className="text-right">Query coverage (%)</th>
|
515
|
+
<th className="text-right">Total score</th>
|
516
|
+
<th className="text-right">E value</th>
|
517
|
+
<th className="text-right" data-toggle="tooltip"
|
518
|
+
data-placement="left" title="Total identity of all hsps / total length of all hsps">
|
519
|
+
Identity (%)
|
520
|
+
</th>
|
521
|
+
</thead>
|
522
|
+
<tbody>
|
523
|
+
{
|
524
|
+
_.map(this.props.query.hits, _.bind(function (hit) {
|
525
|
+
return (
|
526
|
+
<tr key={hit.number}>
|
527
|
+
<td className="text-left">{hit.number + "."}</td>
|
528
|
+
<td>
|
529
|
+
<a href={"#Query_" + this.props.query.number + "_hit_" + hit.number}>
|
530
|
+
{hit.id}
|
531
|
+
</a>
|
532
|
+
</td>
|
533
|
+
{hasName && <td className="text-left">{hit.sciname}</td>}
|
534
|
+
<td className="text-right">{hit.qcovs}</td>
|
535
|
+
<td className="text-right">{hit.score}</td>
|
536
|
+
<td className="text-right">{this.inExponential(hit.hsps[0].evalue)}</td>
|
537
|
+
<td className="text-right">{hit.identity}</td>
|
538
|
+
</tr>
|
539
|
+
)
|
540
|
+
}, this))
|
541
|
+
}
|
542
|
+
</tbody>
|
543
|
+
</table>
|
544
|
+
);
|
545
|
+
}
|
546
|
+
});
|
547
|
+
|
548
|
+
/**
|
549
|
+
* Component for each hit.
|
550
|
+
*/
|
551
|
+
var Hit = React.createClass({
|
552
|
+
mixins: [Utils],
|
553
|
+
|
554
|
+
/**
|
555
|
+
* Returns accession number of the hit sequence.
|
556
|
+
*/
|
557
|
+
accession: function () {
|
558
|
+
return this.props.hit.accession;
|
559
|
+
},
|
560
|
+
|
561
|
+
/**
|
562
|
+
* Returns length of the hit sequence.
|
563
|
+
*/
|
564
|
+
length: function () {
|
565
|
+
return this.props.hit.length;
|
566
|
+
},
|
567
|
+
|
568
|
+
// Internal helpers. //
|
569
|
+
|
570
|
+
/**
|
571
|
+
* Returns id that will be used for the DOM node corresponding to the hit.
|
572
|
+
*/
|
573
|
+
domID: function () {
|
574
|
+
return "Query_" + this.props.query.number + "_hit_" + this.props.hit.number;
|
575
|
+
},
|
576
|
+
|
577
|
+
databaseIDs: function () {
|
578
|
+
return _.map(this.props.querydb, _.iteratee('id'));
|
579
|
+
},
|
580
|
+
|
581
|
+
showSequenceViewer: function (event) {
|
582
|
+
this.setState({ showSequenceViewer: true });
|
583
|
+
event && event.preventDefault();
|
584
|
+
},
|
322
585
|
|
323
586
|
hideSequenceViewer: function () {
|
324
587
|
this.setState({ showSequenceViewer: false });
|
@@ -346,67 +609,6 @@ var Hit = React.createClass({
|
|
346
609
|
aln_exporter.export_alignments(hsps, this.props.query.id+"_"+this.props.hit.id);
|
347
610
|
},
|
348
611
|
|
349
|
-
/**
|
350
|
-
* Return prettified stats for the given hsp and based on the BLAST
|
351
|
-
* algorithm.
|
352
|
-
*/
|
353
|
-
getHSPStats: function (hsp) {
|
354
|
-
var stats = {
|
355
|
-
'Score': this.format_2_tuple([
|
356
|
-
this.inTwoDecimal(hsp.bit_score),
|
357
|
-
hsp.score
|
358
|
-
]),
|
359
|
-
|
360
|
-
'E value': this.inExponential(hsp.evalue),
|
361
|
-
|
362
|
-
'Identities': this.format_2_tuple([
|
363
|
-
this.inFraction(hsp.identity, hsp.length),
|
364
|
-
this.inPercentage(hsp.identity, hsp.length)
|
365
|
-
]),
|
366
|
-
|
367
|
-
'Gaps': this.format_2_tuple([
|
368
|
-
this.inFraction(hsp.gaps, hsp.length),
|
369
|
-
this.inPercentage(hsp.gaps, hsp.length)
|
370
|
-
]),
|
371
|
-
|
372
|
-
'Coverage': hsp.qcovhsp
|
373
|
-
};
|
374
|
-
|
375
|
-
switch (this.props.algorithm) {
|
376
|
-
case 'tblastx':
|
377
|
-
_.extend(stats, {
|
378
|
-
'Frame': this.inFraction(hsp.qframe, hsp.sframe)
|
379
|
-
});
|
380
|
-
// fall-through
|
381
|
-
case 'blastp':
|
382
|
-
_.extend(stats, {
|
383
|
-
'Positives': this.format_2_tuple([
|
384
|
-
this.inFraction(hsp.positives, hsp.length),
|
385
|
-
this.inPercentage(hsp.positives, hsp.length)
|
386
|
-
])
|
387
|
-
});
|
388
|
-
break;
|
389
|
-
case 'blastn':
|
390
|
-
_.extend(stats, {
|
391
|
-
'Strand': (hsp.qframe > 0 ? '+' : '-') +
|
392
|
-
"/" +
|
393
|
-
(hsp.sframe > 0 ? '+' : '-')
|
394
|
-
});
|
395
|
-
break;
|
396
|
-
case 'blastx':
|
397
|
-
_.extend(stats, {
|
398
|
-
'Query Frame': hsp.qframe
|
399
|
-
});
|
400
|
-
break;
|
401
|
-
case 'tblastn':
|
402
|
-
_.extend(stats, {
|
403
|
-
'Hit Frame': hsp.sframe
|
404
|
-
});
|
405
|
-
break;
|
406
|
-
}
|
407
|
-
|
408
|
-
return stats;
|
409
|
-
},
|
410
612
|
|
411
613
|
// Life cycle methods //
|
412
614
|
|
@@ -515,206 +717,200 @@ var Hit = React.createClass({
|
|
515
717
|
<HSPOverview key={"kablammo"+this.props.query.id}
|
516
718
|
query={this.props.query} hit={this.props.hit}
|
517
719
|
algorithm={this.props.algorithm}/>
|
518
|
-
|
519
|
-
className="table hsps">
|
520
|
-
<tbody>
|
521
|
-
{
|
522
|
-
_.map (this.props.hit.hsps, _.bind( function (hsp) {
|
523
|
-
stats_returned = this.getHSPStats(hsp);
|
524
|
-
return (
|
525
|
-
<tr
|
526
|
-
id={"Alignment_Query_" + this.props.query.number + "_hit_"
|
527
|
-
+ this.props.hit.number + "_" + hsp.number}
|
528
|
-
key={"Query_"+this.props.query.id+"_Hit_"+this.props.hit.id+"_"+hsp.number}>
|
529
|
-
<td>
|
530
|
-
{Helpers.toLetters(hsp.number) + "."}
|
531
|
-
</td>
|
532
|
-
<td
|
533
|
-
style={{width: "100%"}}>
|
534
|
-
<div
|
535
|
-
className="hsp"
|
536
|
-
id={"Query_" + this.props.query.number + "_hit_"
|
537
|
-
+ this.props.hit.number + "_" + hsp.number}
|
538
|
-
data-hsp-evalue={hsp.evalue}
|
539
|
-
data-hsp-start={hsp.qstart}
|
540
|
-
data-hsp-end={hsp.qend}
|
541
|
-
data-hsp-frame={hsp.sframe}>
|
542
|
-
<table
|
543
|
-
className="table table-condensed hsp-stats">
|
544
|
-
<thead>
|
545
|
-
{
|
546
|
-
_.map(stats_returned, function (value , key) {
|
547
|
-
return(<th key={value+"_"+key}>{key}</th>);
|
548
|
-
})
|
549
|
-
}
|
550
|
-
</thead>
|
551
|
-
<tbody>
|
552
|
-
<tr>
|
553
|
-
{
|
554
|
-
_.map(stats_returned, _.bind(function (value, key) {
|
555
|
-
return(<th key={value+"_"+key}>{value}</th>);
|
556
|
-
}, this))
|
557
|
-
}
|
558
|
-
</tr>
|
559
|
-
</tbody>
|
560
|
-
</table>
|
561
|
-
<div className="alignment">{hsp.pp}</div>
|
562
|
-
</div>
|
563
|
-
</td>
|
564
|
-
</tr>
|
565
|
-
)
|
566
|
-
}, this))
|
567
|
-
}
|
568
|
-
</tbody>
|
569
|
-
</table>
|
720
|
+
{ this.hspListJSX() }
|
570
721
|
</div>
|
571
722
|
</div>
|
572
723
|
);
|
573
|
-
}
|
574
|
-
});
|
575
|
-
|
576
|
-
/**
|
577
|
-
* Renders summary of all hits per query in a tabular form.
|
578
|
-
*/
|
579
|
-
var HitsTable = React.createClass({
|
580
|
-
mixins: [Utils],
|
581
|
-
render: function () {
|
582
|
-
var count = 0,
|
583
|
-
hasName = _.every(this.props.query.hits, function(hit) {
|
584
|
-
return hit.sciname !== '';
|
585
|
-
});
|
724
|
+
},
|
586
725
|
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
<
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
<th className="text-right">Total score</th>
|
596
|
-
<th className="text-right">E value</th>
|
597
|
-
<th className="text-right" data-toggle="tooltip"
|
598
|
-
data-placement="left" title="Total identity of all hsps / total length of all hsps">
|
599
|
-
Identity (%)
|
600
|
-
</th>
|
601
|
-
</thead>
|
602
|
-
<tbody>
|
603
|
-
{
|
604
|
-
_.map(this.props.query.hits, _.bind(function (hit) {
|
605
|
-
return (
|
606
|
-
<tr key={hit.number}>
|
607
|
-
<td className="text-left">{hit.number + "."}</td>
|
608
|
-
<td>
|
609
|
-
<a href={"#Query_" + this.props.query.number + "_hit_" + hit.number}>
|
610
|
-
{hit.id}
|
611
|
-
</a>
|
612
|
-
</td>
|
613
|
-
{hasName && <td className="text-left">{hit.sciname}</td>}
|
614
|
-
<td className="text-right">{hit.qcovs}</td>
|
615
|
-
<td className="text-right">{hit.score}</td>
|
616
|
-
<td className="text-right">{this.inExponential(hit.hsps[0].evalue)}</td>
|
617
|
-
<td className="text-right">{hit.identity}</td>
|
618
|
-
</tr>
|
619
|
-
)
|
620
|
-
}, this))
|
621
|
-
}
|
622
|
-
</tbody>
|
623
|
-
</table>
|
624
|
-
);
|
726
|
+
hspListJSX: function () {
|
727
|
+
return <div className="hsps">
|
728
|
+
{
|
729
|
+
this.props.hit.hsps.map((hsp) => {
|
730
|
+
return <HSP algorithm={this.props.algorithm} hsp={hsp}
|
731
|
+
query={this.props.query} hit={this.props.hit}/>}, this)
|
732
|
+
}
|
733
|
+
</div>
|
625
734
|
}
|
626
735
|
});
|
627
736
|
|
737
|
+
|
628
738
|
/**
|
629
|
-
*
|
630
|
-
*
|
631
|
-
* Composed of graphical overview, tabular summary (HitsTable),
|
632
|
-
* and a list of Hits.
|
739
|
+
* Component for sequence-viewer links.
|
633
740
|
*/
|
634
|
-
var
|
741
|
+
var SequenceViewer = (function () {
|
635
742
|
|
636
|
-
|
743
|
+
var Viewer = React.createClass({
|
637
744
|
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
},
|
745
|
+
/**
|
746
|
+
* The CSS class name that will be assigned to the widget container. ID
|
747
|
+
* assigned to the widget container is derived from the same.
|
748
|
+
*/
|
749
|
+
widgetClass: 'biojs-vis-sequence',
|
644
750
|
|
645
|
-
|
646
|
-
* Returns number of hits.
|
647
|
-
*/
|
648
|
-
numhits: function () {
|
649
|
-
return this.props.query.hits.length;
|
650
|
-
},
|
751
|
+
// Lifecycle methods. //
|
651
752
|
|
652
|
-
|
753
|
+
render: function () {
|
754
|
+
this.widgetID =
|
755
|
+
this.widgetClass + '-' + (new Date().getUTCMilliseconds());
|
653
756
|
|
654
|
-
|
655
|
-
return (
|
656
|
-
<div
|
657
|
-
className="resultn" id={this.domID()}
|
658
|
-
data-query-len={this.props.query.length}
|
659
|
-
data-algorithm={this.props.data.program}>
|
757
|
+
return (
|
660
758
|
<div
|
661
|
-
className="
|
662
|
-
<
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
759
|
+
className="fastan">
|
760
|
+
<div
|
761
|
+
className="section-header">
|
762
|
+
<h4>
|
763
|
+
{this.props.sequence.id}
|
764
|
+
<small>
|
765
|
+
{this.props.sequence.title}
|
766
|
+
</small>
|
767
|
+
</h4>
|
768
|
+
</div>
|
769
|
+
<div
|
770
|
+
className="section-content">
|
771
|
+
<div
|
772
|
+
className={this.widgetClass} id={this.widgetID}>
|
773
|
+
</div>
|
774
|
+
</div>
|
675
775
|
</div>
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
776
|
+
);
|
777
|
+
},
|
778
|
+
|
779
|
+
componentDidMount: function () {
|
780
|
+
// attach BioJS sequence viewer
|
781
|
+
var widget = new Sequence({
|
782
|
+
sequence: this.props.sequence.value,
|
783
|
+
target: this.widgetID,
|
784
|
+
format: 'PRIDE',
|
785
|
+
columns: {
|
786
|
+
size: 40,
|
787
|
+
spacedEach: 5
|
788
|
+
},
|
789
|
+
formatOptions: {
|
790
|
+
title: false,
|
791
|
+
footer: false
|
792
|
+
}
|
793
|
+
});
|
794
|
+
widget.hideFormatSelector();
|
795
|
+
}
|
796
|
+
});
|
797
|
+
|
798
|
+
return React.createClass({
|
799
|
+
|
800
|
+
// Kind of public API. //
|
801
|
+
|
802
|
+
/**
|
803
|
+
* Shows sequence viewer.
|
804
|
+
*/
|
805
|
+
show: function () {
|
806
|
+
this.modal().modal('show');
|
807
|
+
},
|
808
|
+
|
809
|
+
|
810
|
+
// Internal helpers. //
|
811
|
+
|
812
|
+
modal: function () {
|
813
|
+
return $(React.findDOMNode(this.refs.modal));
|
814
|
+
},
|
815
|
+
|
816
|
+
resultsJSX: function () {
|
817
|
+
return (
|
818
|
+
<div className="modal-body">
|
819
|
+
{
|
820
|
+
_.map(this.state.error_msgs, _.bind(function (error_msg) {
|
821
|
+
return (
|
822
|
+
<div
|
823
|
+
className="fastan">
|
824
|
+
<div
|
825
|
+
className="section-header">
|
826
|
+
<h4>
|
827
|
+
{error_msg[0]}
|
828
|
+
</h4>
|
829
|
+
</div>
|
830
|
+
<div
|
831
|
+
className="section-content">
|
832
|
+
<pre
|
833
|
+
className="pre-reset">
|
834
|
+
{error_msg[1]}
|
835
|
+
</pre>
|
836
|
+
</div>
|
837
|
+
</div>
|
838
|
+
);
|
839
|
+
}, this))
|
840
|
+
}
|
841
|
+
{
|
842
|
+
_.map(this.state.sequences, _.bind(function (sequence) {
|
843
|
+
return (<Viewer sequence={sequence}/>);
|
844
|
+
}, this))
|
845
|
+
}
|
846
|
+
</div>
|
847
|
+
);
|
848
|
+
},
|
849
|
+
|
850
|
+
loadingJSX: function () {
|
851
|
+
return (
|
852
|
+
<div className="modal-body text-center">
|
853
|
+
<i className="fa fa-spinner fa-3x fa-spin"></i>
|
854
|
+
</div>
|
855
|
+
);
|
856
|
+
},
|
857
|
+
|
858
|
+
|
859
|
+
// Lifecycle methods. //
|
860
|
+
|
861
|
+
getInitialState: function () {
|
862
|
+
return {
|
863
|
+
error_msgs: [],
|
864
|
+
sequences: [],
|
865
|
+
requestCompleted: false
|
866
|
+
};
|
867
|
+
},
|
868
|
+
|
869
|
+
render: function () {
|
870
|
+
return (
|
871
|
+
<div
|
872
|
+
className="modal sequence-viewer"
|
873
|
+
ref="modal" tabIndex="-1">
|
874
|
+
<div
|
875
|
+
className="modal-dialog">
|
876
|
+
<div
|
877
|
+
className="modal-content">
|
682
878
|
<div
|
683
|
-
|
684
|
-
|
685
|
-
_.map(this.props.query.hits, _.bind(function (hit) {
|
686
|
-
return (
|
687
|
-
<Hit
|
688
|
-
hit={hit}
|
689
|
-
key={"HIT_"+hit.number}
|
690
|
-
algorithm={this.props.data.program}
|
691
|
-
querydb={this.props.data.querydb}
|
692
|
-
query={this.props.query}
|
693
|
-
selectHit={this.props.selectHit}/>
|
694
|
-
);
|
695
|
-
}, this))
|
696
|
-
}
|
879
|
+
className="modal-header">
|
880
|
+
<h3>View sequence</h3>
|
697
881
|
</div>
|
882
|
+
|
883
|
+
{ this.state.requestCompleted &&
|
884
|
+
this.resultsJSX() || this.loadingJSX() }
|
698
885
|
</div>
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
})
|
886
|
+
</div>
|
887
|
+
</div>
|
888
|
+
);
|
889
|
+
},
|
890
|
+
|
891
|
+
componentDidMount: function () {
|
892
|
+
// Display modal with a spinner.
|
893
|
+
this.show();
|
894
|
+
|
895
|
+
// Fetch sequence and update state.
|
896
|
+
$.getJSON(this.props.url)
|
897
|
+
.done(_.bind(function (response) {
|
898
|
+
this.setState({
|
899
|
+
sequences: response.sequences,
|
900
|
+
error_msgs: response.error_msgs,
|
901
|
+
requestCompleted: true
|
902
|
+
})
|
903
|
+
}, this))
|
904
|
+
.fail(function (jqXHR, status, error) {
|
905
|
+
showErrorModal(jqXHR, function () {
|
906
|
+
this.hide();
|
907
|
+
});
|
908
|
+
});
|
717
909
|
|
910
|
+
this.modal().on('hidden.bs.modal', this.props.onHide);
|
911
|
+
},
|
912
|
+
});
|
913
|
+
})();
|
718
914
|
|
719
915
|
/**
|
720
916
|
* Renders links for downloading hit information in different formats.
|
@@ -912,382 +1108,4 @@ var SideBar = React.createClass({
|
|
912
1108
|
},
|
913
1109
|
});
|
914
1110
|
|
915
|
-
/**
|
916
|
-
* Renders entire report.
|
917
|
-
*
|
918
|
-
* Composed of Query and Sidebar components.
|
919
|
-
*/
|
920
|
-
var Report = React.createClass({
|
921
|
-
|
922
|
-
// Model //
|
923
|
-
|
924
|
-
getInitialState: function () {
|
925
|
-
this.fetchResults();
|
926
|
-
this.updateCycle = 0;
|
927
|
-
|
928
|
-
return {
|
929
|
-
search_id: '',
|
930
|
-
program: '',
|
931
|
-
program_version: '',
|
932
|
-
queries: [],
|
933
|
-
querydb: [],
|
934
|
-
params: [],
|
935
|
-
stats: []
|
936
|
-
};
|
937
|
-
},
|
938
|
-
|
939
|
-
/**
|
940
|
-
* Fetch results.
|
941
|
-
*/
|
942
|
-
fetchResults: function () {
|
943
|
-
var intervals = [200, 400, 800, 1200, 2000, 3000, 5000];
|
944
|
-
var component = this;
|
945
|
-
|
946
|
-
function poll () {
|
947
|
-
$.getJSON(location.pathname + '.json')
|
948
|
-
.complete(function (jqXHR) {
|
949
|
-
switch (jqXHR.status) {
|
950
|
-
case 202:
|
951
|
-
var interval;
|
952
|
-
if (intervals.length === 1) {
|
953
|
-
interval = intervals[0];
|
954
|
-
}
|
955
|
-
else {
|
956
|
-
interval = intervals.shift();
|
957
|
-
}
|
958
|
-
setTimeout(poll, interval);
|
959
|
-
break;
|
960
|
-
case 200:
|
961
|
-
component.updateState(jqXHR.responseJSON);
|
962
|
-
break;
|
963
|
-
case 404:
|
964
|
-
case 400:
|
965
|
-
case 500:
|
966
|
-
showErrorModal(jqXHR.responseJSON);
|
967
|
-
break;
|
968
|
-
}
|
969
|
-
});
|
970
|
-
}
|
971
|
-
|
972
|
-
poll();
|
973
|
-
},
|
974
|
-
|
975
|
-
/**
|
976
|
-
* Incrementally update state so that the rendering process is
|
977
|
-
* not overwhelemed when there are too many queries.
|
978
|
-
*/
|
979
|
-
updateState: function(responseJSON) {
|
980
|
-
var queries = responseJSON.queries;
|
981
|
-
|
982
|
-
// Render results for first 50 queries and set flag if total queries is
|
983
|
-
// more than 250.
|
984
|
-
var numHits = 0;
|
985
|
-
responseJSON.veryBig = queries.length > 250;
|
986
|
-
//responseJSON.veryBig = !_.every(queries, (query) => {
|
987
|
-
//numHits += query.hits.length;
|
988
|
-
//return (numHits <= 500);
|
989
|
-
//});
|
990
|
-
responseJSON.queries = queries.splice(0, 50);
|
991
|
-
this.setState(responseJSON);
|
992
|
-
|
993
|
-
// Render results for remaining queries.
|
994
|
-
var update = function () {
|
995
|
-
if (queries.length > 0) {
|
996
|
-
this.setState({
|
997
|
-
queries: this.state.queries.concat(queries.splice(0, 50))
|
998
|
-
});
|
999
|
-
setTimeout(update.bind(this), 500);
|
1000
|
-
}
|
1001
|
-
else {
|
1002
|
-
this.componentFinishedUpdating();
|
1003
|
-
}
|
1004
|
-
};
|
1005
|
-
setTimeout(update.bind(this), 500);
|
1006
|
-
},
|
1007
|
-
|
1008
|
-
|
1009
|
-
// View //
|
1010
|
-
render: function () {
|
1011
|
-
return this.isResultAvailable() ?
|
1012
|
-
this.resultsJSX() : this.loadingJSX();
|
1013
|
-
},
|
1014
|
-
|
1015
|
-
/**
|
1016
|
-
* Returns loading message
|
1017
|
-
*/
|
1018
|
-
loadingJSX: function () {
|
1019
|
-
return (
|
1020
|
-
<div
|
1021
|
-
className="row">
|
1022
|
-
<div
|
1023
|
-
className="col-md-6 col-md-offset-3 text-center">
|
1024
|
-
<h1>
|
1025
|
-
<i
|
1026
|
-
className="fa fa-cog fa-spin"></i>
|
1027
|
-
BLAST-ing
|
1028
|
-
</h1>
|
1029
|
-
<p>
|
1030
|
-
<br/>
|
1031
|
-
This can take some time depending on the size of your query and
|
1032
|
-
database(s). The page will update automatically when BLAST is
|
1033
|
-
done.
|
1034
|
-
<br/>
|
1035
|
-
<br/>
|
1036
|
-
You can bookmark the page and come back to it later or share
|
1037
|
-
the link with someone.
|
1038
|
-
</p>
|
1039
|
-
</div>
|
1040
|
-
</div>
|
1041
|
-
);
|
1042
|
-
},
|
1043
|
-
|
1044
|
-
/**
|
1045
|
-
* Return results JSX.
|
1046
|
-
*/
|
1047
|
-
resultsJSX: function () {
|
1048
|
-
return (
|
1049
|
-
<div className="row">
|
1050
|
-
{ this.shouldShowSidebar() &&
|
1051
|
-
(
|
1052
|
-
<div
|
1053
|
-
className="col-md-3 hidden-sm hidden-xs">
|
1054
|
-
<SideBar data={this.state} shouldShowIndex={this.shouldShowIndex()}/>
|
1055
|
-
</div>
|
1056
|
-
)
|
1057
|
-
}
|
1058
|
-
<div className={this.shouldShowSidebar() ?
|
1059
|
-
'col-md-9' : 'col-md-12'}>
|
1060
|
-
{ this.overviewJSX() }
|
1061
|
-
{ this.isHitsAvailable()
|
1062
|
-
? <Circos queries={this.state.queries}
|
1063
|
-
program={this.state.program} collapsed="true"/>
|
1064
|
-
: <span></span> }
|
1065
|
-
{
|
1066
|
-
_.map(this.state.queries, _.bind(function (query) {
|
1067
|
-
return (
|
1068
|
-
<Query key={"Query_"+query.id} query={query} data={this.state}
|
1069
|
-
selectHit={this.selectHit}/>
|
1070
|
-
);
|
1071
|
-
}, this))
|
1072
|
-
}
|
1073
|
-
</div>
|
1074
|
-
</div>
|
1075
|
-
);
|
1076
|
-
},
|
1077
|
-
|
1078
|
-
/**
|
1079
|
-
* Renders report overview.
|
1080
|
-
*/
|
1081
|
-
overviewJSX: function () {
|
1082
|
-
return (
|
1083
|
-
<div
|
1084
|
-
className="overview">
|
1085
|
-
<pre
|
1086
|
-
className="pre-reset">
|
1087
|
-
{this.state.program_version}
|
1088
|
-
<br/>
|
1089
|
-
<br/>
|
1090
|
-
{
|
1091
|
-
_.map(this.state.querydb, function (db) {
|
1092
|
-
return db.title;
|
1093
|
-
}).join(", ")
|
1094
|
-
}
|
1095
|
-
<br/>
|
1096
|
-
Total: {this.state.stats.nsequences} sequences,
|
1097
|
-
{this.state.stats.ncharacters} characters
|
1098
|
-
<br/>
|
1099
|
-
<br/>
|
1100
|
-
{
|
1101
|
-
_.map(this.state.params, function (val, key) {
|
1102
|
-
return key + " " + val;
|
1103
|
-
}).join(", ")
|
1104
|
-
}
|
1105
|
-
</pre>
|
1106
|
-
</div>
|
1107
|
-
);
|
1108
|
-
},
|
1109
|
-
|
1110
|
-
|
1111
|
-
// Controller //
|
1112
|
-
|
1113
|
-
/**
|
1114
|
-
* Returns true if results have been fetched.
|
1115
|
-
*
|
1116
|
-
* A holding message is shown till results are fetched.
|
1117
|
-
*/
|
1118
|
-
isResultAvailable: function () {
|
1119
|
-
return this.state.queries.length >= 1;
|
1120
|
-
},
|
1121
|
-
|
1122
|
-
isHitsAvailable: function () {
|
1123
|
-
var cnt = 0;
|
1124
|
-
_.each(this.state.queries, function (query) {
|
1125
|
-
if(query.hits.length == 0) cnt++;
|
1126
|
-
});
|
1127
|
-
return !(cnt == this.state.queries.length);
|
1128
|
-
},
|
1129
|
-
|
1130
|
-
/**
|
1131
|
-
* Returns true if sidebar should be shown.
|
1132
|
-
*
|
1133
|
-
* Sidebar is not shown if there is only one query and there are no hits
|
1134
|
-
* corresponding to the query.
|
1135
|
-
*/
|
1136
|
-
shouldShowSidebar: function () {
|
1137
|
-
return !(this.state.queries.length == 1 &&
|
1138
|
-
this.state.queries[0].hits.length == 0);
|
1139
|
-
},
|
1140
|
-
|
1141
|
-
/**
|
1142
|
-
* Returns true if index should be shown in the sidebar.
|
1143
|
-
*
|
1144
|
-
* Index is not shown in the sidebar if there are more than eight queries
|
1145
|
-
* in total.
|
1146
|
-
*/
|
1147
|
-
shouldShowIndex: function () {
|
1148
|
-
return this.state.queries.length <= 8;
|
1149
|
-
},
|
1150
|
-
|
1151
|
-
/**
|
1152
|
-
* Called after first call to render. The results may not be available at
|
1153
|
-
* this stage and thus results DOM cannot be scripted here, unless using
|
1154
|
-
* delegated events bound to the window, document, or body.
|
1155
|
-
*/
|
1156
|
-
componentDidMount: function () {
|
1157
|
-
// This sets up an event handler which enables users to select text
|
1158
|
-
// from hit header without collapsing the hit.
|
1159
|
-
this.preventCollapseOnSelection();
|
1160
|
-
},
|
1161
|
-
|
1162
|
-
/**
|
1163
|
-
* Called after each state change. Only a part of results DOM may be
|
1164
|
-
* available after a state change.
|
1165
|
-
*/
|
1166
|
-
componentDidUpdate: function () {
|
1167
|
-
// We track the number of updates to the component.
|
1168
|
-
this.updateCycle += 1;
|
1169
|
-
|
1170
|
-
// Lock sidebar in its position on first update of
|
1171
|
-
// results DOM.
|
1172
|
-
if (this.updateCycle === 1 ) this.affixSidebar();
|
1173
|
-
},
|
1174
|
-
|
1175
|
-
/**
|
1176
|
-
* Prevents folding of hits during text-selection, etc.
|
1177
|
-
*/
|
1178
|
-
|
1179
|
-
/**
|
1180
|
-
* Called after all results have been rendered.
|
1181
|
-
*/
|
1182
|
-
componentFinishedUpdating: function () {
|
1183
|
-
this.shouldShowIndex() && this.setupScrollSpy();
|
1184
|
-
},
|
1185
|
-
|
1186
|
-
/**
|
1187
|
-
* Prevents folding of hits during text-selection.
|
1188
|
-
*/
|
1189
|
-
preventCollapseOnSelection: function () {
|
1190
|
-
$('body').on('mousedown', ".hit > .section-header > h4", function (event) {
|
1191
|
-
var $this = $(this);
|
1192
|
-
$this.on('mouseup mousemove', function handler(event) {
|
1193
|
-
if (event.type === 'mouseup') {
|
1194
|
-
// user wants to toggle
|
1195
|
-
$this.attr('data-toggle', 'collapse');
|
1196
|
-
$this.find('.fa-chevron-down').toggleClass('fa-rotate-270');
|
1197
|
-
} else {
|
1198
|
-
// user wants to select
|
1199
|
-
$this.attr('data-toggle', '');
|
1200
|
-
}
|
1201
|
-
$this.off('mouseup mousemove', handler);
|
1202
|
-
});
|
1203
|
-
});
|
1204
|
-
},
|
1205
|
-
|
1206
|
-
/**
|
1207
|
-
* Affixes the sidebar.
|
1208
|
-
*/
|
1209
|
-
affixSidebar: function () {
|
1210
|
-
var $sidebar = $('.sidebar');
|
1211
|
-
$sidebar.affix({
|
1212
|
-
offset: {
|
1213
|
-
top: $sidebar.offset().top
|
1214
|
-
}
|
1215
|
-
});
|
1216
|
-
},
|
1217
|
-
|
1218
|
-
/**
|
1219
|
-
* For the query in viewport, highlights corresponding entry in the index.
|
1220
|
-
*/
|
1221
|
-
setupScrollSpy: function () {
|
1222
|
-
$('body').scrollspy({target: '.sidebar'});
|
1223
|
-
},
|
1224
|
-
|
1225
|
-
/**
|
1226
|
-
* Event-handler when hit is selected
|
1227
|
-
* Adds glow to hit component.
|
1228
|
-
* Updates number of Fasta that can be downloaded
|
1229
|
-
*/
|
1230
|
-
selectHit: function (id) {
|
1231
|
-
|
1232
|
-
var checkbox = $("#" + id);
|
1233
|
-
var num_checked = $('.hit-links :checkbox:checked').length;
|
1234
|
-
|
1235
|
-
if (!checkbox || !checkbox.val()) {
|
1236
|
-
return;
|
1237
|
-
}
|
1238
|
-
|
1239
|
-
var $hit = $(checkbox.data('target'));
|
1240
|
-
|
1241
|
-
// Highlight selected hit and sync checkboxes if sequence viewer is open.
|
1242
|
-
if (checkbox.is(":checked")) {
|
1243
|
-
$hit
|
1244
|
-
.addClass('glow')
|
1245
|
-
.find(":checkbox").not(checkbox).check();
|
1246
|
-
var $a = $('.download-fasta-of-selected');
|
1247
|
-
var $b = $('.download-alignment-of-selected');
|
1248
|
-
$b.enable()
|
1249
|
-
var $n = $a.find('span');
|
1250
|
-
$a
|
1251
|
-
.enable()
|
1252
|
-
}
|
1253
|
-
|
1254
|
-
else {
|
1255
|
-
$hit
|
1256
|
-
.removeClass('glow')
|
1257
|
-
.find(":checkbox").not(checkbox).uncheck();
|
1258
|
-
}
|
1259
|
-
|
1260
|
-
if (num_checked >= 1)
|
1261
|
-
{
|
1262
|
-
var $a = $('.download-fasta-of-selected');
|
1263
|
-
var $b = $('.download-alignment-of-selected');
|
1264
|
-
$a.find('.text-bold').html(num_checked);
|
1265
|
-
$b.find('.text-bold').html(num_checked);
|
1266
|
-
}
|
1267
|
-
|
1268
|
-
if (num_checked == 0) {
|
1269
|
-
var $a = $('.download-fasta-of-selected');
|
1270
|
-
var $b = $('.download-alignment-of-selected');
|
1271
|
-
$a.addClass('disabled').find('.text-bold').html('');
|
1272
|
-
$b.addClass('disabled').find('.text-bold').html('');
|
1273
|
-
}
|
1274
|
-
},
|
1275
|
-
});
|
1276
|
-
|
1277
|
-
var Page = React.createClass({
|
1278
|
-
render: function () {
|
1279
|
-
return (
|
1280
|
-
<div>
|
1281
|
-
<div className="container">
|
1282
|
-
<Report ref="report"/>
|
1283
|
-
</div>
|
1284
|
-
|
1285
|
-
<div id='circos-demo' className='modal'></div>
|
1286
|
-
|
1287
|
-
<canvas id="png-exporter" hidden></canvas>
|
1288
|
-
</div>
|
1289
|
-
);
|
1290
|
-
}
|
1291
|
-
});
|
1292
|
-
|
1293
1111
|
React.render(<Page/>, document.getElementById('view'));
|