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.

@@ -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'));