sequenceserver 2.0.0.beta4 → 2.0.0.rc5

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.
Files changed (119) hide show
  1. checksums.yaml +5 -5
  2. data/.dockerignore +1 -0
  3. data/.travis.yml +7 -4
  4. data/AppImage/sequenceserver.sh +5 -0
  5. data/Dockerfile +14 -12
  6. data/bin/sequenceserver +37 -28
  7. data/lib/sequenceserver.rb +35 -7
  8. data/lib/sequenceserver/blast/job.rb +18 -25
  9. data/lib/sequenceserver/blast/report.rb +68 -34
  10. data/lib/sequenceserver/config.rb +1 -1
  11. data/lib/sequenceserver/database.rb +0 -129
  12. data/lib/sequenceserver/makeblastdb.rb +243 -0
  13. data/lib/sequenceserver/routes.rb +28 -2
  14. data/lib/sequenceserver/version.rb +1 -1
  15. data/public/SequenceServer_logo.png +0 -0
  16. data/public/css/grapher.css +8 -15
  17. data/public/css/sequenceserver.css +119 -55
  18. data/public/css/sequenceserver.min.css +3 -3
  19. data/public/js/circos.js +1 -1
  20. data/public/js/download_fasta.js +17 -0
  21. data/public/js/grapher.js +7 -9
  22. data/public/js/hit.js +217 -0
  23. data/public/js/hits_overview.js +12 -13
  24. data/public/js/hsp.js +104 -84
  25. data/public/js/{sequenceserver.js → jquery_world.js} +1 -18
  26. data/public/js/kablammo.js +337 -334
  27. data/public/js/length_distribution.js +1 -1
  28. data/public/js/query.js +147 -0
  29. data/public/js/report.js +216 -836
  30. data/public/js/search.js +194 -192
  31. data/public/js/sequence_modal.js +167 -0
  32. data/public/js/sidebar.js +210 -0
  33. data/public/js/utils.js +2 -19
  34. data/public/js/visualisation_helpers.js +2 -2
  35. data/public/sequenceserver-report.min.js +19 -19
  36. data/public/sequenceserver-search.min.js +11 -11
  37. data/public/vendor/github/twbs/bootstrap@3.3.5/js/bootstrap.js +2 -2
  38. data/spec/blast_versions/blast_2.2.30/import_spec_capybara_local_2.2.30.rb +15 -15
  39. data/spec/blast_versions/blast_2.2.31/import_spec_capybara_local_2.2.31.rb +15 -15
  40. data/spec/blast_versions/blast_2.3.0/import_spec_capybara_local_2.3.0.rb +15 -15
  41. data/spec/blast_versions/blast_2.4.0/import_spec_capybara_local_2.4.0.rb +15 -15
  42. data/spec/blast_versions/blast_2.5.0/import_spec_capybara_local_2.5.0.rb +15 -15
  43. data/spec/blast_versions/blast_2.6.0/import_spec_capybara_local_2.6.0.rb +15 -15
  44. data/spec/blast_versions/blast_2.7.1/import_spec_capybara_local_2.7.1.rb +15 -15
  45. data/spec/blast_versions/blast_2.8.1/import_spec_capybara_local_2.8.1.rb +15 -15
  46. data/spec/blast_versions/blast_2.9.0/import_spec_capybara_local_2.9.0.rb +15 -15
  47. data/spec/blast_versions/diamond_0.9.24/import_spec_capybara_local_0.9.24.rb +6 -6
  48. data/spec/capybara_spec.rb +14 -3
  49. data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.ndb +0 -0
  50. data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nhr +0 -0
  51. data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nin +0 -0
  52. data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nos +0 -0
  53. data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.not +0 -0
  54. data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.ntf +0 -0
  55. data/spec/database/sample/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nto +0 -0
  56. data/spec/database/sample/proteins/Solenopsis_invicta/Sinvicta2-2-3.prot.subset.fasta.pdb +0 -0
  57. data/spec/database/sample/proteins/Solenopsis_invicta/Sinvicta2-2-3.prot.subset.fasta.phr +0 -0
  58. data/spec/database/sample/proteins/Solenopsis_invicta/Sinvicta2-2-3.prot.subset.fasta.pin +0 -0
  59. data/spec/database/sample/proteins/Solenopsis_invicta/Sinvicta2-2-3.prot.subset.fasta.pos +0 -0
  60. data/spec/database/sample/proteins/Solenopsis_invicta/Sinvicta2-2-3.prot.subset.fasta.pot +0 -0
  61. data/spec/database/sample/proteins/Solenopsis_invicta/Sinvicta2-2-3.prot.subset.fasta.ptf +0 -0
  62. data/spec/database/sample/proteins/Solenopsis_invicta/Sinvicta2-2-3.prot.subset.fasta.pto +0 -0
  63. data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.pdb +0 -0
  64. data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.phr +0 -0
  65. data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.pin +0 -0
  66. data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.pos +0 -0
  67. data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.pot +0 -0
  68. data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.ptf +0 -0
  69. data/spec/database/sample/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.pto +0 -0
  70. data/spec/database/sample/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta.ndb +0 -0
  71. data/spec/database/sample/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta.nhr +0 -0
  72. data/spec/database/sample/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta.nin +0 -0
  73. data/spec/database/sample/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta.nos +0 -0
  74. data/spec/database/sample/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta.not +0 -0
  75. data/spec/database/sample/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta.nsq +0 -0
  76. data/spec/database/sample/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta.ntf +0 -0
  77. data/spec/database/sample/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta.nto +0 -0
  78. data/spec/database/v4/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nhd +8 -0
  79. data/spec/database/v4/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nhi +0 -0
  80. data/spec/database/v4/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nhr +0 -0
  81. data/spec/database/v4/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nin +0 -0
  82. data/spec/database/v4/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nog +0 -0
  83. data/spec/database/{sample → v4}/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nsd +0 -0
  84. data/spec/database/{sample → v4}/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nsi +0 -0
  85. data/spec/database/v4/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.fasta.nsq +0 -0
  86. data/spec/database/v4/genome/Solenopsis_invicta/Solenopsis_invicta_gnG_subset.txt +8 -0
  87. data/spec/database/v4/links.rb +23 -0
  88. data/spec/database/v4/proteins/Solenopsis_invicta/Sinvicta2-2-3.prot.subset.fasta +6449 -0
  89. data/spec/database/v4/proteins/Solenopsis_invicta/Sinvicta2-2-3.prot.subset.fasta.phd +1189 -0
  90. data/spec/database/v4/proteins/Solenopsis_invicta/Sinvicta2-2-3.prot.subset.fasta.phi +0 -0
  91. data/spec/database/v4/proteins/Solenopsis_invicta/Sinvicta2-2-3.prot.subset.fasta.phr +0 -0
  92. data/spec/database/v4/proteins/Solenopsis_invicta/Sinvicta2-2-3.prot.subset.fasta.pin +0 -0
  93. data/spec/database/v4/proteins/Solenopsis_invicta/Sinvicta2-2-3.prot.subset.fasta.pog +0 -0
  94. data/spec/database/{sample → v4}/proteins/Solenopsis_invicta/Sinvicta2-2-3.prot.subset.fasta.psd +0 -0
  95. data/spec/database/{sample → v4}/proteins/Solenopsis_invicta/Sinvicta2-2-3.prot.subset.fasta.psi +0 -0
  96. data/spec/database/v4/proteins/Solenopsis_invicta/Sinvicta2-2-3.prot.subset.fasta.psq +0 -0
  97. data/spec/database/v4/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.phd +9140 -0
  98. data/spec/database/v4/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.phi +0 -0
  99. data/spec/database/v4/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.phr +0 -0
  100. data/spec/database/v4/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.pin +0 -0
  101. data/spec/database/v4/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.pog +0 -0
  102. data/spec/database/{sample → v4}/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.psd +0 -0
  103. data/spec/database/{sample → v4}/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.psi +0 -0
  104. data/spec/database/v4/proteins/uniprot/2018-04-Swiss-Prot_insecta.fasta.psq +0 -0
  105. data/spec/database/v4/proteins/uniprot/URL +1 -0
  106. data/spec/database/v4/si_uniprot_idmap.yml +14180 -0
  107. data/spec/database/v4/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta +5486 -0
  108. data/spec/database/v4/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta.nhd +473 -0
  109. data/spec/database/v4/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta.nhi +0 -0
  110. data/spec/database/v4/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta.nhr +0 -0
  111. data/spec/database/v4/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta.nin +0 -0
  112. data/spec/database/v4/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta.nog +0 -0
  113. data/spec/database/{sample → v4}/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta.nsd +0 -0
  114. data/spec/database/{sample → v4}/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta.nsi +0 -0
  115. data/spec/database/v4/transcripts/Solenopsis_invicta/Sinvicta2-2-3.cdna.subset.fasta.nsq +0 -0
  116. data/spec/database_spec.rb +0 -76
  117. data/spec/makeblastdb_spec.rb +121 -0
  118. data/views/layout.erb +5 -1
  119. metadata +75 -15
@@ -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.split('/').pop();
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,23 @@ 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: '',
82
+ seqserv_version: '',
67
83
  program: '',
68
84
  program_version: '',
69
85
  submitted_at: '',
70
- num_queries: 0,
71
86
  queries: [],
87
+ results: [],
72
88
  querydb: [],
73
89
  params: [],
74
90
  stats: []
@@ -97,7 +113,7 @@ var Report = React.createClass({
97
113
  setTimeout(poll, interval);
98
114
  break;
99
115
  case 200:
100
- component.updateState(jqXHR.responseJSON);
116
+ component.setStateFromJSON(jqXHR.responseJSON);
101
117
  break;
102
118
  case 404:
103
119
  case 400:
@@ -112,38 +128,139 @@ var Report = React.createClass({
112
128
  },
113
129
 
114
130
  /**
115
- * Incrementally update state (50 queries at a time) so that the rendering
116
- * process is not overwhelemed when there are too many queries.
131
+ * Calls setState after any required modification to responseJSON.
117
132
  */
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);
133
+ setStateFromJSON: function(responseJSON) {
134
+ this.lastTimeStamp = Date.now();
123
135
  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
136
  },
139
137
 
140
-
141
- // View //
138
+ // Life-cycle methods //
142
139
  render: function () {
143
140
  return this.isResultAvailable() ?
144
141
  this.resultsJSX() : this.loadingJSX();
145
142
  },
146
143
 
144
+ /**
145
+ * Called as soon as the page has loaded and the user sees the loading spinner.
146
+ * We use this opportunity to setup services that make use of delegated events
147
+ * bound to the window, document, or body.
148
+ */
149
+ componentDidMount: function () {
150
+ // This sets up an event handler which enables users to select text from
151
+ // hit header without collapsing the hit.
152
+ this.preventCollapseOnSelection();
153
+ this.toggleTable();
154
+ },
155
+
156
+ /**
157
+ * Called for the first time after as BLAST results have been retrieved from
158
+ * the server and added to this.state by fetchResults. Only summary overview
159
+ * and circos would have been rendered at this point. At this stage we kick
160
+ * start iteratively updating 10 HSPs (and as many hits and queries) every
161
+ * 25 milli-seconds.
162
+ */
163
+ componentDidUpdate: function () {
164
+ // Log to console how long the last update take?
165
+ console.log((Date.now() - this.lastTimeStamp)/1000);
166
+
167
+ // Lock sidebar in its position on the first update.
168
+ if (this.nextQuery == 0 && this.nextHit == 0 && this.nextHSP == 0) {
169
+ this.affixSidebar();
170
+ }
171
+
172
+ // Queue next update if we have not rendered all results yet.
173
+ if (this.nextQuery < this.state.queries.length) {
174
+ // setTimeout is used to clear call stack and space out
175
+ // the updates giving the browser a chance to respond
176
+ // to user interactions.
177
+ setTimeout(() => this.updateState(), 25);
178
+ }
179
+ else {
180
+ this.componentFinishedUpdating();
181
+ }
182
+ },
183
+
184
+ /**
185
+ * Push next slice of results to React for rendering.
186
+ */
187
+ updateState: function() {
188
+ var results = [];
189
+ var numHSPsProcessed = 0;
190
+ while (this.nextQuery < this.state.queries.length) {
191
+ var query = this.state.queries[this.nextQuery];
192
+ // We may see a query multiple times during rendering because only
193
+ // 3 hsps or are rendered in each cycle, but we want to create the
194
+ // corresponding Query component only the first time we see it.
195
+ if (this.nextHit == 0 && this.nextHSP == 0) {
196
+ results.push(<Query key={'Query_'+query.number} query={query}
197
+ program={this.state.program} querydb={this.state.querydb}
198
+ showQueryCrumbs={this.state.queries.length > 1}
199
+ imported_xml={this.state.imported_xml}
200
+ veryBig={this.state.veryBig} />);
201
+ }
202
+
203
+ while (this.nextHit < query.hits.length) {
204
+ var hit = query.hits[this.nextHit];
205
+ // We may see a hit multiple times during rendering because only
206
+ // 10 hsps are rendered in each cycle, but we want to create the
207
+ // corresponding Hit component only the first time we see it.
208
+ if (this.nextHSP == 0) {
209
+ results.push(<Hit key={'Query_'+query.number+'_Hit_'+hit.number} query={query}
210
+ hit={hit} algorithm={this.state.program} querydb={this.state.querydb}
211
+ selectHit={this.selectHit} imported_xml={this.state.imported_xml}
212
+ showQueryCrumbs={this.state.queries.length > 1}
213
+ showHitCrumbs={query.hits.length > 1}
214
+ veryBig={this.state.veryBig}
215
+ {... this.props} />
216
+ );
217
+ }
218
+
219
+ while (this.nextHSP < hit.hsps.length) {
220
+ // Get nextHSP and increment the counter.
221
+ var hsp = hit.hsps[this.nextHSP++];
222
+ results.push(
223
+ <HSP key={'Query_'+query.number+'_Hit_'+hit.number+'_HSP_'+hsp.number}
224
+ query={query} hit={hit} hsp={hsp} algorithm={this.state.program}
225
+ showHSPNumbers={hit.hsps.length > 1} {... this.props} />
226
+ );
227
+ numHSPsProcessed++;
228
+ if (numHSPsProcessed == this.maxHSPs) break;
229
+ }
230
+ // Are we here because we have iterated over all hsps of a hit,
231
+ // or because of the break clause in the inner loop?
232
+ if (this.nextHSP == hit.hsps.length) {
233
+ this.nextHit = this.nextHit + 1;
234
+ this.nextHSP = 0;
235
+ }
236
+ if (numHSPsProcessed == this.maxHSPs) break;
237
+ }
238
+
239
+ // Are we here because we have iterated over all hits of a query,
240
+ // or because of the break clause in the inner loop?
241
+ if (this.nextHit == query.hits.length) {
242
+ this.nextQuery = this.nextQuery + 1;
243
+ this.nextHit = 0;
244
+ }
245
+ if (numHSPsProcessed == this.maxHSPs) break;
246
+ }
247
+
248
+ // Push the components to react for rendering.
249
+ this.numUpdates++;
250
+ this.lastTimeStamp = Date.now();
251
+ this.setState({
252
+ results: this.state.results.concat(results),
253
+ veryBig: this.numUpdates >= 250
254
+ });
255
+ },
256
+
257
+ /**
258
+ * Called after all results have been rendered.
259
+ */
260
+ componentFinishedUpdating: function () {
261
+ this.shouldShowIndex() && this.setupScrollSpy();
262
+ },
263
+
147
264
  /**
148
265
  * Returns loading message
149
266
  */
@@ -179,25 +296,15 @@ var Report = React.createClass({
179
296
  <div className="row">
180
297
  { this.shouldShowSidebar() &&
181
298
  (
182
- <div
183
- className="col-md-3 hidden-sm hidden-xs">
184
- <SideBar data={this.state} shouldShowIndex={this.shouldShowIndex()}/>
299
+ <div className="col-md-3 hidden-sm hidden-xs">
300
+ <Sidebar data={this.state} shouldShowIndex={this.shouldShowIndex()}/>
185
301
  </div>
186
302
  )
187
303
  }
188
- <div className={this.shouldShowSidebar() ?
189
- 'col-md-9' : 'col-md-12'}>
304
+ <div className={this.shouldShowSidebar() ? 'col-md-9' : 'col-md-12'}>
190
305
  { this.overviewJSX() }
191
306
  { 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
- }
307
+ { this.state.results }
201
308
  </div>
202
309
  </div>
203
310
  );
@@ -209,23 +316,26 @@ var Report = React.createClass({
209
316
  overviewJSX: function () {
210
317
  return (
211
318
  <div className="overview">
212
- <p className="text-monospace">
213
- {this.state.program_version}{this.state.submitted_at
214
- && `, query submitted on ${this.state.submitted_at}`}
319
+ <p>
320
+ <strong>SequenceServer {this.state.seqserv_version}</strong> using <strong>{this.state.program_version}</strong>
321
+ {this.state.submitted_at && `, query submitted on ${this.state.submitted_at}`}
215
322
  </p>
216
- <p className="text-monospace">
217
- Databases: {
323
+ <p>
324
+ <strong> Databases: </strong>{
218
325
  this.state.querydb.map((db) => { return db.title; }).join(', ')
219
326
  } ({this.state.stats.nsequences} sequences,&nbsp;
220
327
  {this.state.stats.ncharacters} characters)
221
328
  </p>
222
- <p className="text-monospace">
223
- Parameters: {
329
+ <p>
330
+ <strong>Parameters: </strong> {
224
331
  _.map(this.state.params, function (val, key) {
225
332
  return key + ' ' + val;
226
333
  }).join(', ')
227
334
  }
228
335
  </p>
336
+ <p>
337
+ Please cite: <a href="https://doi.org/10.1093/molbev/msz185">https://doi.org/10.1093/molbev/msz185</a>
338
+ </p>
229
339
  </div>
230
340
  );
231
341
  },
@@ -258,6 +368,10 @@ var Report = React.createClass({
258
368
  return this.state.queries.some(query => query.hits.length > 0);
259
369
  },
260
370
 
371
+ /**
372
+ * Does the report have at least two hits? This is used to determine
373
+ * whether Circos should be enabled or not.
374
+ */
261
375
  atLeastTwoHits: function () {
262
376
  var hit_num = 0;
263
377
  return this.state.queries.some(query => {
@@ -283,38 +397,7 @@ var Report = React.createClass({
283
397
  */
284
398
  shouldShowIndex: function () {
285
399
  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();
400
+ return num_queries >= 2 && num_queries <= 12;
318
401
  },
319
402
 
320
403
  /**
@@ -326,12 +409,9 @@ var Report = React.createClass({
326
409
  $this.on('mouseup mousemove', function handler(event) {
327
410
  if (event.type === 'mouseup') {
328
411
  // 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');
412
+ var hitID = $this.parents('.hit').attr('id');
413
+ $(`div[data-parent-hit=${hitID}]`).toggle();
414
+ $this.find('i').toggleClass('fa-minus-square-o fa-plus-square-o');
335
415
  } else {
336
416
  // user wants to select
337
417
  $this.attr('data-toggle', '');
@@ -341,16 +421,32 @@ var Report = React.createClass({
341
421
  });
342
422
  },
343
423
 
424
+ /* Handling the fa icon when Hit Table is collapsed */
425
+ toggleTable: function () {
426
+ $('body').on('mousedown', '.resultn > .section-content > .table-hit-overview > .caption', function (event) {
427
+ var $this = $(this);
428
+ $this.on('mouseup mousemove', function handler(event) {
429
+ $this.find('i').toggleClass('fa-minus-square-o fa-plus-square-o');
430
+ $this.off('mouseup mousemove', handler);
431
+ });
432
+ });
433
+ },
434
+
435
+
436
+
344
437
  /**
345
438
  * Affixes the sidebar.
346
439
  */
347
440
  affixSidebar: function () {
348
441
  var $sidebar = $('.sidebar');
349
- $sidebar.affix({
350
- offset: {
351
- top: $sidebar.offset().top
352
- }
353
- });
442
+ var sidebarOffset = $sidebar.offset()
443
+ if (sidebarOffset) {
444
+ $sidebar.affix({
445
+ offset: {
446
+ top: sidebarOffset.top
447
+ }
448
+ });
449
+ }
354
450
  },
355
451
 
356
452
  /**
@@ -379,12 +475,14 @@ var Report = React.createClass({
379
475
  // Highlight selected hit and enable 'Download FASTA/Alignment of
380
476
  // selected' links.
381
477
  if (checkbox.is(':checked')) {
382
- $hit.find('.section-content').addClass('glow');
383
- $('.download-alignment-of-selected').enable();
478
+ $hit.addClass('glow');
479
+ $hit.next('.hsp').addClass('glow');
384
480
  $('.download-fasta-of-selected').enable();
481
+ $('.download-alignment-of-selected').enable();
385
482
  }
386
483
  else {
387
- $hit.find('.section-content').removeClass('glow');
484
+ $hit.removeClass('glow');
485
+ $hit.next('.hsp').removeClass('glow');
388
486
  }
389
487
 
390
488
  if (num_checked >= 1)
@@ -404,722 +502,4 @@ var Report = React.createClass({
404
502
  },
405
503
  });
406
504
 
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
505
  React.render(<Page/>, document.getElementById('view'));