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