1 define([ 2 'jquery', 3 'underscore', 4 'util' 5 ], 6 function($, _, util) { 7 var naturalSort = util.naturalSort; 8 /** 9 * 10 * @class Plottable 11 * 12 * Represents a sample and the associated metadata in the ordination space. 13 * 14 * @param {string} name A string indicating the name of the sample. 15 * @param {string[]} metadata An Array of strings with the metadata values. 16 * @param {float[]} coordinates An Array of floats indicating the position in 17 * space where this sample is located. 18 * @param {integer} [idx = -1] An integer representing the index where the 19 * object is located in a DecompositionModel. 20 * @param {float[]} [ci = []] An array of floats indicating the confidence 21 * intervals in each dimension. 22 * 23 * @return {Plottable} 24 * @constructs Plottable 25 * 26 **/ 27 function Plottable(name, metadata, coordinates, idx, ci) { 28 /** 29 * Sample name. 30 * @type {string} 31 */ 32 this.name = name; 33 /** 34 * Metadata values for the sample. 35 * @type {string[]} 36 */ 37 this.metadata = metadata; 38 /** 39 * Position of the sample in the N-dimensional space. 40 * @type {float[]} 41 */ 42 this.coordinates = coordinates; 43 44 /** 45 * The index of the sample in the array of meshes. 46 * @type {integer} 47 */ 48 this.idx = idx === undefined ? -1 : idx; 49 /** 50 * Confidence intervals. 51 * @type {float[]} 52 */ 53 this.ci = ci === undefined ? [] : ci; 54 55 if (this.ci.length !== 0) { 56 if (this.ci.length !== this.coordinates.length) { 57 throw new Error("The number of confidence intervals doesn't match " + 58 'with the number of dimensions in the coordinates ' + 59 'attribute. coords: ' + this.coordinates.length + 60 ' ci: ' + this.ci.length); 61 } 62 } 63 }; 64 65 /** 66 * 67 * Helper method to convert a Plottable into a string. 68 * 69 * @return {string} A string describing the Plottable object. 70 * 71 */ 72 Plottable.prototype.toString = function() { 73 var ret = 'Sample: ' + this.name + ' located at: (' + 74 this.coordinates.join(', ') + ') metadata: [' + 75 this.metadata.join(', ') + ']'; 76 77 if (this.idx === -1) { 78 ret = ret + ' without index'; 79 } 80 else { 81 ret = ret + ' at index: ' + this.idx; 82 } 83 84 if (this.ci.length === 0) { 85 ret = ret + ' and without confidence intervals.'; 86 } 87 else { 88 ret = ret + ' and with confidence intervals at (' + this.ci.join(', ') + 89 ').'; 90 } 91 92 return ret; 93 }; 94 95 /** 96 * @class DecompositionModel 97 * 98 * Models all the ordination data to be plotted. 99 * 100 * @param {object} data An object with the following attributes (keys): 101 * - `name` A string containing the abbreviated name of the 102 * ordination method. 103 * - `ids` An array of strings where each string is a sample 104 * identifier 105 * - `coords` A 2D Array of floats where each row contains the 106 * coordinates of a sample. The rows are in ids order. 107 * - `names` A 1D Array of strings where each element is the name of one of 108 * the dimensions in the model. 109 * - `pct_var` An Array of floats where each position contains 110 * the percentage explained by that axis 111 * - `low` A 1D Array of floats where each row contains the 112 * coordinates of a sample. The rows are in ids order. 113 * - `high` A 1D Array of floats where each row contains the 114 * coordinates of a sample. The rows are in ids order. 115 * @param {float[]} md_headers An Array of string where each string is a 116 * metadata column header 117 * @param {string[]} metadata A 2D Array of strings where each row contains 118 * the metadata values for a given sample. The rows are in ids order. The 119 * columns are in `md_headers` order. 120 * 121 * @throws {Error} In any of the following cases: 122 * - The number of coordinates does not match the number of samples. 123 * - If there's a coordinate in `coords` that doesn't have the same length as 124 * the rest. 125 * - The number of samples is different than the rows provided as metadata. 126 * - Not all metadata rows have the same number of fields. 127 * 128 * @return {DecompositionModel} 129 * @constructs DecompositionModel 130 * 131 */ 132 function DecompositionModel(data, md_headers, metadata, type) { 133 var coords = data.coordinates, ci = data.ci || []; 134 135 /** 136 * 137 * Model's type of the data, can be either 'scatter' or 'arrow' 138 * @type {string} 139 * 140 */ 141 this.type = type || 'scatter'; 142 143 var num_coords; 144 /** 145 * Abbreviated name of the ordination method used to create the data. 146 * @type {string} 147 */ 148 this.abbreviatedName = data.name || ''; 149 /** 150 * List of sample name identifiers. 151 * @type {string[]} 152 */ 153 this.ids = data.sample_ids; 154 /** 155 * Percentage explained by each of the axes in the ordination. 156 * @type {float[]} 157 */ 158 this.percExpl = data.percents_explained; 159 /** 160 * Column names for the metadata in the samples. 161 * @type {string[]} 162 */ 163 this.md_headers = md_headers; 164 165 if (coords === undefined) { 166 throw new Error('Coordinates are required to initialize this object.'); 167 } 168 169 /* 170 Check that the number of coordinates set provided are the same as the 171 number of samples 172 */ 173 if (this.ids.length !== coords.length) { 174 throw new Error('The number of coordinates differs from the number of ' + 175 'samples. Coords: ' + coords.length + ' samples: ' + 176 this.ids.length); 177 } 178 179 /* 180 Check that all the coords set have the same number of coordinates 181 */ 182 num_coords = coords[0].length; 183 var res = _.find(coords, function(c) {return c.length !== num_coords;}); 184 if (res !== undefined) { 185 throw new Error('Not all samples have the same number of coordinates'); 186 } 187 188 /* 189 Check that we have the percentage explained values for all coordinates 190 */ 191 if (this.percExpl.length !== num_coords) { 192 throw new Error('The number of percentage explained values does not ' + 193 'match the number of coordinates. Perc expl: ' + 194 this.percExpl.length + ' Num coord: ' + num_coords); 195 } 196 197 /* 198 Check that we have the metadata for all samples 199 */ 200 if (this.ids.length !== metadata.length) { 201 throw new Error('The number of metadata rows and the the number of ' + 202 'samples do not match. Samples: ' + this.ids.length + 203 ' Metadata rows: ' + metadata.length); 204 } 205 206 /* 207 Check that we have all the metadata categories in all rows 208 */ 209 res = _.find(metadata, function(m) { 210 return m.length !== md_headers.length; 211 }); 212 if (res !== undefined) { 213 throw new Error('Not all metadata rows have the same number of values'); 214 } 215 216 this.plottable = new Array(this.ids.length); 217 for (i = 0; i < this.ids.length; i++) { 218 // note that ci[i] can be empty 219 this.plottable[i] = new Plottable(this.ids[i], metadata[i], coords[i], i, 220 ci[i]); 221 } 222 223 // use slice to make a copy of the array so we can modify it 224 /** 225 * Minimum and maximum values for each axis in the ordination. More 226 * concretely this object has a `min` and a `max` attributes, each with a 227 * list of floating point arrays that describe the minimum and maximum for 228 * each axis. 229 * @type {Object} 230 */ 231 this.dimensionRanges = {'min': coords[0].slice(), 232 'max': coords[0].slice()}; 233 this.dimensionRanges = _.reduce(this.plottable, 234 DecompositionModel._minMaxReduce, 235 this.dimensionRanges); 236 237 /** 238 * Number of plottables in this decomposition model 239 * @type {integer} 240 */ 241 this.length = this.plottable.length; 242 243 /** 244 * Number of dimensions in this decomposition model 245 * @type {integer} 246 */ 247 this.dimensions = this.dimensionRanges.min.length; 248 249 /** 250 * Names of the axes in the ordination 251 * @type {string[]} 252 */ 253 this.axesNames = data.axes_names === undefined ? [] : data.axes_names; 254 // We call this after all the other attributes have been initialized so we 255 // can use that information safely. Fixes a problem with the ordination 256 // file format, see https://github.com/biocore/emperor/issues/562 257 this._fixAxesNames(); 258 259 /** 260 * Array of pairs of Plottable objects. 261 * @type {Array[]} 262 */ 263 this.edges = this._processEdgeList(data.edges || []); 264 } 265 266 /** 267 * 268 * Whether or not the plottables have confidence intervals 269 * 270 * @return {Boolean} `true` if the plottables have confidence intervals, 271 * `false` otherwise. 272 * 273 */ 274 DecompositionModel.prototype.hasConfidenceIntervals = function() { 275 if (this.plottable.length <= 0) { 276 return false; 277 } 278 else if (this.plottable[0].ci.length > 0) { 279 return true; 280 } 281 return false; 282 }; 283 284 /** 285 * 286 * Retrieve the plottable object with the given id. 287 * 288 * @param {string} id A string with the plottable. 289 * 290 * @return {Plottable} The plottable object for the given id. 291 * 292 */ 293 DecompositionModel.prototype.getPlottableByID = function(id) { 294 idx = this.ids.indexOf(id); 295 if (idx === -1) { 296 throw new Error(id + ' is not found in the Decomposition Model ids'); 297 } 298 return this.plottable[idx]; 299 }; 300 301 /** 302 * 303 * Retrieve all the plottable objects with the given ids. 304 * 305 * @param {integer[]} idArray an Array of strings where each string is a 306 * plottable id. 307 * 308 * @return {Plottable[]} An Array of plottable objects for the given ids. 309 * 310 */ 311 DecompositionModel.prototype.getPlottableByIDs = function(idArray) { 312 dm = this; 313 return _.map(idArray, function(id) {return dm.getPlottableByID(id);}); 314 }; 315 316 /** 317 * 318 * Helper function that returns the index of a given metadata category. 319 * 320 * @param {string} category A string with the metadata header. 321 * 322 * @return {integer} An integer representing the index of the metadata 323 * category in the `md_headers` array. 324 * 325 */ 326 DecompositionModel.prototype._getMetadataIndex = function(category) { 327 var md_idx = this.md_headers.indexOf(category); 328 if (md_idx === -1) { 329 throw new Error('The header ' + category + 330 ' is not found in the metadata categories'); 331 } 332 return md_idx; 333 }; 334 335 /** 336 * 337 * Retrieve all the plottable objects under the metadata header value. 338 * 339 * @param {string} category A string with the metadata header. 340 * @param {string} value A string with the value under the metadata category. 341 * 342 * @return {Plottable[]} An Array of plottable objects for the given category 343 * value pair. 344 * 345 */ 346 DecompositionModel.prototype.getPlottablesByMetadataCategoryValue = function( 347 category, value) { 348 349 var md_idx = this._getMetadataIndex(category); 350 var res = _.filter(this.plottable, function(pl) { 351 return pl.metadata[md_idx] === value; 352 }); 353 354 if (res.length === 0) { 355 throw new Error('The value ' + value + 356 ' is not found in the metadata category ' + category); 357 } 358 return res; 359 }; 360 361 /** 362 * 363 * Retrieve the available values for a given metadata category 364 * 365 * @param {string} category A string with the metadata header. 366 * 367 * @return {string[]} An array of the available values for the given metadata 368 * header sorted first alphabetically and then numerically. 369 * 370 */ 371 DecompositionModel.prototype.getUniqueValuesByCategory = function(category) { 372 var md_idx = this._getMetadataIndex(category); 373 var values = _.map(this.plottable, function(pl) { 374 return pl.metadata[md_idx]; 375 }); 376 377 return naturalSort(_.uniq(values)); 378 }; 379 380 /** 381 * 382 * Method to determine if this is an arrow decomposition 383 * 384 */ 385 DecompositionModel.prototype.isArrowType = function() { 386 return this.type === 'arrow'; 387 }; 388 389 /** 390 * 391 * Method to determine if this is a scatter decomposition 392 * 393 */ 394 DecompositionModel.prototype.isScatterType = function() { 395 return this.type === 'scatter'; 396 }; 397 398 /** 399 * 400 * Executes the provided `func` passing all the plottables as parameters. 401 * 402 * @param {function} func The function to call for each plottable. It should 403 * accept a single parameter which will be the plottable. 404 * 405 * @return {Object[]} An array with the results of executing func over all 406 * plottables. 407 * 408 */ 409 DecompositionModel.prototype.apply = function(func) { 410 return _.map(this.plottable, func); 411 }; 412 413 /** 414 * 415 * Transform observation names into plottable objects. 416 * 417 * @return {Array[]} An array of plottable pairs. 418 * @private 419 * 420 */ 421 DecompositionModel.prototype._processEdgeList = function(edges) { 422 if (edges.length === 0) { 423 return edges; 424 } 425 426 var u, v, scope = this; 427 edges = edges.map(function(edge) { 428 if (edge[0] === edge[1]) { 429 throw new Error('Cannot create edge between two identical nodes (' + 430 edge[0] + ' and ' + edge[1] + ')'); 431 } 432 433 u = scope.getPlottableByID(edge[0]); 434 v = scope.getPlottableByID(edge[1]); 435 436 return [u, v]; 437 }); 438 439 return edges; 440 }; 441 442 /** 443 * 444 * Helper function used to find the minimum and maximum values every 445 * dimension in the plottable objects. This function is used with 446 * underscore.js' reduce function (_.reduce). 447 * 448 * @param {Object} accumulator An object with a "min" and "max" arrays that 449 * store the minimum and maximum values over all the plottables. 450 * @param {Plottable} plottable A plottable object to compare with. 451 * 452 * @return {Object} An updated version of accumulator, integrating the ranges 453 * of the newly seen plottable object. 454 * @private 455 * 456 */ 457 DecompositionModel._minMaxReduce = function(accumulator, plottable) { 458 459 // iterate over every dimension 460 _.each(plottable.coordinates, function(value, index) { 461 if (value > accumulator.max[index]) { 462 accumulator.max[index] = value; 463 } 464 else if (value < accumulator.min[index]) { 465 accumulator.min[index] = value; 466 } 467 }); 468 469 return accumulator; 470 }; 471 472 /** 473 * 474 * Fix the names of the axes. 475 * 476 * Account for missing axes names, and for uninformative names produced by 477 * scikit-bio. In both cases, if we have an abbreviated name, we will use 478 * that string as a prefix for the axes names. 479 * 480 * @private 481 * 482 */ 483 DecompositionModel.prototype._fixAxesNames = function() { 484 var expected = [], replacement = [], prefix, names, cast, i; 485 486 if (this.abbreviatedName === '') { 487 prefix = 'Axis '; 488 } 489 else { 490 prefix = this.abbreviatedName + ' '; 491 } 492 493 if (this.axesNames.length === 0) { 494 for (i = 0; i < this.dimensions; i++) { 495 replacement.push(prefix + (i + 1)); 496 } 497 this.axesNames = replacement; 498 } 499 else { 500 names = util.splitNumericValues(this.axesNames); 501 502 for (i = 0; i < names.numeric.length; i++) { 503 expected.push(i); 504 505 // don't zero-index, doesn't make sense for displaying purposes 506 replacement.push(prefix + (i + 1)); 507 } 508 509 // to truly match scikit-bio's format, all the numeric names should come 510 // after the non-numeric names, and the numeric names should match the 511 // array of expected values. 512 if (_.isEqual(expected, names.numeric) && 513 _.isEqual(this.axesNames, names.nonNumeric.concat(names.numeric))) { 514 this.axesNames = names.nonNumeric.concat(replacement); 515 } 516 } 517 this._buildAxesLabels(); 518 }; 519 520 /** 521 * 522 * Helper method to build labels for all axes 523 * 524 */ 525 DecompositionModel.prototype._buildAxesLabels = function() { 526 var axesLabels = [], index, text; 527 for (index = 0; index < this.axesNames.length; index++) { 528 // when the labels get too long, it's a bit hard to look at 529 if (this.axesNames[index].length > 25) { 530 text = this.axesNames[index].slice(0, 20) + '...'; 531 } 532 else { 533 text = this.axesNames[index]; 534 } 535 536 // account for custom axes (their percentage explained will be -1 to 537 // indicate that this attribute is not meaningful). 538 if (this.percExpl[index] >= 0) { 539 text += ' (' + this.percExpl[index].toPrecision(4) + ' %)'; 540 } 541 542 axesLabels.push(text); 543 } 544 this.axesLabels = axesLabels; 545 }; 546 547 /** 548 * 549 * Helper method to convert a DecompositionModel into a string. 550 * 551 * @return {string} String representation describing the Decomposition 552 * object. 553 * 554 */ 555 DecompositionModel.prototype.toString = function() { 556 return 'name: ' + this.abbreviatedName + '\n' + 557 'Metadata headers: [' + this.md_headers.join(', ') + ']\n' + 558 'Plottables:\n' + _.map(this.plottable, function(plt) { 559 return plt.toString(); 560 }).join('\n'); 561 }; 562 563 return { 'DecompositionModel': DecompositionModel, 564 'Plottable': Plottable}; 565 }); 566