- 1 :
/* eslint-disable linebreak-style */
- 2 :
/* global Spyral, DataTable */
- 3 :
- 4 :
import Chart from './chart.js';
- 5 :
- 6 :
import Util from './util.js';
- 7 :
- 8 :
/**
- 9 :
* The Spyral.Table class in Spyral provides convenience functions for working with tabular
- 10 :
* data.
- 11 :
*
- 12 :
* There are several ways of initializing a Table, here are some of them:
- 13 :
*
- 14 :
* Provide an array of data with 3 rows:
- 15 :
*
- 16 :
* let table = createTable([1,2,3]);
- 17 :
*
- 18 :
*
- 19 :
* Provide a nested array of data with multiple rows:
- 20 :
*
- 21 :
* let table = createTable([[1,2],[3,4]]);
- 22 :
*
- 23 :
* Same nested array, but with a second argument specifying headers
- 24 :
*
- 25 :
* let table = createTable([[1,2],[3,4]], {headers: ["one","two"]});
- 26 :
*
- 27 :
* Create table with comma-separated values:
- 28 :
*
- 29 :
* let table = createTable("one,two\\n1,2\\n3,4");
- 30 :
*
- 31 :
* Create table with tab-separated values
- 32 :
*
- 33 :
* let table = createTable("one\\ttwo\\n1\\t2\\n3\\t4");
- 34 :
*
- 35 :
* Create table with array of objects
- 36 :
*
- 37 :
* let table = createTable([{one:1,two:2},{one:3,two:4}]);
- 38 :
*
- 39 :
* It's also possible simple to create a sorted frequency table from an array of values:
- 40 :
*
- 41 :
* let table = createTable(["one","two","one"], {count: "vertical", headers: ["Term","Count"]})
- 42 :
*
- 43 :
* Working with a Corpus is easy. For instance, we can create a table from the top terms:
- 44 :
*
- 45 :
* loadCorpus("austen").terms({limit:500, stopList: 'auto'}).then(terms => {
- 46 :
* return createTable(terms);
- 47 :
* })
- 48 :
*
- 49 :
* Similarly, we could create a frequency table from the first 1,000 words of the corpus:
- 50 :
*
- 51 :
* loadCorpus("austen").words({limit:1000, docIndex: 0, stopList: 'auto'}).then(words => {
- 52 :
* return createTable(words, {count: "vertical"});
- 53 :
* });
- 54 :
*
- 55 :
* Some of the configuration options are as follows:
- 56 :
*
- 57 :
* * **format**: especially for forcing csv or tsv when the data is a string
- 58 :
* * **hasHeaders**: determines if data has a header row (usually determined automatically)
- 59 :
* * **headers**: a Array of Strings that serve as headers for the table
- 60 :
* * **count**: forces Spyral to create a sorted frequency table from an Array of data, this can be set to "vertical" if the counts are shown vertically or set to true if the counts are shown horizontally
- 61 :
*
- 62 :
* Tables are convenient in Spyral because you can simply show them to preview a version in HTML.
- 63 :
*
- 64 :
* @memberof Spyral
- 65 :
* @class
- 66 :
*/
- 67 :
class Table {
- 68 :
/**
- 69 :
* The Table config object
- 70 :
* @typedef {Object} Spyral.Table~TableConfig
- 71 :
* @property {string} format The format of the provided data, either "tsv" or "csv"
- 72 :
* @property {(Object|Array)} headers The table headers
- 73 :
* @property {boolean} hasHeaders True if the headers are the first item in the data
- 74 :
* @property {string} count Specify "vertical" or "horizontal" to create a table of unique item counts in the provided data
- 75 :
*/
- 76 :
- 77 :
- 78 :
/**
- 79 :
* Create a new Table
- 80 :
* @constructor
- 81 :
* @param {(Object|Array|String|Number)} data An array of data or a string with CSV or TSV.
- 82 :
* @param {Spyral.Table~TableConfig} config an Object for configuring the table initialization
- 83 :
* @returns {Spyral.Table}
- 84 :
*/
- 85 :
constructor(data, config, ...other) {
- 86 :
this._rows = [];
- 87 :
this._headers = {};
- 88 :
this._rowKeyColumnIndex = 0;
- 89 :
- 90 :
if (Util.isPromise(data)) {
- 91 :
throw new Error('Data cannot be a Promise');
- 92 :
}
- 93 :
- 94 :
// we have a configuration object followed by values: create({headers: []}, 1,2,3) …
- 95 :
if (data && typeof data === 'object' && (typeof config === 'string' || typeof config === 'number' || Array.isArray(config))) {
- 96 :
data.rows = [config].concat(other).filter(v => v!==undefined);
- 97 :
config = undefined;
- 98 :
}
- 99 :
- 100 :
// we have a simple variable set of arguments: create(1,2,3) …
- 101 :
if (arguments.length>0 && Array.from(arguments).every(a => a!==undefined && !Array.isArray(a) && typeof a !== 'object')) {
- 102 :
data = [data,config].concat(other).filter(v => v!==undefined);
- 103 :
config = undefined;
- 104 :
}
- 105 :
- 106 :
// could be CSV or TSV
- 107 :
if (Array.isArray(data) && data.length===1 && typeof data[0] === 'string' && (data[0].indexOf(',')>-1 || data[0].indexOf('\t')>-1)) {
- 108 :
data = data[0];
- 109 :
}
- 110 :
- 111 :
// first check if we have a string that might be delimited data
- 112 :
if (data && (typeof data === 'string' || typeof data ==='number')) {
- 113 :
if (typeof data === 'number') {data = String(data);} // convert to string for split
- 114 :
let rows = [];
- 115 :
let format = config && 'format' in config ? config.format : undefined;
- 116 :
data.split(/(\r\n|[\n\v\f\r\x85\u2028\u2029])+/g).forEach((line,i) => {
- 117 :
if (line.trim().length>0) {
- 118 :
let values;
- 119 :
if ((format && format==='tsv') || line.indexOf('\t')>-1) {
- 120 :
values = line.split(/\t/);
- 121 :
} else if ((format && format==='csv') || line.indexOf(',')>-1) {
- 122 :
values = parseCsvLine(line);
- 123 :
} else {
- 124 :
values = [line];
- 125 :
}
- 126 :
- 127 :
// if we can't find any config information for headers then we try to guess
- 128 :
// if the first line doesn't have any numbers - this heuristic may be questionable
- 129 :
if (i===0 && values.every(v => isNaN(v)) &&
- 130 :
((typeof config !== 'object') || (typeof config === 'object' && !('hasHeaders' in config) && !('headers' in config)))) {
- 131 :
this.setHeaders(values);
- 132 :
} else {
- 133 :
rows.push(values.map(v => isNaN(v) ? v : Number(v)));
- 134 :
}
- 135 :
}
- 136 :
});
- 137 :
data = rows;
- 138 :
}
- 139 :
- 140 :
if (data && Array.isArray(data)) {
- 141 :
if (config) {
- 142 :
if (Array.isArray(config)) {
- 143 :
this.setHeaders(config);
- 144 :
} else if (typeof config === 'object') {
- 145 :
if ('headers' in config) {
- 146 :
this.setHeaders(config.headers);
- 147 :
} else if ('hasHeaders' in config && config.hasHeaders) {
- 148 :
this.setHeaders(data.shift());
- 149 :
}
- 150 :
}
- 151 :
}
- 152 :
if (config && 'count' in config && config.count) {
- 153 :
let freqs = Table.counts(data);
- 154 :
if (config.count==='vertical') {
- 155 :
for (let item in freqs) {
- 156 :
this.addRow(item, freqs[item]);
- 157 :
}
- 158 :
this.rowSort((a,b) => Table.cmp(b[1],a[1]));
- 159 :
} else {
- 160 :
this._headers = []; // reset and use the terms as headers
- 161 :
this.addRow(freqs);
- 162 :
this.columnSort((a,b) => Table.cmp(this.cell(0,b),this.cell(0,a)));
- 163 :
}
- 164 :
} else {
- 165 :
this.addRows(data);
- 166 :
}
- 167 :
} else if (data && typeof data === 'object') {
- 168 :
if ('headers' in data && Array.isArray(data.headers)) {
- 169 :
this.setHeaders(data.headers);
- 170 :
} else if ('hasHeaders' in data && 'rows' in data) {
- 171 :
this.setHeaders(data.rows.shift());
- 172 :
}
- 173 :
if ('rows' in data && Array.isArray(data.rows)) {
- 174 :
this.addRows(data.rows);
- 175 :
}
- 176 :
if ('rowKeyColumn' in data) {
- 177 :
if (typeof data.rowKeyColumn === 'number') {
- 178 :
if (data.rowKeyColumn < this.columns()) {
- 179 :
this._rowKeyColumnIndex = data.rowKeyColumn;
- 180 :
} else {
- 181 :
throw new Error('The rowKeyColumn value is higher than the number headers designated: '+data.rowKeyColum);
- 182 :
}
- 183 :
} else if (typeof data.rowKeyColumn === 'string') {
- 184 :
if (data.rowKeyColumn in this._headers) {
- 185 :
this._rowKeyColumnIndex = this._headers[data.rowKeyColumn];
- 186 :
} else {
- 187 :
throw new Error('Unable to find column designated by rowKeyColumn: '+data.rowKeyColumn);
- 188 :
}
- 189 :
}
- 190 :
}
- 191 :
}
- 192 :
}
- 193 :
- 194 :
/**
- 195 :
* Set the headers for the Table
- 196 :
* @param {(Object|Array)} data
- 197 :
* @returns {Spyral.Table}
- 198 :
*/
- 199 :
setHeaders(data) {
- 200 :
if (data && Array.isArray(data)) {
- 201 :
data.forEach(h => this.addColumn(h), this);
- 202 :
} else if (typeof data === 'object') {
- 203 :
if (this.columns()===0 || Object.keys(data).length===this.columns()) {
- 204 :
this._headers = data;
- 205 :
} else {
- 206 :
throw new Error('The number of columns don\'t match: ');
- 207 :
}
- 208 :
} else {
- 209 :
throw new Error('Unrecognized argument for headers, it should be an array or an object.'+data);
- 210 :
}
- 211 :
return this;
- 212 :
}
- 213 :
- 214 :
/**
- 215 :
* Add rows to the Table
- 216 :
* @param {Array} data
- 217 :
* @returns {Spyral.Table}
- 218 :
*/
- 219 :
addRows(data) {
- 220 :
data.forEach(row => this.addRow(row), this);
- 221 :
return this;
- 222 :
}
- 223 :
- 224 :
/**
- 225 :
* Add a row to the Table
- 226 :
* @param {(Array|Object)} data
- 227 :
* @returns {Spyral.Table}
- 228 :
*/
- 229 :
addRow(data, ...other) {
- 230 :
- 231 :
// we have multiple arguments, so call again as an array
- 232 :
if (other.length>0) {
- 233 :
return this.addRow([data].concat(other));
- 234 :
}
- 235 :
- 236 :
this.setRow(this.rows(), data, true);
- 237 :
return this;
- 238 :
}
- 239 :
- 240 :
/**
- 241 :
* Set a row
- 242 :
* @param {(number|string)} ind The row index
- 243 :
* @param {(Object|Array)} data
- 244 :
* @param {boolean} create
- 245 :
* @returns {Spyral.Table}
- 246 :
*/
- 247 :
setRow(ind, data, create) {
- 248 :
- 249 :
let rowIndex = this.getRowIndex(ind, create);
- 250 :
if (rowIndex>=this.rows() && !create) {
- 251 :
throw new Error('Attempt to set row values for a row that does note exist: '+ind+'. Maybe use addRow() instead?');
- 252 :
}
- 253 :
- 254 :
// we have a simple array, so we'll just push to the rows
- 255 :
if (data && Array.isArray(data)) {
- 256 :
if (data.length>this.columns()) {
- 257 :
if (create) {
- 258 :
for (let i = this.columns(); i<data.length; i++) {
- 259 :
this.addColumn();
- 260 :
}
- 261 :
} else {
- 262 :
throw new Error('The row that you\'ve created contains more columns than the current table. Maybe use addColunm() first?');
- 263 :
}
- 264 :
}
- 265 :
data.forEach((d,i) => this.setCell(rowIndex, i, d), this);
- 266 :
}
- 267 :
- 268 :
// we have an object so we'll use the headers
- 269 :
else if (typeof data === 'object') {
- 270 :
for (let column in data) {
- 271 :
if (!this.hasColumn(column)) {
- 272 :
//
- 273 :
}
- 274 :
this.setCell(rowIndex, column, data[column]);
- 275 :
}
- 276 :
}
- 277 :
- 278 :
else if (this.columns()<2 && create) { // hopefully some scalar value
- 279 :
if (this.columns()===0) {
- 280 :
this.addColumn(); // create first column if it doesn't exist
- 281 :
}
- 282 :
this.setCell(rowIndex,0,data);
- 283 :
} else {
- 284 :
throw new Error('setRow() expects an array or an object, maybe setCell()?');
- 285 :
}
- 286 :
- 287 :
return this;
- 288 :
- 289 :
}
- 290 :
- 291 :
/**
- 292 :
* Set a column
- 293 :
* @param {(number|string)} ind The column index
- 294 :
* @param {(Object|Array)} data
- 295 :
* @param {boolean} create
- 296 :
* @returns {Spyral.Table}
- 297 :
*/
- 298 :
setColumn(ind, data, create) {
- 299 :
- 300 :
let columnIndex = this.getColumnIndex(ind, create);
- 301 :
if (columnIndex>=this.columns() && !create) {
- 302 :
throw new Error('Attempt to set column values for a column that does note exist: '+ind+'. Maybe use addColumn() instead?');
- 303 :
}
- 304 :
- 305 :
// we have a simple array, so we'll just push to the rows
- 306 :
if (data && Array.isArray(data)) {
- 307 :
data.forEach((d,i) => this.setCell(i, columnIndex, d, create), this);
- 308 :
}
- 309 :
- 310 :
// we have an object so we'll use the headers
- 311 :
else if (typeof data === 'object') {
- 312 :
for (let row in data) {
- 313 :
this.setCell(row, columnIndex, data[row], create);
- 314 :
}
- 315 :
}
- 316 :
- 317 :
// hope we have a scalar value to assign to the first row
- 318 :
else {
- 319 :
this.setCell(0,columnIndex,data, create);
- 320 :
}
- 321 :
- 322 :
return this;
- 323 :
}
- 324 :
- 325 :
/**
- 326 :
* Add to or set a cell value
- 327 :
* @param {(number|string)} row The row index
- 328 :
* @param {(number|string)} column The column index
- 329 :
* @param {number} value The value to set/add
- 330 :
* @param {boolean} overwrite True to set, false to add to current value
- 331 :
*/
- 332 :
updateCell(row, column, value, overwrite) {
- 333 :
let rowIndex = this.getRowIndex(row, true);
- 334 :
let columnIndex = this.getColumnIndex(column, true);
- 335 :
let val = this.cell(rowIndex, columnIndex);
- 336 :
this._rows[rowIndex][columnIndex] = val && !overwrite ? val+value : value;
- 337 :
return this;
- 338 :
}
- 339 :
- 340 :
/**
- 341 :
* Get the value of a cell
- 342 :
* @param {(number|string)} rowInd The row index
- 343 :
* @param {(number|string)} colInd The column index
- 344 :
* @returns {number}
- 345 :
*/
- 346 :
cell(rowInd, colInd) {
- 347 :
return this._rows[this.getRowIndex(rowInd)][this.getColumnIndex(colInd)];
- 348 :
}
- 349 :
- 350 :
/**
- 351 :
* Set the value of a cell
- 352 :
* @param {(number|string)} row The row index
- 353 :
* @param {(number|string)} column The column index
- 354 :
* @param {number} value The value to set
- 355 :
* @returns {Spyral.Table}
- 356 :
*/
- 357 :
setCell(row, column, value) {
- 358 :
this.updateCell(row,column,value,true);
- 359 :
return this;
- 360 :
}
- 361 :
- 362 :
/**
- 363 :
* Get (and create) the row index
- 364 :
* @param {(number|string)} ind The index
- 365 :
* @param {boolean} create
- 366 :
* @returns {number}
- 367 :
*/
- 368 :
getRowIndex(ind, create) {
- 369 :
if (typeof ind === 'number') {
- 370 :
if (ind < this._rows.length) {
- 371 :
return ind;
- 372 :
} else if (create) {
- 373 :
this._rows[ind] = Array(this.columns());
- 374 :
return ind;
- 375 :
}
- 376 :
throw new Error('The requested row does not exist: '+ind);
- 377 :
} else if (typeof ind === 'string') {
- 378 :
let row = this._rows.findIndex(r => r[this._rowKeyColumnIndex] === ind, this);
- 379 :
if (row>-1) {return row;}
- 380 :
else if (create) {
- 381 :
let arr = Array(this.columns());
- 382 :
arr[this._rowKeyColumnIndex] = ind;
- 383 :
this.addRow(arr);
- 384 :
return this.rows();
- 385 :
}
- 386 :
else {
- 387 :
throw new Error('Unable to find the row named '+ind);
- 388 :
}
- 389 :
}
- 390 :
throw new Error('Please provide a valid row (number or named row)');
- 391 :
}
- 392 :
- 393 :
/**
- 394 :
* Get (and create) the column index
- 395 :
* @param {(number|string)} ind The index
- 396 :
* @param {boolean} create
- 397 :
* @returns {number}
- 398 :
*/
- 399 :
getColumnIndex(ind, create) {
- 400 :
if (typeof ind === 'number') {
- 401 :
if (ind < this.columns()) {
- 402 :
return ind;
- 403 :
} else if (create) {
- 404 :
this.addColumn(ind);
- 405 :
return ind;
- 406 :
}
- 407 :
throw new Error('The requested column does not exist: '+ind);
- 408 :
} else if (typeof ind === 'string') {
- 409 :
if (ind in this._headers) {
- 410 :
return this._headers[ind];
- 411 :
} else if (create) {
- 412 :
this.addColumn({header: ind});
- 413 :
return this._headers[ind];
- 414 :
}
- 415 :
throw new Error('Unable to find column named '+ind);
- 416 :
}
- 417 :
throw new Error('Please provide a valid column (number or named column)');
- 418 :
}
- 419 :
- 420 :
/**
- 421 :
* Add a column (at the specified index)
- 422 :
* @param {(Object|String)} config
- 423 :
* @param {(number|string)} ind
- 424 :
* @returns {Spyral.Table}
- 425 :
*/
- 426 :
addColumn(config, ind) {
- 427 :
// determine col
- 428 :
let col = this.columns(); // default
- 429 :
if (config && typeof config === 'string') {col=config;}
- 430 :
else if (config && (typeof config === 'object') && ('header' in config)) {col = config.header;}
- 431 :
else if (ind!==undefined) {col=ind;}
- 432 :
- 433 :
// check if it exists
- 434 :
if (col in this._headers) {
- 435 :
throw new Error('This column exists already: '+config.header);
- 436 :
}
- 437 :
- 438 :
// add column
- 439 :
let colIndex = this.columns();
- 440 :
this._headers[col] = colIndex;
- 441 :
- 442 :
// determine data
- 443 :
let data = [];
- 444 :
if (config && typeof config === 'object' && 'rows' in config) {data=config.rows;}
- 445 :
else if (Array.isArray(config)) {data = config;}
- 446 :
- 447 :
// make sure we have enough rows for the new data
- 448 :
let columns = this.columns();
- 449 :
while (this._rows.length<data.length) {
- 450 :
this._rows[this._rows.length] = new Array(columns);
- 451 :
}
- 452 :
- 453 :
this._rows.forEach((r,i) => r[colIndex] = data[i]);
- 454 :
return this;
- 455 :
}
- 456 :
- 457 :
/**
- 458 :
* This function returns different values depending on the arguments provided.
- 459 :
* When there are no arguments, it returns the number of rows in this table.
- 460 :
* When the first argument is the boolean value `true` all rows are returned.
- 461 :
* When the first argument is a an array then the rows corresponding to the row
- 462 :
* indices or names are returned. When all arguments except are numbers or strings
- 463 :
* then each of those is returned.
- 464 :
* @param {(Boolean|Array|Number|String)} [inds]
- 465 :
* @param {(Object|Number|String)} [config]
- 466 :
* @returns {(Number|Array)}
- 467 :
*/
- 468 :
rows(inds, config, ...other) {
- 469 :
- 470 :
// return length
- 471 :
if (inds===undefined) {
- 472 :
return this._rows.length;
- 473 :
}
- 474 :
- 475 :
let rows = [];
- 476 :
let asObj = (config && typeof config === 'object' && config.asObj) ||
- 477 :
(other.length>0 && typeof other[other.length-1] === 'object' && other[other.length-1].asObj);
- 478 :
- 479 :
// return all
- 480 :
if (typeof inds === 'boolean' && inds) {
- 481 :
rows = this._rows.map((r,i) => this.row(i, asObj));
- 482 :
}
- 483 :
- 484 :
// return specified rows
- 485 :
else if (Array.isArray(inds)) {
- 486 :
rows = inds.map(ind => this.row(ind));
- 487 :
}
- 488 :
- 489 :
// return specified rows as varargs
- 490 :
else if (typeof inds === 'number' || typeof inds === 'string') {
- 491 :
[inds, config, ...other].every(i => {
- 492 :
if (typeof i === 'number' || typeof i === 'string') {
- 493 :
rows.push(this.row(i, asObj));
- 494 :
return true;
- 495 :
} else {
- 496 :
return false;
- 497 :
}
- 498 :
});
- 499 :
if (other.length>0) { // when config is in last position
- 500 :
if (typeof other[other.length-1] === 'object') {
- 501 :
config = other[other.length-1];
- 502 :
}
- 503 :
}
- 504 :
}
- 505 :
- 506 :
// zip if requested
- 507 :
if (config && typeof config === 'object' && 'zip' in config && config.zip) {
- 508 :
if (rows.length<2) {throw new Error('Only one row available, can\'t zip');}
- 509 :
return Table.zip(rows);
- 510 :
}
- 511 :
else {
- 512 :
return rows;
- 513 :
}
- 514 :
}
- 515 :
- 516 :
/**
- 517 :
* Get the specified row
- 518 :
* @param {(number|string)} ind
- 519 :
* @param {boolean} [asObj]
- 520 :
* @returns {(Object|Number|String)}
- 521 :
*/
- 522 :
row(ind, asObj) {
- 523 :
let row = this._rows[this.getRowIndex(ind)];
- 524 :
if (asObj) {
- 525 :
let obj = {};
- 526 :
for (let key in this._headers) {
- 527 :
obj[key] = row[this._headers[key]];
- 528 :
}
- 529 :
return obj;
- 530 :
} else {
- 531 :
return row;
- 532 :
}
- 533 :
}
- 534 :
- 535 :
/**
- 536 :
* This function returns different values depending on the arguments provided.
- 537 :
* When there are no arguments, it returns the number of columns in this table.
- 538 :
* When the first argument is the boolean value `true` all columns are returned.
- 539 :
* When the first argument is a number a slice of the columns is returned and if
- 540 :
* the second argument is a number it is treated as the length of the slice to
- 541 :
* return (note that it isn't the `end` index like with Array.slice()).
- 542 :
* @param {(Boolean|Array|Number|String)} [inds]
- 543 :
* @param {(Object|Number|String)} [config]
- 544 :
* @returns {(Number|Array)}
- 545 :
*/
- 546 :
columns(inds, config, ...other) {
- 547 :
- 548 :
// return length
- 549 :
if (inds===undefined) {
- 550 :
return Object.keys(this._headers).length;
- 551 :
}
- 552 :
- 553 :
let columns = [];
- 554 :
let asObj = (config && typeof config === 'object' && config.asObj) ||
- 555 :
(other.length>0 && typeof other[other.length-1] === 'object' && other[other.length-1].asObj);
- 556 :
- 557 :
// return all columns
- 558 :
if (typeof inds === 'boolean' && inds) {
- 559 :
for (let i=0, len=this.columns(); i<len; i++) {
- 560 :
columns.push(this.column(i, asObj));
- 561 :
}
- 562 :
}
- 563 :
- 564 :
// return specified columns
- 565 :
else if (Array.isArray(inds)) {
- 566 :
inds.forEach(i => columns.push(this.column(i, asObj)), this);
- 567 :
}
- 568 :
- 569 :
else if (typeof inds === 'number' || typeof inds === 'string') {
- 570 :
[inds, config, ...other].every(i => {
- 571 :
if (typeof i === 'number' || typeof i === 'string') {
- 572 :
columns.push(this.column(i, asObj));
- 573 :
return true;
- 574 :
} else {
- 575 :
return false;
- 576 :
}
- 577 :
});
- 578 :
if (other.length>0) { // when config is in last position
- 579 :
if (typeof other[other.length-1] === 'object') {
- 580 :
config = other[other.length-1];
- 581 :
}
- 582 :
}
- 583 :
}
- 584 :
- 585 :
if (config && typeof config === 'object' && 'zip' in config && config.zip) {
- 586 :
if (columns.length<2) {throw new Error('Only one column available, can\'t zip');}
- 587 :
return Table.zip(columns);
- 588 :
}
- 589 :
else {
- 590 :
return columns;
- 591 :
}
- 592 :
}
- 593 :
- 594 :
/**
- 595 :
* Get the specified column
- 596 :
* @param {(number|string)} ind
- 597 :
* @param {boolean} [asObj]
- 598 :
* @returns {(Object|Number|String)}
- 599 :
*/
- 600 :
column(ind, asObj) {
- 601 :
let column = this.getColumnIndex(ind);
- 602 :
let data = this._rows.forEach(r => r[column]); // TODO
- 603 :
if (asObj) {
- 604 :
let obj = {};
- 605 :
this._rows.forEach(r => {
- 606 :
obj[r[this._rowKeyColumnIndex]] = r[column];
- 607 :
});
- 608 :
return obj;
- 609 :
} else {
- 610 :
return this._rows.map(r => r[column]);
- 611 :
}
- 612 :
}
- 613 :
- 614 :
/**
- 615 :
* Get the specified header
- 616 :
* @param {(number|string)} ind
- 617 :
* @returns {(number|string)}
- 618 :
*/
- 619 :
header(ind) {
- 620 :
let keys = Object.keys(this._headers);
- 621 :
let i = this.getColumnIndex(ind);
- 622 :
return keys[keys.findIndex(k => i===this._headers[k])];
- 623 :
}
- 624 :
- 625 :
/**
- 626 :
* This function returns different values depending on the arguments provided.
- 627 :
* When there are no arguments, it returns the number of headers in this table.
- 628 :
* When the first argument is the boolean value `true` all headers are returned.
- 629 :
* When the first argument is a number a slice of the headers is returned.
- 630 :
* When the first argument is an array the slices specified in the array are returned.
- 631 :
* @param {(Boolean|Array|Number|String)} inds
- 632 :
* @returns {(Number|Array)}
- 633 :
*/
- 634 :
headers(inds, ...other) {
- 635 :
- 636 :
// return length
- 637 :
if (inds===undefined) {
- 638 :
return Object.keys(this._headers).length;
- 639 :
}
- 640 :
- 641 :
// let headers = [];
- 642 :
- 643 :
// return all
- 644 :
if (typeof inds === 'boolean' && inds) {
- 645 :
inds = Array(Object.keys(this._headers).length).fill().map((_,i) => i);
- 646 :
}
- 647 :
- 648 :
// return specified rows
- 649 :
if (Array.isArray(inds)) {
- 650 :
return inds.map(i => this.header(i));
- 651 :
}
- 652 :
- 653 :
// return specified rows as varargs
- 654 :
else if (typeof inds === 'number' || typeof inds === 'string') {
- 655 :
return [inds, ...other].map(i => this.header(i));
- 656 :
}
- 657 :
}
- 658 :
- 659 :
/**
- 660 :
* Does the specified column exist
- 661 :
* @param {(number|string)} ind
- 662 :
* @returns {(number|string)}
- 663 :
*/
- 664 :
hasColumn(ind) {
- 665 :
return ind in this._headers;
- 666 :
}
- 667 :
- 668 :
/**
- 669 :
* Runs the specified function on each row.
- 670 :
* The function is passed the row and the row index.
- 671 :
* @param {Function} fn
- 672 :
*/
- 673 :
forEach(fn) {
- 674 :
this._rows.forEach((r,i) => fn(r,i));
- 675 :
}
- 676 :
- 677 :
/**
- 678 :
* Get the minimum value in the specified row
- 679 :
* @param {(number|string)} ind
- 680 :
* @returns {number}
- 681 :
*/
- 682 :
rowMin(ind) {
- 683 :
return Math.min.apply(null, this.row(ind));
- 684 :
}
- 685 :
- 686 :
/**
- 687 :
* Get the maximum value in the specified row
- 688 :
* @param {(number|string)} ind
- 689 :
* @returns {number}
- 690 :
*/
- 691 :
rowMax(ind) {
- 692 :
return Math.max.apply(null, this.row(ind));
- 693 :
}
- 694 :
- 695 :
/**
- 696 :
* Get the minimum value in the specified column
- 697 :
* @param {(number|string)} ind
- 698 :
* @returns {number}
- 699 :
*/
- 700 :
columnMin(ind) {
- 701 :
return Math.min.apply(null, this.column(ind));
- 702 :
}
- 703 :
- 704 :
/**
- 705 :
* Get the maximum value in the specified column
- 706 :
* @param {(number|string)} ind
- 707 :
* @returns {number}
- 708 :
*/
- 709 :
columnMax(ind) {
- 710 :
return Math.max.apply(null, this.column(ind));
- 711 :
}
- 712 :
- 713 :
/**
- 714 :
* Get the sum of the values in the specified row
- 715 :
* @param {(number|string)} ind
- 716 :
* @returns {number}
- 717 :
*/
- 718 :
rowSum(ind) {
- 719 :
return Table.sum(this.row(ind));
- 720 :
}
- 721 :
- 722 :
/**
- 723 :
* Get the sum of the values in the specified column
- 724 :
* @param {(number|string)} ind
- 725 :
* @returns {number}
- 726 :
*/
- 727 :
columnSum(ind) {
- 728 :
return Table.sum(this.column(ind));
- 729 :
}
- 730 :
- 731 :
/**
- 732 :
* Get the mean of the values in the specified row
- 733 :
* @param {(number|string)} ind
- 734 :
* @returns {number}
- 735 :
*/
- 736 :
rowMean(ind) {
- 737 :
return Table.mean(this.row(ind));
- 738 :
}
- 739 :
- 740 :
/**
- 741 :
* Get the mean of the values in the specified column
- 742 :
* @param {(number|string)} ind
- 743 :
* @returns {number}
- 744 :
*/
- 745 :
columnMean(ind) {
- 746 :
return Table.mean(this.column(ind));
- 747 :
}
- 748 :
- 749 :
/**
- 750 :
* Get the count of each unique value in the specified row
- 751 :
* @param {(number|string)} ind
- 752 :
* @returns {number}
- 753 :
*/
- 754 :
rowCounts(ind) {
- 755 :
return Table.counts(this.row(ind));
- 756 :
}
- 757 :
- 758 :
/**
- 759 :
* Get the count of each unique value in the specified column
- 760 :
* @param {(number|string)} ind
- 761 :
* @returns {number}
- 762 :
*/
- 763 :
columnCounts(ind) {
- 764 :
return Table.counts(this.column(ind));
- 765 :
}
- 766 :
- 767 :
/**
- 768 :
* Get the rolling mean for the specified row
- 769 :
* @param {(number|string)} ind
- 770 :
* @param {number} neighbors
- 771 :
* @param {boolean} overwrite
- 772 :
* @returns {Array}
- 773 :
*/
- 774 :
rowRollingMean(ind, neighbors, overwrite) {
- 775 :
let means = Table.rollingMean(this.row(ind), neighbors);
- 776 :
if (overwrite) {
- 777 :
this.setRow(ind, means);
- 778 :
}
- 779 :
return means;
- 780 :
}
- 781 :
- 782 :
/**
- 783 :
* Get the rolling mean for the specified column
- 784 :
* @param {(number|string)} ind
- 785 :
* @param {number} neighbors
- 786 :
* @param {boolean} overwrite
- 787 :
* @returns {Array}
- 788 :
*/
- 789 :
columnRollingMean(ind, neighbors, overwrite) {
- 790 :
let means = Table.rollingMean(this.column(ind), neighbors);
- 791 :
if (overwrite) {
- 792 :
this.setColumn(ind, means);
- 793 :
}
- 794 :
return means;
- 795 :
}
- 796 :
- 797 :
/**
- 798 :
* Get the variance for the specified row
- 799 :
* @param {(number|string)} ind
- 800 :
* @returns {number}
- 801 :
*/
- 802 :
rowVariance(ind) {
- 803 :
return Table.variance(this.row(ind));
- 804 :
}
- 805 :
- 806 :
/**
- 807 :
* Get the variance for the specified column
- 808 :
* @param {(number|string)} ind
- 809 :
* @returns {number}
- 810 :
*/
- 811 :
columnVariance(ind) {
- 812 :
return Table.variance(this.column(ind));
- 813 :
}
- 814 :
- 815 :
/**
- 816 :
* Get the standard deviation for the specified row
- 817 :
* @param {(number|string)} ind
- 818 :
* @returns {number}
- 819 :
*/
- 820 :
rowStandardDeviation(ind) {
- 821 :
return Table.standardDeviation(this.row(ind));
- 822 :
}
- 823 :
- 824 :
/**
- 825 :
* Get the standard deviation for the specified column
- 826 :
* @param {(number|string)} ind
- 827 :
* @returns {number}
- 828 :
*/
- 829 :
columnStandardDeviation(ind) {
- 830 :
return Table.standardDeviation(this.column(ind));
- 831 :
}
- 832 :
- 833 :
/**
- 834 :
* Get the z scores for the specified row
- 835 :
* @param {(number|string)} ind
- 836 :
* @returns {Array}
- 837 :
*/
- 838 :
rowZScores(ind) {
- 839 :
return Table.zScores(this.row(ind));
- 840 :
}
- 841 :
- 842 :
/**
- 843 :
* Get the z scores for the specified column
- 844 :
* @param {(number|string)} ind
- 845 :
* @returns {Array}
- 846 :
*/
- 847 :
columnZScores(ind) {
- 848 :
return Table.zScores(this.column(ind));
- 849 :
}
- 850 :
- 851 :
/**
- 852 :
* TODO
- 853 :
* Sort the specified rows
- 854 :
* @returns {Spyral.Table}
- 855 :
*/
- 856 :
rowSort(inds, config) {
- 857 :
// no inds, use all columns
- 858 :
if (inds===undefined) {
- 859 :
inds = Array(this.columns()).fill().map((_,i) => i);
- 860 :
}
- 861 :
- 862 :
// wrap a single index as array
- 863 :
if (typeof inds === 'string' || typeof inds === 'number') {
- 864 :
inds = [inds];
- 865 :
}
- 866 :
- 867 :
if (Array.isArray(inds)) {
- 868 :
return this.rowSort((a,b) => {
- 869 :
let ind;
- 870 :
for (let i=0, len=inds.length; i<len; i++) {
- 871 :
ind = this.getColumnIndex(inds[i]);
- 872 :
if (a!==b) {
- 873 :
if (typeof a[ind] === 'string' && typeof b[ind] === 'string') {
- 874 :
return a[ind].localeCompare(b[ind]);
- 875 :
} else {
- 876 :
return a[ind]-b[ind];
- 877 :
}
- 878 :
}
- 879 :
}
- 880 :
return 0;
- 881 :
}, config);
- 882 :
}
- 883 :
- 884 :
if (typeof inds === 'function') {
- 885 :
this._rows.sort((a,b) => {
- 886 :
if (config && 'asObject' in config && config.asObject) {
- 887 :
let c = {};
- 888 :
for (let k in this._headers) {
- 889 :
c[k] = a[this._headers[k]];
- 890 :
}
- 891 :
let d = {};
- 892 :
for (let k in this._headers) {
- 893 :
d[k] = b[this._headers[k]];
- 894 :
}
- 895 :
return inds.apply(this, [c,d]);
- 896 :
} else {
- 897 :
return inds.apply(this, [a,b]);
- 898 :
}
- 899 :
});
- 900 :
if (config && 'reverse' in config && config.reverse) {
- 901 :
this._rows.reverse(); // in place
- 902 :
}
- 903 :
}
- 904 :
- 905 :
return this;
- 906 :
}
- 907 :
- 908 :
/**
- 909 :
* TODO
- 910 :
* Sort the specified columns
- 911 :
* @returns {Spyral.Table}
- 912 :
*/
- 913 :
columnSort(inds, config) {
- 914 :
// no inds, use all columns
- 915 :
if (inds===undefined) {
- 916 :
inds = Array(this.columns()).fill().map((_,i) => i);
- 917 :
}
- 918 :
- 919 :
// wrap a single index as array
- 920 :
if (typeof inds === 'string' || typeof inds === 'number') {
- 921 :
inds = [inds];
- 922 :
}
- 923 :
- 924 :
if (Array.isArray(inds)) {
- 925 :
- 926 :
// convert to column names
- 927 :
let headers = inds.map(ind => this.header(ind));
- 928 :
- 929 :
// make sure we have all columns
- 930 :
Object.keys(this._headers).forEach(h => {
- 931 :
if (!headers.includes(h)) {headers.push(h);}
- 932 :
});
- 933 :
- 934 :
// sort names alphabetically
- 935 :
headers.sort((a,b) => a.localeCompare(b));
- 936 :
- 937 :
// reorder by columns
- 938 :
this._rows = this._rows.map((_,i) => headers.map(h => this.cell(i,h)));
- 939 :
this._headers = {};
- 940 :
headers.forEach((h,i) => this._headers[h]=i);
- 941 :
- 942 :
}
- 943 :
- 944 :
if (typeof inds === 'function') {
- 945 :
let headers = Object.keys(this._headers);
- 946 :
if (config && 'asObject' in headers && headers.asObject) {
- 947 :
headers = headers.map((h,i) => {
- 948 :
return {header: h, data: this._rows.map((r,j) => this.cell(i,j))};
- 949 :
});
- 950 :
}
- 951 :
headers.sort((a,b) => {
- 952 :
return inds.apply(this, [a,b]);
- 953 :
});
- 954 :
headers = headers.map(h => typeof h === 'object' ? h.header : h); // convert back to string
- 955 :
- 956 :
// make sure we have all keys
- 957 :
Object.keys(this._headers).forEach(k => {
- 958 :
if (headers.indexOf(k)===-1) {headers.push(k);}
- 959 :
});
- 960 :
- 961 :
this._rows = this._rows.map((_,i) => headers.map(h => this.cell(i,h)));
- 962 :
this._headers = {};
- 963 :
headers.forEach((h,i) => this._headers[h]=i);
- 964 :
}
- 965 :
}
- 966 :
- 967 :
/**
- 968 :
* Get a CSV representation of the Table
- 969 :
* @param {Object} [config]
- 970 :
* @returns {string}
- 971 :
*/
- 972 :
toCsv(config) {
- 973 :
const cell = function(c) {
- 974 :
let quote = /"/g;
- 975 :
return typeof c === 'string' && (c.indexOf(',')>-1 || c.indexOf('"')>-1) ? '"'+c.replace(quote,'"')+'"' : c;
- 976 :
};
- 977 :
return (config && 'noHeaders' in config && config.noHeaders ? '' : this.headers(true).map(h => cell(h)).join(',') + '\n') +
- 978 :
this._rows.map(row => row.map(c => cell(c)).join(',')).join('\n');
- 979 :
}
- 980 :
- 981 :
/**
- 982 :
* Get a TSV representation of the Table
- 983 :
* @param {Object} [config]
- 984 :
* @returns {string}
- 985 :
*/
- 986 :
toTsv(config) {
- 987 :
return config && 'noHeaders' in config && config.noHeaders ? '' : this.headers(true).join('\t') + '\n' +
- 988 :
this._rows.map(row => row.join('\t')).join('\n');
- 989 :
}
- 990 :
- 991 :
/**
- 992 :
* Set the target's contents to an HTML representation of the Table
- 993 :
* @param {(Function|String|Object)} target
- 994 :
* @param {Object} [config]
- 995 :
* @returns {Spyral.Table}
- 996 :
*/
- 997 :
html(target, config) {
- 998 :
let html = this.toString(config);
- 999 :
if (typeof target === 'function') {
- 1000 :
target(html);
- 1001 :
} else {
- 1002 :
if (typeof target === 'string') {
- 1003 :
target = document.querySelector(target);
- 1004 :
if (!target) {
- 1005 :
throw 'Unable to find specified target: '+target;
- 1006 :
}
- 1007 :
}
- 1008 :
if (typeof target === 'object' && 'innerHTML' in target) {
- 1009 :
target.innerHTML = html;
- 1010 :
}
- 1011 :
}
- 1012 :
return this;
- 1013 :
}
- 1014 :
- 1015 :
/**
- 1016 :
* Same as {@link toString}.
- 1017 :
*/
- 1018 :
toHtml(config={}) {
- 1019 :
return this.toString(config);
- 1020 :
}
- 1021 :
- 1022 :
/**
- 1023 :
* Displays an interactive table using [DataTables]{@link https://datatables.net/}
- 1024 :
* @param {HTMLElement} [target]
- 1025 :
* @param {Object} config
- 1026 :
* @returns {DataTable}
- 1027 :
*/
- 1028 :
toDataTable(target, config={}) {
- 1029 :
if (Util.isNode(target) === false && typeof target === 'object') {
- 1030 :
config = target;
- 1031 :
target = undefined;
- 1032 :
}
- 1033 :
if (target === undefined) {
- 1034 :
if (typeof Spyral !== 'undefined' && Spyral.Notebook) {
- 1035 :
target = Spyral.Notebook.getTarget();
- 1036 :
} else {
- 1037 :
target = document.createElement('div');
- 1038 :
document.body.appendChild(target);
- 1039 :
}
- 1040 :
} else {
- 1041 :
if (Util.isNode(target) && target.isConnected === false) {
- 1042 :
throw new Error('The target node does not exist within the document.');
- 1043 :
}
- 1044 :
}
- 1045 :
target = document.body.appendChild(target);
- 1046 :
this.html(target, config);
- 1047 :
let dataTable = new DataTable(target.firstElementChild);
- 1048 :
return dataTable;
- 1049 :
}
- 1050 :
- 1051 :
/**
- 1052 :
* Get an HTML representation of the Table
- 1053 :
* @param {Object} [config]
- 1054 :
* @returns {string}
- 1055 :
*/
- 1056 :
toString(config={}) {
- 1057 :
if (typeof config === 'number') {
- 1058 :
config = {limit: config};
- 1059 :
}
- 1060 :
if ('top' in config && !('limit' in config)) {
- 1061 :
config.limit = config.top;
- 1062 :
}
- 1063 :
if ('limit' in config && !('bottom' in config)) {
- 1064 :
config.bottom = 0;
- 1065 :
}
- 1066 :
if ('bottom' in config && !('limit' in config)) {
- 1067 :
config.limit=0;
- 1068 :
}
- 1069 :
return '<table'+('id' in config ? ' id="'+config.id+'" ' : ' ')+'class="voyantTable">' +
- 1070 :
((config && 'caption' in config && typeof config.caption === 'string') ?
- 1071 :
'<caption>'+config.caption+'</caption>' : '') +
- 1072 :
((config && 'noHeaders' in config && config.noHeaders) ? '' : ('<thead><tr>'+this.headers(true).map(c => '<th>'+c+'</th>').join('')+'</tr></thead>'))+
- 1073 :
'<tbody>'+
- 1074 :
this._rows.filter((row,i,arr) => ((!('limit' in config) || i<config.limit) || (!('bottom' in config) || i > arr.length-1 - config.bottom)))
- 1075 :
.map(row => '<tr>'+row.map(c => '<td>'+(typeof c === 'number' ? c.toLocaleString() : c)+'</td>').join('')+'</tr>').join('') +
- 1076 :
'</tbody></table>';
- 1077 :
}
- 1078 :
- 1079 :
/**
- 1080 :
* Show a chart representing the Table
- 1081 :
* @param {(String|HTMLElement)} [target]
- 1082 :
* @param {HighchartsConfig} [config]
- 1083 :
* @returns {Highcharts.Chart}
- 1084 :
*/
- 1085 :
chart(target = undefined, config = {}) {
- 1086 :
[target, config] = Chart._handleTargetAndConfig(target, config);
- 1087 :
- 1088 :
config.chart = config.chart || {};
- 1089 :
- 1090 :
let columnsCount = this.columns();
- 1091 :
let rowsCount = this.rows();
- 1092 :
let headers = this.headers(config.columns ? config.columns : true);
- 1093 :
let isHeadersCategories = headers.every(h => isNaN(h));
- 1094 :
- 1095 :
if (isHeadersCategories) {
- 1096 :
Chart._setDefaultChartType(config, 'column');
- 1097 :
}
- 1098 :
- 1099 :
// set categories if not set
- 1100 :
config.xAxis = config.xAxis || {};
- 1101 :
config.xAxis.categories = config.xAxis.categories || headers;
- 1102 :
- 1103 :
// start filling in series
- 1104 :
config.series = config.series || [];
- 1105 :
- 1106 :
if (!('seriesFrom' in config)) {
- 1107 :
// one row, so let's take series from rows
- 1108 :
if (rowsCount === 1) {
- 1109 :
config.dataFrom = config.dataFrom || 'rows';
- 1110 :
} else if (columnsCount === 1 || (!('dataFrom' in config))) {
- 1111 :
config.dataFrom = config.dataFrom || 'columns';
- 1112 :
}
- 1113 :
}
- 1114 :
- 1115 :
if ('dataFrom' in config) {
- 1116 :
if (config.dataFrom === 'rows') {
- 1117 :
config.data = {rows:[]};
- 1118 :
config.data.rows.push(headers);
- 1119 :
config.data.rows = config.data.rows.concat(this.rows(true));
- 1120 :
} else if (config.dataFrom === 'columns') {
- 1121 :
config.data = {columns:[]};
- 1122 :
config.data.columns = config.data.columns.concat(this.columns(true));
- 1123 :
if (config.data.columns.length === headers.length) {
- 1124 :
headers.forEach((h, i) => {
- 1125 :
config.data.columns[i].splice(0, 0, h);
- 1126 :
});
- 1127 :
}
- 1128 :
}
- 1129 :
} else if ('seriesFrom' in config) {
- 1130 :
if (config.seriesFrom === 'rows') {
- 1131 :
this.rows(config.rows ? config.rows : true).forEach((row, i) => {
- 1132 :
config.series[i] = config.series[i] || {};
- 1133 :
config.series[i].data = headers.map(h => this.cell(i, h));
- 1134 :
});
- 1135 :
} else if (config.seriesFrom === 'columns') {
- 1136 :
this.columns(config.columns ? config.columns : true).forEach((col, i) => {
- 1137 :
config.series[i] = config.series[i] || {};
- 1138 :
config.series[i].data = [];
- 1139 :
for (let r = 0; r < rowsCount; r++) {
- 1140 :
config.series[i].data.push(this.cell(r, i));
- 1141 :
}
- 1142 :
});
- 1143 :
}
- 1144 :
}
- 1145 :
- 1146 :
delete config.dataFrom;
- 1147 :
delete config.seriesFrom;
- 1148 :
- 1149 :
return Chart.create(target, config);
- 1150 :
}
- 1151 :
- 1152 :
/**
- 1153 :
* Create a new Table
- 1154 :
* @param {(Object|Array|String|Number)} data
- 1155 :
* @param {Spyral.Table~TableConfig} config
- 1156 :
* @returns {Spyral.Table}
- 1157 :
* @static
- 1158 :
*/
- 1159 :
static create(data, config, ...other) {
- 1160 :
return new Table(data, config, ...other);
- 1161 :
}
- 1162 :
- 1163 :
/**
- 1164 :
* Fetch a Table from a source
- 1165 :
* @param {(String|Request)} input
- 1166 :
* @param {Object} api
- 1167 :
* @param {Object} config
- 1168 :
* @returns {Promise}
- 1169 :
* @static
- 1170 :
*/
- 1171 :
static fetch(input, api, config) {
- 1172 :
return new Promise((resolve, reject) => {
- 1173 :
window.fetch(input, api).then(response => {
- 1174 :
if (!response.ok) {throw new Error(response.status + ' ' + response.statusText);}
- 1175 :
response.text().then(text => {
- 1176 :
resolve(Table.create(text, config || api));
- 1177 :
});
- 1178 :
});
- 1179 :
});
- 1180 :
}
- 1181 :
- 1182 :
/**
- 1183 :
* Get the count of each unique value in the data
- 1184 :
* @param {Array} data
- 1185 :
* @returns {Object}
- 1186 :
* @static
- 1187 :
*/
- 1188 :
static counts(data) {
- 1189 :
let vals = {};
- 1190 :
data.forEach(v => v in vals ? vals[v]++ : vals[v]=1);
- 1191 :
return vals;
- 1192 :
}
- 1193 :
- 1194 :
/**
- 1195 :
* Compare two values
- 1196 :
* @param {(number|string)} a
- 1197 :
* @param {(number|string)} b
- 1198 :
* @returns {number}
- 1199 :
* @static
- 1200 :
*/
- 1201 :
static cmp(a, b) {
- 1202 :
return typeof a === 'string' && typeof b === 'string' ? a.localeCompare(b) : a-b;
- 1203 :
}
- 1204 :
- 1205 :
/**
- 1206 :
* Get the sum of the provided values
- 1207 :
* @param {Array} data
- 1208 :
* @returns {number}
- 1209 :
* @static
- 1210 :
*/
- 1211 :
static sum(data) {
- 1212 :
return data.reduce((a,b) => a+b, 0);
- 1213 :
}
- 1214 :
- 1215 :
/**
- 1216 :
* Get the mean of the provided values
- 1217 :
* @param {Array} data
- 1218 :
* @returns {number}
- 1219 :
* @static
- 1220 :
*/
- 1221 :
static mean(data) {
- 1222 :
return Table.sum(data) / data.length;
- 1223 :
}
- 1224 :
- 1225 :
/**
- 1226 :
* Get rolling mean for the provided values
- 1227 :
* @param {Array} data
- 1228 :
* @param {number} neighbors
- 1229 :
* @returns {Array}
- 1230 :
* @static
- 1231 :
*/
- 1232 :
static rollingMean(data, neighbors) {
- 1233 :
// https://stackoverflow.com/questions/41386083/plot-rolling-moving-average-in-d3-js-v4/41388581#41387286
- 1234 :
return data.map((val, idx, arr) => {
- 1235 :
let start = Math.max(0, idx - neighbors), end = idx + neighbors;
- 1236 :
let subset = arr.slice(start, end + 1);
- 1237 :
let sum = subset.reduce((a,b) => a + b);
- 1238 :
return sum / subset.length;
- 1239 :
});
- 1240 :
}
- 1241 :
- 1242 :
/**
- 1243 :
* Get the variance for the provided values
- 1244 :
* @param {Array} data
- 1245 :
* @returns {number}
- 1246 :
* @static
- 1247 :
*/
- 1248 :
static variance(data) {
- 1249 :
let m = Table.mean(data);
- 1250 :
return Table.mean(data.map(num => Math.pow(num-m, 2)));
- 1251 :
}
- 1252 :
- 1253 :
/**
- 1254 :
* Get the standard deviation for the provided values
- 1255 :
* @param {Array} data
- 1256 :
* @returns {number}
- 1257 :
* @static
- 1258 :
*/
- 1259 :
static standardDeviation(data) {
- 1260 :
return Math.sqrt(Table.variance(data));
- 1261 :
}
- 1262 :
- 1263 :
/**
- 1264 :
* Get the z scores for the provided values
- 1265 :
* @param {Array} data
- 1266 :
* @returns {Array}
- 1267 :
* @static
- 1268 :
*/
- 1269 :
static zScores(data) {
- 1270 :
let m = Table.mean(data);
- 1271 :
let s = Table.standardDeviation(data);
- 1272 :
return data.map(num => (num-m) / s);
- 1273 :
}
- 1274 :
- 1275 :
/**
- 1276 :
* Perform a zip operation of the provided arrays. Learn more about zip on [Wikipedia]{@link https://en.wikipedia.org/wiki/Convolution_%28computer_science%29}.
- 1277 :
* @param {Array} data
- 1278 :
* @returns {Array}
- 1279 :
* @static
- 1280 :
*/
- 1281 :
static zip(...data) {
- 1282 :
- 1283 :
// we have a single nested array, so let's recall with flattened arguments
- 1284 :
if (data.length===1 && Array.isArray(data) && data.every(d => Array.isArray(d))) {
- 1285 :
return Table.zip.apply(null, ...data);
- 1286 :
}
- 1287 :
- 1288 :
// allow arrays to be of different lengths
- 1289 :
let len = Math.max.apply(null, data.map(d => d.length));
- 1290 :
return new Array(len).fill().map((_,i) => data.map(d => d[i]));
- 1291 :
}
- 1292 :
}
- 1293 :
- 1294 :
// this seems like a good balance between a built-in flexible parser and a heavier external parser
- 1295 :
// https://lowrey.me/parsing-a-csv-file-in-es6-javascript/
- 1296 :
const regex = /(?!\s*$)\s*(?:'([^'\\]*(?:\\[\S\s][^'\\]*)*)'|"([^"\\]*(?:\\[\S\s][^"\\]*)*)"|([^,'"\s\\]*(?:\s+[^,'"\s\\]+)*))\s*(?:,|$)/g;
- 1297 :
function parseCsvLine(line) {
- 1298 :
let arr = [];
- 1299 :
line.replace(regex, (m0, m1, m2, m3) => {
- 1300 :
if (m1 !== undefined) {
- 1301 :
arr.push(m1.replace(/\\'/g, '\''));
- 1302 :
} else if (m2 !== undefined) {
- 1303 :
arr.push(m2.replace(/\\"/g, '"'));
- 1304 :
} else if (m3 !== undefined) {
- 1305 :
arr.push(m3);
- 1306 :
}
- 1307 :
return '';
- 1308 :
});
- 1309 :
if (/,\s*$/.test(line)) {arr.push('');}
- 1310 :
return arr;
- 1311 :
- 1312 :
}
- 1313 :
- 1314 :
export default Table;