public/js/hsp.js
import React, { createRef } from "react";
import _ from "underscore";
import Utils from "./utils";
import * as Helpers from "./visualisation_helpers";
var HSPComponents = {};
/**
* Alignment viewer.
*/
export default class HSP extends React.Component {
constructor(props) {
super(props);
this.hsp = props.hsp;
this.hspRef = createRef();
}
domID() {
return (
"Query_" +
this.props.query.number +
"_hit_" +
this.props.hit.number +
"_" +
this.props.hsp.number
);
}
hitDOM_ID() {
return "Query_" + this.props.query.number + "_hit_" + this.props.hit.number;
}
// Renders pretty formatted alignment.
render() {
return (
<div
className="hsp pt-px pb-5 border-l-2 border-transparent pl-1 -ml-1"
id={this.domID()}
ref={this.hspRef}
data-parent-hit={this.hitDOM_ID()}
>
<p className="m-0 p-0 rounded-none border-0 bg-inherit whitespace-pre-wrap break-keep text-sm text-neutral-500 font-semibold tracking-wide">
{this.props.showHSPNumbers &&
`${Helpers.toLetters(this.hsp.number)}. `}
{this.hspStats().map((s, i) => (
<span key={i}>{s}</span>
))}
</p>
{this.hspLines()}
</div>
);
}
componentDidMount() {
HSPComponents[this.domID()] = this;
this.draw();
}
draw() {
var charWidth = this.props.getCharacterWidth();
var containerWidth = $(this.hspRef.current).width();
this.chars = Math.floor((containerWidth - 4) / charWidth);
this.forceUpdate();
}
// See Query.shouldComponentUpdate. The same applies for hsp.
shouldComponentUpdate() {
return !this.props.hsp;
}
/**
* Returns an array of span elements or plain strings (React automatically
* adds span tag around strings). This array is passed as it is to JSX to be
* rendered just above the pairwise alignment (see render method).
*
* We cannot return a string from this method otherwise we wouldn't be able
* to use JSX elements to format text (like, superscript).
*/
hspStats() {
// An array to hold text or span elements that make up the line.
let line = [];
// Bit score and total score.
line.push(
`Score: ${Utils.inTwoDecimal(this.hsp.bit_score)} (${this.hsp.score}), `
);
// E value
line.push("E value: ");
line.push(Utils.inExponential(this.hsp.evalue));
line.push(", ");
// Identity
line.push([
`Identity: ${Utils.inFraction(
this.hsp.identity,
this.hsp.length
)} (${Utils.inPercentage(this.hsp.identity, this.hsp.length)}), `,
]);
// Positives (for protein alignment).
if (
this.props.algorithm === "blastp" ||
this.props.algorithm === "blastx" ||
this.props.algorithm === "tblastn" ||
this.props.algorithm === "tblastx"
) {
line.push(
`Positives: ${Utils.inFraction(
this.hsp.positives,
this.hsp.length
)} (${Utils.inPercentage(this.hsp.positives, this.hsp.length)}), `
);
}
// Gaps
line.push(
`Gaps: ${Utils.inFraction(
this.hsp.gaps,
this.hsp.length
)} (${Utils.inPercentage(this.hsp.gaps, this.hsp.length)})`
);
// Query coverage
//line.push(`Query coverage: ${this.hsp.qcovhsp}%, `)
switch (this.props.algorithm) {
case "tblastx":
line.push(
`, Frame: ${Utils.inFraction(this.hsp.qframe, this.hsp.sframe)}`
);
break;
case "blastn":
line.push(
`, Strand: ${this.hsp.qframe > 0 ? "+" : "-"} / ${
this.hsp.sframe > 0 ? "+" : "-"
}`
);
break;
case "blastx":
line.push(`, Query Frame: ${this.hsp.qframe}`);
break;
case "tblastn":
line.push(`, Hit Frame: ${this.hsp.sframe}`);
break;
}
return line;
}
/**
* Returns array of pre tags containing the three query, middle, and subject
* lines that together comprise one 'rendered line' of HSP.
*/
hspLines() {
// Space reserved for showing coordinates
var width = this.width();
// Number of residues we can draw per line is the total number of
// characters we can have in a line minus space required to show left
// and right coordinates minus 10 characters reserved for displaying
// the words Query, Subject and three blank spaces per line.
var chars = this.chars - 2 * width - 10;
// Number of lines of pairwise-alignment (i.e., each line consists of 3
// lines). We draw as many pre tags.
var lines = Math.ceil(this.hsp.length / chars);
var pp = [];
var nqseq = this.nqseq();
var nsseq = this.nsseq();
for (let i = 1; i <= lines; i++) {
let seq_start_index = chars * (i - 1);
let seq_stop_index = seq_start_index + chars;
let lqstart = nqseq;
let lqseq = this.hsp.qseq.slice(seq_start_index, seq_stop_index);
let lqend =
nqseq +
(lqseq.length - lqseq.split("-").length) *
this.qframe_unit() *
this.qframe_sign();
nqseq = lqend + this.qframe_unit() * this.qframe_sign();
let lmseq = this.hsp.midline.slice(seq_start_index, seq_stop_index);
let lsstart = nsseq;
let lsseq = this.hsp.sseq.slice(seq_start_index, seq_stop_index);
let lsend =
nsseq +
(lsseq.length - lsseq.split("-").length) *
this.sframe_unit() *
this.sframe_sign();
nsseq = lsend + this.sframe_unit() * this.sframe_sign();
pp.push(
<pre key={this.hsp.number + "," + i} className="pre-item m-0 p-0 rounded-none border-0 bg-inherit whitespace-pre-wrap break-keep mt-1 tracking-wider">
<span className="text-gray-500">
{`Query ${this.formatCoords(lqstart, width)} `}
</span>
<span>{lqseq}</span>
<span className="text-gray-500">{` ${lqend}`}</span>
<br />
<span className="text-gray-500">
{`${this.formatCoords("", width + 8)} `}
</span>
<span>{lmseq}</span>
<br />
<span className="text-gray-500">
{`Subject ${this.formatCoords(lsstart, width)} `}
</span>
<span>{lsseq}</span>
<span className="text-gray-500">{` ${lsend}`}</span>
<br />
</pre>
);
}
return pp;
}
// Width of the coordinate part of hsp lines. Essentially the length of
// the largest coordinate.
width() {
return _.max(
_.map(
[this.hsp.qstart, this.hsp.qend, this.hsp.sstart, this.hsp.send],
(n) => {
return n.toString().length;
}
)
);
}
// Alignment start coordinate for query sequence.
//
// This will be qstart or qend depending on the direction in which the
// (translated) query sequence aligned.
nqseq() {
switch (this.props.algorithm) {
case "blastp":
case "blastx":
case "tblastn":
case "tblastx":
return this.hsp.qframe >= 0 ? this.hsp.qstart : this.hsp.qend;
case "blastn":
// BLASTN is a bit weird in that, no matter which direction the query
// sequence aligned in, qstart is taken as alignment start coordinate
// for query.
return this.hsp.qstart;
}
}
// Alignment start coordinate for subject sequence.
//
// This will be sstart or send depending on the direction in which the
// (translated) subject sequence aligned.
nsseq() {
switch (this.props.algorithm) {
case "blastp":
case "blastx":
case "tblastn":
case "tblastx":
return this.hsp.sframe >= 0 ? this.hsp.sstart : this.hsp.send;
case "blastn":
// BLASTN is a bit weird in that, no matter which direction the
// subject sequence aligned in, sstart is taken as alignment
// start coordinate for subject.
return this.hsp.sstart;
}
}
// Jump in query coordinate.
//
// Roughly,
//
// qend = qstart + n * qframe_unit
//
// This will be 1 or 3 depending on whether the query sequence was
// translated or not.
qframe_unit() {
switch (this.props.algorithm) {
case "blastp":
case "blastn":
case "tblastn":
return 1;
case "blastx":
// _Translated_ nucleotide query against protein database.
case "tblastx":
// _Translated_ nucleotide query against translated
// nucleotide database.
return 3;
}
}
// Jump in subject coordinate.
//
// Roughly,
//
// send = sstart + n * sframe_unit
//
// This will be 1 or 3 depending on whether the subject sequence was
// translated or not.
sframe_unit() {
switch (this.props.algorithm) {
case "blastp":
case "blastx":
case "blastn":
return 1;
case "tblastn":
// Protein query against _translated_ nucleotide database.
return 3;
case "tblastx":
// Translated nucleotide query against _translated_
// nucleotide database.
return 3;
}
}
// If we should add or subtract qframe_unit from qstart to arrive at qend.
//
// Roughly,
//
// qend = qstart + (qframe_sign) * n * qframe_unit
//
// This will be +1 or -1, depending on the direction in which the
// (translated) query sequence aligned.
qframe_sign() {
return this.hsp.qframe >= 0 ? 1 : -1;
}
// If we should add or subtract sframe_unit from sstart to arrive at send.
//
// Roughly,
//
// send = sstart + (sframe_sign) * n * sframe_unit
//
// This will be +1 or -1, depending on the direction in which the
// (translated) subject sequence aligned.
sframe_sign() {
return this.hsp.sframe >= 0 ? 1 : -1;
}
/**
* Pad given coord with ' ' till its length == width. Returns undefined if
* width is not supplied.
*/
formatCoords(coord, width) {
if (width) {
let padding = width - coord.toString().length;
return Array(padding + 1)
.join(" ")
.concat([coord]);
}
}
spanCoords(text) {
return <span className="text-gray-700">{text}</span>;
}
}
// Redraw if window resized.
$(window).resize(
_.debounce(function () {
_.each(HSPComponents, (comp) => {
comp.draw();
});
}, 100)
);