289 lines
10 KiB
JavaScript
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);
|
|
}
|
|
};
|