#+startup: showall
* http server
** guix
#+begin_src shell
guix shell -m manifest.scm -- make run
#+end_src
#+RESULTS:
** by hand
start a python http server with:
1. to compile this org document, call ~org-babel-tangle~ (=C-c C-v t=), which will produce =index.html=. alternately, you can just call =make= (a =manifest.scm= is provided for guix)
2. start an http server quickly in python on port 8000 to work around cors issues with file uris:
#+begin_src shell
python3 -m http.server -b :: 8000
#+end_src
3. then navigate to http://localhost:8000/
* html skeleton
#+name: skeleton
#+begin_src html :noweb yes :tangle index.html
functional annotation for varscan
functional annotation for varscan
dowload
name
nucleotide change
protein
amino acid change
#+end_src
* javascript
** entry point
set up the javascript code by loading direct module dependencies and running the analysis when the form is submitted.
#+begin_src javascript :noweb yes :tangle main.mjs
<>
import process from './varscan.mjs';
import Log from './logging.mjs';
function init() {
console.info('initializing');
const varscanForm = document.querySelector('#varscan');
varscanForm.onsubmit = (event) => {
submitForm(varscanForm);
event.preventDefault();
};
const variantsInput = varscanForm.querySelector('input[type="file"]');
variantsInput.onchange = (event) => {
Log.info("file uploaded");
submitButton.disabled = variantsInput.value === "";
};
const submitButton = varscanForm.querySelector('button');
submitButton.disabled = variantsInput.value === "";
}
init();
#+end_src
*** form submission
when the form is submitted, load the reference genome,its protein coding regions, the uploaded variant data, and spit out the changed proteins.
#+name: submit-form
#+begin_src javascript :noweb yes :tangle main.mjs
function submitForm(form) {
Log.clear();
<>
<>
<>
const filename =
form
.querySelector('input[name="variants-data"]')
.files[0]
.name;
const extIndex = filename.lastIndexOf('.');
const resultsFilename =
filename.substring(0, extIndex) + "-varscan" + filename.substring(extIndex);
Promise.all([variantsPromise, genomePromise, protein2PosPromise])
.then(([variants, genome, protein2Pos]) => {
const results = process(genome, protein2Pos, variants);
updateDownloadLink(resultsFilename, results);
fillTable(results);
});
}
#+end_src
*** TODO variants upload
we don't need to upload the file, but we're going to pretend we do for ux reasons. this just returns a promise for the “uploaded” file's csv data.
- [ ] verify file format is csv without (or with) headers
#+name: upload-variant
#+begin_src javascript
const variantsPromise = new Promise((resolve, reject) => {
const file = form.querySelector('input[name="variants-data"]').files[0];
const reader = new FileReader();
reader.onload = (event) => {
resolve(event.target.result);
};
reader.readAsText(file);
});
#+end_src
*** reference genome promise
load the reference genome from the server based on the filename in the form.
#+name: reference-genome
#+begin_src javascript
const referenceGenomeFile =
form.querySelector('select[name="reference-genome"]').value;
const genomePromise = fetch(referenceGenomeFile)
.then((response) => response.text())
.then((text) => text.replace('\r', '').replace('\n', ''))
.catch((err) => Log.error("couldn't load reference genome:", err));
#+end_src
*** TODO protein coding regions promise
load the protein coding regions from the server based on the filename in the form.
- [ ] this should probably be tied to the reference genome
#+name: protein-coding-region
#+begin_src javascript
const proteinCodingRegionsFile =
form.querySelector('select[name="protein-coding-regions"]').value;
const protein2PosPromise = fetch(proteinCodingRegionsFile)
.then((response) => response.text())
.then((text) => {
return text
.replace('\r', '')
.split('\n')
.reduce(
(acc, line) => {
const [name, start, stop] = line.split(',');
acc[name] = [Number(start), Number(stop)];
return acc;
},
{});
})
.catch((err) => Log.error("couldn't load protein coding regions:", err));
#+end_src
*** display results
take the results and stuff them in the output table
#+begin_src javascript :noweb yes :tangle main.mjs
function fillTable(results) {
const outputDiv = document.querySelector('#results')
outputDiv.hidden = false;
const tbody = outputDiv.querySelector('table tbody');
// clear the table
while (tbody.firstChild) {
tbody.removeChild(tbody.firstChild);
}
results.forEach((row) => {
const tr = document.createElement('tr');
row.forEach((col) => {
const td = document.createElement('td');
td.innerHTML = col;
tr.appendChild(td);
})
tbody.appendChild(tr);
});
}
#+end_src
create a download link
#+begin_src javascript :noweb yes :tangle main.mjs
function updateDownloadLink(filename, results) {
const anchor = document.querySelector('#download');
if (anchor.url) {
window.URL.revokeObjectURL(anchor.url);
anchor.url = undefined;
}
const data = results.map((row) => row.join(",") + "\r\n");
const blob = new Blob(
data,
{ type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
anchor.href = url;
anchor.download = filename;
return anchor;
}
#+end_src
** variant to codon whatever i need to name this
#+begin_src javascript :noweb yes :tangle varscan.mjs
<>
import Log from './logging.mjs';
import codon2AA from './codon2AminoAcid.mjs';
import aa2Code from './aminoAcid2Code.mjs';
function process(genome, protein2Pos, variants) {
let lineno = 0;
let name = 'unnamed-change';
return variants
.split('\n')
.reduce(
(acc, line) => {
lineno++;
line.replace('\r', '');
if (line === "") {
return acc;
}
const [nameMaybe, ref, posStr, origNucleotide, newNucleotide] =
line.split(',');
name = nameMaybe || name;
const pos = Number(posStr);
const [protein, proteinStart, aaIndex] =
findProtein(pos, protein2Pos);
if (!protein) {
return acc.concat([[
name,
origNucleotide.toLowerCase()+posStr+newNucleotide.toLowerCase(),
'',
"non-coding"
]]);
}
// start of codon relative to start of protein, 0-based
const changedStart = pos - proteinStart;
// convert position to start of the codon and the
// offset of the change from the start of the codon.
const offset = changedStart % 3;
const codonStart = changedStart - offset;
// pos points to the change within the genome (1-index)
// proteinStart points to the start of the changed protein within the genome (1-index)
// changedStart points to the changed nucleotide within the protein (0-index)
// codonStart points to the start of the changed codon within the protein (0-index)
// offset points to the changed nucleotide within the codon (0-index)
// position of codon within entire genome (0-index)
const absCodonStart = proteinStart + codonStart - 1;
const origCodon = genome.substring(absCodonStart, absCodonStart + 3);
const origAA = aa2Code[codon2AA[origCodon]];
const newCodon = origCodon.substring(0, offset) + newNucleotide + origCodon.substring(offset+1);
const newAA = aa2Code[codon2AA[newCodon]];
// check the change against the reference genome.
const checkNucleotide = origCodon[offset];
if (checkNucleotide !== origNucleotide) {
const checkCodon =
origCodon.substring(0, offset) +
origNucleotide +
origCodon.substring(offset+1);
const checkAA = aa2Code[codon2AA[checkCodon]];
const aaChange = `${checkAA}${aaIndex}${newAA}`
Log.warn(`${name} (line ${lineno}): nucleotide at position ${pos} is “${checkNucleotide.toLowerCase()}” in the reference, but “${origNucleotide.toLowerCase()}” was supplied. If the supplied nucleotide is correct, then the amino acid change is ${aaChange}.`)
}
const nucleotideChange = `${checkNucleotide.toLowerCase()}${pos}${newNucleotide.toLowerCase()}`
const aaChange = `${origAA}${aaIndex}${newAA}`
return acc.concat([[
name,
nucleotideChange,
protein,
aaChange
]]);
},
[]);
}
// pos is 1-based index
//
// returns protein name, 1-index of start of protein in genome, and
// 1-index of of offset of `pos` in its codon.
function findProtein(pos, protein2Pos) {
for (const name in protein2Pos) {
const [start, end] = protein2Pos[name];
if (start <= pos && pos <= end) {
// normal people count from 1, not 0
const index = Math.floor((pos - start) / 3) + 1;
return [name, start, index];
}
}
return [];
}
export default process
#+end_src
** codon to amino acid table
:PROPERTIES:
:VISIBILITY: folded
:END:
#+begin_src javascript :noweb yes :tangle codon2AminoAcid.mjs
<>
const codon2AA = {
'GCT': 'Ala',
'GCC': 'Ala',
'GCA': 'Ala',
'GCG': 'Ala',
'CGT': 'Arg',
'CGC': 'Arg',
'CGA': 'Arg',
'CGG': 'Arg',
'AGA': 'Arg',
'AGG': 'Arg',
'AAT': 'Asn',
'AAC': 'Asn',
'GAT': 'Asp',
'GAC': 'Asp',
'TGT': 'Cys',
'TGC': 'Cys',
'CAA': 'Gln',
'CAG': 'Gln',
'GAA': 'Glu',
'GAG': 'Glu',
'GGT': 'Gly',
'GGC': 'Gly',
'GGA': 'Gly',
'GGG': 'Gly',
'CAT': 'His',
'CAC': 'His',
'ATT': 'Ile',
'ATC': 'Ile',
'ATA': 'Ile',
'CTT': 'Leu',
'CTC': 'Leu',
'CTA': 'Leu',
'CTG': 'Leu',
'TTA': 'Leu',
'TTG': 'Leu',
'AAA': 'Lys',
'AAG': 'Lys',
'ATG': 'Met',
'TTT': 'Phe',
'TTC': 'Phe',
'CCT': 'Pro',
'CCC': 'Pro',
'CCA': 'Pro',
'CCG': 'Pro',
'TCT': 'Ser',
'TCC': 'Ser',
'TCA': 'Ser',
'TCG': 'Ser',
'AGT': 'Ser',
'AGC': 'Ser',
'ACT': 'Thr',
'ACC': 'Thr',
'ACA': 'Thr',
'ACG': 'Thr',
'TGG': 'Trp',
'TAT': 'Tyr',
'TAC': 'Tyr',
'GTT': 'Val',
'GTC': 'Val',
'GTA': 'Val',
'GTG': 'Val',
'TAA': 'STOP',
'TGA': 'STOP',
'TAG': 'STOP'
}
export default codon2AA
#+end_src
** amino acid to letter table
:PROPERTIES:
:VISIBILITY: folded
:END:
#+begin_src javascript :noweb yes :tangle aminoAcid2Code.mjs
<>
const aa2Code = {
'Ala': 'A',
'Arg': 'R',
'Asn': 'N',
'Asp': 'D',
'Cys': 'C',
'Gln': 'Q',
'Glu': 'E',
'Gly': 'G',
'His': 'H',
'Ile': 'I',
'Leu': 'L',
'Lys': 'K',
'Met': 'M',
'Phe': 'F',
'Pro': 'P',
'Ser': 'S',
'Thr': 'T',
'Trp': 'W',
'Tyr': 'Y',
'Val': 'V',
'STOP': '*'
}
export default aa2Code
#+end_src
** logging
#+begin_src javascript :noweb yes :tangle logging.mjs
<>
class Log {
get elt() {
if (!document.querySelector('#log')) {
const elt = document.createElement('div');
elt.id = 'log';
document.body.appendChild(elt);
}
return document.querySelector('#log');
}
clear() {
while (this.elt.firstChild) {
this.elt.removeChild(this.elt.firstChild);
}
}
logAt(level, items) {
const list = document.createElement('ul');
items.forEach((item) => {
const msg = document.createElement('li');
msg.setAttribute('class', level);
msg.innerText = item;
list.appendChild(msg);
});
this.elt.appendChild(list);
}
error(...items) {
this.logAt('error', items);
console.error.apply(null, items)
}
warn(...items) {
this.logAt('warn', items);
console.warn.apply(null, items)
}
info(...items) {
console.info.apply(null, items)
}
debug(...items) {
console.debug.apply(null, items)
}
};
Log.logger = new Log();
Log.clear = Log.logger.clear.bind(Log.logger);
Log.error = Log.logger.error.bind(Log.logger);
Log.warn = Log.logger.warn.bind(Log.logger);
Log.info = Log.logger.info.bind(Log.logger);
Log.debug = Log.logger.debug.bind(Log.logger);
export default Log
#+end_src
** license
#+name: js-license
#+begin_src javascript
/*
,* copyright 2024 brian cully
,*
,* this program is free software: you can redistribute it and/or
,* modify it under the terms of the gnu affero general public license
,* version 3 or later.
,*
,* a copy of this license may be found at https://www.gnu.org/licenses/agpl-3.0.html
,*
,* dear gnu developers: stop making your software so hard to work
,* with. i was going to put in librejs tags, but it's like it goes out
,* of its way to make that as annoying as possible.
,*/
#+end_src