1 define([ 2 'jquery', 3 'underscore', 4 'util', 5 'view', 6 'viewcontroller', 7 'color-editor', 8 'chroma', 9 'three' 10 ], function($, _, util, DecompositionView, ViewControllers, Color, chroma, 11 THREE) { 12 13 // we only use the base attribute class, no need to get the base class 14 var EmperorAttributeABC = ViewControllers.EmperorAttributeABC; 15 var ColorEditor = Color.ColorEditor, ColorFormatter = Color.ColorFormatter; 16 17 /** 18 * @class ColorViewController 19 * 20 * Controls the color changing tab in Emperor. Takes care of changes to 21 * color based on metadata, as well as making colorbars if coloring by a 22 * numeric metadata category. 23 * 24 * @param {UIState} uiState The shared state 25 * @param {Node} container Container node to create the controller in. 26 * @param {Object} decompViewDict This is object is keyed by unique 27 * identifiers and the values are DecompositionView objects referring to a 28 * set of objects presented on screen. This dictionary will usually be shared 29 * by all the tabs in the application. This argument is passed by reference. 30 * 31 * @return {ColorViewController} 32 * @constructs ColorViewController 33 * @extends EmperorAttributeABC 34 */ 35 function ColorViewController(uiState, container, decompViewDict) { 36 var helpmenu = 'Change the colors of the attributes on the plot, such as ' + 37 'spheres, vectors and ellipsoids.'; 38 var title = 'Color'; 39 40 // Constant for width in slick-grid 41 var SLICK_WIDTH = 25, scope = this; 42 var name, value, colorItem; 43 44 // Create scale div and checkbox for whether using scalar data or not 45 /** 46 * @type {Node} 47 * jQuery object holding the colorbar div 48 */ 49 this.$scaleDiv = $('<div>'); 50 /** 51 * @type {Node} 52 * jQuery object holding the SVG colorbar 53 */ 54 this.$colorScale = $("<svg width='90%' height='100%' " + 55 "style='display:block;margin:auto;'></svg>"); 56 this.$scaleDiv.append(this.$colorScale); 57 this.$scaleDiv.hide(); 58 /** 59 * @type {Node} 60 * jQuery object holding the continuous value checkbox 61 */ 62 this.$scaled = $("<input type='checkbox'>"); 63 this.$scaled.prop('hidden', true); 64 /** 65 * @type {Node} 66 * jQuery object holding the continuous value label 67 */ 68 this.$scaledLabel = $("<label for='scaled'>Continuous values</label>"); 69 this.$scaledLabel.prop('hidden', true); 70 71 // this class uses a colormap selector, so populate it before calling super 72 // because otherwise the categorySelectionCallback will be called before the 73 // data is populated 74 /** 75 * @type {Node} 76 * jQuery object holding the select box for the colormaps 77 */ 78 this.$colormapSelect = $("<select class='emperor-tab-drop-down'>"); 79 var currType = ColorViewController.Colormaps[0].type; 80 var selectOpts = $('<optgroup>').attr('label', currType); 81 82 for (var i = 0; i < ColorViewController.Colormaps.length; i++) { 83 var colormap = ColorViewController.Colormaps[i]; 84 // Check if we are in a new optgroup 85 if (colormap.type !== currType) { 86 currType = colormap.type; 87 scope.$colormapSelect.append(selectOpts); 88 selectOpts = $('<optgroup>').attr('label', currType); 89 } 90 var colorItem = $('<option>') 91 .attr('value', colormap.id) 92 .attr('data-type', currType) 93 .text(colormap.name); 94 selectOpts.append(colorItem); 95 } 96 scope.$colormapSelect.append(selectOpts); 97 98 // Build the options dictionary 99 var options = { 100 'valueUpdatedCallback': 101 function(e, args) { 102 var val = args.item.category, color = args.item.value; 103 var group = args.item.plottables; 104 var element = scope.getView(); 105 scope.setPlottableAttributes(element, color, group); 106 }, 107 'categorySelectionCallback': 108 function(evt, params) { 109 // we re-use this same callback regardless of whether the 110 // color or the metadata category changed, maybe we can do 111 // something better about this 112 var category = scope.getMetadataField(); 113 114 var discrete = $('option:selected', scope.$colormapSelect) 115 .attr('data-type') == DISCRETE; 116 var colorScheme = scope.$colormapSelect.val(); 117 118 var decompViewDict = scope.getView(); 119 120 if (discrete) { 121 var palette = ColorViewController.getPaletteColor(colorScheme); 122 scope.$scaled.prop('checked', false); 123 scope.$scaled.prop('hidden', true); 124 scope.$scaledLabel.prop('hidden', true); 125 scope.bodyGrid.selectionPalette = palette; 126 } else { 127 scope.$scaled.prop('hidden', false); 128 scope.$scaledLabel.prop('hidden', false); 129 scope.bodyGrid.selectionPalette = undefined; 130 } 131 var scaled = scope.$scaled.is(':checked'); 132 // getting all unique values per categories 133 var uniqueVals = decompViewDict.decomp.getUniqueValuesByCategory( 134 category); 135 // getting color for each uniqueVals 136 var colorInfo = ColorViewController.getColorList( 137 uniqueVals, colorScheme, discrete, scaled); 138 var attributes = colorInfo[0]; 139 // fetch the slickgrid-formatted data 140 var data = decompViewDict.setCategory( 141 attributes, scope.setPlottableAttributes, category); 142 143 if (scaled) { 144 scope.$searchBar.prop('hidden', true); 145 plottables = ColorViewController._nonNumericPlottables( 146 uniqueVals, data); 147 // Set SlickGrid for color of non-numeric values and show color bar 148 // for rest if there are non numeric categories 149 if (plottables.length > 0) { 150 scope.setSlickGridDataset( 151 [{id: 0, category: 'Non-numeric values', value: '#64655d', 152 plottables: plottables}]); 153 } 154 else { 155 scope.setSlickGridDataset([]); 156 } 157 scope.$scaleDiv.show(); 158 scope.$colorScale.html(colorInfo[1]); 159 } 160 else { 161 scope.$searchBar.prop('hidden', false); 162 scope.setSlickGridDataset(data); 163 scope.$scaleDiv.hide(); 164 } 165 // Call resize to update all methods for new shows/hides/resizes 166 scope.resize(); 167 }, 168 'slickGridColumn': { 169 id: 'title', name: '', field: 'value', 170 sortable: false, maxWidth: SLICK_WIDTH, 171 minWidth: SLICK_WIDTH, 172 editor: ColorEditor, 173 formatter: ColorFormatter 174 } 175 }; 176 177 EmperorAttributeABC.call(this, uiState, container, title, helpmenu, 178 decompViewDict, options); 179 180 // the base-class will try to execute the "ready" callback, so we prevent 181 // that by copying the property and setting the property to undefined. 182 // This controller is not ready until the colormapSelect has signaled that 183 // it is indeed ready. 184 var ready = this.ready; 185 this.ready = undefined; 186 187 // account for the searchbar 188 this.$colormapSelect.insertAfter(this.$select); 189 this.$header.append(this.$scaled); 190 this.$header.append(this.$scaledLabel); 191 this.$body.prepend(this.$scaleDiv); 192 193 // the chosen select can only be set when the document is ready 194 $(function() { 195 scope.$colormapSelect.on('chosen:ready', function() { 196 if (ready !== null) { 197 ready(); 198 scope.ready = ready; 199 } 200 }); 201 scope.$colormapSelect.chosen({width: '100%', search_contains: true}); 202 scope.$colormapSelect.chosen().change(options.categorySelectionCallback); 203 scope.$scaled.on('change', options.categorySelectionCallback); 204 }); 205 206 return this; 207 } 208 ColorViewController.prototype = Object.create(EmperorAttributeABC.prototype); 209 ColorViewController.prototype.constructor = EmperorAttributeABC; 210 211 212 /** 213 * Helper for building the plottables for non-numeric data 214 * 215 * @param {String[]} uniqueVals Array of unique values for the category 216 * @param {Object} data SlickGrid formatted data from setCategory function 217 * 218 * @return {Plottable[]} Array of plottables for all non-numeric values 219 * @private 220 * 221 */ 222 ColorViewController._nonNumericPlottables = function(uniqueVals, data) { 223 // Filter down to only non-numeric data 224 var split = util.splitNumericValues(uniqueVals); 225 var plotList = data.filter(function(x) { 226 return $.inArray(x.category, split.nonNumeric) !== -1; 227 }); 228 // Build list of plottables and return 229 var plottables = []; 230 for (var i = 0; i < plotList.length; i++) { 231 plottables = plottables.concat(plotList[i].plottables); 232 } 233 return plottables; 234 }; 235 236 /** 237 * Sets whether or not elements in the tab can be modified. 238 * 239 * @param {Boolean} trulse option to enable elements. 240 */ 241 ColorViewController.prototype.setEnabled = function(trulse) { 242 EmperorAttributeABC.prototype.setEnabled.call(this, trulse); 243 244 this.$colormapSelect.prop('disabled', !trulse).trigger('chosen:updated'); 245 this.$scaled.prop('disabled', !trulse); 246 }; 247 248 /** 249 * 250 * Private method to reset the color of all the objects in every 251 * decomposition view to red. 252 * 253 * @extends EmperorAttributeABC 254 * @private 255 * 256 */ 257 ColorViewController.prototype._resetAttribute = function() { 258 EmperorAttributeABC.prototype._resetAttribute.call(this); 259 260 _.each(this.decompViewDict, function(view) { 261 view.setColor(0xff0000); 262 }); 263 }; 264 265 /** 266 * Method that returns whether or not the coloring is continuous and the 267 * values have been scaled. 268 * 269 * @return {Boolean} True if the coloring is continuous and the data is 270 * scaled, false otherwise. 271 */ 272 ColorViewController.prototype.isColoringContinuous = function() { 273 // the bodygrid can have at most one element (NA values) 274 return (this.$scaled.is(':checked') && 275 this.getSlickGridDataset().length <= 1); 276 }; 277 278 /** 279 * 280 * Wrapper for generating a list of colors that corresponds to all samples 281 * in the plot by coloring type requested 282 * 283 * @param {String[]} values list of objects to generate a color for, usually a 284 * category in a given metadata column. 285 * @param {String} [map = {'discrete-coloring-qiime'|'Viridis'}] name of the 286 * color map to use, see ColorViewController.Colormaps 287 * @see ColorViewController.Colormaps 288 * @param {Boolean} discrete Whether to treat colormap requested as a 289 * discrete set of colors or use interpolation to create gradient of colors 290 * @param {Boolean} [scaled = false] Whether to use a scaled colormap or 291 * equidistant colors for each value 292 * @see ColorViewController.getDiscreteColors 293 * @see ColorViewController.getInterpolatedColors 294 * @see ColorViewController.getScaledColors 295 * 296 * @return {Object} colors The object containing the hex colors keyed to 297 * each sample 298 * @return {String} gradientSVG The SVG string for the scaled data or null 299 * 300 */ 301 ColorViewController.getColorList = function(values, map, discrete, scaled) { 302 var colors = {}, gradientSVG; 303 scaled = scaled || false; 304 305 if (_.findWhere(ColorViewController.Colormaps, {id: map}) === undefined) { 306 throw new Error('Could not find ' + map + ' as a colormap.'); 307 } 308 309 // 1 color and continuous coloring should return the first element in map 310 if (values.length == 1 && discrete === false) { 311 colors[values[0]] = chroma.brewer[map][0]; 312 return [colors, gradientSVG]; 313 } 314 315 //Call helper function to create the required colormap type 316 if (discrete) { 317 colors = ColorViewController.getDiscreteColors(values, map); 318 } 319 else if (scaled) { 320 try { 321 var info = ColorViewController.getScaledColors(values, map); 322 } catch (e) { 323 alert('Category can not be shown as continuous values. Continuous ' + 324 'coloration requires at least 2 numeric values in the category.'); 325 throw new Error('non-numeric category'); 326 } 327 colors = info[0]; 328 gradientSVG = info[1]; 329 } 330 else { 331 colors = ColorViewController.getInterpolatedColors(values, map); 332 } 333 return [colors, gradientSVG]; 334 }; 335 336 /** 337 * 338 * Retrieve a discrete color set. 339 * 340 * @param {String[]} values list of objects to generate a color for, usually a 341 * category in a given metadata column. 342 * @param {String} [map = 'discrete-coloring-qiime'] name of the color map to 343 * use, see ColorViewController.Colormaps 344 * @see ColorViewController.Colormaps 345 * 346 * @return {Object} colors The object containing the hex colors keyed to 347 * each sample 348 * 349 */ 350 ColorViewController.getDiscreteColors = function(values, map) { 351 map = ColorViewController.getPaletteColor(map); 352 var size = map.length; 353 var colors = {}; 354 for (var i = 0; i < values.length; i++) { 355 mapIndex = i - (Math.floor(i / size) * size); 356 colors[values[i]] = map[mapIndex]; 357 } 358 return colors; 359 }; 360 361 /** 362 * 363 * Retrieve a whole discrete palette color set. 364 * 365 * @param {String} [map = 'discrete-coloring-qiime'] name of the color map to 366 * use, see ColorViewController.Colormaps 367 * @see ColorViewController.Colormaps 368 * 369 * @return {Object} map for selected color palette 370 * 371 */ 372 ColorViewController.getPaletteColor = function(map) { 373 map = map || 'discrete-coloring-qiime'; 374 375 if (map == 'discrete-coloring-qiime') { 376 map = ColorViewController._qiimeDiscrete; 377 } else { 378 map = chroma.brewer[map]; 379 } 380 381 return map; 382 }; 383 384 /** 385 * 386 * Retrieve a scaled color set. 387 * 388 * @param {String[]} values Objects to generate a color for, usually a 389 * category in a given metadata column. 390 * @param {String} [map = 'Viridis'] name of the discrete color map to use. 391 * @param {String} [nanColor = '#64655d'] Color to use for non-numeric values. 392 * 393 * @return {Object} colors The object containing the hex colors keyed to 394 * each sample 395 * @return {String} gradientSVG The SVG string for the scaled data or null 396 * 397 */ 398 ColorViewController.getScaledColors = function(values, map, nanColor) { 399 map = map || 'Viridis'; 400 nanColor = nanColor || '#64655d'; 401 map = chroma.brewer[map]; 402 403 // Get list of only numeric values, error if none 404 var split = util.splitNumericValues(values), numbers; 405 if (split.numeric.length < 2) { 406 throw new Error('non-numeric category'); 407 } 408 409 // convert objects to numbers so we can map them to a color, we keep a copy 410 // of the untransformed object so we can search the metadata 411 numbers = _.map(split.numeric, parseFloat); 412 min = _.min(numbers); 413 max = _.max(numbers); 414 415 var interpolator = chroma.scale(map).domain([min, max]); 416 var colors = {}; 417 418 // Color all the numeric values 419 _.each(split.numeric, function(element) { 420 colors[element] = interpolator(+element).hex(); 421 }); 422 // Gray out (or assign a user-specified color for) non-numeric values 423 _.each(split.nonNumeric, function(element) { 424 colors[element] = nanColor; 425 }); 426 // Build the SVG showing the gradient of colors for numeric values 427 var mid = (min + max) / 2; 428 // We retrieve 101 colors from along the gradient. This is because we want 429 // to specify a color for each integer percentage in the range [0%, 100%], 430 // which contains 101 integers (since we're starting at 0: 431 // 100 - 0 + 1 = 101). See https://github.com/biocore/emperor/issues/788. 432 var stopColors = interpolator.colors(101); 433 var gradientSVG = '<defs>'; 434 gradientSVG += '<linearGradient id="Gradient" x1="0" x2="0" y1="1" y2="0">'; 435 for (var pos = 0; pos < stopColors.length; pos++) { 436 gradientSVG += '<stop offset="' + pos + '%" stop-color="' + 437 stopColors[pos] + '"/>'; 438 } 439 gradientSVG += '</linearGradient></defs><rect id="gradientRect" ' + 440 'width="20" height="95%" fill="url(#Gradient)"/>'; 441 442 gradientSVG += '<text x="25" y="12px" font-family="sans-serif" ' + 443 'font-size="12px" text-anchor="start">' + max + '</text>'; 444 gradientSVG += '<text x="25" y="50%" font-family="sans-serif" ' + 445 'font-size="12px" text-anchor="start">' + mid + '</text>'; 446 gradientSVG += '<text x="25" y="95%" font-family="sans-serif" ' + 447 'font-size="12px" text-anchor="start">' + min + '</text>'; 448 return [colors, gradientSVG]; 449 }; 450 451 /** 452 * 453 * Retrieve an interpolatd color set. 454 * 455 * @param {String[]} values Objects to generate a color for, usually a 456 * category in a given metadata column. 457 * @param {String} [map = 'Viridis'] name of the color map to use. 458 * 459 * @return {Object} colors The object containing the hex colors keyed to 460 * each sample. 461 * 462 */ 463 ColorViewController.getInterpolatedColors = function(values, map) { 464 map = map || 'Viridis'; 465 map = chroma.brewer[map]; 466 467 var total = values.length; 468 // Logic here adapted from Colorer.assignOrdinalScaledColors() in Empress' 469 // codebase 470 var interpolator = chroma.scale(map).domain([0, values.length - 1]); 471 var colors = {}; 472 for (var i = 0; i < values.length; i++) { 473 colors[values[i]] = interpolator(i).hex(); 474 } 475 return colors; 476 }; 477 478 /** 479 * Converts the current instance into a JSON string. 480 * 481 * @return {Object} JSON ready representation of self. 482 */ 483 ColorViewController.prototype.toJSON = function() { 484 var json = EmperorAttributeABC.prototype.toJSON.call(this); 485 json.colormap = this.$colormapSelect.val(); 486 json.continuous = this.$scaled.is(':checked'); 487 return json; 488 }; 489 490 /** 491 * Decodes JSON string and modifies its own instance variables accordingly. 492 * 493 * @param {Object} Parsed JSON string representation of self. 494 */ 495 ColorViewController.prototype.fromJSON = function(json) { 496 var data; 497 498 // NOTE: We do not call super here because of the non-numeric values issue 499 // Order here is important. We want to set all the extra controller 500 // settings before we load from json, as they can override the JSON when set 501 this.setMetadataField(json.category); 502 503 this.setEnabled(true); 504 505 // if the category is null, then there's nothing to set about the state 506 // of the controller 507 if (json.category === null) { 508 return; 509 } 510 511 this.$colormapSelect.val(json.colormap); 512 this.$colormapSelect.trigger('chosen:updated'); 513 this.$scaled.prop('checked', json.continuous); 514 this.$scaled.trigger('change'); 515 516 // Fetch and set the SlickGrid-formatted data 517 // Need to take into account the existence of the non-numeric values grid 518 // information from the continuous data. 519 var decompViewDict = this.getView(); 520 if (this.$scaled.is(':checked')) { 521 // Get the current SlickGrid data and update with the saved color 522 data = this.getSlickGridDataset(); 523 data[0].value = json.data['Non-numeric values']; 524 this.setPlottableAttributes( 525 decompViewDict, json.data['Non-numeric values'], data[0].plottables); 526 } 527 else { 528 data = decompViewDict.setCategory( 529 json.data, this.setPlottableAttributes, json.category); 530 } 531 532 if (!_.isEmpty(data)) { 533 this.setSlickGridDataset(data); 534 } 535 }; 536 537 /** 538 * Resizes the container and the individual elements. 539 * 540 * Note, the consumer of this class, likely the main controller should call 541 * the resize function any time a resizing event happens. 542 * 543 * @param {Float} width the container width. 544 * @param {Float} height the container height. 545 */ 546 ColorViewController.prototype.resize = function(width, height) { 547 this.$body.height(this.$canvas.height() - this.$header.height()); 548 this.$body.width(this.$canvas.width()); 549 550 if (this.$scaled.is(':checked')) { 551 this.$scaleDiv.css('height', (this.$body.height() / 2) + 'px'); 552 this.$gridDiv.css('height', (this.$body.height() / 2 - 20) + 'px'); 553 } 554 else { 555 this.$gridDiv.css('height', '100%'); 556 } 557 // call super, most of the header and body resizing logic is done there 558 EmperorAttributeABC.prototype.resize.call(this, width, height); 559 }; 560 561 /** 562 * Helper function to set the color of plottable 563 * 564 * @param {scope} object , the scope where the plottables exist 565 * @param {color} string , hexadecimal representation of a color, which will 566 * be applied to the plottables 567 * @param {group} array of objects, list of object that should be changed in 568 * scope 569 */ 570 ColorViewController.prototype.setPlottableAttributes = 571 function(scope, color, group) { 572 scope.setColor(color, group); 573 }; 574 575 var DISCRETE = 'Discrete'; 576 var SEQUENTIAL = 'Sequential'; 577 var DIVERGING = 'Diverging'; 578 /** 579 * @type {Object} 580 * Color maps available, along with what type of colormap they are. 581 */ 582 ColorViewController.Colormaps = [ 583 {id: 'discrete-coloring-qiime', name: 'Classic QIIME Colors', 584 type: DISCRETE}, 585 {id: 'Paired', name: 'Paired', type: DISCRETE}, 586 {id: 'Accent', name: 'Accent', type: DISCRETE}, 587 {id: 'Dark2', name: 'Dark', type: DISCRETE}, 588 {id: 'Set1', name: 'Set1', type: DISCRETE}, 589 {id: 'Set2', name: 'Set2', type: DISCRETE}, 590 {id: 'Set3', name: 'Set3', type: DISCRETE}, 591 {id: 'Pastel1', name: 'Pastel1', type: DISCRETE}, 592 {id: 'Pastel2', name: 'Pastel2', type: DISCRETE}, 593 594 {id: 'Viridis', name: 'Viridis', type: SEQUENTIAL}, 595 {id: 'Reds', name: 'Reds', type: SEQUENTIAL}, 596 {id: 'RdPu', name: 'Red-Purple', type: SEQUENTIAL}, 597 {id: 'Oranges', name: 'Oranges', type: SEQUENTIAL}, 598 {id: 'OrRd', name: 'Orange-Red', type: SEQUENTIAL}, 599 {id: 'YlOrBr', name: 'Yellow-Orange-Brown', type: SEQUENTIAL}, 600 {id: 'YlOrRd', name: 'Yellow-Orange-Red', type: SEQUENTIAL}, 601 {id: 'YlGn', name: 'Yellow-Green', type: SEQUENTIAL}, 602 {id: 'YlGnBu', name: 'Yellow-Green-Blue', type: SEQUENTIAL}, 603 {id: 'Greens', name: 'Greens', type: SEQUENTIAL}, 604 {id: 'GnBu', name: 'Green-Blue', type: SEQUENTIAL}, 605 {id: 'Blues', name: 'Blues', type: SEQUENTIAL}, 606 {id: 'BuGn', name: 'Blue-Green', type: SEQUENTIAL}, 607 {id: 'BuPu', name: 'Blue-Purple', type: SEQUENTIAL}, 608 {id: 'Purples', name: 'Purples', type: SEQUENTIAL}, 609 {id: 'PuRd', name: 'Purple-Red', type: SEQUENTIAL}, 610 {id: 'PuBuGn', name: 'Purple-Blue-Green', type: SEQUENTIAL}, 611 {id: 'Greys', name: 'Greys', type: SEQUENTIAL}, 612 613 {id: 'Spectral', name: 'Spectral', type: DIVERGING}, 614 {id: 'RdBu', name: 'Red-Blue', type: DIVERGING}, 615 {id: 'RdYlGn', name: 'Red-Yellow-Green', type: DIVERGING}, 616 {id: 'RdYlBu', name: 'Red-Yellow-Blue', type: DIVERGING}, 617 {id: 'RdGy', name: 'Red-Grey', type: DIVERGING}, 618 {id: 'PiYG', name: 'Pink-Yellow-Green', type: DIVERGING}, 619 {id: 'BrBG', name: 'Brown-Blue-Green', type: DIVERGING}, 620 {id: 'PuOr', name: 'Purple-Orange', type: DIVERGING}, 621 {id: 'PRGn', name: 'Purple-Green', type: DIVERGING} 622 ]; 623 624 // taken from the qiime/colors.py module; a total of 24 colors 625 /** @private */ 626 ColorViewController._qiimeDiscrete = ['#ff0000', '#0000ff', '#f27304', 627 '#008000', '#91278d', '#ffff00', '#7cecf4', '#f49ac2', '#5da09e', '#6b440b', 628 '#808080', '#f79679', '#7da9d8', '#fcc688', '#80c99b', '#a287bf', '#fff899', 629 '#c49c6b', '#c0c0c0', '#ed008a', '#00b6ff', '#a54700', '#808000', '#008080']; 630 631 return ColorViewController; 632 }); 633