public/js/report.js in sequenceserver-1.1.0.beta6 vs public/js/report.js in sequenceserver-1.1.0.beta7
- old
+ new
@@ -1,20 +1,18 @@
-import SequenceServer from './sequenceserver';
-import showErrorModal from './errormodal';
-
-import _ from 'underscore';
+import './sequenceserver' // for custom $.tooltip function
import React from 'react';
-import d3 from 'd3';
+import _ from 'underscore';
-import * as Helpers from './visualisation_helpers';
-import GraphicalOverview from './alignmentsoverview';
-//import Kablammo from './kablammo';
-import './sequence';
-import AlignmentExporter from './alignment_exporter';
-import LengthDistribution from './lengthdistribution';
import Circos from './circos';
+import HitsOverview from './hits_overview';
+import LengthDistribution from './length_distribution'; // length distribution of hits
+import HSPOverview from './kablammo';
+import AlignmentExporter from './alignment_exporter'; // to download textual alignment
+import './sequence';
+import * as Helpers from './visualisation_helpers'; // for toLetters
+import showErrorModal from './error_modal';
/**
* Dynamically create form and submit.
*/
var downloadFASTA = function (sequence_ids, database_ids) {
@@ -512,13 +510,13 @@
_.map(this.props.hit.links, _.bind(function (link) {
return [<span> | </span>, this.a(link)];
}, this))
}
</div>
- {/*
- <Kablammo key={"Kablammo"+this.props.query.id} query={this.props.query} hit={this.props.hit} algorithm={this.props.algorithm}/>
- */}
+ <HSPOverview key={"kablammo"+this.props.query.id}
+ query={this.props.query} hit={this.props.hit}
+ algorithm={this.props.algorithm}/>
<table
className="table hsps">
<tbody>
{
_.map (this.props.hit.hsps, _.bind( function (hsp) {
@@ -676,11 +674,11 @@
</span>
</div>
{this.numhits() &&
(
<div className="section-content">
- <GraphicalOverview key={"GO_"+this.props.query.number} query={this.props.query} program={this.props.data.program} collapsed={this.props.data.veryBig}/>
+ <HitsOverview key={"GO_"+this.props.query.number} query={this.props.query} program={this.props.data.program} collapsed={this.props.data.veryBig}/>
<LengthDistribution key={"LD_"+this.props.query.id} query={this.props.query} algorithm={this.props.data.program} collapsed="true"/>
<HitsTable key={"HT_"+this.props.query.number} query={this.props.query}/>
<div
id="hits">
{
@@ -919,69 +917,16 @@
*
* Composed of Query and Sidebar components.
*/
var Report = React.createClass({
- // Kind of public API //
+ // Model //
- /**
- * Event-handler when hit is selected
- * Adds glow to hit component.
- * Updates number of Fasta that can be downloaded
- */
- selectHit: function (id) {
-
- var checkbox = $("#" + id);
- var num_checked = $('.hit-links :checkbox:checked').length;
-
- if (!checkbox || !checkbox.val()) {
- return;
- }
-
- var $hit = $(checkbox.data('target'));
-
- // Highlight selected hit and sync checkboxes if sequence viewer is open.
- if (checkbox.is(":checked")) {
- $hit
- .addClass('glow')
- .find(":checkbox").not(checkbox).check();
- var $a = $('.download-fasta-of-selected');
- var $b = $('.download-alignment-of-selected');
- $b.enable()
- var $n = $a.find('span');
- $a
- .enable()
- }
-
- else {
- $hit
- .removeClass('glow')
- .find(":checkbox").not(checkbox).uncheck();
- }
-
- if (num_checked >= 1)
- {
- var $a = $('.download-fasta-of-selected');
- var $b = $('.download-alignment-of-selected');
- $a.find('.text-bold').html(num_checked);
- $b.find('.text-bold').html(num_checked);
- }
-
- if (num_checked == 0) {
- var $a = $('.download-fasta-of-selected');
- var $b = $('.download-alignment-of-selected');
- $a.addClass('disabled').find('.text-bold').html('');
- $b.addClass('disabled').find('.text-bold').html('');
- }
- },
-
-
- // Internal helpers. //
-
- // Life-cycle methods. //
-
getInitialState: function () {
+ this.fetchResults();
+ this.updateCycle = 0;
+
return {
search_id: '',
program: '',
program_version: '',
queries: [],
@@ -989,23 +934,83 @@
params: [],
stats: []
};
},
- render: function () {
- return (this.isResultAvailable() ? this.resultsJSX() : this.loadingJSX());
+ /**
+ * Fetch results.
+ */
+ fetchResults: function () {
+ var intervals = [200, 400, 800, 1200, 2000, 3000, 5000];
+
+ (function poll (comp) {
+ $.getJSON(location.pathname + '.json')
+ .complete(function (jqXHR) {
+ switch (jqXHR.status) {
+ case 202:
+ var interval;
+ if (intervals.length === 1) {
+ interval = intervals[0];
+ }
+ else {
+ interval = intervals.shift;
+ }
+ setTimeout(poll, interval);
+ break;
+ case 200:
+ comp.updateState(jqXHR.responseJSON);
+ break;
+ case 404:
+ case 400:
+ case 500:
+ showErrorModal(jqXHR.responseJSON);
+ break;
+ }
+ });
+ }(this));
},
/**
- * Returns true if results have been fetched.
- *
- * A holding message is shown till results are fetched.
+ * Incrementally update state so that the rendering process is
+ * not overwhelemed when there are too many queries.
*/
- isResultAvailable: function () {
- return this.state.queries.length >= 1;
+ updateState: function(responseJSON) {
+ var queries = responseJSON.queries;
+
+ // Render results for first 50 queries and set flag if total queries is
+ // more than 250.
+ var numHits = 0;
+ responseJSON.veryBig = queries.length > 250;
+ //responseJSON.veryBig = !_.every(queries, (query) => {
+ //numHits += query.hits.length;
+ //return (numHits <= 500);
+ //});
+ responseJSON.queries = queries.splice(0, 50);
+ this.setState(responseJSON);
+
+ // Render results for remaining queries.
+ var update = function () {
+ if (queries.length > 0) {
+ this.setState({
+ queries: this.state.queries.concat(queries.splice(0, 50))
+ });
+ setTimeout(update.bind(this), 500);
+ }
+ else {
+ this.componentFinishedUpdating();
+ }
+ };
+ setTimeout(update.bind(this), 500);
},
+
+ // View //
+ render: function () {
+ return this.isResultAvailable() ?
+ this.resultsJSX() : this.loadingJSX();
+ },
+
/**
* Returns loading message
*/
loadingJSX: function () {
return (
@@ -1047,13 +1052,15 @@
</div>
)
}
<div className={this.shouldShowSidebar() ?
'col-md-9' : 'col-md-12'}>
- { this.overview() }
- <Circos queries={this.state.queries}
- program={this.state.program} collapsed="true"/>
+ { this.overviewJSX() }
+ { this.isHitsAvailable()
+ ? <Circos queries={this.state.queries}
+ program={this.state.program} collapsed="true"/>
+ : <span></span> }
{
_.map(this.state.queries, _.bind(function (query) {
return (
<Query key={"Query_"+query.id} query={query} data={this.state}
selectHit={this.selectHit}/>
@@ -1064,34 +1071,13 @@
</div>
);
},
/**
- * Returns true if sidebar should be shown.
- *
- * Sidebar is not shown if there is only one query and there are no hits
- * corresponding to the query.
- */
- shouldShowSidebar: function () {
- return !(this.state.queries.length == 1 &&
- this.state.queries[0].hits.length == 0);
- },
-
- /**
- * Returns true if index should be shown in the sidebar.
- *
- * Index is not shown in the sidebar if there are more than eight queries
- * in total.
- */
- shouldShowIndex: function () {
- return this.state.queries.length <= 8;
- },
-
- /**
* Renders report overview.
*/
- overview: function () {
+ overviewJSX: function () {
return (
<div
className="overview">
<pre
className="pre-reset">
@@ -1102,12 +1088,12 @@
_.map(this.state.querydb, function (db) {
return db.title;
}).join(", ")
}
<br/>
- Total: {this.state.stats.nsequences} sequences, {this.state
- .stats.ncharacters} characters
+ Total: {this.state.stats.nsequences} sequences,
+ {this.state.stats.ncharacters} characters
<br/>
<br/>
{
_.map(this.state.params, function (val, key) {
return key + " " + val;
@@ -1116,110 +1102,90 @@
</pre>
</div>
);
},
- componentDidMount: function () {
- this.fetchResults();
- },
+ // Controller //
+
/**
- * Fetch results.
+ * Returns true if results have been fetched.
+ *
+ * A holding message is shown till results are fetched.
*/
- fetchResults: function () {
- var intervals = [200, 400, 800, 1200, 2000, 3000, 5000];
+ isResultAvailable: function () {
+ return this.state.queries.length >= 1;
+ },
- $.getJSON(location.pathname + '.json')
- .complete(_.bind(function (jqXHR) {
- switch (jqXHR.status) {
- case 202:
- var interval;
- if (intervals.length === 1) {
- interval = intervals[0];
- }
- else {
- interval = intervals.shift;
- }
- setTimeout(this.fetchResults, interval);
- break;
- case 200:
- this.updatePage(jqXHR.responseJSON);
- break;
- case 404:
- case 400:
- case 500:
- showErrorModal(jqXHR.responseJSON);
- break;
- }
- }, this));
+ isHitsAvailable: function () {
+ var cnt = 0;
+ _.each(this.state.queries, function (query) {
+ if(query.hits.length == 0) cnt++;
+ });
+ return !(cnt == this.state.queries.length);
},
- updatePage: function(responseJSON) {
- var queries = responseJSON.queries;
+ /**
+ * Returns true if sidebar should be shown.
+ *
+ * Sidebar is not shown if there is only one query and there are no hits
+ * corresponding to the query.
+ */
+ shouldShowSidebar: function () {
+ return !(this.state.queries.length == 1 &&
+ this.state.queries[0].hits.length == 0);
+ },
- // Render results for first 50 queries and set flag if total queries is
- // more than 250.
- var numHits = 0;
- responseJSON.veryBig = queries.length > 250;
- //responseJSON.veryBig = !_.every(queries, (query) => {
- //numHits += query.hits.length;
- //return (numHits <= 500);
- //});
- responseJSON.queries = queries.splice(0, 50);
- this.setState(responseJSON);
-
- // Render results for remaining queries.
- var update = function () {
- if (queries.length > 0) {
- this.setState({
- queries: this.state.queries.concat(queries.splice(0, 50))
- });
- setTimeout(update.bind(this), 500);
- }
- else {
- this.componentFinishedUpdating();
- }
- };
- setTimeout(update.bind(this), 500);
+ /**
+ * Returns true if index should be shown in the sidebar.
+ *
+ * Index is not shown in the sidebar if there are more than eight queries
+ * in total.
+ */
+ shouldShowIndex: function () {
+ return this.state.queries.length <= 8;
},
/**
- * Locks Sidebar in its position, prevents folding of hits during
- * text-selection, etc.
+ * Called after first call to render. The results may not be available at
+ * this stage and thus results DOM cannot be scripted here, unless using
+ * delegated events bound to the window, document, or body.
*/
- componentFinishedUpdating: function () {
- this.affixSidebar();
- this.shouldShowIndex() && this.setupScrollSpy();
- this.setupHitSelection();
- this.setupDownloadLinks();
+ componentDidMount: function () {
+ // This sets up an event handler which enables users to select text
+ // from hit header without collapsing the hit.
+ this.preventCollapseOnSelection();
},
/**
- * Affixes the sidebar.
- *
- * TODO: can't this be done with CSS?
+ * Called after each state change. Only a part of results DOM may be
+ * available after a state change.
*/
- affixSidebar: function () {
- var $sidebar = $('.sidebar');
- $sidebar.affix({
- offset: {
- top: $sidebar.offset().top
- }
- });
+ componentDidUpdate: function () {
+ // We track the number of updates to the component.
+ this.updateCycle += 1;
+
+ // Lock sidebar in its position on first update of
+ // results DOM.
+ if (this.updateCycle === 1 ) this.affixSidebar();
},
/**
- * For the query in viewport, highlights corresponding entry in the index.
+ * Prevents folding of hits during text-selection, etc.
*/
- setupScrollSpy: function () {
- $('body').scrollspy({target: '.sidebar'});
+
+ /**
+ * Called after all results have been rendered.
+ */
+ componentFinishedUpdating: function () {
+ this.shouldShowIndex() && this.setupScrollSpy();
},
/**
* Prevents folding of hits during text-selection.
*/
- setupHitSelection: function () {
+ preventCollapseOnSelection: function () {
$('body').on('mousedown', ".hit > .section-header > h4", function (event) {
var $this = $(this);
$this.on('mouseup mousemove', function handler(event) {
if (event.type === 'mouseup') {
// user wants to toggle
@@ -1232,31 +1198,77 @@
$this.off('mouseup mousemove', handler);
});
});
},
- // Download links.
- //
- // Handles downloading files referenced by links with class 'download'.
- setupDownloadLinks: function () {
- $(document).on('click', '.download', function (event) {
- event.preventDefault();
- event.stopPropagation();
+ /**
+ * Affixes the sidebar.
+ */
+ affixSidebar: function () {
+ var $sidebar = $('.sidebar');
+ $sidebar.affix({
+ offset: {
+ top: $sidebar.offset().top
+ }
+ });
+ },
- var $anchor = $(this);
+ /**
+ * For the query in viewport, highlights corresponding entry in the index.
+ */
+ setupScrollSpy: function () {
+ $('body').scrollspy({target: '.sidebar'});
+ },
- if ($anchor.is(':disabled')) return;
+ /**
+ * Event-handler when hit is selected
+ * Adds glow to hit component.
+ * Updates number of Fasta that can be downloaded
+ */
+ selectHit: function (id) {
- var url = $anchor.attr('href');
+ var checkbox = $("#" + id);
+ var num_checked = $('.hit-links :checkbox:checked').length;
- $.get(url)
- .done(function (data) {
- window.location.href = url;
- })
- .fail(function (jqXHR, status, error) {
- SequenceServer.showErrorModal(jqXHR, function () {});
- });
- });
+ if (!checkbox || !checkbox.val()) {
+ return;
+ }
+
+ var $hit = $(checkbox.data('target'));
+
+ // Highlight selected hit and sync checkboxes if sequence viewer is open.
+ if (checkbox.is(":checked")) {
+ $hit
+ .addClass('glow')
+ .find(":checkbox").not(checkbox).check();
+ var $a = $('.download-fasta-of-selected');
+ var $b = $('.download-alignment-of-selected');
+ $b.enable()
+ var $n = $a.find('span');
+ $a
+ .enable()
+ }
+
+ else {
+ $hit
+ .removeClass('glow')
+ .find(":checkbox").not(checkbox).uncheck();
+ }
+
+ if (num_checked >= 1)
+ {
+ var $a = $('.download-fasta-of-selected');
+ var $b = $('.download-alignment-of-selected');
+ $a.find('.text-bold').html(num_checked);
+ $b.find('.text-bold').html(num_checked);
+ }
+
+ if (num_checked == 0) {
+ var $a = $('.download-fasta-of-selected');
+ var $b = $('.download-alignment-of-selected');
+ $a.addClass('disabled').find('.text-bold').html('');
+ $b.addClass('disabled').find('.text-bold').html('');
+ }
},
});
var Page = React.createClass({
render: function () {