sequenceserver 2.0.0.beta4 → 2.0.0.rc1

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