1 /** @module trajectory */ 2 define([ 3 'underscore' 4 ], function(_) { 5 /** 6 * 7 * @class TrajectoryOfSamples 8 * 9 * Represents an ordered set of samples and their position in PCoA space. 10 * 11 * @param {string[]} sampleNames Array of sample identifiers. 12 * @param {string} metadataCategoryName The name of the category in the 13 * mapping file used to generate this trajectory. 14 * @param {float[]} gradientPoints The position of the samples in the 15 * gradient. 16 * @param {Object[]} coordinates Array of objects with x, y and z properties 17 * where each corresponds to the position of a sample in PCoA space. 18 * @param {float} minimumDelta Minimum differential between the ordered 19 * gradientPoints this value must be non-zero. Note that this value should be 20 * computed taking into account all the other trajectories that will be 21 * animated together, usually by an AnimationDirector object. 22 * @param {integer} [suppliedN = 5] Determines how many points should be found 23 * in the the trajectory. 24 * @param {integer} [maxN = 10] Maximum number of samples allowed per 25 * interpolation interval. 26 * 27 * @return {TrajectoryOfSamples} An instance of TrajectoryOfSamples 28 * @constructs TrajectoryOfSamples 29 **/ 30 function TrajectoryOfSamples(sampleNames, metadataCategoryName, 31 gradientPoints, coordinates, minimumDelta, 32 suppliedN, maxN) { 33 /** 34 * Sample identifiers 35 * @type {string[]} 36 */ 37 this.sampleNames = sampleNames; 38 /** 39 * The name of the category in the mapping file used to generate this 40 * trajectory. 41 * @type {string} 42 */ 43 this.metadataCategoryName = metadataCategoryName; 44 45 // array of the values that samples have through the gradient 46 this.gradientPoints = gradientPoints; 47 48 // the first three axes of the data points 49 this.coordinates = coordinates; 50 51 /** 52 * Minimum differential between samples in the trajectory; the value is 53 * computed using the gradientPoints array. 54 * @type {float} 55 */ 56 this.minimumDelta = minimumDelta; 57 58 /** 59 * Minimum number of frames a distance will have in the gradient. 60 * This value determines how fast the animation will run. 61 * For now we use 5 as a good default value; 60 was way too slow. 62 * @type {float} 63 * @default 5 64 */ 65 this.suppliedN = suppliedN !== undefined ? suppliedN : 5; 66 /** 67 * Maximum number of samples allowed per interpolation interval. 68 * @type {float} 69 * @default 10 70 */ 71 this.maxN = maxN !== undefined ? maxN : 10; 72 73 if (this.coordinates.length != this.gradientPoints.length) { 74 throw new Error('The number of coordinate points and gradient points is' + 75 'different, make sure these values are consistent.'); 76 } 77 78 // initialize as an empty array but fill it up upon request 79 /** 80 * Array of objects with the corresponding interpolated x, y and z values. 81 * The interpolation operation takes place between subsequent samples. 82 * @type {Object[]} 83 */ 84 this.interpolatedCoordinates = null; 85 this._generateInterpolatedCoordinates(); 86 87 return this; 88 } 89 90 /** 91 * 92 * Helper method to iterate over all the coordinates and generate 93 * interpolated arrays. 94 * @private 95 * 96 */ 97 TrajectoryOfSamples.prototype._generateInterpolatedCoordinates = function() { 98 var pointsPerStep = 0, delta = 0; 99 var interpolatedCoordinatesBuffer = new Array(), 100 intervalBuffer = new Array(); 101 var currInterpolation; 102 103 // iterate over the gradient points to compute the interpolated distances 104 for (var index = 0; index < this.gradientPoints.length - 1; index++) { 105 106 // calculate the absolute difference of the current pair of points 107 delta = Math.abs(Math.abs(this.gradientPoints[index]) - Math.abs( 108 this.gradientPoints[index + 1])); 109 110 pointsPerStep = this.calculateNumberOfPointsForDelta(delta); 111 if (pointsPerStep > this.maxN) { 112 pointsPerStep = this.maxN; 113 } 114 115 currInterpolation = linearInterpolation(this.coordinates[index]['x'], 116 this.coordinates[index]['y'], 117 this.coordinates[index]['z'], 118 this.coordinates[index + 1]['x'], 119 this.coordinates[index + 1]['y'], 120 this.coordinates[index + 1]['z'], 121 pointsPerStep); 122 123 // extend to include these interpolated points, do not include the last 124 // element of the array to avoid repeating the number per interval 125 interpolatedCoordinatesBuffer = interpolatedCoordinatesBuffer.concat( 126 currInterpolation.slice(0, -1)); 127 128 // extend the interval buffer 129 for (var i = 0; i < pointsPerStep; i++) { 130 intervalBuffer.push(index); 131 } 132 } 133 134 // add the last point to make sure the trajectory is closed 135 this.interpolatedCoordinates = interpolatedCoordinatesBuffer.concat( 136 currInterpolation.slice(-1)); 137 this._intervalValues = intervalBuffer; 138 139 return; 140 }; 141 142 /** 143 * 144 * Helper method to calculate the number of points that there should be for a 145 * differential. 146 * 147 * @param {float} delta Value for which to determine the required number of 148 * points. 149 * 150 * @return {integer} The number of suggested frames for the differential 151 * 152 */ 153 TrajectoryOfSamples.prototype.calculateNumberOfPointsForDelta = 154 function(delta) { 155 return Math.floor((delta * this.suppliedN) / this.minimumDelta); 156 }; 157 158 /** 159 * 160 * Retrieve the representative coordinates needed for a trajectory to be 161 * drawn. 162 * 163 ** Note that this implementation is naive and will return points that lay on 164 * a rect line if these were part of the original set of coordinates. 165 * 166 * @param {integer} idx Value for which to determine the required number of 167 * points. 168 * 169 * @return {Array[]} Array containing the representative float x, y, z 170 * coordinates needed to draw a trajectory at the given index. 171 */ 172 TrajectoryOfSamples.prototype.representativeCoordinatesAtIndex = 173 function(idx) { 174 175 if (idx === 0) { 176 return [this.coordinates[0]]; 177 } 178 179 // we only need to show the edges and none of the interpolated points 180 if (this.interpolatedCoordinates.length - 1 <= idx) { 181 return this.coordinates; 182 } 183 184 var output = this.coordinates.slice(0, this._intervalValues[idx] + 1); 185 output = output.concat(this.interpolatedCoordinates[idx]); 186 187 return output; 188 }; 189 190 /** 191 * 192 * Grab only the interpolated portion of representativeCoordinatesAtIndex. 193 * 194 * @param {integer} idx Value for which to determine the required number of 195 * points. 196 * 197 * @return {Array[]} Array containing the representative float x, y, z 198 * coordinates needed to draw the interpolated portion of a trajectory at the 199 * given index. 200 */ 201 TrajectoryOfSamples.prototype.representativeInterpolatedCoordinatesAtIndex = 202 function(idx) { 203 if (idx === 0) 204 return null; 205 if (this.interpolatedCoordinates.length - 1 <= idx) 206 return null; 207 208 lastStaticPoint = this.coordinates[this._intervalValues[idx]]; 209 interpPoint = this.interpolatedCoordinates[idx]; 210 if (lastStaticPoint.x === interpPoint.x && 211 lastStaticPoint.y === interpPoint.y && 212 lastStaticPoint.z === interpPoint.z) { 213 return null; //Shouldn't pass on a zero length segment 214 } 215 216 return [lastStaticPoint, interpPoint]; 217 }; 218 219 /** 220 * 221 * Function to interpolate a certain number of steps between two three 222 * dimensional points. 223 * 224 * This code is based on the function found in: 225 * http://snipplr.com/view.php?codeview&id=47206 226 * 227 * @param {float} x_1 Initial value of a position in the first dimension 228 * @param {float} y_1 Initial value of a position in the second dimension 229 * @param {float} z_1 Initial value of a position in the third dimension 230 * @param {float} x_2 Final value of a position in the first dimension 231 * @param {float} y_2 Final value of a position in the second dimension 232 * @param {float} z_2 Final value of a position in the third dimension 233 * @param {integer} steps Number of steps that we want the interpolation to 234 * run 235 * 236 * @return {Object[]} Array of objects that have the x, y and z attributes 237 * @function linearInterpolation 238 * 239 */ 240 241 function linearInterpolation(x_1, y_1, z_1, x_2, y_2, z_2, steps) { 242 var xAbs = Math.abs(x_1 - x_2); 243 var yAbs = Math.abs(y_1 - y_2); 244 var zAbs = Math.abs(z_1 - z_2); 245 var xDiff = x_2 - x_1; 246 var yDiff = y_2 - y_1; 247 var zDiff = z_2 - z_1; 248 249 // and apparetnly this makes takes no effect whatsoever 250 var length = Math.sqrt(xAbs * xAbs + yAbs * yAbs + zAbs * zAbs); 251 var xStep = xDiff / steps; 252 var yStep = yDiff / steps; 253 var zStep = zDiff / steps; 254 255 var newx = 0; 256 var newy = 0; 257 var newz = 0; 258 var result = new Array(); 259 260 for (var s = 0; s <= steps; s++) { 261 newx = x_1 + (xStep * s); 262 newy = y_1 + (yStep * s); 263 newz = z_1 + (zStep * s); 264 265 result.push({'x': newx, 'y': newy, 'z': newz}); 266 } 267 268 return result; 269 } 270 271 /** 272 * 273 * Function to compute the distance between two three dimensional points. 274 * 275 * This code is based on the function found in: 276 * {@link http://snipplr.com/view.php?codeview&id=47207} 277 * 278 * @param {float} x_1 Initial value of a position in the first dimension 279 * @param {float} y_1 Initial value of a position in the second dimension 280 * @param {float} z_1 Initial value of a position in the third dimension 281 * @param {float} x_2 Final value of a position in the first dimension 282 * @param {float} y_2 Final value of a position in the second dimension 283 * @param {float} z_2 Final value of a position in the third dimension 284 * 285 * @return {float} Value of the distance between the two points 286 * @function distanceBetweenPoints 287 * 288 */ 289 function distanceBetweenPoints(x_1, y_1, z_1, x_2, y_2, z_2) { 290 var xs = 0; 291 var ys = 0; 292 var zs = 0; 293 294 // Math.pow is faster than simple multiplication 295 xs = Math.pow(Math.abs(x_2 - x_1), 2); 296 ys = Math.pow(Math.abs(y_2 - y_1), 2); 297 zs = Math.pow(Math.abs(z_2 - z_1), 2); 298 299 return Math.sqrt(xs + ys + zs); 300 } 301 302 /** 303 * 304 * Helper data wrangling function, takes as inputs a mapping file and the 305 * coordinates to synthesize the information into an array. Mainly used by 306 * the AnimationDirector object. 307 * 308 * @param {string[]} mappingFileHeaders The metadata mapping file headers. 309 * @param {Array[]} mappingFileData An Array where the indices are sample 310 * identifiers and each of the contained elements is an Array of strings where 311 * the first element corresponds to the first data for the first column in the 312 * mapping file (`mappingFileHeaders`). 313 * @param {Object[]} coordinatesData An Array of Objects where the indices are 314 * the sample identifiers and each of the objects has the following 315 * properties: x, y, z, name, color, P1, P2, P3, ... PN where N is the number 316 * of dimensions in this dataset. 317 * @param {string} trajectoryCategory a string with the name of the mapping 318 * file header where the data that groups the samples is contained, this will 319 * usually be BODY_SITE, HOST_SUBJECT_ID, etc.. 320 * @param {string} gradientCategory a string with the name of the mapping file 321 * header where the data that spreads the samples over a gradient is 322 * contained, usually time or days_since_epoch. Note that this should be an 323 * all numeric category. 324 * 325 * @return {Object[]} An Array with the contained data indexed by the sample 326 * identifiers. 327 * @throws {Error} Any of the following: 328 * * gradientIndex === -1 329 * * trajectoryIndex === -1 330 * @function getSampleNamesAndDataForSortedTrajectories 331 * 332 */ 333 function getSampleNamesAndDataForSortedTrajectories(mappingFileHeaders, 334 mappingFileData, 335 coordinatesData, 336 trajectoryCategory, 337 gradientCategory) { 338 var gradientIndex = mappingFileHeaders.indexOf(gradientCategory); 339 var trajectoryIndex = mappingFileHeaders.indexOf(trajectoryCategory); 340 341 var processedData = {}, out = {}; 342 var trajectoryBuffer = null, gradientBuffer = null; 343 344 // this is the most utterly annoying thing ever 345 if (gradientIndex === -1) { 346 throw new Error('Gradient category not found in mapping file header'); 347 } 348 if (trajectoryIndex === -1) { 349 throw new Error('Trajectory category not found in mapping file header'); 350 } 351 352 for (var sampleId in mappingFileData) { 353 354 trajectoryBuffer = mappingFileData[sampleId][trajectoryIndex]; 355 gradientBuffer = mappingFileData[sampleId][gradientIndex]; 356 357 // check if there's already an element for this trajectory, if not 358 // initialize a new array for this element of the processed data 359 if (processedData[trajectoryBuffer] === undefined) { 360 processedData[trajectoryBuffer] = []; 361 } 362 processedData[trajectoryBuffer].push({'name': sampleId, 363 'value': gradientBuffer, 'x': coordinatesData[sampleId]['x'], 364 'y': coordinatesData[sampleId]['y'], 365 'z': coordinatesData[sampleId]['z']}); 366 } 367 368 // we need this custom sorting function to make the values be sorted in 369 // ascending order but accounting for the data structure that we just built 370 var sortingFunction = function(a, b) { 371 return parseFloat(a['value']) - parseFloat(b['value']); 372 }; 373 374 // sort all the values using the custom anonymous function 375 for (var key in processedData) { 376 processedData[key].sort(sortingFunction); 377 } 378 379 // Don't add a trajectory unless it has more than one sample in the 380 // gradient. For example, there's no reason why we should animate a 381 // trajectory that has 3 samples at timepoint 0 ([0, 0, 0]) or a trajectory 382 // that has just one sample at timepoint 0 ([0]) 383 for (key in processedData) { 384 var uniqueValues = _.uniq(processedData[key], false, function(sample) { 385 return sample.value; 386 }); 387 388 if (uniqueValues.length > 1 && processedData[key].length >= 1) { 389 out[key] = processedData[key]; 390 } 391 } 392 393 // we need a placeholder object as we filter trajectories below 394 processedData = out; 395 out = {}; 396 397 // note that min finds the trajectory with the oldest sample, once found 398 // we get the first sample and the first point in the gradient 399 var earliestSample = _.min(processedData, function(trajectory) { 400 return parseInt(trajectory[0].value); 401 })[0].value; 402 403 // Left-pad all trajectories so they start at the same time, but they are 404 // not visibly different. 405 // 406 // Note: THREE.js won't display geometries with overlapping vertices, 407 // therefore we add a small amount of noise in the Z coordinate. 408 _.each(processedData, function(value, key) { 409 out[key] = processedData[key]; 410 var first = processedData[key][0]; 411 if (first.value !== earliestSample) { 412 out[key].unshift({'name': first.name, 'value': earliestSample, 413 'x': first.x, 'y': first.y, 'z': first.z + 0.0001}); 414 } 415 }); 416 417 return out; 418 } 419 420 /** 421 * 422 * Function to calculate the minimum delta from an array of wrangled data by 423 * getSampleNamesAndDataForSortedTrajectories. 424 * 425 * This function will not take into account as a minimum delta zero values 426 * i.e. the differential between two samples that lie at the same position in 427 * the gradient. 428 * 429 * @param {Object[]} sampleData An Array as computed from mapping file data 430 * and coordinates by getSampleNamesAndDataForSortedTrajectories. 431 * 432 * @return {float} The minimum difference between two samples across the 433 * defined gradient. 434 * 435 * @throws {Error} Input data is undefined. 436 * @function getMinimumDelta 437 */ 438 function getMinimumDelta(sampleData) { 439 if (sampleData === undefined) { 440 throw new Error('The sample data cannot be undefined'); 441 } 442 443 var bufferArray = new Array(), deltasArray = new Array(); 444 445 // go over all the data and compute the deltas for all trajectories 446 for (var key in sampleData) { 447 for (var index = 0; index < sampleData[key].length; index++) { 448 bufferArray.push(sampleData[key][index]['value']); 449 } 450 for (var index = 0; index < bufferArray.length - 1; index++) { 451 deltasArray.push(Math.abs(bufferArray[index + 1] - bufferArray[index])); 452 } 453 454 // clean buffer array 455 bufferArray.length = 0; 456 } 457 458 // remove all the deltas of zero so we don't skew our results 459 deltasArray = _.filter(deltasArray, function(num) { return num !== 0; }); 460 461 // return the minimum of these values 462 return _.min(deltasArray); 463 } 464 465 return {'TrajectoryOfSamples': TrajectoryOfSamples, 466 'getMinimumDelta': getMinimumDelta, 467 'getSampleNamesAndDataForSortedTrajectories': 468 getSampleNamesAndDataForSortedTrajectories, 469 'distanceBetweenPoints': distanceBetweenPoints, 470 'linearInterpolation': linearInterpolation}; 471 }); 472