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 () {