public/js/report.js in sequenceserver-2.0.0.beta3 vs public/js/report.js in sequenceserver-2.0.0.beta4

- old
+ new

@@ -1,6 +1,6 @@ -import './sequenceserver' // for custom $.tooltip function +import './sequenceserver'; // for custom $.tooltip function import React from 'react'; import _ from 'underscore'; import Circos from './circos'; import HitsOverview from './hits_overview'; @@ -17,20 +17,20 @@ /** * Dynamically create form and submit. */ var downloadFASTA = function (sequence_ids, database_ids) { var form = $('<form/>').attr('method', 'post').attr('action', 'get_sequence'); - addField("sequence_ids", sequence_ids); - addField("database_ids", database_ids); + addField('sequence_ids', sequence_ids); + addField('database_ids', database_ids); form.appendTo('body').submit().remove(); function addField(name, val) { form.append( $('<input>').attr('type', 'hidden').attr('name', name).val(val) ); } -} +}; /** * Base component of report page. This component is later rendered into page's * '#view' element. */ @@ -84,28 +84,28 @@ function poll () { $.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: - component.updateState(jqXHR.responseJSON); - break; - case 404: - case 400: - case 500: - showErrorModal(jqXHR.responseJSON); - break; + case 202: + var interval; + if (intervals.length === 1) { + interval = intervals[0]; + } + else { + interval = intervals.shift(); + } + setTimeout(poll, interval); + break; + case 200: + component.updateState(jqXHR.responseJSON); + break; + case 404: + case 400: + case 500: + showErrorModal(jqXHR.responseJSON); + break; } }); } poll(); @@ -186,23 +186,18 @@ ) } <div className={this.shouldShowSidebar() ? 'col-md-9' : 'col-md-12'}> { this.overviewJSX() } - { this.isHitsAvailable() - ? <Circos queries={this.state.queries} - program={this.state.program} collapsed="true"/> - : <span></span> } + { this.circosJSX() } { _.map(this.state.queries, _.bind(function (query) { return ( - <Query key={"Query_"+query.id} - program={this.state.program} querydb={this.state.querydb} - query={query} num_queries={this.state.num_queries} - veryBig={this.state.veryBig} selectHit={this.selectHit} - imported_xml={this.state.imported_xml} /> - ); + <Query key={'Query_'+query.id} query={query} showQueryCrumbs={this.state.num_queries > 1} + selectHit={this.selectHit} program={this.state.program} querydb={this.state.querydb} + veryBig={this.state.veryBig} imported_xml={this.state.imported_xml} /> + ); }, this)) } </div> </div> ); @@ -212,29 +207,40 @@ * Renders report overview. */ overviewJSX: function () { return ( <div className="overview"> - <pre className="pre-reset"> + <p className="text-monospace"> {this.state.program_version}{this.state.submitted_at - && `; query submitted on ${this.state.submitted_at}`} - <br/> - Databases ({this.state.stats.nsequences} sequences,&nbsp; - {this.state.stats.ncharacters} characters): { - this.state.querydb.map((db) => { return db.title }).join(", ") - } - <br/> + && `, query submitted on ${this.state.submitted_at}`} + </p> + <p className="text-monospace"> + Databases: { + this.state.querydb.map((db) => { return db.title; }).join(', ') + } ({this.state.stats.nsequences} sequences,&nbsp; + {this.state.stats.ncharacters} characters) + </p> + <p className="text-monospace"> Parameters: { _.map(this.state.params, function (val, key) { - return key + " " + val; - }).join(", ") + return key + ' ' + val; + }).join(', ') } - </pre> + </p> </div> ); - }, + }, + /** + * Return JSX for circos if we have at least one hit. + */ + circosJSX: function () { + return this.atLeastTwoHits() + ? <Circos queries={this.state.queries} + program={this.state.program} collapsed="true"/> + : <span></span>; + }, // Controller // /** * Returns true if results have been fetched. @@ -243,16 +249,23 @@ */ isResultAvailable: function () { return this.state.queries.length >= 1; }, - isHitsAvailable: function () { - var cnt = 0; - _.each(this.state.queries, function (query) { - if(query.hits.length == 0) cnt++; + /** + * Returns true if we have at least one hit. + */ + atLeastOneHit: function () { + return this.state.queries.some(query => query.hits.length > 0); + }, + + atLeastTwoHits: function () { + var hit_num = 0; + return this.state.queries.some(query => { + hit_num += query.hits.length; + return hit_num > 1; }); - return !(cnt == this.state.queries.length); }, /** * Returns true if sidebar should be shown. * @@ -263,17 +276,16 @@ 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. + * Returns true if index should be shown in the sidebar. Index is shown + * only for 2 and 8 queries. */ shouldShowIndex: function () { - return this.state.queries.length <= 8; + var num_queries = this.state.queries.length; + return num_queries >= 2 && num_queries <= 8; }, /** * 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 @@ -307,11 +319,11 @@ /** * Prevents folding of hits during text-selection. */ preventCollapseOnSelection: function () { - $('body').on('mousedown', ".hit > .section-header > h4", function (event) { + $('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 $this.attr('data-toggle', 'collapse'); @@ -353,22 +365,22 @@ * Adds glow to hit component. * Updates number of Fasta that can be downloaded */ selectHit: function (id) { - var checkbox = $("#" + 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 enable 'Download FASTA/Alignment of // selected' links. - if (checkbox.is(":checked")) { + if (checkbox.is(':checked')) { $hit.find('.section-content').addClass('glow'); $('.download-alignment-of-selected').enable(); $('.download-fasta-of-selected').enable(); } else { @@ -404,13 +416,17 @@ /** * Returns the id of query. */ domID: function () { - return "Query_" + this.props.query.number; + return 'Query_' + this.props.query.number; }, + queryLength: function () { + return this.props.query.length; + }, + /** * Returns number of hits. */ numhits: function () { return this.props.query.hits.length; @@ -421,65 +437,63 @@ render: function () { return ( <div className="resultn" id={this.domID()} data-query-len={this.props.query.length} data-algorithm={this.props.program}> - <div className="section-header"> - <h3> - Query= {this.props.query.id} - &nbsp; - <small> - {this.props.query.title} - </small> - </h3> - <span - className="label label-reset pos-label" - title={"Query" + this.props.query.number + "."} - data-toggle="tooltip"> - {this.props.query.number + "/" + this.props.num_queries} - </span> - </div> - {this.numhits() && - ( - <div className="section-content"> - <HitsOverview key={"GO_"+this.props.query.number} query={this.props.query} program={this.props.program} collapsed={this.props.veryBig}/> - <LengthDistribution key={"LD_"+this.props.query.id} query={this.props.query} algorithm={this.props.program} collapsed="true"/> - <HitsTable key={"HT_"+this.props.query.number} query={this.props.query} imported_xml={this.props.imported_xml} /> - <div id="hits"> - { - _.map(this.props.query.hits, _.bind(function (hit) { - return ( - <Hit hit={hit} - key={"HIT_"+hit.number} - algorithm={this.props.program} - querydb={this.props.querydb} - query={this.props.query} - imported_xml={this.props.imported_xml} - selectHit={this.props.selectHit}/> - ); - }, this)) - } - </div> - </div> - ) || ( - <div - className="section-content"> - <p> - Query length: {this.props.query.length} - </p> - <br/> - <br/> - <p> - <strong> ****** No hits found ****** </strong> - </p> - </div> - ) + { this.headerJSX() } + { this.numhits() && this.hitsListJSX() || this.noHitsJSX() } + </div> + ); + }, + + headerJSX: function () { + var meta = `length: ${this.queryLength().toLocaleString()}`; + if (this.props.showQueryCrumbs) { + meta = `query ${this.props.query.number}, ` + meta; + } + return <div className="section-header"> + <h3> + Query= {this.props.query.id}&nbsp; + <small>{this.props.query.title}</small> + </h3> + <span className="label label-reset pos-label">{ meta }</span> + </div>; + }, + + hitsListJSX: function () { + return <div className="section-content"> + <HitsOverview key={'GO_' + this.props.query.number} query={this.props.query} program={this.props.program} collapsed={this.props.veryBig} /> + <LengthDistribution key={'LD_' + this.props.query.id} query={this.props.query} algorithm={this.props.program} collapsed="true" /> + <HitsTable key={'HT_' + this.props.query.number} query={this.props.query} imported_xml={this.props.imported_xml} /> + <div id="hits"> + { + _.map(this.props.query.hits, _.bind(function (hit) { + return ( + <Hit key={'HIT_' + hit.number} hit={hit} + algorithm={this.props.program} + querydb={this.props.querydb} + query={this.props.query} + imported_xml={this.props.imported_xml} + selectHit={this.props.selectHit} + showHitCrumbs={this.numhits() > 1} + showQueryCrumbs={this.props.showQueryCrumbs} /> + ); + }, this)) } </div> - ) + </div>; }, + noHitsJSX: function () { + return <div className="section-content"> + <br /> + <p> + <strong> ****** No hits found ****** </strong> + </p> + </div>; + }, + shouldComponentUpdate: function (nextProps, nextState) { if (!this.props.query) return true; } }); @@ -488,13 +502,13 @@ */ var HitsTable = React.createClass({ mixins: [Utils], render: function () { var count = 0, - hasName = _.every(this.props.query.hits, function(hit) { - return hit.sciname !== ''; - }); + hasName = _.every(this.props.query.hits, function(hit) { + return hit.sciname !== ''; + }); return ( <table className="table table-hover table-condensed tabular-view"> <thead> @@ -512,23 +526,23 @@ <tbody> { _.map(this.props.query.hits, _.bind(function (hit) { return ( <tr key={hit.number}> - <td className="text-left">{hit.number + "."}</td> + <td className="text-left">{hit.number + '.'}</td> <td> - <a href={"#Query_" + this.props.query.number + "_hit_" + hit.number}> + <a href={'#Query_' + this.props.query.number + '_hit_' + hit.number}> {hit.id} </a> </td> {hasName && <td className="text-left">{hit.sciname}</td>} {!this.props.imported_xml && <td className="text-right">{hit.qcovs}</td>} <td className="text-right">{hit.score}</td> <td className="text-right">{this.inExponential(hit.hsps[0].evalue)}</td> <td className="text-right">{hit.identity}</td> </tr> - ) + ); }, this)) } </tbody> </table> ); @@ -549,21 +563,21 @@ }, /** * Returns length of the hit sequence. */ - length: function () { + hitLength: function () { return this.props.hit.length; }, // Internal helpers. // /** * Returns id that will be used for the DOM node corresponding to the hit. */ domID: function () { - return "Query_" + this.props.query.number + "_hit_" + this.props.hit.number; + return 'Query_' + this.props.query.number + '_hit_' + this.props.hit.number; }, databaseIDs: function () { return _.map(this.props.querydb, _.iteratee('id')); }, @@ -591,14 +605,14 @@ downloadAlignment: function (event) { var hsps = _.map(this.props.hit.hsps, _.bind(function (hsp) { hsp.query_id = this.props.query.id; hsp.hit_id = this.props.hit.id; return hsp; - }, this)) + }, this)); var aln_exporter = new AlignmentExporter(); - aln_exporter.export_alignments(hsps, this.props.query.id+"_"+this.props.hit.id); + aln_exporter.export_alignments(hsps, this.props.query.id+'_'+this.props.hit.id); }, // Life cycle methods // @@ -606,11 +620,11 @@ return { showSequenceViewer: false }; }, // Return JSX for view sequence button. viewSequenceButton: function () { - if (this.length() > 10000) { + if (this.hitLength() > 10000) { return ( <button className="btn btn-link view-sequence disabled" title="Sequence too long" disabled="true"> <i className="fa fa-eye"></i> Sequence @@ -628,53 +642,61 @@ } }, render: function () { return ( - <div className="hit" id={this.domID()} - data-hit-def={this.props.hit.id} data-hit-evalue={this.props.hit.evalue} - data-hit-len={this.props.hit.length}> - <div className="section-header"> - <h4 data-toggle="collapse" - data-target={this.domID() + "_content"}> - <i className="fa fa-chevron-down"></i> - &nbsp; - <span> - {this.props.hit.id} - &nbsp; - <small> - {this.props.hit.title} - </small> - </span> - </h4> - <span className="label label-reset pos-label" - title={"Query " + this.props.query.number + ". Hit " - + this.props.hit.number + " of " - + this.props.query.hits.length + "."} - data-toggle="tooltip"> - {this.props.hit.number + "/" + this.props.query.hits.length} - </span> - </div> - <div id={this.domID() + "_content"} - className="section-content collapse in"> - { this.hitLinks() } - <HSPOverview key={"kablammo"+this.props.query.id} - query={this.props.query} hit={this.props.hit} - algorithm={this.props.algorithm}/> - { this.hspListJSX() } - </div> + <div className="hit" id={this.domID()} data-hit-def={this.props.hit.id} + data-hit-len={this.props.hit.length} data-hit-evalue={this.props.hit.evalue}> + { this.headerJSX() } { this.contentJSX() } </div> ); }, + headerJSX: function () { + var meta = `length: ${this.hitLength().toLocaleString()}`; + + if (this.props.showQueryCrumbs && this.props.showHitCrumbs) { + // Multiper queries, multiple hits + meta = `hit ${this.props.hit.number} of query ${this.props.query.number}, ` + meta; + } + else if (this.props.showQueryCrumbs && !this.props.showHitCrumbs) { + // Multiple queries, single hit + meta = `the only hit of query ${this.props.query.number}, ` + meta; + } + else if (!this.props.showQueryCrumbs && this.props.showHitCrumbs) { + // Single query, multiple hits + meta = `hit ${this.props.hit.number}, ` + meta; + } + + return <div className="section-header"> + <h4 data-toggle="collapse" data-target={this.domID() + '_content'}> + <i className="fa fa-chevron-down"></i>&nbsp; + <span> + {this.props.hit.id}&nbsp; + <small>{this.props.hit.title}</small> + </span> + </h4> + <span className="label label-reset pos-label">{ meta }</span> + </div>; + }, + + contentJSX: function () { + return <div id={this.domID() + '_content'} className="section-content collapse in"> + { this.hitLinks() } + <HSPOverview key={'kablammo' + this.props.query.id} query={this.props.query} + hit={this.props.hit} algorithm={this.props.algorithm} /> + { this.hspListJSX() } + </div>; + }, + hitLinks: function () { return ( <div className="hit-links"> <label> - <input type="checkbox" id={this.domID() + "_checkbox"} + <input type="checkbox" id={this.domID() + '_checkbox'} value={this.accession()} onChange={function () { - this.props.selectHit(this.domID() + "_checkbox"); + this.props.selectHit(this.domID() + '_checkbox'); }.bind(this)} data-target={'#' + this.domID()} /> Select </label> { !this.props.imported_xml && [ @@ -712,14 +734,14 @@ { this.props.hit.hsps.map((hsp) => { return <HSP key={hsp.number} algorithm={this.props.algorithm} queryNumber={this.props.query.number} - hitNumber={this.props.hit.number} hsp={hsp}/> + hitNumber={this.props.hit.number} hsp={hsp}/>; }, this) } - </div> + </div>; } }); /** @@ -884,11 +906,11 @@ .done(_.bind(function (response) { this.setState({ sequences: response.sequences, error_msgs: response.error_msgs, requestCompleted: true - }) + }); }, this)) .fail(function (jqXHR, status, error) { showErrorModal(jqXHR, function () { this.hide(); }); @@ -939,15 +961,15 @@ _.each(query.hits, function (hit) { _.each(hit.hsps, function (hsp) { hsp.hit_id = hit.id; hsp.query_id = query.id; hsps_arr.push(hsp); - }) - }) + }); + }); }, this)); console.log('len '+hsps_arr.length); - aln_exporter.export_alignments(hsps_arr, "alignment-"+sequence_ids.length+"_hits"); + aln_exporter.export_alignments(hsps_arr, 'alignment-'+sequence_ids.length+'_hits'); return false; }, downloadAlignmentOfSelected: function () { var sequence_ids = $('.hit-links :checkbox:checked').map(function () { @@ -965,11 +987,11 @@ hsps_arr.push(hsp); }); } }); }, this)); - aln_exporter.export_alignments(hsps_arr, "alignment-"+sequence_ids.length+"_hits"); + aln_exporter.export_alignments(hsps_arr, 'alignment-'+sequence_ids.length+'_hits'); return false; }, // JSX // @@ -977,33 +999,33 @@ return ( <div className="sidebar"> { this.props.shouldShowIndex && this.index() } { this.downloads() } </div> - ) + ); }, index: function () { return ( <div className="index"> <div - className="section-header"> - <h4> - { this.summary() } - </h4> + className="section-header"> + <h4> + { this.summary() } + </h4> </div> <ul className="nav hover-reset active-bold"> { _.map(this.props.data.queries, _.bind(function (query) { return ( - <li key={"Side_bar_"+query.id}> + <li key={'Side_bar_'+query.id}> <a className="nowrap-ellipsis hover-bold" - href={"#Query_" + query.number} - title={"Query= " + query.id + ' ' + query.title}> - {"Query= " + query.id} + href={'#Query_' + query.number} + title={'Query= ' + query.id + ' ' + query.title}> + {'Query= ' + query.id} </a> </li> ); }, this)) } @@ -1017,11 +1039,11 @@ var numqueries = this.props.data.queries.length; var numquerydb = this.props.data.querydb.length; return ( program.toUpperCase() + ': ' + - numqueries + ' ' + (numqueries > 1 ? 'queries' : 'query') + ", " + + numqueries + ' ' + (numqueries > 1 ? 'queries' : 'query') + ', ' + numquerydb + ' ' + (numquerydb > 1 ? 'databases' : 'database') ); }, downloads: function () { @@ -1066,11 +1088,11 @@ <a className="download" data-toggle="tooltip" title="15 columns: query and subject ID; scientific name, alignment length, mismatches, gaps, identity, start and end coordinates, e value, bitscore, query coverage per subject and per HSP." - href={"download/" + this.props.data.search_id + ".std_tsv"}> + href={'download/' + this.props.data.search_id + '.std_tsv'}> Standard tabular report </a> </li> } { @@ -1078,19 +1100,19 @@ <a className="download" data-toggle="tooltip" title="44 columns: query and subject ID, GI, accessions, and length; alignment details; taxonomy details of subject sequence(s) and query coverage per subject and per HSP." - href={"download/" + this.props.data.search_id + ".full_tsv"}> + href={'download/' + this.props.data.search_id + '.full_tsv'}> Full tabular report </a> </li> } { !this.props.data.imported_xml && <li> <a className="download" data-toggle="tooltip" title="Results in XML format." - href={"download/" + this.props.data.search_id + ".xml"}> + href={'download/' + this.props.data.search_id + '.xml'}> Full XML report </a> </li> } </ul>