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