web-components/i-gene/i-gene.js

1136 lines
38 KiB
JavaScript
Raw Normal View History

2024-11-20 18:43:24 +01:00
class iGeneIndexer {
#parsedTSV = null;
#genome = null;
#pam = null;
#helper = null;
#currentDataLength = 0;
#pamOutDistances = null;
#pamInDistances = null;
#pamNoneDistances = null;
#chromosome = null;
#minIndex = null;
#maxIndex = null;
#genomeInterestingExtract = null;
static restrictionEnzymes = null;
static {
iGeneIndexer.fetchAndParseAllRestrictionEnzymesFileFromREBASE().then(reArray => {
iGeneIndexer.restrictionEnzymes = reArray;
}).catch(err => alert(err));
}
constructor(parsedTSV, genome, pam, helper) {
this.#parsedTSV = parsedTSV;
this.#genome = genome;
this.#pam = pam;
this.#helper = helper;
this.#currentDataLength = parsedTSV.length;
// Preparing distance matrices
this.#pamOutDistances = new Array(this.#currentDataLength);
this.#pamInDistances = new Array(this.#currentDataLength);
this.#pamNoneDistances = new Array(this.#currentDataLength);
for(var i = 0; i < this.#currentDataLength; i++) {
this.#pamOutDistances[i] = new Array(this.#currentDataLength);
this.#pamOutDistances[i].fill(NaN);
this.#pamInDistances[i] = new Array(this.#currentDataLength);
this.#pamInDistances[i].fill(NaN);
this.#pamNoneDistances[i] = new Array(this.#currentDataLength);
this.#pamNoneDistances[i].fill(NaN);
}
this.#parsedTSV.forEach((arrayItem, aiIndex) => {
const rank = this.getHelper().hasRank() ? this.getHelper().getRank(arrayItem) : aiIndex + 1;
// Make every entry to have a distance of zero from itself
this.#pamOutDistances[rank - 1][rank - 1] = 0;
this.#pamInDistances[rank - 1][rank - 1] = 0;
this.#pamNoneDistances[rank - 1][rank - 1] = 0;
const sequence = this.getHelper().getSequence(arrayItem);
const chromosome = this.getHelper().hasChromosome() ? this.getHelper().getChromosome(arrayItem) : this.getHelper().getDefaultChromosome();
const index = this.getHelper().getIndex(arrayItem);
const strand = this.getHelper().getStrand(arrayItem);
parsedTSV.forEach((comparedEntry, ceIndex) => {
const comparedEntryRank = this.getHelper().hasRank() ? this.getHelper().getRank(comparedEntry) : ceIndex + 1;
if (comparedEntryRank == rank) {
// Skipping comparison with itself
return;
}
const comparedEntrySequence = this.getHelper().getSequence(comparedEntry);
const comparedEntryStrand = this.getHelper().getStrand(comparedEntry);
const comparedEntryChromosome = this.getHelper().hasChromosome() ? this.getHelper().getChromosome(comparedEntry) : this.getHelper().getDefaultChromosome();
const comparedEntryIndex = this.getHelper().getIndex(comparedEntry);
// Checking chromosome, should be an impossible case but...
if (chromosome != null && comparedEntryChromosome != null && chromosome != comparedEntryChromosome) {
console.error("Different chromosome in genomic location, expected: " + chromosome + ", found: " + comparedEntryChromosome)
return;
}
this.#pamOutDistances[rank - 1][comparedEntryRank - 1] = this.computePamOutDistance(index, strand, sequence, comparedEntryIndex, comparedEntryStrand, comparedEntrySequence);
this.#pamInDistances[rank - 1][comparedEntryRank - 1] = this.computePamInDistance(index, strand, sequence, comparedEntryIndex, comparedEntryStrand, comparedEntrySequence);
this.#pamNoneDistances[rank - 1][comparedEntryRank - 1] = this.computePamNoneDistance(index, strand, sequence, comparedEntryIndex, comparedEntryStrand, comparedEntrySequence);
});
if (this.#chromosome == null) {
this.#chromosome = chromosome;
}
if (this.#minIndex == null || index < this.#minIndex) {
this.#minIndex = index;
}
if (this.#maxIndex == null || index > this.#maxIndex) {
this.#maxIndex = index;
}
});
this.fetchActualGenomePartFromUSCS().then(dna => {
this.#genomeInterestingExtract = dna;
}).catch(err => alert(err));
}
getGenome() {
return this.#genome;
}
getChromosome() {
return this.#chromosome;
}
getPAM() {
return this.#pam;
}
getHelper() {
return this.#helper;
}
getMinIndex() {
return this.#minIndex;
}
getMaxIndex() {
return this.#maxIndex;
}
getGenomeInterestingExtract() {
return this.#genomeInterestingExtract;
}
computeStartIndex(index, strand, sequence) {
if (strand == '+') {
return index;
} else {
return (index + sequence.length + (this.getHelper().hasPAMInSequence() ? 0 : this.#pam.length)) - 1;
}
}
computeEndIndex(index, strand, sequence) {
if (strand == '+') {
return (index + sequence.length - (this.getHelper().hasPAMInSequence() ? this.#pam.length : 0)) - 1;
} else {
return (index + (this.getHelper().hasPAMInSequence() ? this.#pam.length : 0)) - 1; // The PAM are the first n basis and have to be subtracted in this case
}
}
computePamOutDistance(index, strand, sequence, comparedEntryIndex, comparedEntryStrand, comparedEntrySequence) {
if (strand != comparedEntryStrand) {
const start = this.computeStartIndex(index, strand, sequence);
const comparedStart = this.computeStartIndex(comparedEntryIndex, comparedEntryStrand, comparedEntrySequence);
var distance = 0;
if (strand == '+') {
distance = start - comparedStart;
} else {
distance = comparedStart - start;
}
if (distance > 0) {
return distance;
}
}
return NaN;
}
computePamInDistance(index, strand, sequence, comparedEntryIndex, comparedEntryStrand, comparedEntrySequence) {
if (strand != comparedEntryStrand) {
const end = this.computeEndIndex(comparedEntryIndex, comparedEntryStrand, comparedEntrySequence);
const comparedEnd = this.computeEndIndex(index, strand, sequence);
var distance = 0;
if (strand == '+') {
distance = comparedEnd - end;
} else {
distance = end - comparedEnd;
}
if (distance > 0) {
return distance;
}
}
return NaN;
}
computePamNoneDistance(index, strand, sequence, comparedEntryIndex, comparedEntryStrand, comparedEntrySequence) {
const firstStart = this.computeStartIndex(index, strand, sequence);
const firstEnd = this.computeEndIndex(index, strand, sequence);
const secondStart = this.computeStartIndex(comparedEntryIndex, comparedEntryStrand, comparedEntrySequence);
const secondEnd = this.computeEndIndex(comparedEntryIndex, comparedEntryStrand, comparedEntrySequence);
return Math.max(firstStart, firstEnd, secondStart, secondEnd) - Math.min(firstStart, firstEnd, secondStart, secondEnd);
}
getMatchesOf(distancesMatrix, rank, distance) {
const matches = new Map();
for (var i = 0; i < this.#currentDataLength; i++) {
const currentDistance = distancesMatrix[rank - 1][i];
if ((i + 1) != rank && currentDistance != NaN && currentDistance > 0 && currentDistance <= distance) {
matches.set(i + 1, currentDistance);
}
}
return matches;
}
getPamOutMatches(rank, distance) {
return this.getMatchesOf(this.#pamOutDistances, rank, distance);
}
getPamInMatches(rank, distance) {
return this.getMatchesOf(this.#pamInDistances, rank, distance);
}
getNoneMatches(rank, distance) {
return this.getMatchesOf(this.#pamNoneDistances, rank, distance);
}
evaluateMatches(rank, restrictionEnzymeSequence, distance) {
const minIndex = this.getMinIndex();
// Getting the RE matches relative index and mapping to their absolute index by adding the chromosome extract start index (the minIndex)
const reMatchesIndexes = this.getAllMatches(
iGeneIndexer.iub2regexp(restrictionEnzymeSequence),
this.getGenomeInterestingExtract()
).map(m => {return {index: minIndex + m.index, value: m.value}; });
const reReverseStrandMatchesIndexes = this.getAllMatches(
iGeneIndexer.iub2regexp(restrictionEnzymeSequence),
iGeneIndexer.reverseComplementOf(this.getGenomeInterestingExtract())
).map(m => {return {index: minIndex + m.index + m.value.length, value: m.value}; });
const matches = [];
let gRNA = null;
this.#parsedTSV.forEach((arrayItem, aiIndex) => {
const aiRank = this.getHelper().hasRank() ? this.getHelper().getRank(arrayItem) : aiIndex + 1;
if (rank == aiRank) {
gRNA = arrayItem;
}
});
const sequence = this.getHelper().getSequence(gRNA);
const index = this.getHelper().getIndex(gRNA);
const strand = this.getHelper().getStrand(gRNA);
const start = this.computeStartIndex(index, strand, sequence);
const end = this.computeEndIndex(index, strand, sequence);
if (strand == '+') {
reMatchesIndexes.forEach((reMatch) => {
const reStartDistanceFromEnd = reMatch.index - end;
if (reStartDistanceFromEnd > 0 && reStartDistanceFromEnd <= distance) {
matches.push({chromosome: this.#chromosome, index: reMatch.index, sequence: reMatch.value, strand: '+', distance: reStartDistanceFromEnd});
}
const reEndDistanceFromStart = start - (reMatch.index + reMatch.value.length);
if (reEndDistanceFromStart > 0 && reEndDistanceFromStart <= distance) {
matches.push({chromosome: this.#chromosome, index: reMatch.index, sequence: reMatch.value, strand: '+', distance: -1 * reEndDistanceFromStart});
}
});
} else {
reReverseStrandMatchesIndexes.forEach((reRSMatch) => {
const reStartDistanceFromEnd = end - (reRSMatch.index + reRSMatch.value.length);
if (reStartDistanceFromEnd > 0 && reStartDistanceFromEnd <= distance) {
matches.push({chromosome: this.#chromosome, index: reRSMatch.index, sequence: reRSMatch.value, strand: '+', distance: reStartDistanceFromEnd});
}
const reEndDistanceFromStart = reRSMatch.index - start;
if (reEndDistanceFromStart > 0 && reEndDistanceFromStart <= distance) {
matches.push({chromosome: this.#chromosome, index: reRSMatch.index, sequence: reRSMatch.value, strand: '+', distance: -1 * reEndDistanceFromStart});
}
});
}
return matches;
}
getAllMatches(searchStr, str) {
var searchStrLen = searchStr.length;
if (searchStrLen == 0) {
return [];
}
const regEx = new RegExp(searchStr, "g");
const matches = [];
let match;
while ((match = regEx.exec(str)) !== null) {
matches.push({index: match.index, value: match[0]});
}
return matches;
}
async fetchActualGenomePartFromUSCS() {
return this.fetchGenomePartFromUSCS(this.getGenome(), this.getChromosome(), this.getMinIndex(), this.getMaxIndex());
}
/*
Respone JSON example:
{
"downloadTime": "2023:05:23T16:54:33Z",
"downloadTimeStamp": 1684860873,
"genome": "danRer10",
"chrom": "chr15",
"start": 43776601,
"end": 43794868,
"dna": "GTGCGCCGGA..."
}
*/
async fetchGenomePartFromUSCS(genome, chromosome, start, end) {
const resp = await iGeneIndexer.fetchViaCORSProxy('https://genome-euro.ucsc.edu/cgi-bin/hubApi/getData/sequence?genome=' + genome + '&chrom=' + chromosome + '&start=' + start + '&end=' + end);
if (resp.ok) {
let json = await resp.json();
return json.dna;
} else {
let json = await resp.json();
throw "Error downloading genome part from USCS ([" + json.statusCode + ' - ' + json.statusMessage + '] - ' + json.error.split(' for endpoint')[0] + ')';
}
}
/*
Respone JSON example:
{
"downloadTime": "2023:05:23T17:11:47Z",
"downloadTimeStamp": 1684861907,
"genome": "danRer10",
"dataTime": "2015-01-22T19:35:19",
"dataTimeStamp": 1421951719,
"chromCount": 1061,
"chromosomes": {
"chr4": 76625712,
"chrUn_KN150525v1": 650,
"chrUn_KN150247v1": 728,
[...]
"chr3": 62385949,
"chr5": 71715914,
"chr7": 74082188
}
}
*/
static async fetchGenomeChromosomesFromUSCS(genome) {
const resp = await iGeneIndexer.fetchViaCORSProxy('https://genome-euro.ucsc.edu/cgi-bin/hubApi/list/chromosomes?genome=' + genome);
if (resp.ok) {
let json = await resp.json();
return json.chromosomes;
} else {
throw "Error downloading '" + genome + "' genome's chromosomes from USCS";
}
}
static async loadAllRestrictionEnzymesFileFromREBASE() {
const allEnzymesResponse = await iGeneIndexer.fetchViaCORSProxy("http://rebase.neb.com/rebase/link_allenz");
if (allEnzymesResponse.ok) {
return await allEnzymesResponse.text();
} else {
const message = await allEnzymesResponse.text();
throw "Error downloading Restriction Enzymes list file from REBASE\n\n[" + allEnzymesResponse.status + (allEnzymesResponse.status == 404 ? ' - Not found' : '') + '] Message from service: ' + message;
}
}
static async fetchAndParseAllRestrictionEnzymesFileFromREBASE() {
return await this.loadAllRestrictionEnzymesFileFromREBASE().then(allEnzymesText => {
const enzymesSequence = allEnzymesText.substring(allEnzymesText.indexOf("<1>")).split("References:\n")[0].trim();
const enzymesToParseLines = enzymesSequence.split("\n");
const enzymes = [];
for (let i = 0; i < enzymesToParseLines.length; i += 9) {
enzymes.push({name: enzymesToParseLines[i].substring(3), sequence: enzymesToParseLines[i + 4].substring(3) });
}
return enzymes;
}
);
}
/**
* IUB2regexp
*
* A function that, given a sequence with IUB ambiguity codes,
* outputs a translation with IUB codes changed to a regular expression
*
* It is based on the IUB ambiguity codes:
* (Eur. J. Biochem. 150: 1-5, 1985):
* R = G or A
* Y = C or T
* M = A or C
* K = G or T
* S = G or C
* W = A or T
* B = not A (C or G or T)
* D = not C (A or G or T)
* H = not G (A or C or T)
* V = not T (A or C or G)
* N = A or C or G or T
*
*/
static iub2regexp(iub) {
const iub2characterOrRegexp = {
'A' : 'A',
'C' : 'C',
'G' : 'G',
'T' : 'T',
'R' : '[GA]',
'Y' : '[CT]',
'M' : '[AC]',
'K' : '[GT]',
'S' : '[GC]',
'W' : '[AT]',
'B' : '[CGT]',
'D' : '[AGT]',
'H' : '[ACT]',
'V' : '[ACG]',
'N' : '[ACGT]'
};
// Remove the ^ signs from the recognition sites
iub = iub.replace('\^', '');
let regularExpression = '';
// Translate each character in the iub sequence
[...iub].forEach(c => regularExpression += iub2characterOrRegexp[c]);
return regularExpression;
}
/**
* Returns the complement of a base, cosnidering the complete IUB notations
*
* Info source: https://arep.med.harvard.edu/labgc/adnan/projects/Utilities/revcomp.html
*
* @param {*} base the base to complement in IUB notation
* @returns the complement base (e.g.: For 'A' returns 'T')
*/
static complement(base) {
return { A: 'T', T: 'A', G: 'C', C: 'G', Y: 'R', R: 'Y', S: 'S', W: 'W', K: 'M', M: 'K', B: 'V', D: 'H', H: 'D', V: 'B', N: 'N'}[base];
}
/**
* Converts a sequence to its reverse complement, considering the complete IUB notations by using the `complement(base)` function.
* @param {*} sequence the sequence to be converted
* @returns the reverse complement of the sequence
*/
static reverseComplementOf(sequence) {
return sequence.split('').reverse().map(iGeneIndexer.complement).join('');
}
static fetchViaCORSProxy(url) {
return fetch("https://corsproxy.io/?" + encodeURIComponent(url));
}
}
class AbstractImporter extends HTMLElement {
#rootdoc = null;
#headers = null
#results = null;
#indexer = null;
#helper = null;
constructor(helper){
super()
this.#helper = helper;
this.#rootdoc = this.attachShadow({ "mode" : "open"})
this.render()
}
getRootDoc() {
return this.#rootdoc;
}
render(){
this.getRootDoc().innerHTML = ``;
}
parseTSV(tsvString) {
const results = tsvString.split("\n");
this.#headers = results[0].split("\t");
this.#results = [];
results.forEach((line, i)=>{ if(i > 0 && line.trim().length > 0) this.#results.push(line.split("\t")) });
}
importTSVExport(file, genome, pam) {
const reader = new FileReader();
reader.addEventListener("load", () => {
this.parseTSV(reader.result);
this.setIndexer(new iGeneIndexer(this.#results, genome, pam, this.getHelper()));
this.enableData();
})
reader.readAsText(file);
}
importTSVURL(tsvURL, genome, pam) {
fetch(tsvURL).then(resp => {
if (resp.ok) {
resp.text().then(textData => {
this.parseTSV(textData);
this.setIndexer(new iGeneIndexer(this.#results, genome, pam, this.getHelper()));
this.enableData();
})
} else {
resp.text().then(page => {
throw "Error loading TSV from URL: " + tsvURL + " (" + page + ")";
});
}
});
}
enableData() {
}
#API
setIndexer(indexer) {
this.#indexer = indexer;
}
getIndexer() {
return this.#indexer;
}
getHelper() {
return this.#helper;
}
getHeader() {
// Abstract, to be ovverrided in subclass
}
getRenderer(filter, output){
if (filter.mode == 're') {
if(output === "html") {
return new DefaultHTMLRETableRenderer(this, filter);
} else if(output === "tsv") {
return alert("This is not supported in R.E. mode");
} else if(output === "tsv2") {
return new DefaultTSVRETableRenderer(this, filter, "tsv2");
}
} else {
if(output === "html") {
return new DefaultHTMLDimerTableRenderer(this, filter);
} else if(output === "tsv") {
return new DefaultTSVDimerTableRenderer(this, filter);
} else if(output === "tsv2") {
return new DefaultTSVDimerTableRenderer(this, filter, "tsv2");
}
}
}
setTableHeaders(headers){
this.#headers = headers;
}
getTableHeaders(){
return this.#headers;
}
hasRank() {
return this.getHelper().hasRank();
}
getRank(line) {
return this.getHelper().getRank(line);
}
setTableLines(results){
this.#results = results;
}
getTableLines(){
return this.#results;
}
getTableLine(i){
return this.#results[i];
}
getTableLineByRank(r){
return this.#results[r-1];
}
getMatches(pam, rank, distance){
if(pam === "in"){
return this.getIndexer().getPamInMatches(rank, distance)
} else if(pam === "out"){
return this.getIndexer().getPamOutMatches(rank, distance)
} else {
var m = new Map();
this.getIndexer().getPamInMatches(rank, distance).forEach((v, k) => { m.set(k, v) })
this.getIndexer().getPamOutMatches(rank, distance).forEach((v, k) => { m.set(k, v) })
this.getIndexer().getNoneMatches(rank, distance).forEach((v, k) => { m.set(k, v) })
return m;
}
}
getREMatchesForRankAt(rank, re, distance) {
return this.getIndexer().evaluateMatches(rank, re, distance);
}
getDatadisplay() {
return document.querySelector("igene-data-display");
}
}
class AbstractHelper {
#defaultChromosome
constructor() {
if (this.constructor == AbstractHelper) {
throw new Error("AbstractHelper class can't be instantiated");
}
}
hasPAMInSequence() {
throw new Error("Method 'hasPAMInSequence' must be implementd");
}
hasRank() {
throw new Error("Method 'hasRank' must be implementd");
}
getRank(line) {
throw new Error("Method 'getRank' must be implementd");
}
getSequence(line) {
throw new Error("Method 'getTargetSequence' must be implementd");
}
hasChromosome() {
throw new Error("Method 'hasChromosome' must be implementd");
}
getChromosome(line) {
throw new Error("Method 'getChromosome' must be implementd");
}
getIndex(line) {
throw new Error("Method 'getIndex' must be implementd");
}
getStrand(line) {
throw new Error("Method 'getStrand' must be implementd");
}
setDefaultChromosome(chromosome) {
this.#defaultChromosome = chromosome;
}
getDefaultChromosome() {
return this.#defaultChromosome;
}
}
class ChopChopHelper extends AbstractHelper {
hasPAMInSequence() {
return true;
}
hasRank() {
return true;
}
getRank(line) {
return +line[0];
}
getSequence(line) {
return line[1];
}
hasChromosome() {
return true;
}
getChromosome(line) {
return line[2].split(':')[0];
}
getIndex(line) {
return +line[2].split(':')[1];
}
getStrand(line) {
return line[3];
}
}
class CrisprscanHelper extends AbstractHelper {
constructor() {
super();
}
hasPAMInSequence() {
return true;
}
hasRank() {
return false;
}
getSequence(line) {
return line[5];
}
hasChromosome() {
return false;
}
getChromosome(line) {
return null;
}
getIndex(line) {
return +line[1];
}
getStrand(line) {
return line[3];
}
}
class AbstractTableRenderer{
constructor(){}
renderTable(){
alert("Not yet implemented")
}
renderHeaders(){
alert("Not yet implemented")
}
renderRows(){
alert("Not yet implemented")
}
renderMatchRows(){
alert("Not yet implemented")
}
}
class DefaultHTMLDimerTableRenderer extends AbstractTableRenderer {
#source = null
#output = ""
#filter = null
constructor(source, filter){
super()
this.#source = source
this.#filter = filter
}
getOutput(){
return this.#output
}
renderTable(){
const s = this.#source
this.#output = `
<table class="table table-bordered">
<thead>
${this.renderHeaders()}
</thead>
<tbody>
${this.renderRows()}
</tbody>
</table>
`
return this.#output
}
renderHeaders(){
const s = this.#source
return `
<tr class="table-dark">
${s.hasRank() ? `` : `<th>Rank</th>`}${s.getTableHeaders().reduce((acc, h)=>`${acc}<th>${h}</th>`, "")}
</tr>
`
}
renderRows(){
const s = this.#source
const rows = s.getTableLines()
var outputs = []
const suffix = "</tr>"
for(let i=0; i < rows.length; i++){
const row = rows[i]
const rankedrow = s.hasRank() ? row : [i + 1].concat(row);
var prefix = ""
var mrows = []
const matches = s.getMatches(this.#filter.mode, rankedrow[0], this.#filter.distance)
const tds = rankedrow.reduce((acc, d, i)=>{
if(i === 0) return acc + `<td class="rank-td"><div class="d-flex justify-content-between"><span>${d}</span><span title="match count" class="badge bg-primary">${matches.size}</span></div></td>`;
return acc + `<td><span>${d}</span></td>`
}, "")
if(matches.size === 0 && !this.#filter.zeromatches){
prefix = `<tr class="d-none table-primary table-row-nomatch" data-rank="${rankedrow[0]}">`
} else if(matches.size > 0){
prefix = `<tr class="table-primary table-row-match" data-rank="${rankedrow[0]}">`
mrows = this.renderMatchRows(rankedrow, matches)
}
outputs.push(prefix + tds + suffix)
outputs = outputs.concat(mrows)
}
return outputs.join("");
}
renderMatchRows(pivot, matches){
const s = this.#source
var prefix = `<tr class="table-light d-none" data-rel-rank="${pivot[0]}">`
const outputs = []
for (const [key, value] of matches) {
const row = s.getTableLineByRank(key)
const rankedrow = s.hasRank() ? row : [key].concat(row);
if(row){
const tds = row.reduce((acc, d, ind)=>{
if(ind === 0) return acc + `<td class="rank-td"><div class="d-flex" style="padding-left:5px;justify-content:space-between"><span>${key}</span><span title="distance" class="badge bg-secondary">${value}</span></div></td>`;
return acc + `<td><span>${d}</span></td>`
}, prefix)
outputs.push(tds + `</tr>`)
}
}
return outputs
}
}
class CrisprScanHTMLDimerTableRenderer extends DefaultHTMLDimerTableRenderer {
#source = null
constructor(source, filter){
super(source, filter)
this.#source = source
}
renderMatchRows(pivot, matches){
const s = this.#source
var prefix = `<tr class="table-light d-none" data-rel-rank="${pivot[0]}">`
const outputs = []
for (const [key, value] of matches) {
const row = s.getTableLineByRank(key)
const rankedrow = s.hasRank() ? row : [key].concat(row);
if(row){
const tds = row.reduce((acc, d, ind)=>{
if(ind === 0) return acc + `
<td class="rank-td"><div class="d-flex" style="padding-left:5px;justify-content:space-between"><span>${key}</span><span title="distance" class="badge bg-secondary">${value}</span></div></td>
<td><!--empty cell for aligning with name attribute--></td>
`;
return acc + `<td><span>${d}</span></td>`
}, prefix)
outputs.push(tds + `</tr>`)
}
}
return outputs
}
}
class DefaultHTMLRETableRenderer extends AbstractTableRenderer {
#source = null
#output = ""
#filter = null
constructor(source, filter){
super(source, filter)
this.#source = source
this.#filter = filter
}
getOutput(){
return this.#output
}
renderTable(){
if (this.#filter.enzyme != '') {
const s = this.#source
this.#output = `
<table class="table table-bordered">
<thead>
${this.renderHeaders()}
</thead>
<tbody>
${this.renderRows()}
</tbody>
</table>
`
return this.#output
} else {
return `<h5>Please, select the restriction enzyme from the list</h5>`;
}
}
renderHeaders(){
const s = this.#source
return `
<tr class="table-dark">
${s.hasRank() ? `` : `<th>Rank</th>`}${s.getTableHeaders().reduce((acc, h)=>`${acc}<th>${h}</th>`, "")}
</tr>
`
}
renderRows(){
const s = this.#source
const rows = s.getTableLines()
var outputs = []
const suffix = "</tr>"
for(let i=0; i < rows.length; i++){
const row = rows[i]
const rankedrow = s.hasRank() ? row : [i + 1].concat(row);
var prefix = ""
var mrows = []
const matches = s.getREMatchesForRankAt(rankedrow[0], this.#filter.enzyme, this.#filter.distance);
const tds = rankedrow.reduce((acc, d, i)=>{
if(i === 0) return acc + `<td class="rank-td"><div class="d-flex justify-content-between"><span>${d}</span><span title="match count" class="badge bg-primary">${matches.length}</span></div></td>`;
return acc + `<td><span>${d}</span></td>`
}, "")
if(matches.length === 0 && !this.#filter.zeromatches){
prefix = `<tr class="d-none table-primary table-row-nomatch" data-rank="${rankedrow[0]}">`
} else if(matches.length > 0){
prefix = `<tr class="table-primary table-row-match" data-rank="${rankedrow[0]}">`
mrows = this.renderMatchRows(rankedrow, matches)
}
outputs.push(prefix + tds + suffix)
outputs = outputs.concat(mrows)
}
return outputs.join("")
}
renderMatchRows(pivot, matches){
const s = this.#source
var outputs = []
matches.forEach(match => {
outputs.push(
`<tr class="table-light d-none" data-rel-rank="${pivot[0]}">
<td class="rank-td"><div class="d-flex" style="padding-left:5px;justify-content:space-between"><span>&nbsp;</span><span title="distance" class="badge bg-secondary">${match.distance}</span></div></td>
<td colspan="5">
<span>${match.sequence}</span>
<span>at ${match.index}</span>
<span>on strand ${match.strand}</span>
</td>
</tr>`);
});
/*matches.forEach(match => {
outputs.push(
`<tr class="table-light d-none" data-rel-rank="${pivot[0]}">
<td class="rank-td"><div class="d-flex" style="padding-left:5px;justify-content:space-between"><span>&nbsp;</span><span title="distance" class="badge bg-secondary">${match.distance}</span></div></td>
<td><span>${match.sequence}</span></td>
<td><span>${match.index}</span></td>
<td><span>${match.strand}</span></td>
</tr>`);
});*/
return outputs
}
}
class DefaultTSVDimerTableRenderer extends AbstractTableRenderer {
#source = null
#output = ""
#filter = null
#sep = "\t"
#strategy
constructor(source, filter, strategy){
super()
this.#source = source
this.#filter = filter
this.#strategy = strategy
}
getOutput(){
return this.#output
}
renderTable(){
if(this.#strategy === "tsv2"){
this.#output = this.renderHeaders2() + "\n" + this.renderRows2()
}else{
this.#output = this.renderHeaders() + "\n" + this.renderRows()
}
return this.#output
}
renderHeaders(){
const hdrs = [...this.#source.getTableHeaders()]
const rankedhdrs = this.#source.hasRank() ? hdrs : ["Rank"].concat(hdrs)
rankedhdrs.splice(1,0,"Distance")
return rankedhdrs.join(this.#sep)
}
renderRows(){
const s = this.#source
const rows = s.getTableLines()
var outputs = []
for(let i=0; i < rows.length; i++){
const row = rows[i]
const rankedrow = s.hasRank() ? row : [i + 1].concat(row);
const matches = s.getMatches(this.#filter.mode, rankedrow[0], this.#filter.distance)
const tds = rankedrow.map((d, ind)=>{
if(ind === 0) return [`${d}${this.#sep}--------`];
return `${d}`
}).join(this.#sep)
if(matches.size === 0 && this.#filter.zeromatches){
outputs.push(tds)
} else if(matches.size > 0) {
outputs.push(tds)
outputs = outputs.concat(this.renderMatchRows(rankedrow, matches))
}
}
return outputs.join("\n")
}
renderMatchRows(pivot, matches){
const s = this.#source
var outputs = []
for (const [key, value] of matches) {
const row = s.getTableLineByRank(key)
if(row){
const rankedrow = s.hasRank() ? row : [key].concat(row);
const tds = rankedrow.map((d, ind)=>{
if(ind === 0) return `${d}${this.#sep}${value}`;
return `${d}`
}).join(this.#sep)
outputs.push(tds)
}
}
return outputs
}
renderHeaders2(){
const hdrs = [...this.#source.getTableHeaders()]
const rankedhdrs = this.#source.hasRank() ? hdrs : ["Rank"].concat(hdrs)
const outhdrs = rankedhdrs.map(h=>`${h}${this.#sep}Match ${h}`)
outhdrs.splice(1,0,"Distance")
return outhdrs.join(this.#sep)
}
renderRows2(){
const s = this.#source
const rows = s.getTableLines()
var outputs = []
for(let i=0; i < rows.length; i++){
const row = rows[i]
const rankedrow = s.hasRank() ? row : [i + 1].concat(row);
const matches = s.getMatches(this.#filter.mode, rankedrow[0], this.#filter.distance)
if(matches.size > 0){
const mrows = this.renderMatchRows2(rankedrow, matches)
outputs = outputs.concat(mrows)
}
}
return outputs.join("\n")
}
renderMatchRows2(pivot, matches){
const s = this.#source
var outputs = []
for (const [key, value] of matches) {
const row = s.getTableLineByRank(key)
if(row){
const rankedrow = s.hasRank() ? row : [key].concat(row);
const tds = rankedrow.map((d, i)=>{
if(i === 0) return `${pivot[i]}${this.#sep}${d}${this.#sep}${value}`;
return `${pivot[i]}${this.#sep}${d}`
}).join(this.#sep)
outputs.push(tds)
}
}
return outputs
}
}
class DefaultTSVRETableRenderer extends AbstractTableRenderer {
#source = null
#output = ""
#filter = null
#sep = "\t"
#strategy
constructor(source, filter, strategy){
super()
this.#source = source
this.#filter = filter
this.#strategy = strategy
}
getOutput(){
return this.#output
}
renderTable(){
if(this.#strategy === "tsv2"){
this.#output = this.renderHeaders2() + "\n" + this.renderRows2()
}else{
this.#output = this.renderHeaders() + "\n" + this.renderRows()
}
return this.#output
}
renderHeaders(){
throw "Not yet implemented"
}
renderRows(){
throw "Not yet implemented"
}
renderMatchRows(pivot, matches){
throw "Not yet implemented"
}
renderHeaders2(){
const hdrs = [...this.#source.getTableHeaders()]
const rankedhdrs = this.#source.hasRank() ? hdrs : ["Rank"].concat(hdrs)
rankedhdrs.splice(0,0,"Distance")
rankedhdrs.splice(0,0,"RE strand")
rankedhdrs.splice(0,0,"RE index")
rankedhdrs.splice(0,0,"RE sequence")
return rankedhdrs.join(this.#sep)
}
renderRows2(){
const s = this.#source
const rows = s.getTableLines()
var outputs = []
for(let i=0; i < rows.length; i++){
const row = rows[i]
const rankedrow = s.hasRank() ? row : [i + 1].concat(row);
const matches = s.getREMatchesForRankAt(rankedrow[0], this.#filter.enzyme, this.#filter.distance);
if(matches.length > 0){
const mrows = this.renderMatchRows2(rankedrow, matches)
outputs = outputs.concat(mrows)
}
}
return outputs.join("\n")
}
renderMatchRows2(pivot, matches){
const s = this.#source
var outputs = []
for (let i=0; i < matches.length; i++) {
const m = matches[i]
const tds = pivot.map((d, i)=>{
// TODO: check whether misalignment for RE matches occurs also in TSV2 exported data
if(i === 0) return `${m.sequence}${this.#sep}${m.index}${this.#sep}${m.strand}${this.#sep}${m.distance}${this.#sep}${d}`;
else return `${d}`
}).join(this.#sep)
outputs.push(tds)
}
return outputs
}
}
class CrisprScanTSVDimerTableRenderer extends DefaultTSVDimerTableRenderer {
#source
#sep = "\t"
constructor(source, filter, strategy){
super(source, filter, strategy)
this.#source = source
}
renderMatchRows2(pivot, matches){
const s = this.#source
var outputs = []
for (const [key, value] of matches) {
const row = s.getTableLineByRank(key)
if(row){
const rankedrow = s.hasRank() ? row : [key].concat(row);
const tds = rankedrow.map((d, i)=>{
if(i === 0) return `${pivot[i]}${this.#sep}${d}${this.#sep}${value}`;
return `${pivot[i]}${this.#sep}${d}`
}).join(this.#sep)
outputs.push(tds)
}
}
return outputs
}
}