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); } };