assets/js/csv.js

289 lines
10 KiB
JavaScript

function CSV (url, callback, xColumnParse, httpReadyCallback) {
var xhttp = new XMLHttpRequest();
var _this = this;
if (typeof url == 'object') { // create from existing CSV-as-object
this.interpolation = url.interpolation;
this.columnNames = url.columnNames;
this.matrix = url.matrix;
this.periodY = (url.periodY)?url.periodY:0;
this.periodX = (url.periodX)?url.periodX:0;
if (url.buf) this.buf = url.buf;
else {
try {
_this.buf = new SharedArrayBuffer(url.columns * url.rows * 8);
} catch (e) {
_this.buf = new ArrayBuffer(url.columns * url.rows * 8);
}
}
this.sorted = url.sorted;
if (url.columns == 0) this.data = null;
else {
this.data = new Array();
let nRows = Math.trunc(this.buf.byteLength / (8 * url.columns));
for (let i = 0; i < url.columns; i++)
this.data.push(new Float64Array(this.buf, nRows * i * 8, url.rows));
if (url.data) {
for (let i = 0; i < url.data.length; i++)
for (let j = 0; j < url.data[i].length; j++)
this.data[i][j] = url.data[i][j];
}
}
if (callback) callback(_this);
return;
}
xhttp.onreadystatechange = function () {
if (this.readyState != 4 || this.status != 200) return;
if (httpReadyCallback) httpReadyCallback(_this);
try {
if (xhttp.responseText.indexOf("\r") >= 0) var lines = xhttp.responseText.split("\r").join('').split("\n");
else var lines = xhttp.responseText.split("\n");
} catch (e) { }
let nColumns = -1;
let nRows = 0;
let row = 0;
_this.columnNames = new Array();
_this.data = new Array();
for (let i = 0; i < lines.length; i++) {
if (lines[i] == '' || lines[i].indexOf('#') == 0) continue; // comment
lines[i] = lines[i].split("\t");
if (nColumns < 0) {
nColumns = lines[i].length;
nRows = lines.length - i - 1;
try {
_this.buf = new SharedArrayBuffer(nColumns * nRows * 8);
} catch (e) {
_this.buf = new ArrayBuffer(nColumns * nRows * 8);
_this.err('Your browser does not support SharedArrayBuffer or it is disabled. This can reduce performance!');
}
for (let j = 0; j < lines[i].length; j++) {
if (lines[i][j] == '' || _this.columnNames[lines[i][j]]) _this.err('Column name invalid!');
else _this.columnNames[lines[i][j]] = j;
_this.data.push(new Float64Array(_this.buf, nRows * j * 8, nRows));
}
} else {
let j = 0;
if (xColumnParse && lines[i].length > 0 && nColumns > 0) {
_this.data[j][row] = xColumnParse(lines[i][j]);
j = 1;
}
for (j = j; j < lines[i].length && j < nColumns; j++)
_this.data[j][row] = parseFloat(lines[i][j]);
for (j = j; j < nColumns; j++)
_this.data[j][row] = Number.NaN;
if (_this.sorted) {
if (row > 0 && (isNaN(_this.data[0][row]) || isNaN(_this.data[0][row-1]) || _this.data[0][row] <= _this.data[0][row-1]))
_this.sorted = false;
}
row++;
}
}
if (!_this.data || _this.data.length == 0 || _this.data[0].length == 0) _this.data = null; // never accept empty array
else if (row < nRows)
for (let i = 0; i < _this.data.length; i++)
_this.data[i] = new Float64Array(_this.buf, nRows * i * 8, row); // just reduce mapped length of views to real data set
if (callback) callback(_this);
};
xhttp.open("GET", url, true);
xhttp.send();
}
CSV.prototype = {
// public
interpolation : 1, // 0=nearest 1=linear 2=linear, return NaN outside of range
// private
buf : null,
// public readonly
columnNames : null,
sorted : true,
matrix : false, // if true, data is handled as an 2D-map, where first column is the (sorted) x-axis and first row is the (sorted) y-axis (data[0][0] is NaN)
periodY : 0,
periodX : 0,
data : null,
get loaded () { return this.data != null; },
get xMin () {
if (!this.sorted || this.data === null) return Number.NaN;
else return this.data[0][(this.matrix)?1:0];
},
get xMax () {
if (!this.sorted || this.data === null) return Number.NaN;
else return this.data[0][this.data[0].length-1];
},
asObject : /* public */ function (clone=false) { // meant to be used to create a copy of CSV in a web-worker.
let obj = {
interpolation: this.interpolation,
columnNames: this.columnNames,
buf: this.buf,
columns: (this.data === null)?0:this.data.length,
rows: (this.data === null)?0:this.data[0].length,
sorted: this.sorted,
matrix: this.matrix,
periodY: this.periodY,
periodX: this.periodX
};
if (clone) {
try {
obj.buf = new SharedArrayBuffer(this.buf.byteLength);
} catch (e) {
obj.buf = new ArrayBuffer(this.buf.byteLength);
}
new Uint8Array(obj.buf).set(new Uint8Array(this.buf));
}
return obj;
},
toString : /* public */ function (indentation=0, digits=0) {
let space = '';
for (let i = 0; i < indentation; i++) space += ' ';
let s = "{\n";
let obj = this.asObject();
for (let key in obj) {
if (key == 'buf') continue;
s += space+' "'+key+'": '+JSON.stringify(obj[key])+",\n";
}
s += space+' "data": ['+"\n";
for (let i = 0; i < this.data.length; i++) {
if (i != 0) s += ",\n";
s += space+" [";
for (let j = 0; j < this.data[i].length; j++) {
if (j != 0) s += ',';
if (digits == 0) s += (isNaN(this.data[i][j]))?'"NaN"':this.data[i][j];
else s += (isNaN(this.data[i][j]))?'"NaN"':this.data[i][j].toPrecision(digits).replace(/\.?0+$/,"");
}
s += ']';
}
s += "\n"+space+" ]\n";
s += space+"}";
return s;
},
columnName : /* public */ function (i) {
for (var key in this.columnNames)
if (this.columnNames[key] == i)
return key;
return '';
},
prevNext : /* private */ function (column, x) {
if (!this.sorted || this.data === null) return null;
var prev = (this.matrix)?1:0;
var next = this.data[0].length-1;
while (prev < next-1) {
let m = Math.floor((prev + next) / 2);
if (this.data[0][m] <= x) prev = m;
else next = m;
}
if (this.matrix) {
var prevY = 1;
var nextY = this.data.length-1;
while (prevY < nextY-1) {
let m = Math.floor((prevY + nextY) / 2);
if (this.data[m][0] <= column) prevY = m;
else nextY = m;
}
return [prev, next, prevY, nextY];
} else {
var j = this.columnNames[column];
if (!j) return null;
while (prev > 0 && isNaN(this.data[j][prev])) prev--;
while (next < this.data[0].length-1 && isNaN(this.data[j][next])) next++;
if (isNaN(this.data[j][next])) next = prev;
else if (isNaN(this.data[j][prev])) prev = next;
return [prev, next];
}
},
interpolate : /* public */ function (column, x) {
var pn = this.prevNext(column, x);
if (!pn) return Number.NaN;
if (this.periodX > 0) {
x %= this.periodX;
if (x < this.data[0][pn[0]] || x > this.data[0][pn[1]]) {
pn[0] = this.data.length-1;
pn[1] = (this.matrix)?1:0;
}
}
if (this.matrix) {
if (isNaN(column)) return Number.NaN;
if (this.periodY > 0) {
column %= this.periodY;
if (column < this.data[pn[2]][0] || column > this.data[pn[3]][0]) {
pn[2] = this.data.length-1;
pn[3] = 1;
}
}
let sx = (x - this.data[0][pn[0]] + ((x < this.data[0][1])?this.periodX:0)) / (this.data[0][pn[1]] - this.data[0][pn[0]] + ((pn[0] > pn[1])?this.periodX:0));
let sy = (column - this.data[pn[2]][0] + ((column < this.data[1][0])?this.periodY:0)) / (this.data[pn[3]][0] - this.data[pn[2]][0] + ((pn[2] > pn[3])?this.periodY:0));
return this.data[pn[2]][pn[0]]*(1-sx)*(1-sy) + this.data[pn[2]][pn[1]]*(sx)*(1-sy) + this.data[pn[3]][pn[0]]*(1-sx)*(sy) + this.data[pn[3]][pn[1]]*(sx)*(sy);
} else {
var j = this.columnNames[column];
if (pn[0] == pn[1]) {
if (this.interpolation == 2) return Number.NaN;
else return this.data[j][pn[0]]; // just one interpolation point
}
if (this.interpolation == 0) {
if (x - this.data[0][pn[0]] < this.data[0][pn[1]] - x) return this.data[j][pn[0]]; // TODO: regard periodX special case here
else return this.data[j][pn[1]];
} else {
if (this.periodX == 0) {
if (x < this.data[0][pn[0]]) return this.data[j][pn[0]];
if (x > this.data[0][pn[1]]) return this.data[j][pn[1]];
}
let s = (x - this.data[0][pn[0]] + ((x < this.data[0][0])?this.periodX:0)) / (this.data[0][pn[1]] - this.data[0][pn[0]] + ((pn[0] > pn[1])?this.periodX:0));
return this.data[j][pn[1]] * s + this.data[j][pn[0]] * (1-s);
}
}
},
integrate : /* public */ function (column, x1, x2) {
if (this.matrix) return Number.NaN;
var pn = this.prevNext(column, x1);
if (!pn) return Number.NaN;
var j = this.columnNames[column];
if (!j) return Number.NaN;
if (pn[0] == pn[1]) return this.data[j][pn[0]] * (x2-x1);
var area = 0;
if (this.data[0][pn[0]] > x1) area += this.data[j][pn[0]] * (this.data[0][pn[0]] - x1); // add area lower outside of data range
do {
// add interval area
var _x1 = this.data[0][pn[0]];
var y1 = this.data[j][pn[0]];
var _x2 = this.data[0][pn[1]];
var y2 = this.data[j][pn[1]];
if (this.interpolation == 0) {
var m = (_x1 + _x2) / 2;
if (_x1 < x1) _x1 = x1;
if (_x2 > x2) _x2 = x2;
if (_x1 < m) area += y1 * (m-_x1);
if (m < _x2) area += y2 * (_x2-m);
} else {
if (_x1 < x1) {
y1 += (y2-y1) * (x1-_x1) / (_x2-_x1);
_x1 = x1;
}
if (_x2 > x2) {
y2 -= (y2-y1) * (_x2-x2) / (_x2-_x1);
_x2 = x2;
}
area += (y1+y2)/2 * (_x2-_x1);
}
// calc next interval
pn[0] = pn[1];
do pn[1]++;
while (pn[1] < this.data[j].length && isNaN(this.data[j][pn[1]]));
} while (pn[1] < this.data[0].length && this.data[0][pn[0]] < x2);
if (this.data[0][pn[1]] < x2) area += this.data[j][pn[1]] * (x2 - this.data[0][pn[1]]); // add area upper outside of data range
return area;
},
average : /* public */ function (column, x1, x2) {
var av = this.integrate(column, x1, x2);
if (!isNaN(av)) av = av / (x2-x1);
return av;
},
err : function (msg) {
console.log(msg);
}
};