aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--csv.mjs79
-rw-r--r--index.html41
-rw-r--r--main.css7
-rw-r--r--main.mjs7
-rw-r--r--pnit-form.mjs77
-rw-r--r--worker.js53
7 files changed, 265 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..15d7aef
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+inputs
diff --git a/csv.mjs b/csv.mjs
new file mode 100644
index 0000000..b096ab4
--- /dev/null
+++ b/csv.mjs
@@ -0,0 +1,79 @@
+export default class {
+ constructor() {
+ this.state = this.awaitingTokenOrComma;
+ this.inProgress = '';
+ this._rowResults = [];
+ this._results = [];
+ }
+
+ push(c) {
+ this.state(c);
+ }
+
+ get results() {
+ return this._results.concat([this._rowResults.concat([this.inProgress])]);
+ }
+
+ awaitingTokenOrComma(c) {
+ switch (c) {
+ case ',':
+ // empty column
+ this._rowResults.push('');
+ break;
+ case '\n':
+ this._results.push(this._rowResults);
+ this._rowResults = [];
+ break;
+ default:
+ if (/\s/.test(c)) {
+ return;
+ }
+ this.inProgress += c;
+ this.state = this.awaitingComma;
+ }
+ }
+
+ awaitingComma(c) {
+ switch (c) {
+ case ',':
+ this._rowResults.push(this.inProgress);
+ this.inProgress = '';
+ this.state = this.awaitingTokenOrComma;
+ break;
+ case '\r':
+ break;
+ case '\n':
+ this._results.push(this._rowResults.concat(this.inProgress));
+ this._rowResults = [];
+ this.inProgress = '';
+ this.state = this.awaitingTokenOrComma;
+ break;
+ case '"':
+ this.state = this.checkForDoubleQuote;
+ break;
+ default:
+ this.inProgress += c;
+ }
+ }
+
+ checkForDoubleQuote(c) {
+ this.inProgress += c
+ switch (c) {
+ case '"':
+ this.state = this.awaitingComma;
+ break;
+ default:
+ this.state = this.awaitingQuote;
+ }
+ }
+
+ awaitingQuote(c) {
+ switch (c) {
+ case '"':
+ this.state = this.awaitingComma;
+ break;
+ default:
+ this.inProgress += c;
+ }
+ }
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..8ae4fa2
--- /dev/null
+++ b/index.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html lang='en'>
+ <head>
+ <meta charset='utf-8'>
+ <meta name='viewport' content='width=device-width, initial-scale=1'>
+ <link rel='stylesheet' href='main.css'>
+ <title>partial nucleotide identity threshold</title>
+ </head>
+
+ <body>
+ <h1>partial nucleotide identity threshold</h1>
+ <x-pnit-form>
+ <form>
+ <label for='pni-threshold'>threshold:</label>
+ <input id='pni-threshold' name='pni-threshold' type='text' size='5' value='90.5'>%
+ <br>
+ <label for='ignore-header'>ignore header line:</label>
+ <input id='ignore-header' name='ignore-header' type=checkbox checked>
+ <br>
+ <label for='csv-input'>upload:</label>
+ <input id='csv-input' name='csv-input' type='file' accept='text/csv' required>
+ </form>
+
+ <button>download pnit csv</button>
+ <table id='pnit-results'>
+ <tr>
+ <td>foo</td>
+ <td>bar</td>
+ <td>pnit</td>
+ </tr>
+ <tr>
+ <td>foo</td>
+ <td>bar</td>
+ <td>pnit</td>
+ </tr>
+ </table>
+ </x-pnit-form>
+
+ <script type='module' src='./main.mjs'></script>
+ </body>
+</html>
diff --git a/main.css b/main.css
new file mode 100644
index 0000000..b6ae759
--- /dev/null
+++ b/main.css
@@ -0,0 +1,7 @@
+form {
+ margin-bottom: 2ex;
+}
+
+input[name='pni-threshold'] {
+ text-align: right;
+}
diff --git a/main.mjs b/main.mjs
new file mode 100644
index 0000000..8bebffd
--- /dev/null
+++ b/main.mjs
@@ -0,0 +1,7 @@
+import PNITForm from './pnit-form.mjs';
+
+function init() {
+ PNITForm.register();
+}
+
+document.addEventListener('DOMContentLoaded', init);
diff --git a/pnit-form.mjs b/pnit-form.mjs
new file mode 100644
index 0000000..c1fa16c
--- /dev/null
+++ b/pnit-form.mjs
@@ -0,0 +1,77 @@
+const worker = new Worker('worker.js', { type: 'module' });
+
+export default class extends HTMLElement {
+ static name = 'x-pnit-form';
+ static register() {
+ console.debug('Registering Element', this.name, this);
+ customElements.define(this.name, this);
+ }
+
+ get form() {
+ return this.querySelector('form');
+ }
+
+ get ignoreHeader() {
+ return this.form.querySelector('input[name="ignore-header"]').checked;
+ }
+
+ get threshold() {
+ return Number(this.form.querySelector('input[name="pni-threshold"]').value);
+ }
+
+ get files() {
+ const inputs = Array.from(this.form.querySelectorAll('input[type="file"]'));
+ return inputs.flatMap(inp => Array.from(inp.files));
+ }
+
+ get resultTable() {
+ return document.getElementById('pnit-results');
+ }
+
+ connectedCallback() {
+ console.debug('PNITForm#connectedCallback', this);
+
+ worker.onmessage = this.handleWorkerMessage.bind(this);
+ worker.onmessageerror = this.handleWorkerMessageError.bind(this);
+ worker.onerror = this.handleWorkerError.bind(this);
+
+ this.form.onchange = this.process.bind(this);
+ this.form.onchange();
+ }
+
+ handleWorkerMessage(e) {
+ const row = document.createElement('tr');
+ e.data.forEach(v => {
+ const col = document.createElement('td');
+ col.innerText = v;
+ row.appendChild(col);
+ });
+ this.resultTable.appendChild(row);
+ }
+
+ handleWorkerMessageError(e) {
+ console.error('onmessageerror', e);
+ }
+
+ handleWorkerError(e) {
+ console.error('onerror', e);
+ }
+
+ process() {
+ console.debug('PNITForm#process', this);
+
+ this.resultTable.innerHTML = '';
+
+ if (isNaN(this.threshold)) {
+ console.error('threshold invalid');
+ return;
+ }
+ this.files.forEach(file => {
+ worker.postMessage({
+ ignoreHeader: this.ignoreHeader,
+ threshold: this.threshold,
+ file
+ });
+ });
+ }
+}
diff --git a/worker.js b/worker.js
new file mode 100644
index 0000000..77a861b
--- /dev/null
+++ b/worker.js
@@ -0,0 +1,53 @@
+import CSVParse from './csv.mjs';
+
+let ignoreLines = 0;
+let sequenceNames = [];
+
+function process(threshold, row) {
+ const name = row[0];
+ if (name === undefined || /^\s*$/.test(name)) {
+ return;
+ }
+ if (ignoreLines > 0) {
+ ignoreLines--;
+ return;
+ }
+ sequenceNames.push(name);
+
+ let i = 0;
+ for (const col of row) {
+ const val = Number(col);
+ if (isNaN(val)) {
+ continue;
+ }
+ if (val >= threshold && name !== sequenceNames[i]) {
+ postMessage([name, sequenceNames[i], val]);
+ }
+ i++;
+ }
+}
+
+onmessage = e => {
+ console.debug('got message', e);
+ console.debug('reading file');
+
+ console.debug('e.data', e.data);
+ const { ignoreHeader, threshold, file } = e.data;
+ console.debug('threshold', threshold, 'file', file);
+
+ const reader = new FileReader();
+ reader.onload = e => {
+ const parser = new CSVParse();
+ for (const c of e.target.result) {
+ parser.push(c);
+ }
+ if (ignoreHeader) {
+ ignoreLines = 1;
+ }
+ parser.results.forEach(row => process(threshold, row));
+ }
+ reader.progress = e => {
+ console.debug('reader.progress', e);
+ }
+ reader.readAsText(file);
+}