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