sequenceserver 1.1.0.beta8 → 1.1.0.beta10

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,7 +11,7 @@ class Graph {
11
11
  }
12
12
 
13
13
  static className() {
14
- return 'circos-distribution collapse';
14
+ return 'circos';
15
15
  }
16
16
 
17
17
  static collapseId(props) {
@@ -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 className="grapher" ref="grapher">
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.setState({ collapsed: true });
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.setState({ collapsed: false });
123
+ if (component) {
124
+ component.setState({ collapsed: false });
125
+ }
121
126
  });
@@ -6,7 +6,7 @@ import * as Helpers from './visualisation_helpers';
6
6
  class Graph {
7
7
 
8
8
  static name() {
9
- return 'Matching sequences';
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: 2,
216
- barPadding: 1,
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 + options.barPadding) +
234
+ var height = hits.length * (options.barHeight) +
237
235
  2 * options.legend + 5 * options.margin;
238
236
  // var height = $graphDiv.height();
239
237
 
@@ -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) + "."}&nbsp;{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));
@@ -19,7 +19,7 @@ import * as Helpers from './visualisation_helpers';
19
19
 
20
20
  class Graph {
21
21
  static name() {
22
- return 'Matching region(s)';
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 = 17.5;
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 + 5; // + 5 for font-height 10px
215
+ return a + middle + 2; // for font-height 10px
216
216
  })
217
217
  .text(function(hsp) {
218
218
  return Helpers.toLetters(hsp.number)
@@ -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
- * Pretty formats number
34
+ * Base component of report page. This component is later rendered into page's
35
+ * '#view' element.
33
36
  */
34
- var Utils = {
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
- * Render URL for sequence-viewer.
78
+ * Fetch results.
38
79
  */
39
- a: function (link) {
40
- if (link.title && link.url)
41
- {
42
- return (
43
- <a href={link.url} className={link.class} target='_blank'>
44
- {link.icon && <i className={"fa " + link.icon}></i>}
45
- {" " + link.title + " "}
46
- </a>
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
- // Formats an array of two elements as "first (last)".
57
- format_2_tuple: function (tuple) {
58
- return (tuple[0] + " (" + tuple[tuple.length - 1] + ")");
147
+ // View //
148
+ render: function () {
149
+ return this.isResultAvailable() ?
150
+ this.resultsJSX() : this.loadingJSX();
59
151
  },
60
152
 
61
153
  /**
62
- * Returns fraction as percentage
154
+ * Returns loading message
63
155
  */
64
- inPercentage: function (num , den) {
65
- return (num * 100.0 / den).toFixed(2);
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>&nbsp; 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
- * Returns fractional representation as String.
181
+ * Return results JSX.
70
182
  */
71
- inFraction: function (num , den) {
72
- return num + "/" + den;
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
- * Returns given Float as String formatted to two decimal places.
215
+ * Renders report overview.
77
216
  */
78
- inTwoDecimal: function (num) {
79
- return num.toFixed(2)
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,&nbsp;
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 zero if num is zero. Returns two decimal representation of num
84
- * if num is between [1..10). Returns num in scientific notation otherwise.
243
+ * Returns true if results have been fetched.
244
+ *
245
+ * A holding message is shown till results are fetched.
85
246
  */
86
- inExponential: function (num) {
87
- // Nothing to do if num is 0.
88
- if (num === 0) {
89
- return 0
90
- }
247
+ isResultAvailable: function () {
248
+ return this.state.queries.length >= 1;
249
+ },
91
250
 
92
- // Round to two decimal places if in the rane [1..10).
93
- if (num >= 1 && num < 10)
94
- {
95
- return this.inTwoDecimal(num)
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
- // Return numbers in the range [0..1) and [10..Inf] in
99
- // scientific format.
100
- var exp = num.toExponential(2);
101
- var parts = exp.split("e");
102
- var base = parts[0];
103
- var power = parts[1];
104
- return <span>{base} &times; 10<sup>{power}</sup></span>;
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
- * Component for sequence-viewer links.
110
- */
111
- var SequenceViewer = (function () {
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
- var Viewer = React.createClass({
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
- * The CSS class name that will be assigned to the widget container. ID
117
- * assigned to the widget container is derived from the same.
118
- */
119
- widgetClass: 'biojs-vis-sequence',
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
- // Lifecycle methods. //
299
+ // Lock sidebar in its position on first update of
300
+ // results DOM.
301
+ if (this.updateCycle === 1 ) this.affixSidebar();
302
+ },
122
303
 
123
- render: function () {
124
- this.widgetID =
125
- this.widgetClass + '-' + (new Date().getUTCMilliseconds());
304
+ /**
305
+ * Prevents folding of hits during text-selection, etc.
306
+ */
126
307
 
127
- return (
128
- <div
129
- className="fastan">
130
- <div
131
- className="section-header">
132
- <h4>
133
- {this.props.sequence.id}
134
- <small>
135
- &nbsp; {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
- componentDidMount: function () {
150
- // attach BioJS sequence viewer
151
- var widget = new Sequence({
152
- sequence: this.props.sequence.value,
153
- target: this.widgetID,
154
- format: 'PRIDE',
155
- columns: {
156
- size: 40,
157
- spacedEach: 5
158
- },
159
- formatOptions: {
160
- title: false,
161
- footer: false
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
- widget.hideFormatSelector();
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
- resultsJSX: function () {
187
- return (
188
- <div className="modal-body">
189
- {
190
- _.map(this.state.error_msgs, _.bind(function (error_msg) {
191
- return (
192
- <div
193
- className="fastan">
194
- <div
195
- className="section-header">
196
- <h4>
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
- loadingJSX: function () {
221
- return (
222
- <div className="modal-body text-center">
223
- <i className="fa fa-spinner fa-3x fa-spin"></i>
224
- </div>
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
- // Lifecycle methods. //
361
+ var checkbox = $("#" + id);
362
+ var num_checked = $('.hit-links :checkbox:checked').length;
230
363
 
231
- getInitialState: function () {
232
- return {
233
- error_msgs: [],
234
- sequences: [],
235
- requestCompleted: false
236
- };
237
- },
364
+ if (!checkbox || !checkbox.val()) {
365
+ return;
366
+ }
238
367
 
239
- render: function () {
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
- { this.state.requestCompleted &&
254
- this.resultsJSX() || this.loadingJSX() }
255
- </div>
256
- </div>
257
- </div>
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
- componentDidMount: function () {
262
- // Display modal with a spinner.
263
- this.show();
383
+ else {
384
+ $hit
385
+ .removeClass('glow')
386
+ .find(":checkbox").not(checkbox).uncheck();
387
+ }
264
388
 
265
- // Fetch sequence and update state.
266
- $.getJSON(this.props.url)
267
- .done(_.bind(function (response) {
268
- this.setState({
269
- sequences: response.sequences,
270
- error_msgs: response.error_msgs,
271
- requestCompleted: true
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
- this.modal().on('hidden.bs.modal', this.props.onHide);
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
- * Component for each hit.
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 Hit = React.createClass({
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 length of the hit sequence.
417
+ * Returns the id of query.
300
418
  */
301
- length: function () {
302
- return this.props.hit.length;
419
+ domID: function () {
420
+ return "Query_" + this.props.query.number;
303
421
  },
304
422
 
305
- // Internal helpers. //
306
-
307
423
  /**
308
- * Returns id that will be used for the DOM node corresponding to the hit.
424
+ * Returns number of hits.
309
425
  */
310
- domID: function () {
311
- return "Query_" + this.props.query.number + "_hit_" + this.props.hit.number;
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
- showSequenceViewer: function (event) {
319
- this.setState({ showSequenceViewer: true });
320
- event && event.preventDefault();
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
+ &nbsp;
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
- <table
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
- return (
588
- <table
589
- className="table table-hover table-condensed tabular-view">
590
- <thead>
591
- <th className="text-left">#</th>
592
- <th>Similar sequences</th>
593
- {hasName && <th className="text-left">Species</th>}
594
- <th className="text-right">Query coverage (%)</th>
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
- * Renders report for each query sequence.
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 Query = React.createClass({
741
+ var SequenceViewer = (function () {
635
742
 
636
- // Kind of public API //
743
+ var Viewer = React.createClass({
637
744
 
638
- /**
639
- * Returns the id of query.
640
- */
641
- domID: function () {
642
- return "Query_" + this.props.query.number;
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
- // Life cycle methods //
753
+ render: function () {
754
+ this.widgetID =
755
+ this.widgetClass + '-' + (new Date().getUTCMilliseconds());
653
756
 
654
- render: function () {
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="section-header">
662
- <h3>
663
- Query= {this.props.query.id}
664
- &nbsp;
665
- <small>
666
- {this.props.query.title}
667
- </small>
668
- </h3>
669
- <span
670
- className="label label-reset pos-label"
671
- title={"Query" + this.props.query.number + "."}
672
- data-toggle="tooltip">
673
- {this.props.query.number + "/" + this.props.data.queries.length}
674
- </span>
759
+ className="fastan">
760
+ <div
761
+ className="section-header">
762
+ <h4>
763
+ {this.props.sequence.id}
764
+ <small>
765
+ &nbsp; {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
- {this.numhits() &&
677
- (
678
- <div className="section-content">
679
- <HitsOverview key={"GO_"+this.props.query.number} query={this.props.query} program={this.props.data.program} collapsed={this.props.data.veryBig}/>
680
- <LengthDistribution key={"LD_"+this.props.query.id} query={this.props.query} algorithm={this.props.data.program} collapsed="true"/>
681
- <HitsTable key={"HT_"+this.props.query.number} query={this.props.query}/>
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
- id="hits">
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
- <div
701
- className="section-content">
702
- <p>
703
- Query length: {this.props.query.length}
704
- </p>
705
- <br/>
706
- <br/>
707
- <p>
708
- <strong> ****** No hits found ****** </strong>
709
- </p>
710
- </div>
711
- )
712
- }
713
- </div>
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>&nbsp;
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'));