import React, { Component, createRef } from 'react'; import _ from 'underscore'; import HitsOverview from './hits_overview'; import LengthDistribution from './length_distribution'; // length distribution of hits import Utils from './utils'; import { fastqToFasta } from './fastq_to_fasta'; import CollapsePreferences from './collapse_preferences'; /** * Query component displays query defline, graphical overview, length * distribution, and hits table. */ export class ReportQuery extends Component { // Each update cycle will cause all previous queries to be re-rendered. // We avoid that by implementing shouldComponentUpdate life-cycle hook. // The trick is to simply check if the components has recieved props // before. shouldComponentUpdate() { // If the component has received props before, query property will // be set on it. If it is, we return false so that the component // is not re-rendered. If the query property is not set, we return // true: this must be the first time react is trying to render the // component. return !this.props.query; } // Kind of public API // /** * Returns the id of query. */ domID() { return 'Query_' + this.props.query.number; } queryLength() { return this.props.query.length; } /** * Returns number of hits. */ numhits() { return this.props.query.hits.length; } headerJSX() { var meta = `length: ${this.queryLength().toLocaleString()}`; if (this.props.showQueryCrumbs) { meta = `query ${this.props.query.number}, ` + meta; } return

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

{meta}
; } hitsListJSX() { return
; } noHitsJSX() { return
****** No BLAST hits found ******
; } render() { return (
{this.headerJSX()} {this.numhits() && this.hitsListJSX() || this.noHitsJSX()}
); } } /** * Query widget for Search component. */ export class SearchQueryWidget extends Component { constructor(props) { super(props); this.state = { value: $('input#input_sequence').val() || '' }; this.value = this.value.bind(this); this.clear = this.clear.bind(this); this.focus = this.focus.bind(this); this.isEmpty = this.isEmpty.bind(this); this.textarea = this.textarea.bind(this); this.controls = this.controls.bind(this); this.handleInput = this.handleInput.bind(this); this.hideShowButton = this.hideShowButton.bind(this); this.indicateError = this.indicateError.bind(this); this.indicateNormal = this.indicateNormal.bind(this); this.type = this.type.bind(this); this.guessSequenceType = this.guessSequenceType.bind(this); this.preProcessSequence = this.preProcessSequence.bind(this); this.notify = this.notify.bind(this); this.textareaRef = createRef(); this.controlsRef = createRef(); } // LIFECYCLE Methods componentDidMount() { $('body').click(function () { $('[data-notifications] [data-role=notification].active').hide('drop', { direction: 'up' }).removeClass('active'); }); } componentDidUpdate() { this.hideShowButton(); this.preProcessSequence(); this.props.onSequenceChanged(this.residuesCount()); var type = this.type(); if (!type || type !== this._type) { this._type = type; this.notify(type); this.props.onSequenceTypeChanged(type); } } // Kind of public API. // /** * Returns query sequence if no argument is provided (or null or undefined * is provided as argument). Otherwise, sets query sequence to the given * value and returns `this`. * * Default/initial state of query sequence is an empty string. Caller must * explicitly provide empty string as argument to "reset" query sequence. */ value(val) { if (val == null) { // i.e., val is null or undefined return this.state.value; } else { this.setState({ value: val }); return this; } } residuesCount() { const sequence = this.value(); const lines = sequence.split('\n'); const residuesCount = lines.reduce((count, line) => { if (!line.startsWith('>')) { return count + line.length; } return count; }, 0); return residuesCount; } /** * Clears textarea. Returns `this`. * * Clearing textarea also causes it to be focussed. */ clear() { return this.value('').focus(); } /** * Focuses textarea. Returns `this`. */ focus() { this.textarea().focus(); return this; } /** * Returns true if query is absent ('', undefined, null), false otherwise. */ isEmpty() { return !this.value(); } // Internal helpers. // textarea() { return $(this.textareaRef.current); } controls() { return $(this.controlsRef.current); } handleInput(evt) { this.value(evt.target.value); } /** * Hides or shows 'clear sequence' button. * * Rendering the 'clear sequence' button takes into account presence or * absence of a scrollbar. * * Called by `componentDidUpdate`. */ hideShowButton() { if (!this.isEmpty()) { // Calculation below is based on - // http://chris-spittles.co.uk/jquery-calculate-scrollbar-width/ // FIXME: can reflow be avoided here? var textareaNode = this.textarea()[0]; var sequenceControlsRight = textareaNode.offsetWidth - textareaNode.clientWidth; this.controls().css('right', sequenceControlsRight + 17); this.controls().removeClass('hidden'); } else { // FIXME: what are lines 1, 2, & 3 doing here? this.textarea().parent().removeClass('has-error'); this.$sequenceFile = $('#sequence-file'); this.$sequenceFile.empty(); this.controls().addClass('hidden'); } } /** * Put red border around textarea. */ indicateError() { this.textarea().parent().addClass('has-error'); } /** * Put normal blue border around textarea. */ indicateNormal() { this.textarea().parent().removeClass('has-error'); } /** * Returns type of the query sequence (nucleotide, protein, mixed). * * Query widget supports executing a callback when the query type changes. * Components interested in query type should register a callback instead * of directly calling this method. */ type() { let sequence = this.value().trim(); // FASTQ detected, but we don't know if conversion has succeeded yet // will notify separately if it does if (sequence.startsWith('@') ) { return undefined; } var sequences = sequence.split(/>.*/); var type, tmp; for (var i = 0; i < sequences.length; i++) { tmp = this.guessSequenceType(sequences[i]); // could not guess the sequence type; try the next sequence if (!tmp) { continue; } if (!type) { // successfully guessed the type of atleast one sequence type = tmp; } else if (tmp !== type) { // user has mixed different type of sequences return 'mixed'; } } return type; } preProcessSequence() { var sequence = this.value(); var updatedSequence = fastqToFasta(sequence); if (sequence !== updatedSequence) { this.value(updatedSequence); this.notify('fastq'); } } /** * Guesses and returns the type of the given sequence (nucleotide, * protein). */ guessSequenceType(sequence) { // remove 'noisy' characters sequence = sequence.replace(/[^A-Z]/gi, ''); // non-letter characters sequence = sequence.replace(/[NX]/gi, ''); // ambiguous characters // can't determine the type of ultrashort queries if (sequence.length < 10) { return undefined; } // count the number of putative NA var putative_NA_count = 0; for (var i = 0; i < sequence.length; i++) { if (sequence[i].match(/[ACGTU]/i)) { putative_NA_count += 1; } } var threshold = 0.9 * sequence.length; return putative_NA_count > threshold ? 'nucleotide' : 'protein'; } notify(type) { this.indicateNormal(); clearTimeout(this.notification_timeout); // $('[data-notifications] [data-role=notification].active').hide().removeClass('active'); if (type) { $('#' + type + '-sequence-notification').show('drop', { direction: 'up' }).addClass('active'); this.notification_timeout = setTimeout(function () { $('[data-notifications] [data-role=notification].active').hide('drop', { direction: 'up' }).removeClass('active'); }, 5000); if (type === 'mixed') { this.indicateError(); } } } render() { return (
); } } /** * Renders summary of all hits per query in a tabular form. */ class HitsTable extends Component { constructor(props) { super(props); this.name = 'Hit sequences producing significant alignments'; this.collapsePreferences = new CollapsePreferences(this); this.state = { collapsed: this.collapsePreferences.preferenceStoredAsCollapsed() }; } tableJSX() { var hasName = _.every(this.props.query.hits, function (hit) { return hit.sciname !== ''; }); // Width of sequence column is 55% when species name is not shown and // query coverage is. var seqwidth = 55; // If we are going to show species name, then reduce the width of // sequence column by the width of species column. if (hasName) seqwidth -= 15; // If we are not going to show query coverage (i.e. for imported XML), // then increase the width of sequence column by the width of coverage // column. if (this.props.imported_xml) seqwidth += 15; return {hasName && } {!this.props.imported_xml && } { _.map(this.props.query.hits, _.bind(function (hit) { return ( {hasName && } {!this.props.imported_xml && } ); }, this)) }
# Similar sequencesSpeciesQuery coverage (%)Total score E value Identity (%)
{hit.number + '.'} {hit.id} {hit.title} {hit.sciname} {hit.qcovs}{hit.total_score} {Utils.inExponential(hit.hsps[0].evalue)} {Utils.inPercentage(hit.hsps[0].identity, hit.hsps[0].length)}
; } render() { return (

this.collapsePreferences.toggleCollapse()}> {this.collapsePreferences.renderCollapseIcon()} {this.name}

{!this.state.collapsed && this.tableJSX()}
); } }