import SequenceServer from './sequenceserver'; import _ from 'underscore'; import React from 'react'; import d3 from 'd3'; 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'; /** * Dynamically create form and submit. */ var downloadFASTA = function (sequence_ids, database_ids) { var form = $('
').attr('method', 'post').attr('action', '/get_sequence'); addField("sequence_ids", sequence_ids); addField("database_ids", database_ids); form.appendTo('body').submit().remove(); function addField(name, val) { form.append( $('').attr('type', 'hidden').attr('name', name).val(val) ); } } /** * Pretty formats number */ var Utils = { /** * Render URL for sequence-viewer. */ a: function (link) { if (link.title && link.url) { return ( {link.icon && } {" " + link.title + " "} ); } }, /*********************************** * Formatters for hits & hsp table * ***********************************/ // Formats an array of two elements as "first (last)". format_2_tuple: function (tuple) { return (tuple[0] + " (" + tuple[tuple.length - 1] + ")"); }, /** * Returns fraction as percentage */ inPercentage: function (num , den) { return (num * 100.0 / den).toFixed(2); }, /** * Returns fractional representation as String. */ inFraction: function (num , den) { return num + "/" + den; }, /** * Returns given Float as String formatted to two decimal places. */ inTwoDecimal: function (num) { return num.toFixed(2) }, /** * Returns zero if num is zero. Returns two decimal representation of num * if num is between [1..10). Returns num in scientific notation otherwise. */ inExponential: function (num) { // Nothing to do if num is 0. if (num === 0) { return 0 } // Round to two decimal places if in the rane [1..10). if (num >= 1 && num < 10) { return this.inTwoDecimal(num) } // Return numbers in the range [0..1) and [10..Inf] in // scientific format. var exp = num.toExponential(2); var parts = exp.split("e"); var base = parts[0]; var power = parts[1]; return {base} × 10{power}; } }; /** * Component for sequence-viewer links. */ var SequenceViewer = (function () { var Viewer = React.createClass({ /** * The CSS class name that will be assigned to the widget container. ID * assigned to the widget container is derived from the same. */ widgetClass: 'biojs-vis-sequence', // Lifecycle methods. // render: function () { this.widgetID = this.widgetClass + '-' + (new Date().getUTCMilliseconds()); return (

{this.props.sequence.id}   {this.props.sequence.title}

); }, componentDidMount: function () { // attach BioJS sequence viewer var widget = new Sequence({ sequence: this.props.sequence.value, target: this.widgetID, format: 'PRIDE', columns: { size: 40, spacedEach: 5 }, formatOptions: { title: false, footer: false } }); widget.hideFormatSelector(); } }); return React.createClass({ // Kind of public API. // /** * Shows sequence viewer. */ show: function () { this.modal().modal('show'); }, // Internal helpers. // modal: function () { return $(React.findDOMNode(this.refs.modal)); }, resultsJSX: function () { return (
{ _.map(this.state.error_msgs, _.bind(function (error_msg) { return (

{error_msg[0]}

                                            {error_msg[1]}
                                        
); }, this)) } { _.map(this.state.sequences, _.bind(function (sequence) { return (); }, this)) }
); }, loadingJSX: function () { return (
); }, // Lifecycle methods. // getInitialState: function () { return { error_msgs: [], sequences: [], requestCompleted: false }; }, render: function () { return (

View sequence

{ this.state.requestCompleted && this.resultsJSX() || this.loadingJSX() }
); }, componentDidMount: function () { // Display modal with a spinner. this.show(); // Fetch sequence and update state. $.getJSON(this.props.url) .done(_.bind(function (response) { this.setState({ sequences: response.sequences, error_msgs: response.error_msgs, requestCompleted: true }) }, this)) .fail(function (jqXHR, status, error) { SequenceServer.showErrorModal(jqXHR, function () { this.hide(); }); }); this.modal().on('hidden.bs.modal', this.props.onHide); }, }); })(); /** * Component for each hit. */ var Hit = React.createClass({ mixins: [Utils], /** * Returns accession number of the hit sequence. */ accession: function () { return this.props.hit.accession; }, /** * Returns length of the hit sequence. */ length: 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; }, databaseIDs: function () { return _.map(this.props.querydb, _.iteratee('id')); }, showSequenceViewer: function (event) { this.setState({ showSequenceViewer: true }); event && event.preventDefault(); }, hideSequenceViewer: function () { this.setState({ showSequenceViewer: false }); }, viewSequenceLink: function () { return encodeURI(`get_sequence/?sequence_ids=${this.accession()}&database_ids=${this.databaseIDs()}`); }, downloadFASTA: function (event) { var accessions = [this.accession()]; downloadFASTA(accessions, this.databaseIDs()); }, // Event-handler for exporting alignments. // Calls relevant method on AlignmentExporter defined in alignment_exporter.js. 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)) var aln_exporter = new AlignmentExporter(); aln_exporter.export_alignments(hsps, this.props.query.id+"_"+this.props.hit.id); }, /** * Return prettified stats for the given hsp and based on the BLAST * algorithm. */ getHSPStats: function (hsp) { var stats = { 'Score': this.format_2_tuple([ this.inTwoDecimal(hsp.bit_score), hsp.score ]), 'E value': this.inExponential(hsp.evalue), 'Identities': this.format_2_tuple([ this.inFraction(hsp.identity, hsp.length), this.inPercentage(hsp.identity, hsp.length) ]), 'Gaps': this.format_2_tuple([ this.inFraction(hsp.gaps, hsp.length), this.inPercentage(hsp.gaps, hsp.length) ]), 'Coverage': hsp.qcovhsp }; switch (this.props.algorithm) { case 'tblastx': _.extend(stats, { 'Frame': this.inFraction(hsp.qframe, hsp.sframe) }); // fall-through case 'blastp': _.extend(stats, { 'Positives': this.format_2_tuple([ this.inFraction(hsp.positives, hsp.length), this.inPercentage(hsp.positives, hsp.length) ]) }); break; case 'blastn': _.extend(stats, { 'Strand': (hsp.qframe > 0 ? '+' : '-') + "/" + (hsp.sframe > 0 ? '+' : '-') }); break; case 'blastx': _.extend(stats, { 'Query Frame': hsp.qframe }); break; case 'tblastn': _.extend(stats, { 'Hit Frame': hsp.sframe }); break; } return stats; }, // Life cycle methods // getInitialState: function () { return { showSequenceViewer: false }; }, render: function () { return (

  {this.props.hit.id}   {this.props.hit.title}

{this.props.hit.number + "/" + this.props.query.hits.length}
| { this.state.showSequenceViewer && } | | { _.map(this.props.hit.links, _.bind(function (link) { return [ | , this.a(link)]; }, this)) }
{/* */} { _.map (this.props.hit.hsps, _.bind( function (hsp) { stats_returned = this.getHSPStats(hsp); return ( ) }, this)) }
{Helpers.toLetters(hsp.number) + "."}
{ _.map(stats_returned, function (value , key) { return(); }) } { _.map(stats_returned, _.bind(function (value, key) { return(); }, this)) }
{key}
{value}
{hsp.pp}
); } }); /** * Renders summary of all hits per query in a tabular form. */ var HitsTable = React.createClass({ mixins: [Utils], render: function () { var count = 0, hasName = _.every(this.props.query.hits, function(hit) { return hit.sciname !== ''; }); return ( {hasName && } { _.map(this.props.query.hits, _.bind(function (hit) { return ( {hasName && } ) }, this)) }
# Similar sequencesSpeciesQuery coverage (%) Total score E value Identity (%)
{hit.number + "."} {hit.id} {hit.sciname}{hit.qcovs} {hit.score} {this.inExponential(hit.hsps[0].evalue)} {hit.identity}
); } }); /** * Renders report for each query sequence. * * Composed of graphical overview, tabular summary (HitsTable), * and a list of Hits. */ var Query = React.createClass({ // Kind of public API // /** * Returns the id of query. */ domID: function () { return "Query_" + this.props.query.number; }, /** * Returns number of hits. */ numhits: function () { return this.props.query.hits.length; }, // Life cycle methods // render: function () { return (

Query= {this.props.query.id}   {this.props.query.title}

{this.props.query.number + "/" + this.props.data.queries.length}
{this.numhits() && (
{/* */}
{ _.map(this.props.query.hits, _.bind(function (hit) { return ( ); }, this)) }
) || (

Query length: {this.props.query.length}



****** No hits found ******

) }
) }, }); /** * Renders links for downloading hit information in different formats. * Renders links for navigating to each query. */ var SideBar = React.createClass({ /** * Event-handler for downloading fasta of all hits. */ downloadFastaOfAll: function () { var sequence_ids = $('.hit-links :checkbox').map(function () { return this.value; }).get(); var database_ids = _.map(this.props.data.querydb, _.iteratee('id')); downloadFASTA(sequence_ids, database_ids); }, /** * Handles downloading fasta of selected hits. */ downloadFastaOfSelected: function () { var sequence_ids = $('.hit-links :checkbox:checked').map(function () { return this.value; }).get(); var database_ids = _.map(this.props.data.querydb, _.iteratee('id')); downloadFASTA(sequence_ids, database_ids); }, downloadAlignmentOfAll: function() { var sequence_ids = $('.hit-links :checkbox').map(function () { return this.value; }).get(); var hsps_arr = []; var aln_exporter = new AlignmentExporter(); _.each(this.props.data.queries, _.bind(function (query) { _.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"); }, downloadAlignmentOfSelected: function () { var sequence_ids = $('.hit-links :checkbox:checked').map(function () { return this.value; }).get(); var hsps_arr = []; var aln_exporter = new AlignmentExporter(); console.log('check '+sequence_ids.toString()); _.each(this.props.data.queries, _.bind(function (query) { _.each(query.hits, function (hit) { if (_.indexOf(sequence_ids, hit.id) != -1) { _.each(hit.hsps, function (hsp) { hsp.hit_id = hit.id; hsp.query_id = query.id; hsps_arr.push(hsp); }); } }); }, this)); aln_exporter.export_alignments(hsps_arr, "alignment-"+sequence_ids.length+"_hits"); }, // JSX // render: function () { return (
{ this.showIndex() && this.index() } { this.downloads() }
) }, showIndex: function () { return this.props.data.queries.length <= 8; }, index: function () { return (

{ this.summary() }

); }, summary: function () { var program = this.props.data.program; var numqueries = this.props.data.queries.length; var numquerydb = this.props.data.querydb.length; return ( program.toUpperCase() + ': ' + numqueries + ' ' + (numqueries > 1 ? 'queries' : 'query') + ", " + numquerydb + ' ' + (numquerydb > 1 ? 'databases' : 'database') ); }, downloads: function () { return (

Download FASTA, XML, TSV

); }, }); /** * Renders entire report. * * Composed of Query and Sidebar components. */ var Report = React.createClass({ // Kind of public API // /** * 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. // /** * Fetch results. */ fetch_results: function () { var intervals = [200, 400, 800, 1200, 2000, 3000, 5000]; $.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.fetch_results, interval); break; case 200: this.setState(jqXHR.responseJSON); break; case 500: SequenceServer.showErrorModal(jqXHR, function () {}); break; } }, this)); }, /** * Returns true if results have been fetched. * * A holding message is shown till results are fetched. */ isResultAvailable: function () { return this.state.queries.length >= 1; }, /** * 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); }, loading: function () { return (

  BLAST-ing


This can take some time depending on the size of your query and database(s). The page will update automatically when BLAST is done.

You can bookmark the page and come back to it later or share the link with someone.

); }, /** * Renders report overview. */ overview: function () { return (
                    {this.state.program_version}
                    

{ _.map(this.state.querydb, function (db) { return db.title; }).join(", ") }
Total: {this.state.stats.nsequences} sequences, {this.state .stats.ncharacters} characters

{ _.map(this.state.params, function (val, key) { return key + " " + val; }).join(", ") }
); }, /** * Renders results per query. */ results: function () { return (
{ this.shouldShowSidebar() && (
) }
{ this.overview() } {/* */} { _.map(this.state.queries, _.bind(function (query) { return ( ); }, this)) }
); }, /** * Affixes the sidebar. * * TODO: can't this be done with CSS? */ affixSidebar: function () { var $sidebar = $('.sidebar'); $sidebar.affix({ offset: { top: $sidebar.offset().top } }); }, /** * For the query in viewport, highlights corresponding entry in the index. */ setupScrollSpy: function () { $('body').scrollspy({target: '.sidebar'}); }, /** * Prevents folding of hits during text-selection. */ setupHitSelection: 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 $this.attr('data-toggle', 'collapse'); $this.find('.fa-chevron-down').toggleClass('fa-rotate-270'); } else { // user wants to select $this.attr('data-toggle', ''); } $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(); var $anchor = $(this); if ($anchor.is(':disabled')) return; var url = $anchor.attr('href'); $.get(url) .done(function (data) { window.location.href = url; }) .fail(function (jqXHR, status, error) { SequenceServer.showErrorModal(jqXHR, function () {}); }); }); }, // Life-cycle methods. // getInitialState: function () { return { search_id: '', program: '', program_version: '', queries: [], querydb: [], params: [], stats: [] }; }, render: function () { return (this.isResultAvailable() && this.results() || this.loading()); }, componentDidMount: function () { this.fetch_results(); }, /** * Locks Sidebar in its position. * Prevents folding of hits during text-selection */ componentDidUpdate: function () { this.affixSidebar(); this.setupScrollSpy(); this.setupHitSelection(); this.setupDownloadLinks(); } }); var Page = React.createClass({ render: function () { return (
); } }); React.render(, document.getElementById('view'));