sequenceserver 2.0.0.beta4 → 2.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sequenceserver might be problematic. Click here for more details.

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