import './sequenceserver' // for custom $.tooltip function
import React from 'react';
import _ from 'underscore';
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 HSP from './hsp';
import './sequence';
import * as Helpers from './visualisation_helpers'; // for toLetters
import Utils from './utils'; // to use as mixin in Hit and HitsTable
import showErrorModal from './error_modal';
/**
* 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)
);
}
}
/**
* Base component of report page. This component is later rendered into page's
* '#view' element.
*/
var Page = React.createClass({
render: function () {
return (
{/* Provide bootstrap .container element inside the #view for
the Report component to render itself in. */}
{/* Required by Grapher for SVG and PNG download */}
);
}
});
/**
* Renders entire report.
*
* Composed of Query and Sidebar components.
*/
var Report = React.createClass({
// Model //
getInitialState: function () {
this.fetchResults();
this.updateCycle = 0;
return {
search_id: '',
program: '',
program_version: '',
submitted_at: '',
num_queries: 0,
queries: [],
querydb: [],
params: [],
stats: []
};
},
/**
* Fetch results.
*/
fetchResults: function () {
var intervals = [200, 400, 800, 1200, 2000, 3000, 5000];
var component = this;
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;
}
});
}
poll();
},
/**
* Incrementally update state so that the rendering process is
* not overwhelemed when there are too many queries.
*/
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);
responseJSON.num_queries = queries.length;
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 (
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.
);
},
// Controller //
/**
* Returns true if results have been fetched.
*
* A holding message is shown till results are fetched.
*/
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++;
});
return !(cnt == this.state.queries.length);
},
/**
* 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;
},
/**
* 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.
*/
componentDidMount: function () {
// This sets up an event handler which enables users to select text
// from hit header without collapsing the hit.
this.preventCollapseOnSelection();
},
/**
* Called after each state change. Only a part of results DOM may be
* available after a state change.
*/
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();
},
/**
* Prevents folding of hits during text-selection, etc.
*/
/**
* Called after all results have been rendered.
*/
componentFinishedUpdating: function () {
this.shouldShowIndex() && this.setupScrollSpy();
},
/**
* Prevents folding of hits during text-selection.
*/
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
$this.attr('data-toggle', 'collapse');
// Get the element indicated in the data-target attribute
// and toggle the 'in' class for collapsing/expanding.
var target = $('#' + $this.attr('data-target'));
target.toggleClass('in');
$this.find('.fa-chevron-down').toggleClass('fa-rotate-270');
} else {
// user wants to select
$this.attr('data-toggle', '');
}
$this.off('mouseup mousemove', handler);
});
});
},
/**
* Affixes the sidebar.
*/
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'});
},
/**
* 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 enable 'Download FASTA/Alignment of
// selected' links.
if (checkbox.is(":checked")) {
$hit.find('.section-content').addClass('glow');
$('.download-alignment-of-selected').enable();
$('.download-fasta-of-selected').enable();
}
else {
$hit.find('.section-content').removeClass('glow');
}
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('');
}
},
});
/**
* 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 (
}
});
/**
* 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 (